Files
wiki-plugin-useraccesstokens/server/server.js
Christian Galo 73109c42a0
Some checks failed
CI / build (20.x) (push) Has been cancelled
CI / build (22.x) (push) Has been cancelled
Make it work.
2025-07-20 04:26:50 -05:00

155 lines
4.0 KiB
JavaScript

// useraccesstokens plugin, TokenManager class
// Provides token management functionality for the security enhancer
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 => JSON.stringify(t.user) === JSON.stringify(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()
const filteredTokens = tokens.filter(t => {
return JSON.stringify(t.user) === JSON.stringify(user)
})
return filteredTokens.map(t => {
const { tokenHash, ...safeToken } = t
return safeToken
})
}
async revokeToken(user, name) {
const tokens = await this.loadTokens()
const tokenIndex = tokens.findIndex(t => JSON.stringify(t.user) === JSON.stringify(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 => JSON.stringify(t.user) === JSON.stringify(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
}
}
export { TokenManager }