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

Loading tokens...

\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()} + +
+
+

Loading tokens...

+
+
+

+
+
+ ` + + $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() + } + }) }) })