feat: scaffold Antonym fashion store
Anonymous Bitcoin-only fashion e-commerce with: - Vue 3 + Tailwind 4 frontend with glassmorphism dark/light design system - Express 5 + SQLite backend with BTCPay Server integration - Nostr identity (NIP-07/keypair) for anonymous purchase tracking - ChaCha20-Poly1305 encrypted shipping addresses - Admin panel with order/product/stock management - SVG logo splash animation with clip-path reveal - 5 seeded products across 4 categories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
48
server/middleware/adminAuth.ts
Normal file
48
server/middleware/adminAuth.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import crypto from 'node:crypto'
|
||||
import { getDb } from '../db/connection.js'
|
||||
|
||||
export function adminAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
const token = req.cookies?.admin_session
|
||||
if (!token) { res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }); return }
|
||||
const db = getDb()
|
||||
const session = db.prepare("SELECT token FROM admin_sessions WHERE token = ? AND expires_at > datetime('now')").get(token) as { token: string } | undefined
|
||||
if (!session) { res.status(401).json({ error: { code: 'SESSION_EXPIRED', message: 'Session expired' } }); return }
|
||||
next()
|
||||
}
|
||||
|
||||
const loginAttempts = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export function rateLimit(req: Request, res: Response, next: NextFunction): void {
|
||||
const ip = req.ip || 'unknown'
|
||||
const now = Date.now()
|
||||
const entry = loginAttempts.get(ip)
|
||||
if (entry) {
|
||||
if (now > entry.resetAt) { loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 }) }
|
||||
else if (entry.count >= 5) { res.status(429).json({ error: { code: 'RATE_LIMITED', message: 'Too many attempts' } }); return }
|
||||
else { entry.count++ }
|
||||
} else { loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 }) }
|
||||
next()
|
||||
}
|
||||
|
||||
export function createSession(): string {
|
||||
const token = crypto.randomBytes(32).toString('hex')
|
||||
const db = getDb()
|
||||
db.prepare("INSERT INTO admin_sessions (token, expires_at) VALUES (?, datetime('now', '+24 hours'))").run(token)
|
||||
db.prepare("DELETE FROM admin_sessions WHERE expires_at < datetime('now')").run()
|
||||
return token
|
||||
}
|
||||
|
||||
export function deleteSession(token: string): void {
|
||||
const db = getDb()
|
||||
db.prepare('DELETE FROM admin_sessions WHERE token = ?').run(token)
|
||||
}
|
||||
|
||||
export function verifyPassword(input: string): boolean {
|
||||
const expected = process.env.ADMIN_PASSWORD
|
||||
if (!expected) return false
|
||||
const inputBuf = Buffer.from(input)
|
||||
const expectedBuf = Buffer.from(expected)
|
||||
if (inputBuf.length !== expectedBuf.length) return false
|
||||
return crypto.timingSafeEqual(inputBuf, expectedBuf)
|
||||
}
|
||||
Reference in New Issue
Block a user