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>
50 lines
3.7 KiB
TypeScript
50 lines
3.7 KiB
TypeScript
import { Router } from 'express'
|
|
import { getDb } from '../db/connection.js'
|
|
import { adminAuth } from '../middleware/adminAuth.js'
|
|
import { requireBody, sanitizeString } from '../middleware/validate.js'
|
|
import { decrypt } from '../lib/crypto.js'
|
|
import { sendStatusUpdate } from '../lib/mailer.js'
|
|
import { notifyStatusUpdate } from '../lib/nostr.js'
|
|
import type { Order, OrderItem, OrderEvent, OrderStatus, ShippingAddress } from '../../shared/types.js'
|
|
|
|
export const adminOrdersRouter = Router()
|
|
adminOrdersRouter.use(adminAuth)
|
|
|
|
interface OrderRow { id: string; nostr_pubkey: string | null; email: string | null; btcpay_invoice_id: string | null; status: string; shipping_address_encrypted: string | null; items: string; total_sats: number; note: string | null; created_at: string; updated_at: string }
|
|
|
|
const VALID_STATUSES: OrderStatus[] = ['pending', 'paid', 'confirmed', 'shipped', 'delivered', 'cancelled']
|
|
|
|
adminOrdersRouter.get('/', (req, res) => {
|
|
const db = getDb()
|
|
const status = req.query.status as string | undefined
|
|
let rows: OrderRow[]
|
|
if (status) { rows = db.prepare('SELECT * FROM orders WHERE status = ? ORDER BY created_at DESC').all(status) as OrderRow[] }
|
|
else { rows = db.prepare('SELECT * FROM orders ORDER BY created_at DESC').all() as OrderRow[] }
|
|
res.json(rows.map((row) => ({ id: row.id, nostrPubkey: row.nostr_pubkey, email: row.email, btcpayInvoiceId: row.btcpay_invoice_id, status: row.status as OrderStatus, items: JSON.parse(row.items) as OrderItem[], totalSats: row.total_sats, note: row.note, createdAt: row.created_at, updatedAt: row.updated_at })))
|
|
})
|
|
|
|
adminOrdersRouter.get('/:id', (req, res) => {
|
|
const db = getDb()
|
|
const row = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id) as OrderRow | undefined
|
|
if (!row) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Order not found' } }); return }
|
|
let shippingAddress: ShippingAddress | null = null
|
|
if (row.shipping_address_encrypted) { try { shippingAddress = JSON.parse(decrypt(row.shipping_address_encrypted)) as ShippingAddress } catch { shippingAddress = null } }
|
|
const events = db.prepare('SELECT * FROM order_events WHERE order_id = ? ORDER BY created_at ASC').all(req.params.id) as OrderEvent[]
|
|
const order: Order & { shippingAddress: ShippingAddress | null; events: OrderEvent[] } = { id: row.id, nostrPubkey: row.nostr_pubkey, email: row.email, btcpayInvoiceId: row.btcpay_invoice_id, status: row.status as OrderStatus, items: JSON.parse(row.items) as OrderItem[], totalSats: row.total_sats, note: row.note, createdAt: row.created_at, updatedAt: row.updated_at, shippingAddress, events }
|
|
res.json(order)
|
|
})
|
|
|
|
adminOrdersRouter.patch('/:id/status', requireBody('status'), (req, res) => {
|
|
const db = getDb()
|
|
const { status, note } = req.body as { status: string; note?: string }
|
|
if (!VALID_STATUSES.includes(status as OrderStatus)) { res.status(400).json({ error: { code: 'INVALID_STATUS', message: `Status must be one of: ${VALID_STATUSES.join(', ')}` } }); return }
|
|
const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id) as OrderRow | undefined
|
|
if (!order) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Order not found' } }); return }
|
|
const noteText = note ? sanitizeString(note) : null
|
|
db.prepare("UPDATE orders SET status = ?, updated_at = datetime('now') WHERE id = ?").run(status, req.params.id)
|
|
db.prepare('INSERT INTO order_events (order_id, status, note) VALUES (?, ?, ?)').run(req.params.id, status, noteText)
|
|
if (order.email) sendStatusUpdate(order.email, order.id, status, noteText ?? undefined).catch(() => {})
|
|
if (order.nostr_pubkey) notifyStatusUpdate(order.nostr_pubkey, order.id, status, noteText ?? undefined).catch(() => {})
|
|
res.json({ ok: true, status })
|
|
})
|