mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
a50fbc721e
Introduce a shared useApiTokens hook to centralize API token CRUD and UI state (fetch, create, delete, copy, formatting). Refactor ApiTab and ApiTokensSection to consume the hook and remove duplicated logic. Add getInstanceUrl utility for client origin used in curl examples. Include an id alias in TokenPayload and add id into generated JWTs across auth routes and providers; update tests accordingly. Improve auth middleware typing and add debug logging around lastUsedAt updates. Add admin logging when creating a token with a role that differs from the target user's role.
122 lines
3.1 KiB
TypeScript
122 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
|
|
id: string; // User ID (alias for sub, used by req.user.id throughout the codebase)
|
|
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;
|
|
}
|
|
}
|