Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJy6X45by9tzgQjaHHGMvW
49 lines
1.9 KiB
JavaScript
49 lines
1.9 KiB
JavaScript
import { scryptSync, randomBytes, timingSafeEqual, createHash } from 'node:crypto'
|
|
import { db } from './db.js'
|
|
|
|
export const SESSION_DAYS = 7
|
|
export const hashPassword = (pw) => {
|
|
const salt = randomBytes(16)
|
|
return `${salt.toString('hex')}:${scryptSync(pw, salt, 64).toString('hex')}`
|
|
}
|
|
export const verifyPassword = (pw, stored) => {
|
|
const [salt, hash] = stored.split(':')
|
|
const a = Buffer.from(hash, 'hex')
|
|
const b = scryptSync(pw, Buffer.from(salt, 'hex'), 64)
|
|
return a.length === b.length && timingSafeEqual(a, b)
|
|
}
|
|
const sha = (t) => createHash('sha256').update(t).digest('hex')
|
|
|
|
export function createSession(staffId) {
|
|
const token = randomBytes(32).toString('hex')
|
|
db.prepare('INSERT INTO sessions VALUES (?,?,?)').run(sha(token), staffId, Date.now() + SESSION_DAYS * 864e5)
|
|
return token
|
|
}
|
|
export const destroySession = (token) => db.prepare('DELETE FROM sessions WHERE token_hash=?').run(sha(token))
|
|
|
|
function cookieToken(req) {
|
|
const m = /(?:^|;\s*)session=([a-f0-9]{64})/.exec(req.headers.cookie || '')
|
|
return m ? m[1] : null
|
|
}
|
|
export { cookieToken }
|
|
|
|
export function requireStaff(req, res, next) {
|
|
const token = cookieToken(req)
|
|
const row = token && db.prepare('SELECT s.id, s.email, s.name, x.expires FROM sessions x JOIN staff s ON s.id=x.staff_id WHERE x.token_hash=?').get(sha(token))
|
|
if (!row || row.expires < Date.now()) return res.status(401).json({ error: 'Please log in.' })
|
|
req.staff = row
|
|
next()
|
|
}
|
|
|
|
// Tiny in-memory rate limiter: `max` hits per `windowMs` per IP+bucket.
|
|
const hits = new Map()
|
|
export const rateLimit = (bucket, max, windowMs) => (req, res, next) => {
|
|
const key = `${bucket}:${req.ip}`
|
|
const now = Date.now()
|
|
const recent = (hits.get(key) || []).filter((t) => now - t < windowMs)
|
|
if (recent.length >= max) return res.status(429).json({ error: 'Too many tries. Please wait a bit.' })
|
|
recent.push(now)
|
|
hits.set(key, recent)
|
|
next()
|
|
}
|