Files
ReadMeABook/src/lib/utils/jwt.ts
T
razzamatazm e1629ce516 Address PR review: dedicated download secret, shared constants, strip filePath, streaming zip
- jwt.ts: Use JWT_DOWNLOAD_SECRET instead of JWT_SECRET for download tokens
- audio-formats.ts: Add EBOOK_EXTENSIONS export alongside AUDIO_EXTENSIONS
- request-statuses.ts: New shared COMPLETED_STATUSES constant used across requests API, download route, and RequestCard
- requests/route.ts: Import COMPLETED_STATUSES; strip filePath from audiobook in API response
- download/route.ts: Import format/status constants; add archiver dependency and replace adm-zip with streaming archiver for multi-file zips
- RequestCard.tsx: Use shared COMPLETED_STATUSES constant
2026-02-26 16:20:37 -08:00

121 lines
3.0 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;
}
export interface RefreshTokenPayload {
sub: string;
type: 'refresh';
}
/**
* 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;
}
}