131 lines
3.3 KiB
TypeScript
131 lines
3.3 KiB
TypeScript
import jwt from 'jsonwebtoken';
|
|
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
// Initialize secrets (auto-generate if not provided)
|
|
const secrets = initializeSecrets();
|
|
|
|
// Token expiry times
|
|
const TOKEN_EXPIRY = '72h';
|
|
const REFRESH_TOKEN_EXPIRY = '30d';
|
|
|
|
/**
|
|
* Initialize JWT secrets - auto-generate and persist if not provided in environment
|
|
*/
|
|
function initializeSecrets(): { secret: string; refreshSecret: string } {
|
|
const dataDir = path.join(process.cwd(), 'data');
|
|
const secretsFile = path.join(dataDir, 'secrets.json');
|
|
|
|
// If provided in environment, use those
|
|
if (process.env.SECRET) {
|
|
return {
|
|
secret: process.env.SECRET,
|
|
refreshSecret: process.env.REFRESH_SECRET || process.env.SECRET, // Use same secret if refresh not provided
|
|
};
|
|
}
|
|
|
|
// Ensure data directory exists
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
|
|
// Try to load existing secrets
|
|
if (fs.existsSync(secretsFile)) {
|
|
try {
|
|
const data = JSON.parse(fs.readFileSync(secretsFile, 'utf-8'));
|
|
return data;
|
|
} catch {
|
|
// Failed to load, will generate new ones
|
|
}
|
|
}
|
|
|
|
// Generate new cryptographically secure secrets (512 bits each)
|
|
const newSecrets = {
|
|
secret: crypto.randomBytes(64).toString('hex'),
|
|
refreshSecret: crypto.randomBytes(64).toString('hex'),
|
|
};
|
|
|
|
// Save to file with restricted permissions
|
|
try {
|
|
fs.writeFileSync(secretsFile, JSON.stringify(newSecrets, null, 2), { mode: 0o600 });
|
|
} catch (error) {
|
|
console.error('⚠️ Failed to save secrets file:', error);
|
|
console.error('⚠️ WARNING: Using in-memory secrets - tokens will be invalid after restart!');
|
|
}
|
|
|
|
return newSecrets;
|
|
}
|
|
|
|
export interface TokenPayload {
|
|
username: string;
|
|
type: 'access' | 'refresh';
|
|
}
|
|
|
|
/**
|
|
* Generate an access token
|
|
*/
|
|
export function generateAccessToken(username: string): string {
|
|
return jwt.sign(
|
|
{ username, type: 'access' } as TokenPayload,
|
|
secrets.secret,
|
|
{ expiresIn: TOKEN_EXPIRY } as jwt.SignOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate a refresh token
|
|
*/
|
|
export function generateRefreshToken(username: string): string {
|
|
return jwt.sign(
|
|
{ username, type: 'refresh' } as TokenPayload,
|
|
secrets.refreshSecret,
|
|
{ expiresIn: REFRESH_TOKEN_EXPIRY } as jwt.SignOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify an access token
|
|
*/
|
|
export function verifyAccessToken(token: string): TokenPayload | null {
|
|
try {
|
|
const payload = jwt.verify(token, secrets.secret) as TokenPayload;
|
|
if (payload.type !== 'access') {
|
|
return null;
|
|
}
|
|
return payload;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a refresh token
|
|
*/
|
|
export function verifyRefreshToken(token: string): TokenPayload | null {
|
|
try {
|
|
const payload = jwt.verify(token, secrets.refreshSecret) as TokenPayload;
|
|
if (payload.type !== 'refresh') {
|
|
return null;
|
|
}
|
|
return payload;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate admin credentials against environment variables
|
|
*/
|
|
export function validateAdminCredentials(username: string, password: string): boolean {
|
|
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
|
|
if (!adminPassword) {
|
|
console.error('❌ ADMIN_PASSWORD not set in environment variables');
|
|
return false;
|
|
}
|
|
|
|
return username === adminUsername && password === adminPassword;
|
|
}
|