printhive/lib/auth.js
dlawler489 5af88eb5ce
All checks were successful
build-and-push / docker (push) Successful in 29s
Add email/password auth + SMTP password resets
- 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>
2026-06-22 22:33:11 +10:00

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,
};