Files
website/server/src/utils/auth.js
whitlocktech eef79e2403 Initial commit: UOMysticmoon backend (Express + MariaDB + JWT)
- Layered API (router -> controller -> model -> db), serverlinkr pattern
- Public / auth / admin route groups; posts, wiki, settings, users, activity models
- JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS)
- Site LIVE/MAINTENANCE mode with admin preview bypass
- Dual file+console logging (info/warn/error/debug) + HTTP access logs
- Docker Compose (app + MariaDB), schema.sql + seed, .env.example
- Verified end-to-end against MariaDB (27/27 smoke checks)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:58:32 -05:00

97 lines
2.6 KiB
JavaScript

const jwt = require('jsonwebtoken')
require('dotenv').config()
const log = require('./logger')('auth')
const JWT_SECRET = process.env.JWT_SECRET
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'
const COOKIE_NAME = process.env.COOKIE_NAME || 'uomm_token'
if (!JWT_SECRET) {
log.warn('JWT_SECRET is not set — set it in .env before going to production')
}
function signToken(user) {
const payload = { id: user.id, username: user.username, role: user.role }
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN })
}
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET)
} catch (err) {
return null
}
}
// Rough max-age (ms) for the cookie, parsed from JWT_EXPIRES_IN (e.g. 1d, 12h, 30m).
function cookieMaxAge() {
const m = /^(\d+)([dhms])$/.exec(String(JWT_EXPIRES_IN).trim())
if (!m) return 24 * 60 * 60 * 1000
const n = Number(m[1])
const unit = { d: 86400000, h: 3600000, m: 60000, s: 1000 }[m[2]]
return n * unit
}
/**
* Decide the cookie Secure flag. COOKIE_SECURE=auto (default) uses req.secure,
* which is true behind Pangolin (HTTPS, X-Forwarded-Proto) and false over plain
* HTTP on the LAN IP — so login works in both. Requires app.set('trust proxy').
*/
function cookieSecure(req) {
const mode = (process.env.COOKIE_SECURE || 'auto').toLowerCase()
if (mode === 'true') return true
if (mode === 'false') return false
return Boolean(req.secure)
}
function cookieOptions(req) {
return {
httpOnly: true,
sameSite: 'lax',
secure: cookieSecure(req),
path: '/',
}
}
function setAuthCookie(req, res, token) {
res.cookie(COOKIE_NAME, token, { ...cookieOptions(req), maxAge: cookieMaxAge() })
}
function clearAuthCookie(req, res) {
res.clearCookie(COOKIE_NAME, cookieOptions(req))
}
// Extract a token from the cookie or an Authorization: Bearer header.
function extractToken(req) {
if (req.cookies && req.cookies[COOKIE_NAME]) return req.cookies[COOKIE_NAME]
const header = req.headers.authorization
if (header && header.startsWith('Bearer ')) return header.substring(7)
return null
}
// Returns the decoded user or null without rejecting the request.
function getUserFromRequest(req) {
const token = extractToken(req)
if (!token) return null
return verifyToken(token)
}
// Gate middleware for protected (admin) routes.
function isLoggedIn(req, res, next) {
const user = getUserFromRequest(req)
if (!user) return res.status(401).json({ message: 'Unauthorized' })
req.user = user
return next()
}
module.exports = {
COOKIE_NAME,
signToken,
verifyToken,
setAuthCookie,
clearAuthCookie,
getUserFromRequest,
isLoggedIn,
}