All checks were successful
build-and-push / docker (push) Successful in 29s
- users + password_resets tables; bcrypt hashing; JWT session in an httpOnly cookie (auth_secret auto-generated in settings) - /api gated behind auth (except /api/config and /api/auth/*) - First-run creates the account (no open registration after that); login, logout, forgot/reset-password flows - SMTP settings (host/port/secure/user/pass/from) in Settings menu; nodemailer sends reset links - Auth screen (sign in / create account / forgot / reset) gates the SPA; Sign out button; COOKIE_SECURE=true in compose for HTTPS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.2 KiB
JavaScript
78 lines
2.2 KiB
JavaScript
const crypto = require('crypto');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const store = require('./store');
|
|
|
|
const COOKIE = 'ph_session';
|
|
const TOKEN_TTL = '30d';
|
|
|
|
// JWT signing secret — generated once and persisted in settings.
|
|
let cachedSecret = null;
|
|
async function getSecret() {
|
|
if (cachedSecret) return cachedSecret;
|
|
let secret = await store.getSetting('auth_secret');
|
|
if (!secret) {
|
|
secret = crypto.randomBytes(48).toString('hex');
|
|
await store.setSetting('auth_secret', secret);
|
|
}
|
|
cachedSecret = secret;
|
|
return secret;
|
|
}
|
|
|
|
function hashPassword(password) {
|
|
return bcrypt.hash(password, 12);
|
|
}
|
|
function verifyPassword(password, hash) {
|
|
return bcrypt.compare(password, hash);
|
|
}
|
|
|
|
async function issueToken(user) {
|
|
return jwt.sign({ sub: user.id, email: user.email }, await getSecret(), { expiresIn: TOKEN_TTL });
|
|
}
|
|
|
|
function setAuthCookie(res, token) {
|
|
res.cookie(COOKIE, token, {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: process.env.COOKIE_SECURE === 'true',
|
|
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
});
|
|
}
|
|
function clearAuthCookie(res) {
|
|
res.clearCookie(COOKIE);
|
|
}
|
|
|
|
// Resolve the current user from the session cookie, or null.
|
|
async function currentUser(req) {
|
|
const token = req.cookies && req.cookies[COOKIE];
|
|
if (!token) return null;
|
|
try {
|
|
const payload = jwt.verify(token, await getSecret());
|
|
return store.getUserById(payload.sub);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Express middleware: 401 unless authenticated.
|
|
async function requireAuth(req, res, next) {
|
|
const user = await currentUser(req);
|
|
if (!user) return res.status(401).json({ error: 'Authentication required' });
|
|
req.user = user;
|
|
next();
|
|
}
|
|
|
|
// Password-reset tokens: return { token, hash } — store the hash, email the token.
|
|
function makeResetToken() {
|
|
const token = crypto.randomBytes(32).toString('hex');
|
|
const hash = crypto.createHash('sha256').update(token).digest('hex');
|
|
return { token, hash };
|
|
}
|
|
function hashResetToken(token) {
|
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
}
|
|
|
|
module.exports = {
|
|
COOKIE, hashPassword, verifyPassword, issueToken, setAuthCookie, clearAuthCookie,
|
|
currentUser, requireAuth, makeResetToken, hashResetToken,
|
|
};
|