import jwt from 'jsonwebtoken'; import crypto from 'crypto'; export function parseExpiryToSeconds(expiry: string): number { const match = expiry.match(/^(\d+)([smhd])$/); if (!match) return 15 * 60; const [, num, unit] = match; const n = parseInt(num, 10); switch (unit) { case 's': return n; case 'm': return n * 60; case 'h': return n * 3600; case 'd': return n * 86400; default: return 15 * 60; } } function requireSecret(name: string): string { const value = process.env[name]; if (!value) { throw new Error( `FATAL: JWT secrets not configured — missing ${name}.\n` + 'Set JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, JWT_SUPERADMIN_SECRET in environment variables.' ); } return value; } const ACCESS_TOKEN_SECRET = requireSecret('JWT_ACCESS_SECRET'); const REFRESH_TOKEN_SECRET = requireSecret('JWT_REFRESH_SECRET'); const SUPERADMIN_TOKEN_SECRET = requireSecret('JWT_SUPERADMIN_SECRET'); const BOT_TOKEN_SECRET = process.env.JWT_BOT_SECRET || ACCESS_TOKEN_SECRET; const ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_EXPIRES || '30d'; const REFRESH_TOKEN_EXPIRY_REMEMBER = process.env.JWT_REFRESH_EXPIRES_REMEMBER || '90d'; const REFRESH_TOKEN_EXPIRY_SESSION = process.env.JWT_REFRESH_EXPIRES_SESSION || '30d'; const SUPERADMIN_TOKEN_EXPIRY = process.env.JWT_SUPERADMIN_EXPIRES || '1h'; const BOT_TOKEN_EXPIRY = process.env.JWT_BOT_EXPIRES || '10m'; const BOT_LOGIN_TOKEN_EXPIRY = process.env.JWT_BOT_LOGIN_EXPIRES || '30d'; const BOT_LOGIN_REFRESH_EXPIRY = process.env.JWT_BOT_LOGIN_REFRESH_EXPIRES || '90d'; export interface TokenPayload { userId: number; organizationId: number; appRole: string; role?: string; // legacy fallback for old tokens } export interface BotServiceTokenPayload { botId: number; organizationId: number; type: 'bot_service'; } // Payload access-токена бота, выданного через POST /api/bot/auth/login. // Отдельный тип (не пользовательский TokenPayload): такой токен НЕ проходит // verifyAccessToken (другой audience) и отклоняется authenticateToken. export interface BotLoginTokenPayload { botId: number; organizationId: number; type: 'bot_login'; } // Payload refresh-токена бота (хранится в bot_sessions, для будущего refresh-эндпоинта). // Отдельный type, чтобы refresh нельзя было использовать как access. export interface BotLoginRefreshTokenPayload { botId: number; organizationId: number; type: 'bot_login_refresh'; } export interface SuperAdminTokenPayload { superAdminId: number; email: string; isSuperAdmin: true; } export interface TokenPair { accessToken: string; refreshToken: string; expiresIn: number; familyId?: string; } export function generateTokens(payload: TokenPayload, remember: boolean = false, familyId?: string): TokenPair { // Ensure we don't include legacy role in new tokens const { role, ...cleanPayload } = payload as any; const accessToken = jwt.sign(cleanPayload, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-users' }); const refreshToken = jwt.sign(cleanPayload, REFRESH_TOKEN_SECRET, { expiresIn: (remember ? REFRESH_TOKEN_EXPIRY_REMEMBER : REFRESH_TOKEN_EXPIRY_SESSION) as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-users' }); return { accessToken, refreshToken, expiresIn: parseExpiryToSeconds(ACCESS_TOKEN_EXPIRY), familyId }; } export function verifyAccessToken(token: string): TokenPayload { const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET, { algorithms: ['HS256'], issuer: 'workflow-system', audience: 'workflow-users', }) as TokenPayload; // Fallback for old tokens that used 'role' instead of 'appRole' if (!decoded.appRole && decoded.role) { decoded.appRole = decoded.role; } return decoded; } export function verifyRefreshToken(token: string): TokenPayload { const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET, { algorithms: ['HS256'], issuer: 'workflow-system', audience: 'workflow-users', }) as TokenPayload; // Fallback for old tokens that used 'role' instead of 'appRole' if (!decoded.appRole && decoded.role) { decoded.appRole = decoded.role; } return decoded; } export function generateVerificationToken(): string { return crypto.randomBytes(32).toString('hex'); } export function generateResetToken(): string { return crypto.randomBytes(32).toString('hex'); } export function generateBotServiceToken(botId: number, organizationId: number): string { const payload: BotServiceTokenPayload = { botId, organizationId, type: 'bot_service' }; return jwt.sign(payload, BOT_TOKEN_SECRET, { expiresIn: BOT_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-bots' }); } export function verifyBotServiceToken(token: string): BotServiceTokenPayload { const payload = jwt.verify(token, BOT_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-bots' }) as BotServiceTokenPayload; if (payload.type !== 'bot_service') { throw new Error('Invalid token type'); } return payload; } // ── Bot login tokens (POST /api/bot/auth/login) ────────────────────────────── // Access/refresh пара для ботов. Секрет и audience общие с bot_service, // но type отдельный — verifyBotLoginToken принимает только 'bot_login'. export function generateBotLoginToken(botId: number, organizationId: number): string { const payload: BotLoginTokenPayload = { botId, organizationId, type: 'bot_login' }; return jwt.sign(payload, BOT_TOKEN_SECRET, { expiresIn: BOT_LOGIN_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-bots' }); } export function generateBotLoginRefreshToken(botId: number, organizationId: number): string { const payload: BotLoginRefreshTokenPayload = { botId, organizationId, type: 'bot_login_refresh' }; return jwt.sign(payload, BOT_TOKEN_SECRET, { expiresIn: BOT_LOGIN_REFRESH_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-bots' }); } // Пара access+refresh для bot login — формат ответа совпадает с TokenPair. export function generateBotLoginTokens(botId: number, organizationId: number): TokenPair { return { accessToken: generateBotLoginToken(botId, organizationId), refreshToken: generateBotLoginRefreshToken(botId, organizationId), expiresIn: parseExpiryToSeconds(BOT_LOGIN_TOKEN_EXPIRY), }; } export function verifyBotLoginToken(token: string): BotLoginTokenPayload { const payload = jwt.verify(token, BOT_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-bots' }) as BotLoginTokenPayload; if (payload.type !== 'bot_login') { throw new Error('Invalid token type'); } return payload; } export function verifyBotLoginRefreshToken(token: string): BotLoginRefreshTokenPayload { const payload = jwt.verify(token, BOT_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-bots' }) as BotLoginRefreshTokenPayload; if (payload.type !== 'bot_login_refresh') { throw new Error('Invalid token type'); } return payload; } export function generateSuperAdminToken(payload: SuperAdminTokenPayload): string { return jwt.sign(payload, SUPERADMIN_TOKEN_SECRET, { expiresIn: SUPERADMIN_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-superadmin' }); } export function verifySuperAdminToken(token: string): SuperAdminTokenPayload { const payload = jwt.verify(token, SUPERADMIN_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-superadmin' }) as SuperAdminTokenPayload; if (!payload.isSuperAdmin) { throw new Error('Invalid super admin token'); } return payload; }