import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import type { StorageManager } from '../storage/index.js'; import type { IStoredApiToken, IApiTokenInfo, TApiTokenScope, } from '../../ts_interfaces/data/route-management.js'; const TOKENS_PREFIX = '/config-api/tokens/'; const TOKEN_PREFIX_STR = 'dcr_'; export class ApiTokenManager { private tokens = new Map(); constructor(private storageManager: StorageManager) {} public async initialize(): Promise { await this.loadTokens(); if (this.tokens.size > 0) { logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`); } } // ========================================================================= // Token lifecycle // ========================================================================= /** * Create a new API token. Returns the raw token value (shown once). */ public async createToken( name: string, scopes: TApiTokenScope[], expiresInDays: number | null, createdBy: string, ): Promise<{ id: string; rawToken: string }> { const id = plugins.uuid.v4(); const randomBytes = plugins.crypto.randomBytes(32); const rawPayload = `${id}:${randomBytes.toString('base64url')}`; const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`; const tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex'); const now = Date.now(); const stored: IStoredApiToken = { id, name, tokenHash, scopes, createdAt: now, expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null, lastUsedAt: null, createdBy, enabled: true, }; this.tokens.set(id, stored); await this.persistToken(stored); logger.log('info', `API token '${name}' created (id: ${id})`); return { id, rawToken }; } /** * Validate a raw token string. Returns the stored token if valid, null otherwise. * Also updates lastUsedAt. */ public async validateToken(rawToken: string): Promise { if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null; const hash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex'); for (const stored of this.tokens.values()) { if (stored.tokenHash === hash) { if (!stored.enabled) return null; if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null; // Update lastUsedAt (fire and forget) stored.lastUsedAt = Date.now(); this.persistToken(stored).catch(() => {}); return stored; } } return null; } /** * Check if a token has a specific scope. */ public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean { return token.scopes.includes(scope); } /** * List all tokens (safe info only, no hashes). */ public listTokens(): IApiTokenInfo[] { const result: IApiTokenInfo[] = []; for (const stored of this.tokens.values()) { result.push({ id: stored.id, name: stored.name, scopes: stored.scopes, createdAt: stored.createdAt, expiresAt: stored.expiresAt, lastUsedAt: stored.lastUsedAt, enabled: stored.enabled, }); } return result; } /** * Revoke (delete) a token. */ public async revokeToken(id: string): Promise { if (!this.tokens.has(id)) return false; const token = this.tokens.get(id)!; this.tokens.delete(id); await this.storageManager.delete(`${TOKENS_PREFIX}${id}.json`); logger.log('info', `API token '${token.name}' revoked (id: ${id})`); return true; } /** * Enable or disable a token. */ public async toggleToken(id: string, enabled: boolean): Promise { const stored = this.tokens.get(id); if (!stored) return false; stored.enabled = enabled; await this.persistToken(stored); logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`); return true; } // ========================================================================= // Private // ========================================================================= private async loadTokens(): Promise { const keys = await this.storageManager.list(TOKENS_PREFIX); for (const key of keys) { if (!key.endsWith('.json')) continue; const stored = await this.storageManager.getJSON(key); if (stored?.id) { this.tokens.set(stored.id, stored); } } } private async persistToken(stored: IStoredApiToken): Promise { await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored); } }