mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-04 05:10:11 +00:00
e1629ce516
- 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
121 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
}
|