- Add admin-next/: full Next.js + Prisma + shadcn admin panel (45 API routes, 76 components) - docker-compose: tg_shop_admin service (port 3000, shared db/shop.db, prisma), bot talks to it via ADMIN_CHAT_URL=http://tg_shop_admin:3000/api/chat - tor-proxy now proxies onion admin to tg_shop_admin:3000 (new panel) - chatbotService: ADMIN_CHAT_URL default = http://tg_shop_admin:3000/api/chat (removed localhost:3100 anachronism) - .env admin secrets gitignored (admin-next/.env)
70 lines
1.9 KiB
TypeScript
Executable File
70 lines
1.9 KiB
TypeScript
Executable File
import crypto from 'crypto';
|
|
|
|
const ADMIN_SECRET = process.env.ADMIN_SECRET || 'changeme';
|
|
const SUPER_ADMIN_SECRET = process.env.SUPER_ADMIN_SECRET || '';
|
|
|
|
export type AdminRole = 'admin' | 'super_admin';
|
|
|
|
export interface AdminPayload {
|
|
role: AdminRole;
|
|
jti: string;
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
|
|
export function createToken(secret: string): string {
|
|
const isSuper = SUPER_ADMIN_SECRET && secret === SUPER_ADMIN_SECRET;
|
|
const isAdmin = secret === ADMIN_SECRET;
|
|
|
|
if (!isAdmin && !isSuper) return '';
|
|
|
|
// If no super secret set, all admins are super
|
|
const role: AdminRole = (!SUPER_ADMIN_SECRET || isSuper) ? 'super_admin' : 'admin';
|
|
|
|
const payload: AdminPayload = {
|
|
role,
|
|
jti: crypto.randomUUID(),
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
};
|
|
|
|
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
const signingKey = SUPER_ADMIN_SECRET || ADMIN_SECRET;
|
|
const sig = crypto
|
|
.createHmac('sha256', signingKey)
|
|
.update(payloadB64)
|
|
.digest('base64url');
|
|
|
|
return `${payloadB64}.${sig}`;
|
|
}
|
|
|
|
export function verifyToken(token: string): AdminPayload | null {
|
|
try {
|
|
const [payloadB64, sig] = token.split('.');
|
|
if (!payloadB64 || !sig) return null;
|
|
|
|
const signingKey = SUPER_ADMIN_SECRET || ADMIN_SECRET;
|
|
const expected = crypto
|
|
.createHmac('sha256', signingKey)
|
|
.update(payloadB64)
|
|
.digest('base64url');
|
|
|
|
if (sig !== expected) return null;
|
|
|
|
const payload: AdminPayload = JSON.parse(
|
|
Buffer.from(payloadB64, 'base64url').toString()
|
|
);
|
|
|
|
if (payload.exp < Math.floor(Date.now() / 1000)) return null;
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function verifyReAuth(token: string): boolean {
|
|
const isSuper = SUPER_ADMIN_SECRET && token === SUPER_ADMIN_SECRET;
|
|
const isAdmin = token === ADMIN_SECRET;
|
|
return isSuper || isAdmin;
|
|
}
|