Files
iistwin/server/utils/jwt.ts
Ильяс Султанов af8920f920 fix(auth): не выкидывать пользователя при сетевой ошибке refresh, access token 30 дней
- queryClient: refreshSession возвращает reason (network/unauthorized);
  при network-ошибке не делаем logout, а бросаем network_error_during_refresh.
- useAuth: checkAuth и refreshUser не сбрасывают сессию при network-ошибке
  во время refresh, переводят в офлайн-режим.
- auth.service: remember берётся из сессии, race tolerance 5 минут.
- access token lifetime унифицирован до 30 дней по умолчанию, cookie maxAge
  теперь совпадает с JWT expiry (было 15 минут fallback).
2026-07-10 10:04:12 +03:00

170 lines
5.0 KiB
TypeScript

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';
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';
}
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;
}
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;
}