126 lines
4.9 KiB
JavaScript
126 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 };
|
||
|
|
}
|