Phase 3: Express + SQLite server, staff login, ticket email, admin dashboard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJy6X45by9tzgQjaHHGMvW
This commit is contained in:
16 files changed
+1339
-30
No files matched your search
@@ -0,0 +1,48 @@
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user