diff --git a/client/useraccesstokens.js.map b/client/useraccesstokens.js.map
new file mode 100644
index 0000000..a5a3a3f
--- /dev/null
+++ b/client/useraccesstokens.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["../src/client/useraccesstokens.js"],
+ "sourcesContent": ["const expand = text => {\n return text\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\\*(.+?)\\*/g, '$1')\n}\n\nconst error = text => {\n return `
${text}
`\n}\n\nconst success = text => {\n return `${text}
`\n}\n\nconst tokenForm = () => {\n return `\n \n \n \n
\n \n \n
\n `\n}\n\nconst tokenList = (tokens) => {\n if (!tokens || tokens.length === 0) {\n return 'No tokens created yet.
'\n }\n\n const tokenRows = tokens.map(token => {\n const status = token.revoked ? 'revoked' : \n (token.expires && new Date(token.expires) < new Date()) ? 'expired' :\n 'active'\n \n const lastUsed = token.lastUsed ? new Date(token.lastUsed).toLocaleDateString() : 'never'\n const expires = token.expires ? new Date(token.expires).toLocaleDateString() : 'never'\n \n return `\n ${expand(token.name)} (...${token.displayHint}) - ${status}\n ${!token.revoked ? `` : ''}\n \n
created: ${new Date(token.created).toLocaleDateString()}, expires: ${expires}, last used: ${lastUsed}\n
\n `\n }).join('')\n\n return `\n \n ${tokenRows}\n \n
\n `\n}\n\nconst emit = ($item, item) => {\n const content = `\n \n
\n
${window.location.host}
\n ${expand(item.text)}
\n \n ${tokenForm()}\n \n \n
\n
\n \n \n
\n `\n \n $item.html(content)\n \n // Load tokens on initial render\n loadTokens($item)\n}\n\nconst loadTokens = ($item) => {\n fetch('/plugin/useraccesstokens/list')\n .then(res => {\n if (!res.ok) {\n throw new Error(`${res.status} ${res.statusText}`)\n }\n return res.json()\n })\n .then(tokens => {\n $item.find('.tokens-container').html(tokenList(tokens))\n })\n .catch(err => {\n $item.find('.tokens-container').html(error(`Failed to load tokens: ${err.message}`))\n })\n}\n\nconst createToken = ($item) => {\n const $form = $item.find('.fields')\n const name = $form.find('input[name=\"tokenName\"]').val().trim()\n const expiresInDays = $form.find('select[name=\"expiresInDays\"]').val()\n \n $item.find('.error, .success').remove()\n \n if (!name) {\n $item.find('.create-result').html(error('Token name is required'))\n return\n }\n \n const data = { name }\n if (expiresInDays) {\n data.expiresInDays = parseInt(expiresInDays)\n }\n \n fetch('/plugin/useraccesstokens/create', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data)\n })\n .then(res => {\n if (!res.ok) {\n return res.json().then(err => Promise.reject(err))\n }\n return res.json()\n })\n .then(result => {\n $item.find('.create-result').html(`\n ${success('Token created successfully!')}\n Save this token now - you won't see it again:
\n ${result.token}
\n `)\n \n // Clear form\n $form.find('input[name=\"tokenName\"]').val('')\n $form.find('select[name=\"expiresInDays\"]').val('')\n \n // Refresh token list\n loadTokens($item)\n })\n .catch(err => {\n $item.find('.create-result').html(error(err.error || 'Failed to create token'))\n })\n}\n\nconst revokeToken = ($item, tokenName) => {\n if (!confirm(`Are you sure you want to revoke the token \"${tokenName}\"?`)) {\n return\n }\n \n fetch('/plugin/useraccesstokens/revoke', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: tokenName })\n })\n .then(res => {\n if (!res.ok) {\n return res.json().then(err => Promise.reject(err))\n }\n return res.json()\n })\n .then(result => {\n $item.find('.list-result').html(success(result.message))\n loadTokens($item)\n })\n .catch(err => {\n $item.find('.list-result').html(error(err.error || 'Failed to revoke token'))\n })\n}\n\nconst deleteToken = ($item, tokenName) => {\n if (!confirm(`Are you sure you want to permanently delete the token \"${tokenName}\"? This cannot be undone.`)) {\n return\n }\n \n fetch('/plugin/useraccesstokens/delete', {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: tokenName })\n })\n .then(res => {\n if (!res.ok) {\n return res.json().then(err => Promise.reject(err))\n }\n return res.json()\n })\n .then(result => {\n $item.find('.list-result').html(success(result.message))\n loadTokens($item)\n })\n .catch(err => {\n $item.find('.list-result').html(error(err.error || 'Failed to delete token'))\n })\n}\n\nconst bind = ($item, item) => {\n // Handle create token button\n $item.on('click', '.create-token', () => {\n createToken($item)\n })\n \n // Handle refresh tokens button\n $item.on('click', '.refresh-tokens', () => {\n loadTokens($item)\n })\n \n // Handle revoke token buttons\n $item.on('click', '.revoke-token', (e) => {\n const tokenName = $(e.target).data('token-name')\n revokeToken($item, tokenName)\n })\n \n // Handle delete token buttons\n $item.on('click', '.delete-token', (e) => {\n const tokenName = $(e.target).data('token-name')\n deleteToken($item, tokenName)\n })\n \n // Handle double-click for text editing\n $item.dblclick(() => {\n return wiki.textEditor($item, item)\n })\n}\n\nif (typeof window !== 'undefined') {\n window.plugins.useraccesstokens = { emit, bind }\n}\n\nexport const useraccesstokens = typeof window == 'undefined' ? { expand } : undefined\n"],
+ "mappings": "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",
+ "names": ["expand", "text", "error", "success", "tokenForm", "tokenList", "tokens", "token", "status", "lastUsed", "expires", "emit", "$item", "item", "content", "loadTokens", "res", "err", "createToken", "$form", "name", "expiresInDays", "data", "result", "revokeToken", "tokenName", "deleteToken", "bind", "e", "useraccesstokens"]
+}
diff --git a/package-lock.json b/package-lock.json
index 4ba98eb..048eea2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"name": "wiki-plugin-useraccesstokens",
"version": "0.1.0",
"license": "MIT",
+ "dependencies": {
+ "bcrypt": "^5.1.1"
+ },
"devDependencies": {
"@eslint/js": "^9.29.0",
"esbuild": "^0.25.5",
@@ -677,6 +680,26 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+ "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.7",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -691,6 +714,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -714,6 +743,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -731,6 +772,15 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -747,6 +797,26 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/aproba": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+ "license": "ISC"
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -758,14 +828,26 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/bcrypt": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
+ "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.11",
+ "node-addon-api": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -799,6 +881,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -819,13 +910,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -845,7 +950,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -866,6 +970,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@@ -1147,6 +1272,78 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -1183,6 +1380,25 @@
"node": ">=8"
}
},
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC"
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1220,6 +1436,23 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1230,6 +1463,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1331,11 +1573,34 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -1344,11 +1609,56 @@
"node": "*"
}
},
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
@@ -1358,6 +1668,78 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-addon-api": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -1431,6 +1813,15 @@
"node": ">=8"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -1477,6 +1868,20 @@
"node": ">=6"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -1487,6 +1892,60 @@
"node": ">=4"
}
},
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1510,6 +1969,47 @@
"node": ">=8"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -1536,6 +2036,29 @@
"node": ">=8"
}
},
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -1559,6 +2082,28 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -1575,6 +2120,15 @@
"node": ">= 8"
}
},
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -1585,6 +2139,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 146b475..3264f11 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "wiki-plugin-useraccesstokens",
"version": "0.1.0",
- "description": "Add description here.",
+ "description": "Federated Wiki plugin for managing User Access Tokens (API tokens) for programmatic authentication",
"keywords": [
"wiki",
"federated wiki",
@@ -22,6 +22,9 @@
"engines": {
"node": ">=20.x"
},
+ "dependencies": {
+ "bcrypt": "^5.1.1"
+ },
"devDependencies": {
"@eslint/js": "^9.29.0",
"esbuild": "^0.25.5",
diff --git a/pages/about-useraccesstokens-plugin b/pages/about-useraccesstokens-plugin
index 08c0ce9..5982bd7 100644
--- a/pages/about-useraccesstokens-plugin
+++ b/pages/about-useraccesstokens-plugin
@@ -1,8 +1,21 @@
{
"title": "About UserAccessTokens Plugin",
"story": [
- { "type": "paragraph", "id": "03770af2dea78190", "text": "Describe the plugin here." },
- { "type": "useraccesstokens", "id": "76e9b31c71a6a853", "text": "SAMPLE Markup" }
+ {
+ "type": "paragraph",
+ "id": "03770af2dea78190",
+ "text": "The UserAccessTokens plugin allows users to create, manage, and revoke User Access Tokens for programmatic authentication with Federated Wiki. These tokens provide a secure way to authenticate API requests without requiring interactive login."
+ },
+ {
+ "type": "useraccesstokens",
+ "id": "76e9b31c71a6a853",
+ "text": "Create tokens for scripts, apps, or automated tools that need to interact with your wiki."
+ },
+ {
+ "type": "markdown",
+ "id": "263207b563ef6712",
+ "text": "**Using Your Tokens:**\n\nOnce you create a token, you can use it in API requests by including it in the Authorization header:\n\n```\nAuthorization: Bearer fwuat-your-token-here\n```\n\n**Security Notes:**\n\n• Store tokens securely - they won't be shown again after creation\n• Tokens are prefixed with `fwuat-` to identify them as User Access Tokens\n• Revoke tokens you no longer need\n• Set expiration dates for temporary access\n• Each token shows only the last 4 characters for identification"
+ }
],
"journal": [
{
diff --git a/server/server.js b/server/server.js
new file mode 100644
index 0000000..23c7c35
--- /dev/null
+++ b/server/server.js
@@ -0,0 +1,271 @@
+// useraccesstokens plugin, server-side component
+// These handlers are launched with the wiki server.
+
+import fs from 'node:fs'
+import fsp from 'node:fs/promises'
+import path from 'node:path'
+import crypto from 'node:crypto'
+import bcrypt from 'bcrypt'
+
+const SALT_ROUNDS = 12
+const TOKEN_PREFIX = 'fwuat-'
+const TOKEN_BYTES = 32
+
+class TokenManager {
+ constructor(statusPath) {
+ this.statusPath = statusPath
+ this.tokensFile = path.join(statusPath, 'user-access-tokens.json')
+ }
+
+ async ensureTokensFile() {
+ try {
+ await fsp.access(this.tokensFile)
+ } catch {
+ await fsp.writeFile(this.tokensFile, JSON.stringify([], null, 2))
+ }
+ }
+
+ async loadTokens() {
+ await this.ensureTokensFile()
+ const data = await fsp.readFile(this.tokensFile, 'utf8')
+ return JSON.parse(data)
+ }
+
+ async saveTokens(tokens) {
+ await fsp.writeFile(this.tokensFile, JSON.stringify(tokens, null, 2))
+ }
+
+ generateToken() {
+ const tokenBytes = crypto.randomBytes(TOKEN_BYTES)
+ const token = TOKEN_PREFIX + tokenBytes.toString('base64url')
+ return token
+ }
+
+ async hashToken(token) {
+ return await bcrypt.hash(token, SALT_ROUNDS)
+ }
+
+ async verifyToken(token, hash) {
+ return await bcrypt.compare(token, hash)
+ }
+
+ getDisplayHint(token) {
+ return token.slice(-4)
+ }
+
+ async createToken(user, name, expiresInDays = null) {
+ const tokens = await this.loadTokens()
+
+ // Check if token name already exists for this user
+ const existingToken = tokens.find(t => t.user === user && t.name === name && !t.revoked)
+ if (existingToken) {
+ throw new Error(`Token with name "${name}" already exists`)
+ }
+
+ const token = this.generateToken()
+ const tokenHash = await this.hashToken(token)
+ const created = new Date().toISOString()
+ const expires = expiresInDays ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString() : null
+
+ const tokenRecord = {
+ name,
+ user,
+ tokenHash,
+ displayHint: this.getDisplayHint(token),
+ created,
+ expires,
+ lastUsed: null,
+ revoked: false,
+ scopes: ['site:read', 'site:write'] // Default scopes
+ }
+
+ tokens.push(tokenRecord)
+ await this.saveTokens(tokens)
+
+ return { token, record: tokenRecord }
+ }
+
+ async listTokens(user) {
+ const tokens = await this.loadTokens()
+ return tokens
+ .filter(t => t.user === user)
+ .map(t => {
+ const { tokenHash, ...safeToken } = t
+ return safeToken
+ })
+ }
+
+ async revokeToken(user, name) {
+ const tokens = await this.loadTokens()
+ const tokenIndex = tokens.findIndex(t => t.user === user && t.name === name)
+
+ if (tokenIndex === -1) {
+ throw new Error(`Token "${name}" not found`)
+ }
+
+ tokens[tokenIndex].revoked = true
+ await this.saveTokens(tokens)
+
+ return tokens[tokenIndex]
+ }
+
+ async deleteToken(user, name) {
+ const tokens = await this.loadTokens()
+ const tokenIndex = tokens.findIndex(t => t.user === user && t.name === name)
+
+ if (tokenIndex === -1) {
+ throw new Error(`Token "${name}" not found`)
+ }
+
+ const deletedToken = tokens.splice(tokenIndex, 1)[0]
+ await this.saveTokens(tokens)
+
+ return deletedToken
+ }
+
+ async validateToken(token) {
+ if (!token.startsWith(TOKEN_PREFIX)) {
+ return null
+ }
+
+ const tokens = await this.loadTokens()
+ const now = new Date()
+
+ for (const tokenRecord of tokens) {
+ if (tokenRecord.revoked) continue
+ if (tokenRecord.expires && new Date(tokenRecord.expires) < now) continue
+
+ const isValid = await this.verifyToken(token, tokenRecord.tokenHash)
+ if (isValid) {
+ // Update last used timestamp
+ tokenRecord.lastUsed = now.toISOString()
+ await this.saveTokens(tokens)
+ return tokenRecord
+ }
+ }
+
+ return null
+ }
+}
+
+const startServer = async function (params) {
+ const { app, argv } = params
+
+ // Initialize token manager
+ const tokenManager = new TokenManager(argv.status)
+
+ // Middleware to check if user is authenticated
+ const authenticated = function (req, res, next) {
+ if (!app.securityhandler.isAuthorized(req)) {
+ return res.status(401).json({ error: 'Must be authenticated' })
+ }
+ return next()
+ }
+
+ // Middleware to get current user
+ const getCurrentUser = function (req) {
+ return app.securityhandler.getUser(req)
+ }
+
+ // API Routes
+
+ // POST /plugin/useraccesstokens/create - Create a new token
+ app.post('/plugin/useraccesstokens/create', authenticated, async (req, res) => {
+ try {
+ const user = getCurrentUser(req)
+ const { name, expiresInDays } = req.body
+
+ if (!name || typeof name !== 'string' || name.trim() === '') {
+ return res.status(400).json({ error: 'Token name is required' })
+ }
+
+ const result = await tokenManager.createToken(user, name.trim(), expiresInDays)
+
+ res.json({
+ token: result.token,
+ name: result.record.name,
+ displayHint: result.record.displayHint,
+ created: result.record.created,
+ expires: result.record.expires,
+ scopes: result.record.scopes
+ })
+ } catch (error) {
+ res.status(400).json({ error: error.message })
+ }
+ })
+
+ // GET /plugin/useraccesstokens/list - List user's tokens
+ app.get('/plugin/useraccesstokens/list', authenticated, async (req, res) => {
+ try {
+ const user = getCurrentUser(req)
+ const tokens = await tokenManager.listTokens(user)
+ res.json(tokens)
+ } catch (error) {
+ res.status(500).json({ error: error.message })
+ }
+ })
+
+ // POST /plugin/useraccesstokens/revoke - Revoke a token
+ app.post('/plugin/useraccesstokens/revoke', authenticated, async (req, res) => {
+ try {
+ const user = getCurrentUser(req)
+ const { name } = req.body
+
+ if (!name) {
+ return res.status(400).json({ error: 'Token name is required' })
+ }
+
+ await tokenManager.revokeToken(user, name)
+ res.json({ message: `Token "${name}" revoked successfully` })
+ } catch (error) {
+ res.status(400).json({ error: error.message })
+ }
+ })
+
+ // DELETE /plugin/useraccesstokens/delete - Delete a token
+ app.delete('/plugin/useraccesstokens/delete', authenticated, async (req, res) => {
+ try {
+ const user = getCurrentUser(req)
+ const { name } = req.body
+
+ if (!name) {
+ return res.status(400).json({ error: 'Token name is required' })
+ }
+
+ await tokenManager.deleteToken(user, name)
+ res.json({ message: `Token "${name}" deleted successfully` })
+ } catch (error) {
+ res.status(400).json({ error: error.message })
+ }
+ })
+
+ // Middleware for token-based authentication
+ app.use('/api', async (req, res, next) => {
+ const authHeader = req.headers.authorization
+
+ if (authHeader && authHeader.startsWith('Bearer ')) {
+ const token = authHeader.substring(7)
+
+ try {
+ const tokenRecord = await tokenManager.validateToken(token)
+ if (tokenRecord) {
+ // Set user context for token-based requests
+ req.tokenAuth = {
+ user: tokenRecord.user,
+ scopes: tokenRecord.scopes,
+ tokenName: tokenRecord.name
+ }
+ return next()
+ }
+ } catch (error) {
+ console.error('Token validation error:', error)
+ }
+ }
+
+ next()
+ })
+
+ console.log('UserAccessTokens plugin server started')
+}
+
+export { startServer, TokenManager }
diff --git a/src/client/useraccesstokens.js b/src/client/useraccesstokens.js
index e78443b..a8d4fb1 100644
--- a/src/client/useraccesstokens.js
+++ b/src/client/useraccesstokens.js
@@ -6,15 +6,231 @@ const expand = text => {
.replace(/\*(.+?)\*/g, '$1')
}
+const error = text => {
+ return `${text}
`
+}
+
+const success = text => {
+ return `${text}
`
+}
+
+const tokenForm = () => {
+ return `
+
+
+
+
+
+
+
+ `
+}
+
+const tokenList = (tokens) => {
+ if (!tokens || tokens.length === 0) {
+ return 'No tokens created yet.
'
+ }
+
+ const tokenRows = tokens.map(token => {
+ const status = token.revoked ? 'revoked' :
+ (token.expires && new Date(token.expires) < new Date()) ? 'expired' :
+ 'active'
+
+ const lastUsed = token.lastUsed ? new Date(token.lastUsed).toLocaleDateString() : 'never'
+ const expires = token.expires ? new Date(token.expires).toLocaleDateString() : 'never'
+
+ return `
+ ${expand(token.name)} (...${token.displayHint}) - ${status}
+ ${!token.revoked ? `` : ''}
+
+
created: ${new Date(token.created).toLocaleDateString()}, expires: ${expires}, last used: ${lastUsed}
+
+ `
+ }).join('')
+
+ return `
+
+ ${tokenRows}
+
+
+ `
+}
+
const emit = ($item, item) => {
- return $item.append(`
-
- ${expand(item.text)}
-
`)
+ const content = `
+
+
+
${window.location.host}
+ ${expand(item.text)}
+
+ ${tokenForm()}
+
+
+
+
+
+
+
+ `
+
+ $item.html(content)
+
+ // Load tokens on initial render
+ loadTokens($item)
+}
+
+const loadTokens = ($item) => {
+ fetch('/plugin/useraccesstokens/list')
+ .then(res => {
+ if (!res.ok) {
+ throw new Error(`${res.status} ${res.statusText}`)
+ }
+ return res.json()
+ })
+ .then(tokens => {
+ $item.find('.tokens-container').html(tokenList(tokens))
+ })
+ .catch(err => {
+ $item.find('.tokens-container').html(error(`Failed to load tokens: ${err.message}`))
+ })
+}
+
+const createToken = ($item) => {
+ const $form = $item.find('.fields')
+ const name = $form.find('input[name="tokenName"]').val().trim()
+ const expiresInDays = $form.find('select[name="expiresInDays"]').val()
+
+ $item.find('.error, .success').remove()
+
+ if (!name) {
+ $item.find('.create-result').html(error('Token name is required'))
+ return
+ }
+
+ const data = { name }
+ if (expiresInDays) {
+ data.expiresInDays = parseInt(expiresInDays)
+ }
+
+ fetch('/plugin/useraccesstokens/create', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ })
+ .then(res => {
+ if (!res.ok) {
+ return res.json().then(err => Promise.reject(err))
+ }
+ return res.json()
+ })
+ .then(result => {
+ $item.find('.create-result').html(`
+ ${success('Token created successfully!')}
+ Save this token now - you won't see it again:
+ ${result.token}
+ `)
+
+ // Clear form
+ $form.find('input[name="tokenName"]').val('')
+ $form.find('select[name="expiresInDays"]').val('')
+
+ // Refresh token list
+ loadTokens($item)
+ })
+ .catch(err => {
+ $item.find('.create-result').html(error(err.error || 'Failed to create token'))
+ })
+}
+
+const revokeToken = ($item, tokenName) => {
+ if (!confirm(`Are you sure you want to revoke the token "${tokenName}"?`)) {
+ return
+ }
+
+ fetch('/plugin/useraccesstokens/revoke', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: tokenName })
+ })
+ .then(res => {
+ if (!res.ok) {
+ return res.json().then(err => Promise.reject(err))
+ }
+ return res.json()
+ })
+ .then(result => {
+ $item.find('.list-result').html(success(result.message))
+ loadTokens($item)
+ })
+ .catch(err => {
+ $item.find('.list-result').html(error(err.error || 'Failed to revoke token'))
+ })
+}
+
+const deleteToken = ($item, tokenName) => {
+ if (!confirm(`Are you sure you want to permanently delete the token "${tokenName}"? This cannot be undone.`)) {
+ return
+ }
+
+ fetch('/plugin/useraccesstokens/delete', {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ name: tokenName })
+ })
+ .then(res => {
+ if (!res.ok) {
+ return res.json().then(err => Promise.reject(err))
+ }
+ return res.json()
+ })
+ .then(result => {
+ $item.find('.list-result').html(success(result.message))
+ loadTokens($item)
+ })
+ .catch(err => {
+ $item.find('.list-result').html(error(err.error || 'Failed to delete token'))
+ })
}
const bind = ($item, item) => {
- return $item.dblclick(() => {
+ // Handle create token button
+ $item.on('click', '.create-token', () => {
+ createToken($item)
+ })
+
+ // Handle refresh tokens button
+ $item.on('click', '.refresh-tokens', () => {
+ loadTokens($item)
+ })
+
+ // Handle revoke token buttons
+ $item.on('click', '.revoke-token', (e) => {
+ const tokenName = $(e.target).data('token-name')
+ revokeToken($item, tokenName)
+ })
+
+ // Handle delete token buttons
+ $item.on('click', '.delete-token', (e) => {
+ const tokenName = $(e.target).data('token-name')
+ deleteToken($item, tokenName)
+ })
+
+ // Handle double-click for text editing
+ $item.dblclick(() => {
return wiki.textEditor($item, item)
})
}
diff --git a/test/test.js b/test/test.js
index 042a85e..155e28b 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,12 +1,153 @@
import { useraccesstokens } from '../src/client/useraccesstokens.js'
import { suite, test } from 'node:test'
import assert from 'node:assert'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { TokenManager } from '../server/server.js'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
suite('useraccesstokens plugin', () => {
- suite('expand', () => {
+ suite('client expand', () => {
test('can escape html markup characters', () => {
const result = useraccesstokens.expand('try < & >')
assert.equal(result, 'try < & >')
})
+
+ test('can format italic text', () => {
+ const result = useraccesstokens.expand('This is *italic* text')
+ assert.equal(result, 'This is italic text')
+ })
+ })
+
+ suite('server TokenManager', async () => {
+ let tempDir
+ let tokenManager
+
+ // Setup before each test
+ const setup = async () => {
+ tempDir = path.join(__dirname, 'temp-' + Date.now())
+ await fs.mkdir(tempDir, { recursive: true })
+ tokenManager = new (class extends TokenManager {
+ constructor(statusPath) {
+ super(statusPath)
+ }
+ })(tempDir)
+ }
+
+ // Cleanup after each test
+ const cleanup = async () => {
+ if (tempDir) {
+ await fs.rm(tempDir, { recursive: true, force: true })
+ }
+ }
+
+ test('can generate tokens with correct prefix', async () => {
+ await setup()
+ try {
+ const token = tokenManager.generateToken()
+ assert(token.startsWith('fwuat-'))
+ assert(token.length > 20) // Should be reasonably long
+ } finally {
+ await cleanup()
+ }
+ })
+
+ test('can create and validate tokens', async () => {
+ await setup()
+ try {
+ const result = await tokenManager.createToken('testuser', 'test-token')
+
+ assert(result.token.startsWith('fwuat-'))
+ assert.equal(result.record.name, 'test-token')
+ assert.equal(result.record.user, 'testuser')
+ assert.equal(result.record.revoked, false)
+ assert(result.record.created)
+ assert(result.record.displayHint)
+
+ const validation = await tokenManager.validateToken(result.token)
+ assert(validation)
+ assert.equal(validation.user, 'testuser')
+ assert.equal(validation.name, 'test-token')
+ } finally {
+ await cleanup()
+ }
+ })
+
+ test('can list user tokens safely', async () => {
+ await setup()
+ try {
+ await tokenManager.createToken('user1', 'token1')
+ await tokenManager.createToken('user1', 'token2')
+ await tokenManager.createToken('user2', 'token3')
+
+ const user1Tokens = await tokenManager.listTokens('user1')
+ assert.equal(user1Tokens.length, 2)
+
+ // Verify tokenHash is not included in the response
+ user1Tokens.forEach(token => {
+ assert(!token.tokenHash)
+ assert(token.displayHint)
+ assert.equal(token.user, 'user1')
+ })
+
+ const user2Tokens = await tokenManager.listTokens('user2')
+ assert.equal(user2Tokens.length, 1)
+ } finally {
+ await cleanup()
+ }
+ })
+
+ test('can revoke tokens', async () => {
+ await setup()
+ try {
+ const result = await tokenManager.createToken('testuser', 'test-token')
+
+ // Token should work before revocation
+ let validation = await tokenManager.validateToken(result.token)
+ assert(validation)
+
+ // Revoke the token
+ await tokenManager.revokeToken('testuser', 'test-token')
+
+ // Token should not work after revocation
+ validation = await tokenManager.validateToken(result.token)
+ assert.equal(validation, null)
+ } finally {
+ await cleanup()
+ }
+ })
+
+ test('rejects duplicate token names for same user', async () => {
+ await setup()
+ try {
+ await tokenManager.createToken('testuser', 'duplicate-name')
+
+ try {
+ await tokenManager.createToken('testuser', 'duplicate-name')
+ assert.fail('Should have thrown an error for duplicate name')
+ } catch (error) {
+ assert(error.message.includes('already exists'))
+ }
+ } finally {
+ await cleanup()
+ }
+ })
+
+ test('handles token expiration', async () => {
+ await setup()
+ try {
+ // Create token that expires immediately (for testing)
+ const result = await tokenManager.createToken('testuser', 'expired-token', -1)
+
+ // Token should be expired and not validate
+ const validation = await tokenManager.validateToken(result.token)
+ assert.equal(validation, null)
+ } finally {
+ await cleanup()
+ }
+ })
})
})