Files

138 lines
4.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// Path for auth users file
const AUTH_FILE = path.join(process.cwd(), 'utils/auth-users.json');
// In-memory cache
let usersCache = null;
let lastLoaded = 0;
const LOAD_INTERVAL_MS = 30 * 1000; // reload at most every 30s
function ensureAuthFile() {
if (fs.existsSync(AUTH_FILE)) return;
const defaultPassword = 'change-me';
const { hash } = hashPassword(defaultPassword);
const data = {
info: 'Initial default credentials. CHANGE THE PASSWORD IMMEDIATELY using /auth/change-password after logging in.',
users: [ { username: 'admin', hash } ]
};
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
console.warn('[auth] Created default auth-users.json with username "admin" and password "change-me". CHANGE IT NOW.');
}
function loadUsers(force=false){
if (!force && usersCache && (Date.now() - lastLoaded) < LOAD_INTERVAL_MS) return usersCache;
ensureAuthFile();
try {
const raw = fs.readFileSync(AUTH_FILE, 'utf8');
const parsed = JSON.parse(raw);
usersCache = Array.isArray(parsed.users) ? parsed.users : [];
lastLoaded = Date.now();
} catch (e) {
console.error('[auth] Failed loading users file:', e.message);
usersCache = [];
}
return usersCache;
}
function hashPassword(password, salt){
salt = salt || crypto.randomBytes(16).toString('hex');
const derived = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
return { hash: `${salt}:${derived}`, salt };
}
function verifyPassword(password, stored){
if (!stored || !stored.includes(':')) return false;
const [salt, derived] = stored.split(':');
const test = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
return crypto.timingSafeEqual(Buffer.from(derived,'hex'), Buffer.from(test,'hex'));
}
function findUser(username){
return loadUsers().find(u => u.username === username);
}
function updatePassword(username, newPassword){
const users = loadUsers(true);
const u = users.find(x => x.username === username);
if (!u) return false;
u.hash = hashPassword(newPassword).hash;
try {
fs.writeFileSync(AUTH_FILE, JSON.stringify({ users }, null, 2));
usersCache = users;
lastLoaded = Date.now();
return true;
} catch (e) {
console.error('[auth] Failed writing updated password:', e.message);
return false;
}
}
// Session issuance & validation
const SESSION_SECRET = crypto.randomBytes(32); // ephemeral; restart invalidates sessions
const SESSION_TTL_MS = 12 * 60 * 60 * 1000; // 12h
function base64url(input){
return Buffer.from(input).toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');
}
function sign(payloadB64){
return base64url(crypto.createHmac('sha256', SESSION_SECRET).update(payloadB64).digest());
}
function issueSession(username){
const payload = { u: username, exp: Date.now() + SESSION_TTL_MS };
const payloadB64 = base64url(JSON.stringify(payload));
const sig = sign(payloadB64);
return `${payloadB64}.${sig}`;
}
function parseCookies(req){
const h = req.headers.cookie; if (!h) return {};
return h.split(';').map(v=>v.trim()).reduce((acc, part)=>{ const eq = part.indexOf('='); if(eq>0){ acc[part.slice(0,eq)] = decodeURIComponent(part.slice(eq+1)); } return acc; },{});
}
function validateSessionToken(token){
if (!token || !token.includes('.')) return null;
const [payloadB64, sig] = token.split('.');
if (sign(payloadB64) !== sig) return null;
try {
const payload = JSON.parse(Buffer.from(payloadB64.replace(/-/g,'+').replace(/_/g,'/'),'base64').toString('utf8'));
if (typeof payload.exp !== 'number' || Date.now() > payload.exp) return null;
return payload;
} catch (e) {
return null;
}
}
function authenticate(username, password){
const user = findUser(username);
if (!user) return false;
return verifyPassword(password, user.hash);
}
function getSession(req){
const cookies = parseCookies(req);
if (!cookies.session) return null;
return validateSessionToken(cookies.session);
}
function requireAuth(req, res, next){
const sess = getSession(req);
if (!sess) return res.status(401).json({ success:false, error:'AUTH_REQUIRED' });
req.session = sess;
next();
}
module.exports = {
loadUsers,
authenticate,
issueSession,
requireAuth,
getSession,
updatePassword,
AUTH_FILE
};