mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
138 lines
4.3 KiB
JavaScript
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
|
|
};
|