Files
memory-infrastructure-palace/code/websites/pokedex.online/server/utils/jwt-utils.js

137 lines
3.7 KiB
JavaScript

/**
* JWT Authentication Utilities
*
* Provides functions for creating, validating, and decoding JWT tokens
* Used by both backend OAuth proxy and frontend storage/validation
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Try to use native crypto, fallback to import
let jwt;
try {
jwt = await import('jsonwebtoken');
} catch (err) {
console.warn('jsonwebtoken not installed, using mock implementation');
jwt = createMockJWT();
}
function createMockJWT() {
return {
sign: (payload, secret, options) => {
// Mock JWT: base64(header).base64(payload).base64(signature)
const header = Buffer.from(
JSON.stringify({ alg: 'HS256', typ: 'JWT' })
).toString('base64');
const body = Buffer.from(JSON.stringify(payload)).toString('base64');
const signature = Buffer.from('mock-signature').toString('base64');
return `${header}.${body}.${signature}`;
},
verify: (token, secret) => {
try {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid token format');
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
// Check expiration
if (payload.exp && Date.now() >= payload.exp * 1000) {
throw new Error('Token expired');
}
return payload;
} catch (err) {
throw new Error(`Invalid token: ${err.message}`);
}
},
decode: token => {
const parts = token.split('.');
if (parts.length !== 3) return null;
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
}
};
}
/**
* Create a JWT token for admin access
* @param {Object} payload - Token payload (user data, permissions, etc.)
* @param {string} secret - Secret key for signing
* @param {number} expiresIn - Expiration time in seconds (default: 7 days)
* @returns {string} JWT token
*/
export function createToken(payload, secret, expiresIn = 7 * 24 * 60 * 60) {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
...payload,
iat: now,
exp: now + expiresIn
},
secret,
{ algorithm: 'HS256' }
);
}
/**
* Verify and decode a JWT token
* @param {string} token - JWT token to verify
* @param {string} secret - Secret key for verification
* @returns {Object} Decoded token payload
* @throws {Error} If token is invalid or expired
*/
export function verifyToken(token, secret) {
return jwt.verify(token, secret);
}
/**
* Decode a JWT token without verification (use with caution)
* @param {string} token - JWT token to decode
* @returns {Object|null} Decoded token payload or null if invalid
*/
export function decodeToken(token) {
return jwt.decode(token);
}
/**
* Check if a token is expired
* @param {string} token - JWT token
* @returns {boolean} True if token is expired
*/
export function isTokenExpired(token) {
try {
const decoded = jwt.decode(token);
if (!decoded || !decoded.exp) return true;
return Date.now() >= decoded.exp * 1000;
} catch {
return true;
}
}
/**
* Get remaining time before token expiration
* @param {string} token - JWT token
* @returns {number} Milliseconds until expiration, or 0 if expired
*/
export function getTokenExpiresIn(token) {
try {
const decoded = jwt.decode(token);
if (!decoded || !decoded.exp) return 0;
const remaining = decoded.exp * 1000 - Date.now();
return Math.max(0, remaining);
} catch {
return 0;
}
}
/**
* Generate a random secret for testing (NOT for production)
* @returns {string} Random secret
*/
export function generateSecret() {
return Buffer.from(Math.random().toString()).toString('base64');
}