AI Implementation feature(871): Admin Area System: Database Backup/Restore and Danger Zone (#61)

This commit was merged in pull request #61.
This commit is contained in:
2026-07-23 12:10:41 +00:00
parent 6bac67fad7
commit 470ddd30c3
42 changed files with 3996 additions and 55 deletions
@@ -0,0 +1,136 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import * as crypto from 'crypto';
import { AdminOperationTokenEntity, AdminOperationKind } from '../../../database/entities/admin-operation-token.entity';
import { ApiError } from '../../../common/errors/api-error';
import { ERROR_CODES } from '../../../common/errors/error-codes';
export interface IssueTokenInput {
userId: string;
operation: AdminOperationKind;
}
export interface IssueTokenResult {
token: string;
expiresAt: string;
id: string;
}
export interface ConsumeTokenInput {
userId: string;
operation: AdminOperationKind;
token: string;
stagingId?: string;
}
@Injectable()
export class ConfirmationTokenService {
private readonly logger = new Logger(ConfirmationTokenService.name);
private readonly ttlMs: number;
constructor(
config: ConfigService,
@InjectRepository(AdminOperationTokenEntity)
private readonly repo: Repository<AdminOperationTokenEntity>,
private readonly dataSource: DataSource,
) {
this.ttlMs = config.get<number>('SYSTEM_OP_CONFIRM_TOKEN_TTL_MS', 5 * 60_000);
}
/**
* Issue a cryptographically random single-use token for the supplied
* administrator and operation. Only the SHA-256 hash is persisted; raw
* tokens are returned exactly once and never logged.
*/
async issue(input: IssueTokenInput): Promise<IssueTokenResult> {
if (!input.userId) throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_INVALID, 'Missing user', 400);
if ((input.operation as string) === undefined) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_INVALID, 'Missing operation', 400);
}
const raw = crypto.randomBytes(32).toString('base64url');
const hash = ConfirmationTokenService.hash(raw);
const now = new Date();
const expiresAt = new Date(now.getTime() + this.ttlMs);
const id = crypto.randomUUID();
await this.repo.insert({
id,
userId: input.userId,
operation: input.operation,
tokenHash: hash,
issuedAt: now.toISOString(),
expiresAt: expiresAt.toISOString(),
consumedAt: null,
});
return { token: raw, expiresAt: expiresAt.toISOString(), id };
}
/**
* Atomically consume a token: only one caller may succeed. Tokens that
* don't match (unknown, wrong user, wrong operation, wrong stage,
* expired, or previously consumed) produce distinct stable codes.
*/
async consume(input: ConsumeTokenInput): Promise<{ id: string }> {
if (!input || !input.token || !input.userId) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_INVALID, 'Confirmation token required', 400);
}
const hash = ConfirmationTokenService.hash(input.token);
return this.dataSource.transaction(async (manager) => {
const repo = manager.getRepository(AdminOperationTokenEntity);
const row = await repo.findOne({ where: { tokenHash: hash } });
if (!row) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_INVALID, 'Unknown confirmation token', 400);
}
if (row.userId !== input.userId) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_MISMATCH, 'Token was not issued to this administrator', 400);
}
if (row.operation !== input.operation) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_MISMATCH, 'Token was not issued for this operation', 400);
}
if (row.consumedAt) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_REUSED, 'Confirmation token already used', 400);
}
const expiryMs = new Date(row.expiresAt).getTime();
if (!Number.isFinite(expiryMs) || expiryMs < Date.now()) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_EXPIRED, 'Confirmation token expired', 400);
}
const consumedAt = new Date().toISOString();
const result = await repo
.createQueryBuilder()
.update(AdminOperationTokenEntity)
.set({ consumedAt })
.where('id = :id', { id: row.id })
.andWhere('consumedAt IS NULL')
.execute();
if (!result.affected) {
throw new ApiError(ERROR_CODES.SYSTEM_TOKEN_REUSED, 'Confirmation token already used', 400);
}
return { id: row.id };
});
}
/**
* Best-effort expiry sweep. Safe to call frequently.
*/
async purgeExpired(): Promise<number> {
const result = await this.repo
.createQueryBuilder()
.delete()
.from(AdminOperationTokenEntity)
.where('expiresAt < :now', { now: new Date().toISOString() })
.orWhere('consumedAt IS NOT NULL AND issued_at < :old', { old: new Date(Date.now() - 24 * 3600_000).toISOString() })
.execute()
.catch(() => ({ affected: 0 } as any));
return result.affected ?? 0;
}
/**
* Constant-time SHA-256 hashing of a raw token string. Tokens are stored
* only as their hash so a leaked database row alone cannot be used as a
* bearer credential.
*/
static hash(raw: string): string {
return crypto.createHash('sha256').update(raw).digest('hex');
}
}