Files
AtreidesAssetsFree/server/auth.mjs
T
2026-10-29 11:57:29 +01:00

127 lines
4.9 KiB
JavaScript

// Generated, do not edit. sha256:b90a0342814d40cb5e6f51f946a82feca678a01b63d4cdb3e948bcbb7e125654
import { createHash, timingSafeEqual } from 'node:crypto';
import { CONFIG } from '../shared/config.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { readSecretsStore, writeSecretsStore } from './keys.mjs';
import { installDir } from './portfolios.mjs';
import { fail } from '../shared/app-error.js';
const AUTH_SLEUTEL = 'assetapp_auth';
const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
const digest = (waarde) => createHash('sha256').update(String(waarde), 'utf-8').digest();
export function passwordMatches(presented, expected) {
if (!expected || !expected.password) return false;
if (typeof presented !== 'string' || !presented) return false;
return timingSafeEqual(digest(presented), digest(expected.password));
}
export function parseCookies(header) {
const uit = {};
if (typeof header !== 'string') return uit;
for (const deel of header.split(';')) {
const i = deel.indexOf('=');
if (i < 0) continue;
const naam = deel.slice(0, i).trim();
if (!naam || naam in uit) continue;
uit[naam] = deel.slice(i + 1).trim();
}
return uit;
}
const vlaggen = (secure) => `Path=/; HttpOnly; SameSite=Lax${secure ? '; Secure' : ''}`;
export function sessionCookie(token, { secure = false } = {}) {
const seconden = Math.floor(CONFIG.AUTH.SESSION_TTL_MS / 1000);
return `${CONFIG.AUTH.COOKIE_NAME}=${token}; Max-Age=${seconden}; ${vlaggen(secure)}`;
}
export function clearedSessionCookie({ secure = false } = {}) {
return `${CONFIG.AUTH.COOKIE_NAME}=; Max-Age=0; ${vlaggen(secure)}`;
}
export function sessionTokenOf(req) {
return parseCookies(req?.headers?.cookie)[CONFIG.AUTH.COOKIE_NAME] || null;
}
export function sendUnauthorized(res, { document = false } = {}) {
if (document) {
res.writeHead(302, {
Location: CONFIG.AUTH.LOGIN_PATH,
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
});
return res.end();
}
res.writeHead(401, {
'Content-Type': 'application/json; charset=utf-8',
'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'Not logged in.' }));
}
export async function loadCredentials() {
const { ASSETAPP_PASSWORD: password } = process.env;
if (password) return { password };
const opgeslagen = (await readSecretsStore())[AUTH_SLEUTEL];
if (opgeslagen && opgeslagen.password) return { password: String(opgeslagen.password) };
return null;
}
export async function credentialsStatus() {
const { ASSETAPP_PASSWORD: password } = process.env;
if (password) return { ingesteld: true, bron: 'omgeving', wijzigbaar: false };
const opgeslagen = (await readSecretsStore())[AUTH_SLEUTEL];
const ingesteld = Boolean(opgeslagen && opgeslagen.password);
return { ingesteld, bron: ingesteld ? 'portefeuille' : 'geen', wijzigbaar: true };
}
export async function setCredentials(password) {
const w = String(password ?? '');
if (!w) fail('portfolios.passwordRequired', null, 'A password is required.');
const store = await readSecretsStore();
await writeSecretsStore({ ...store, [AUTH_SLEUTEL]: { password: w } });
return { ...await credentialsStatus(), naHerstart: true };
}
export async function clearCredentials() {
const store = await readSecretsStore();
if (!(AUTH_SLEUTEL in store)) return { ...await credentialsStatus(), naHerstart: false };
delete store[AUTH_SLEUTEL];
await writeSecretsStore(store);
return { ...await credentialsStatus(), naHerstart: true };
}
const leesOfNull = async (pad) => {
try { return await readFile(pad); }
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
};
export async function loadTlsOptions() {
const certPad = process.env.ASSETAPP_TLS_CERT || join(installDir(), 'tls', 'cert.pem');
const keyPad = process.env.ASSETAPP_TLS_KEY || join(installDir(), 'tls', 'key.pem');
const [cert, key] = await Promise.all([leesOfNull(certPad), leesOfNull(keyPad)]);
if (!cert && !key) return null;
if (!cert) throw new Error(`TLS half configured: key found (${keyPad}) but no certificate (${certPad}).`);
if (!key) throw new Error(`TLS half configured: certificate found (${certPad}) but no key (${keyPad}).`);
return { cert, key };
}
export function resolveBinding({ tls = false, credentials = null, host = null } = {}) {
const beschermd = Boolean(tls) && Boolean(credentials);
if (host) {
if (LOOPBACK.has(host) || beschermd) return { host, waarschuwing: null };
const mist = [Boolean(tls) ? null : 'TLS', credentials ? null : 'inloggegevens'].filter(Boolean).join(' en ');
return {
host,
waarschuwing: `ASSETAPP_HOST=${host} stelt de app open voor het hele netwerk terwijl ${mist} ontbreekt. `
+ 'Iedereen die deze machine kan bereiken, kan de app openen.',
};
}
return { host: beschermd ? '0.0.0.0' : '127.0.0.1', waarschuwing: null };
}