mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
4322c3af90
Add sessions_invalidated_at to users (migration + Prisma schema) to support immediate session revocation. Set sessionsInvalidatedAt when an admin revokes a user's login token and enforce revocation checks in auth middleware and the refresh endpoint (compare token iat against sessionsInvalidatedAt). Add optional iat fields to JWT payload types. Scrub token from browser history after token-login. Consolidate rate-limiting logic into src/lib/utils/rateLimit.ts (rename/merge previous auth/apiToken rate limiter implementations), remove the old apiTokenRateLimit.ts, and update imports and tests to use the new module.
123 lines
3.1 KiB
TypeScript
123 lines
3.1 KiB
TypeScript
/**
|
|
* Component: JWT Token Utilities
|
|
* Documentation: documentation/backend/services/auth.md
|
|
*/
|
|
|
|
import jwt from 'jsonwebtoken';
|
|
import { RMABLogger } from './logger';
|
|
|
|
const logger = RMABLogger.create('JWT');
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'change-this-to-a-random-secret-key';
|
|
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'change-this-to-another-random-secret-key';
|
|
const JWT_DOWNLOAD_SECRET = process.env.JWT_DOWNLOAD_SECRET || JWT_SECRET + '-download';
|
|
|
|
const ACCESS_TOKEN_EXPIRY = '1h'; // 1 hour
|
|
const REFRESH_TOKEN_EXPIRY = '7d'; // 7 days
|
|
|
|
export interface TokenPayload {
|
|
sub: string; // User ID
|
|
plexId: string;
|
|
username: string;
|
|
role: string;
|
|
iat?: number; // Issued-at (auto-set by jsonwebtoken)
|
|
}
|
|
|
|
export interface RefreshTokenPayload {
|
|
sub: string;
|
|
type: 'refresh';
|
|
iat?: number; // Issued-at (auto-set by jsonwebtoken)
|
|
}
|
|
|
|
/**
|
|
* Generate access token (short-lived)
|
|
*/
|
|
export function generateAccessToken(payload: TokenPayload): string {
|
|
return jwt.sign(payload, JWT_SECRET, {
|
|
expiresIn: ACCESS_TOKEN_EXPIRY,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate refresh token (long-lived)
|
|
*/
|
|
export function generateRefreshToken(userId: string): string {
|
|
const payload: RefreshTokenPayload = {
|
|
sub: userId,
|
|
type: 'refresh',
|
|
};
|
|
|
|
return jwt.sign(payload, JWT_REFRESH_SECRET, {
|
|
expiresIn: REFRESH_TOKEN_EXPIRY,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify access token
|
|
*/
|
|
export function verifyAccessToken(token: string): TokenPayload | null {
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET) as TokenPayload;
|
|
return decoded;
|
|
} catch (error) {
|
|
logger.error('Access token verification failed', { error: error instanceof Error ? error.message : String(error) });
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify refresh token
|
|
*/
|
|
export function verifyRefreshToken(token: string): RefreshTokenPayload | null {
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_REFRESH_SECRET) as RefreshTokenPayload;
|
|
if (decoded.type !== 'refresh') {
|
|
return null;
|
|
}
|
|
return decoded;
|
|
} catch (error) {
|
|
logger.error('Refresh token verification failed', { error: error instanceof Error ? error.message : String(error) });
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const DOWNLOAD_TOKEN_EXPIRY = '30d';
|
|
|
|
export interface DownloadTokenPayload {
|
|
sub: string; // userId
|
|
requestId: string;
|
|
type: 'download';
|
|
}
|
|
|
|
/**
|
|
* Generate download token (30-day, stateless, URL-embeddable)
|
|
*/
|
|
export function generateDownloadToken(userId: string, requestId: string): string {
|
|
const payload: DownloadTokenPayload = { sub: userId, requestId, type: 'download' };
|
|
return jwt.sign(payload, JWT_DOWNLOAD_SECRET, { expiresIn: DOWNLOAD_TOKEN_EXPIRY });
|
|
}
|
|
|
|
/**
|
|
* Verify download token
|
|
*/
|
|
export function verifyDownloadToken(token: string): DownloadTokenPayload | null {
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_DOWNLOAD_SECRET) as DownloadTokenPayload;
|
|
if (decoded.type !== 'download') return null;
|
|
return decoded;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode token without verification (for debugging)
|
|
*/
|
|
export function decodeToken(token: string): any {
|
|
try {
|
|
return jwt.decode(token);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|