import crypto from 'node:crypto'; import type { Request } from 'express'; export interface AuthConfig { login: string; password: string; ttlMs: number; secret?: string; } interface TokenPayload { sub: string; exp: number; } export class AuthService { private readonly secret: string; constructor(private readonly config: AuthConfig) { this.secret = config.secret ?? `${config.login}:${config.password}`; } login(login: string, password: string) { if (!safeEqual(login, this.config.login) || !safeEqual(password, this.config.password)) { throw new Error('Wrong panel credentials.'); } const payload: TokenPayload = { sub: this.config.login, exp: Date.now() + this.config.ttlMs, }; const encoded = encodeBase64Url(JSON.stringify(payload)); const signature = this.sign(encoded); return { token: `${encoded}.${signature}`, expiresAt: new Date(payload.exp).toISOString(), ttlMs: this.config.ttlMs, }; } verify(token: string): TokenPayload | null { const [encoded, signature] = token.split('.'); if (!encoded || !signature || !safeEqual(this.sign(encoded), signature)) { return null; } try { const payload = JSON.parse(decodeBase64Url(encoded)) as TokenPayload; if (payload.sub !== this.config.login || !Number.isFinite(payload.exp) || payload.exp <= Date.now()) { return null; } return payload; } catch { return null; } } extractBearerToken(request: Request): string | null { const header = request.header('authorization'); if (!header) { return null; } const [scheme, token] = header.split(' '); if (!scheme || !token || scheme.toLowerCase() !== 'bearer') { return null; } return token; } unauthorizedError() { return { error: 'Panel authorization required.' }; } invalidTokenError() { return { error: 'Panel session is missing or expired.' }; } private sign(value: string): string { return hmac(value, this.secret); } } function safeEqual(left: string, right: string): boolean { const leftBuffer = Buffer.from(left); const rightBuffer = Buffer.from(right); if (leftBuffer.length !== rightBuffer.length) { return false; } return crypto.timingSafeEqual(leftBuffer, rightBuffer); } function encodeBase64Url(value: string): string { return Buffer.from(value, 'utf8').toString('base64url'); } function decodeBase64Url(value: string): string { return Buffer.from(value, 'base64url').toString('utf8'); } function hmac(value: string, secret: string): string { return crypto.createHmac('sha256', secret).update(value).digest('base64url'); }