Files
antonym/server/routes/adminProducts.ts
Dorian 54500a68e6 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>
2026-03-17 00:23:21 +00:00

68 lines
4.4 KiB
TypeScript

import { Router } from 'express'
import { nanoid } from 'nanoid'
import { getDb } from '../db/connection.js'
import { adminAuth } from '../middleware/adminAuth.js'
import { requireBody, sanitizeString, sanitizeInt } from '../middleware/validate.js'
import { rowToProduct } from './products.js'
export const adminProductsRouter = Router()
adminProductsRouter.use(adminAuth)
interface ProductRow { id: string; name: string; slug: string; description: string; price_sats: number; images: string; sizes: string; category: string; is_active: number; created_at: string; updated_at: string }
adminProductsRouter.get('/', (_req, res) => {
const db = getDb()
const rows = db.prepare('SELECT * FROM products ORDER BY created_at DESC').all() as ProductRow[]
res.json(rows.map(rowToProduct))
})
adminProductsRouter.post('/', requireBody('name', 'slug', 'priceSats'), (req, res) => {
const db = getDb()
const { name, slug, description, priceSats, images, sizes, category } = req.body
const price = sanitizeInt(priceSats)
if (price === null || price <= 0) { res.status(400).json({ error: { code: 'INVALID_PRICE', message: 'Price must be a positive integer (sats)' } }); return }
const id = nanoid()
try {
db.prepare('INSERT INTO products (id, name, slug, description, price_sats, images, sizes, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, sanitizeString(name), sanitizeString(slug), sanitizeString(description || ''), price, JSON.stringify(images || []), JSON.stringify(sizes || []), sanitizeString(category || 'general'))
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(id) as ProductRow
res.status(201).json(rowToProduct(row))
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('UNIQUE')) { res.status(409).json({ error: { code: 'SLUG_EXISTS', message: 'A product with this slug already exists' } }); return }
throw err
}
})
adminProductsRouter.put('/:id', (req, res) => {
const db = getDb()
const { name, slug, description, priceSats, images, sizes, category, isActive } = req.body
const existing = db.prepare('SELECT id FROM products WHERE id = ?').get(req.params.id)
if (!existing) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Product not found' } }); return }
const price = sanitizeInt(priceSats)
if (price !== null && price <= 0) { res.status(400).json({ error: { code: 'INVALID_PRICE', message: 'Price must be a positive integer (sats)' } }); return }
try {
db.prepare("UPDATE products SET name = COALESCE(?, name), slug = COALESCE(?, slug), description = COALESCE(?, description), price_sats = COALESCE(?, price_sats), images = COALESCE(?, images), sizes = COALESCE(?, sizes), category = COALESCE(?, category), is_active = COALESCE(?, is_active), updated_at = datetime('now') WHERE id = ?").run(name ? sanitizeString(name) : null, slug ? sanitizeString(slug) : null, description !== undefined ? sanitizeString(description) : null, price, images ? JSON.stringify(images) : null, sizes ? JSON.stringify(sizes) : null, category ? sanitizeString(category) : null, isActive !== undefined ? (isActive ? 1 : 0) : null, req.params.id)
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id) as ProductRow
res.json(rowToProduct(row))
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('UNIQUE')) { res.status(409).json({ error: { code: 'SLUG_EXISTS', message: 'A product with this slug already exists' } }); return }
throw err
}
})
adminProductsRouter.delete('/:id', (req, res) => {
const db = getDb()
const result = db.prepare("UPDATE products SET is_active = 0, updated_at = datetime('now') WHERE id = ?").run(req.params.id)
if (result.changes === 0) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Product not found' } }); return }
res.json({ ok: true })
})
adminProductsRouter.patch('/:id/stock', requireBody('sizes'), (req, res) => {
const db = getDb()
const { sizes } = req.body
if (!Array.isArray(sizes)) { res.status(400).json({ error: { code: 'INVALID_SIZES', message: 'Sizes must be an array' } }); return }
const result = db.prepare("UPDATE products SET sizes = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(sizes), req.params.id)
if (result.changes === 0) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Product not found' } }); return }
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id) as ProductRow
res.json(rowToProduct(row))
})