156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
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<string, IStoredApiToken>();
|
|
|
|
constructor(private storageManager: StorageManager) {}
|
|
|
|
public async initialize(): Promise<void> {
|
|
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<IStoredApiToken | null> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<void> {
|
|
const keys = await this.storageManager.list(TOKENS_PREFIX);
|
|
for (const key of keys) {
|
|
if (!key.endsWith('.json')) continue;
|
|
const stored = await this.storageManager.getJSON<IStoredApiToken>(key);
|
|
if (stored?.id) {
|
|
this.tokens.set(stored.id, stored);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async persistToken(stored: IStoredApiToken): Promise<void> {
|
|
await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored);
|
|
}
|
|
}
|