Files
radio-explorer/server/auth.js
Marco Mooren 00246389bc Add API documentation and underground station importer
- Introduced a new HTML documentation page for the oradio API, including a JavaScript file to handle dynamic content and API requests.
- Added a CSS file for styling the documentation page.
- Implemented an underground station importer script that fetches data from Radio-Browser and writes it to a JSON file.
- Created a stats module to compute and manage vote and play statistics for radio stations.
- Added a polyfill for modulepreload to ensure compatibility with older browsers.
2026-05-11 02:06:48 +02:00

89 lines
2.8 KiB
JavaScript

import bcrypt from 'bcryptjs';
import { randomBytes } from 'node:crypto';
import { getDb } from './db/index.js';
const SESSION_DAYS = 30;
const COOKIE_NAME = 'oradio_sid';
export function hashPassword(plain) {
return bcrypt.hashSync(plain, 10);
}
export function verifyPassword(plain, hash) {
return bcrypt.compareSync(plain, hash);
}
export function createSession(userId) {
const token = randomBytes(32).toString('hex');
const expires = new Date(Date.now() + SESSION_DAYS * 86400e3).toISOString();
getDb().prepare('INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)')
.run(token, userId, expires);
return { token, expires };
}
export function destroySession(token) {
if (token) getDb().prepare('DELETE FROM sessions WHERE token = ?').run(token);
}
export function getUserBySession(token) {
if (!token) return null;
return getDb().prepare(`
SELECT u.id, u.username, u.role
FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token = ? AND s.expires_at > datetime('now')
`).get(token);
}
export function readSessionToken(req) {
const raw = req.headers.cookie || '';
for (const part of raw.split(';')) {
const [k, v] = part.trim().split('=');
if (k === COOKIE_NAME) return decodeURIComponent(v || '');
}
return null;
}
export function setSessionCookie(res, token, expires) {
const attrs = [
`${COOKIE_NAME}=${encodeURIComponent(token)}`,
'Path=/',
'HttpOnly',
'SameSite=Lax',
`Expires=${new Date(expires).toUTCString()}`
];
res.setHeader('Set-Cookie', attrs.join('; '));
}
export function clearSessionCookie(res) {
res.setHeader('Set-Cookie', `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
}
export function authMiddleware(req, _res, next) {
const token = readSessionToken(req);
req.session = { token };
req.user = getUserBySession(token);
next();
}
export function requireUser(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'auth required' });
next();
}
export function requireAdmin(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'auth required' });
if (req.user.role !== 'admin') return res.status(403).json({ error: 'admin only' });
next();
}
export function ensureBootstrapAdmin({ username, password }) {
if (!username || !password) return;
const db = getDb();
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
if (existing) return;
const info = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
.run(username, hashPassword(password), 'admin');
db.prepare('INSERT INTO profiles (user_id, display_name) VALUES (?, ?)')
.run(info.lastInsertRowid, username);
console.log(`[auth] bootstrap admin '${username}' created`);
}