create with
> wiki-plugin-useraccesstokens@0.1.0 npx > create-wiki-plugin Usage: npm create wiki-plugin <new-plugin-name> e.g. npm create wiki-plugin CoolThing and write requirements in README.
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
34
.github/workflows/check.yml
vendored
Normal file
34
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Prettier Check
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Prettier check
|
||||||
|
run: npm run prettier:check
|
||||||
|
|
||||||
|
- name: Annotate Pull Request with Results
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: '⚠️ Prettier found formatting issues. Please fix them.'
|
||||||
|
})
|
27
.github/workflows/test.yml
vendored
Normal file
27
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# Releases https://github.com/nodejs/release#release-schedule
|
||||||
|
node-version:
|
||||||
|
- 20.x # Maintenance
|
||||||
|
- 22.x # Active
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
assets
|
||||||
|
client/useraccesstokens.js
|
||||||
|
client/useraccesstokens.map
|
||||||
|
commons
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
meta-client.json
|
||||||
|
recycle
|
||||||
|
status
|
14
.npmignore
Normal file
14
.npmignore
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
coverage
|
||||||
|
scripts
|
||||||
|
src
|
||||||
|
test
|
||||||
|
meta-client.json
|
||||||
|
eslint.config.js
|
||||||
|
.github
|
||||||
|
.prettier*
|
||||||
|
.vscode
|
||||||
|
.zed
|
||||||
|
assets
|
||||||
|
commons
|
||||||
|
recycle
|
||||||
|
status
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
client/*.js
|
||||||
|
coverage
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||||
|
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
7
.zed/settings.json
Normal file
7
.zed/settings.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Folder-specific settings
|
||||||
|
//
|
||||||
|
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||||
|
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||||
|
{
|
||||||
|
"format_on_save": "on"
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <YEAR> <COPYRIGHT HOLDER>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Federated Wiki - UserAccessTokens Plugin
|
||||||
|
|
||||||
|
This plugin allows users to create, manage, and revoke User Access Tokens. User Access Tokens, also known elsewhere as "API tokens" or "personal access tokens", offer a way to authenticate users without requiring them to enter their username and password for every request. This allows for interactions with FedWiki through scripts or other applications while maintaining security.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Token generation:
|
||||||
|
- Generate cryptographically secure random tokens (32+ bytes) using `crypto` with a secure hashing algorithm (e.g., bcrypt or Argon2).
|
||||||
|
- Include a token type prefix `fwuat-` to distinguish User Access Tokens from other types of tokens in the system.
|
||||||
|
- Token storage:
|
||||||
|
- Store tokens in the file system, specifically in the site's `status` subdirectory in a file named `user-access-tokens.json`.
|
||||||
|
- Token format:
|
||||||
|
- Each token should be a JSON object with the following structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "TOKEN_NAME",
|
||||||
|
"user": "USERNAME",
|
||||||
|
"tokenHash": "UNIQUE_TOKEN_STRING",
|
||||||
|
"displayHint": "LAST_FOUR_CHARACTERS_OF_TOKEN",
|
||||||
|
"created": "DATE_STRING_ISO8601",
|
||||||
|
"expires": "DATE_STRING_ISO8601",
|
||||||
|
"lastUsed": "DATE_STRING_ISO8601",
|
||||||
|
"revoked": false, // or true
|
||||||
|
"scopes": ["site:read", "site:write"] // optional, for future use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `name`: A human-readable name for the token. This must be unique for the site.
|
||||||
|
- `user`: The username of the user who created the token.
|
||||||
|
- `tokenHash`: A unique, securely generated token string stored in a hashed format using a secure hashing algorithm (e.g., bcrypt or Argon2).
|
||||||
|
- `displayHint`: The last four characters of the token, used for display purposes. Helps users identify tokens without revealing the full token.
|
||||||
|
- `created`: The date and time when the token was created, in ISO 8601 format.
|
||||||
|
- `expires`: The date and time when the token will expire, in ISO 8601 format. If not set, the token does not expire.
|
||||||
|
- `lastUsed`: The date and time when the token was last used, in ISO 8601 format.
|
||||||
|
- `revoked`: A boolean indicating whether the token has been revoked.
|
||||||
|
- `scopes`: An array of strings representing the scopes assigned to the token. If empty, the token has full access. This is a placeholder for future enhancements.
|
||||||
|
- Token management:
|
||||||
|
- Provide UI and API endpoints for creating, listing, revoking, and deleting tokens.
|
||||||
|
- Return the full token only once (upon creation) to the user. Make sure to inform the user to store it securely, as it will not be retrievable later.
|
||||||
|
- Tokens will not work if they are expired, revoked, or deleted.
|
||||||
|
- The user may GET a token or list of tokens, however, the hashed token string should be stripped from the response to prevent accidental exposure.
|
||||||
|
- Token usage:
|
||||||
|
- Tokens should be included in the `Authorization` HTTP header of API requests, using the `Bearer` scheme.
|
||||||
|
- Token validation and security:
|
||||||
|
- Check only tokens with the `fwuat-` prefix.
|
||||||
|
- Tokens should be treated as sensitive information. They should not be logged or exposed in any way.
|
||||||
|
- Implement rate limiting to prevent abuse of the API using tokens. This is necessary because tokens can be used without user interaction.
|
||||||
|
- Tokens should only be transmitted over secure connections (HTTPS) to prevent interception.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
`npm install`
|
||||||
|
`npm run build`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import globals from 'globals'
|
||||||
|
import pluginJs from '@eslint/js'
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
|
export default [
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ ignores: ['client/*'] },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
wiki: 'readonly',
|
||||||
|
...globals.browser,
|
||||||
|
...globals.jquery,
|
||||||
|
...globals.mocha,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
1602
package-lock.json
generated
Normal file
1602
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "wiki-plugin-useraccesstokens",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Add description here.",
|
||||||
|
"keywords": [
|
||||||
|
"wiki",
|
||||||
|
"federated wiki",
|
||||||
|
"plugin"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run clean; npm run test; node --no-warnings scripts/build-client.js",
|
||||||
|
"about": "wiki -p 3010 -d . --security_legacy",
|
||||||
|
"clean": "rm client/useraccesstokens.js client/useraccesstokens.js.map",
|
||||||
|
"clean-about": "rm -r assets commons recycle status",
|
||||||
|
"prettier:format": "prettier --write './**/*.js'",
|
||||||
|
"prettier:check": "prettier --check ./**/*.js",
|
||||||
|
"test": "node --test"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.x"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.29.0",
|
||||||
|
"esbuild": "^0.25.5",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"globals": "^16.2.0",
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
|
}
|
20
pages/about-useraccesstokens-plugin
Normal file
20
pages/about-useraccesstokens-plugin
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"title": "About UserAccessTokens Plugin",
|
||||||
|
"story": [
|
||||||
|
{ "type": "paragraph", "id": "03770af2dea78190", "text": "Describe the plugin here." },
|
||||||
|
{ "type": "useraccesstokens", "id": "76e9b31c71a6a853", "text": "SAMPLE Markup" }
|
||||||
|
],
|
||||||
|
"journal": [
|
||||||
|
{
|
||||||
|
"type": "create",
|
||||||
|
"item": {
|
||||||
|
"title": "About UserAccessTokens Plugin",
|
||||||
|
"story": [
|
||||||
|
{ "type": "paragraph", "id": "03770af2dea78190", "text": "Describe the plugin here." },
|
||||||
|
{ "type": "useraccesstokens", "id": "76e9b31c71a6a853" , "text":"SAMPLE Markup" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"date": 1750459617960
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
scripts/build-client.js
Normal file
22
scripts/build-client.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import packJSON from '../package.json' with { type: 'json' }
|
||||||
|
|
||||||
|
const version = packJSON.version
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
let results = await esbuild.build({
|
||||||
|
entryPoints: ['src/client/useraccesstokens.js'],
|
||||||
|
bundle: true,
|
||||||
|
banner: {
|
||||||
|
js: `/* wiki-plugin-useraccesstokens - ${version} - ${now.toUTCString()} */`,
|
||||||
|
},
|
||||||
|
minify: true,
|
||||||
|
sourcemap: true,
|
||||||
|
logLevel: 'info',
|
||||||
|
metafile: true,
|
||||||
|
outfile: 'client/useraccesstokens.js',
|
||||||
|
})
|
||||||
|
|
||||||
|
await fs.writeFile('meta-client.json', JSON.stringify(results.metafile))
|
||||||
|
console.log("\n esbuild metadata written to 'meta-client.json'.")
|
15
scripts/update-authors.cjs
Normal file
15
scripts/update-authors.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const gitAuthors = require('grunt-git-authors')
|
||||||
|
|
||||||
|
gitAuthors.updatePackageJson({ order: 'date' }, error => {
|
||||||
|
if (error) {
|
||||||
|
console.log('Error: ', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gitAuthors.updateAuthors((error, filename) => {
|
||||||
|
if (error) {
|
||||||
|
console.log('Error: ', error)
|
||||||
|
} else {
|
||||||
|
console.log(filename, 'updated')
|
||||||
|
}
|
||||||
|
})
|
26
src/client/useraccesstokens.js
Normal file
26
src/client/useraccesstokens.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const expand = text => {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\*(.+?)\*/g, '<i>$1</i>')
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = ($item, item) => {
|
||||||
|
return $item.append(`
|
||||||
|
<p style="background-color:#eee;padding:15px;">
|
||||||
|
${expand(item.text)}
|
||||||
|
</p>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bind = ($item, item) => {
|
||||||
|
return $item.dblclick(() => {
|
||||||
|
return wiki.textEditor($item, item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.plugins.useraccesstokens = { emit, bind }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useraccesstokens = typeof window == 'undefined' ? { expand } : undefined
|
12
test/test.js
Normal file
12
test/test.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useraccesstokens } from '../src/client/useraccesstokens.js'
|
||||||
|
import { suite, test } from 'node:test'
|
||||||
|
import assert from 'node:assert'
|
||||||
|
|
||||||
|
suite('useraccesstokens plugin', () => {
|
||||||
|
suite('expand', () => {
|
||||||
|
test('can escape html markup characters', () => {
|
||||||
|
const result = useraccesstokens.expand('try < & >')
|
||||||
|
assert.equal(result, 'try < & >')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user