diff --git a/index.html b/index.html index 714b706..a595b9e 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,12 @@ Antonym + + + + + + diff --git a/public/images/contradiction-cargo.svg b/public/images/contradiction-cargo.svg new file mode 100644 index 0000000..c3dc062 --- /dev/null +++ b/public/images/contradiction-cargo.svg @@ -0,0 +1,10 @@ + + + C + + CONTRADICTION CARGO + diff --git a/public/images/duality-jacket.svg b/public/images/duality-jacket.svg new file mode 100644 index 0000000..bfeeb03 --- /dev/null +++ b/public/images/duality-jacket.svg @@ -0,0 +1,10 @@ + + + D + + DUALITY JACKET + diff --git a/public/images/inverse-tee.svg b/public/images/inverse-tee.svg new file mode 100644 index 0000000..5f27da2 --- /dev/null +++ b/public/images/inverse-tee.svg @@ -0,0 +1,10 @@ + + + I + + INVERSE TEE + diff --git a/public/images/paradox-cap.svg b/public/images/paradox-cap.svg new file mode 100644 index 0000000..7de4a66 --- /dev/null +++ b/public/images/paradox-cap.svg @@ -0,0 +1,10 @@ + + + P + + PARADOX CAP + diff --git a/public/images/shadow-hoodie.svg b/public/images/shadow-hoodie.svg new file mode 100644 index 0000000..7e57ad4 --- /dev/null +++ b/public/images/shadow-hoodie.svg @@ -0,0 +1,10 @@ + + + S + + SHADOW HOODIE + diff --git a/server/db/seed.ts b/server/db/seed.ts index 14823da..1f16d22 100644 --- a/server/db/seed.ts +++ b/server/db/seed.ts @@ -6,11 +6,11 @@ export function seedProducts(db: Database.Database): void { if (count.n > 0) return const products = [ - { id: nanoid(), name: 'Shadow Hoodie', slug: 'shadow-hoodie', description: 'Heavyweight cotton hoodie with embroidered Antonym logo. Oversized fit.', price_sats: 250_000, images: JSON.stringify(['/images/shadow-hoodie.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 10 }, { size: 'M', stock: 15 }, { size: 'L', stock: 12 }, { size: 'XL', stock: 8 }]), category: 'tops' }, - { id: nanoid(), name: 'Inverse Tee', slug: 'inverse-tee', description: 'Premium organic cotton t-shirt. Minimalist design with contrast stitching.', price_sats: 85_000, images: JSON.stringify(['/images/inverse-tee.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 20 }, { size: 'M', stock: 25 }, { size: 'L', stock: 18 }, { size: 'XL', stock: 10 }]), category: 'tops' }, - { id: nanoid(), name: 'Contradiction Cargo', slug: 'contradiction-cargo', description: 'Relaxed cargo pants with hidden zip pockets. Washed black.', price_sats: 180_000, images: JSON.stringify(['/images/contradiction-cargo.jpg']), sizes: JSON.stringify([{ size: '28', stock: 8 }, { size: '30', stock: 12 }, { size: '32', stock: 15 }, { size: '34', stock: 10 }, { size: '36', stock: 6 }]), category: 'bottoms' }, - { id: nanoid(), name: 'Paradox Cap', slug: 'paradox-cap', description: 'Unstructured six-panel cap with tonal embroidery. One size.', price_sats: 45_000, images: JSON.stringify(['/images/paradox-cap.jpg']), sizes: JSON.stringify([{ size: 'ONE SIZE', stock: 30 }]), category: 'accessories' }, - { id: nanoid(), name: 'Duality Jacket', slug: 'duality-jacket', description: 'Reversible bomber jacket. Matte black one side, reflective silver the other.', price_sats: 450_000, images: JSON.stringify(['/images/duality-jacket.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 5 }, { size: 'M', stock: 8 }, { size: 'L', stock: 6 }, { size: 'XL', stock: 4 }]), category: 'outerwear' }, + { id: nanoid(), name: 'Shadow Hoodie', slug: 'shadow-hoodie', description: 'Heavyweight cotton hoodie with embroidered Antonym logo. Oversized fit.', price_sats: 250_000, images: JSON.stringify(['/images/shadow-hoodie.svg']), sizes: JSON.stringify([{ size: 'S', stock: 10 }, { size: 'M', stock: 15 }, { size: 'L', stock: 12 }, { size: 'XL', stock: 8 }]), category: 'tops' }, + { id: nanoid(), name: 'Inverse Tee', slug: 'inverse-tee', description: 'Premium organic cotton t-shirt. Minimalist design with contrast stitching.', price_sats: 85_000, images: JSON.stringify(['/images/inverse-tee.svg']), sizes: JSON.stringify([{ size: 'S', stock: 20 }, { size: 'M', stock: 25 }, { size: 'L', stock: 18 }, { size: 'XL', stock: 10 }]), category: 'tops' }, + { id: nanoid(), name: 'Contradiction Cargo', slug: 'contradiction-cargo', description: 'Relaxed cargo pants with hidden zip pockets. Washed black.', price_sats: 180_000, images: JSON.stringify(['/images/contradiction-cargo.svg']), sizes: JSON.stringify([{ size: '28', stock: 8 }, { size: '30', stock: 12 }, { size: '32', stock: 15 }, { size: '34', stock: 10 }, { size: '36', stock: 6 }]), category: 'bottoms' }, + { id: nanoid(), name: 'Paradox Cap', slug: 'paradox-cap', description: 'Unstructured six-panel cap with tonal embroidery. One size.', price_sats: 45_000, images: JSON.stringify(['/images/paradox-cap.svg']), sizes: JSON.stringify([{ size: 'ONE SIZE', stock: 30 }]), category: 'accessories' }, + { id: nanoid(), name: 'Duality Jacket', slug: 'duality-jacket', description: 'Reversible bomber jacket. Matte black one side, reflective silver the other.', price_sats: 450_000, images: JSON.stringify(['/images/duality-jacket.svg']), sizes: JSON.stringify([{ size: 'S', stock: 5 }, { size: 'M', stock: 8 }, { size: 'L', stock: 6 }, { size: 'XL', stock: 4 }]), category: 'outerwear' }, ] const insert = db.prepare('INSERT INTO products (id, name, slug, description, price_sats, images, sizes, category) VALUES (@id, @name, @slug, @description, @price_sats, @images, @sizes, @category)') diff --git a/server/index.ts b/server/index.ts index 445553b..5d0ac16 100644 --- a/server/index.ts +++ b/server/index.ts @@ -12,6 +12,8 @@ import { adminRouter } from './routes/admin.js' import { adminProductsRouter } from './routes/adminProducts.js' import { adminOrdersRouter } from './routes/adminOrders.js' import { uploadRouter } from './routes/upload.js' +import { adminNostrRouter } from './routes/adminNostr.js' +import { ordersByPubkeyRouter } from './routes/ordersByPubkey.js' const app = express() const port = Number(process.env.PORT) || 3141 @@ -32,6 +34,8 @@ app.use('/api/admin', adminRouter) app.use('/api/admin/products', adminProductsRouter) app.use('/api/admin/orders', adminOrdersRouter) app.use('/api/admin/upload', uploadRouter) +app.use('/api/admin', adminNostrRouter) +app.use('/api/orders/by-pubkey', ordersByPubkeyRouter) app.get('/api/health', (_req, res) => { res.json({ ok: true, timestamp: new Date().toISOString() }) }) diff --git a/server/routes/adminNostr.ts b/server/routes/adminNostr.ts new file mode 100644 index 0000000..b444c0e --- /dev/null +++ b/server/routes/adminNostr.ts @@ -0,0 +1,26 @@ +import { Router } from 'express' +import { adminAuth } from '../middleware/adminAuth.js' + +export const adminNostrRouter = Router() +adminNostrRouter.use(adminAuth) + +adminNostrRouter.get('/nostr-info', async (_req, res) => { + const privkeyHex = process.env.NOSTR_PRIVATE_KEY + if (!privkeyHex) { + res.status(404).json({ error: { code: 'NOT_CONFIGURED', message: 'NOSTR_PRIVATE_KEY not set' } }) + return + } + + try { + const { getPublicKey } = await import('nostr-tools/pure') + const { npubEncode } = await import('nostr-tools/nip19') + const privkey = Uint8Array.from(Buffer.from(privkeyHex, 'hex')) + const pubkey = getPublicKey(privkey) + const npub = npubEncode(pubkey) + privkey.fill(0) + + res.json({ npub, pubkey }) + } catch { + res.status(500).json({ error: { code: 'NOSTR_ERROR', message: 'Failed to derive public key' } }) + } +}) diff --git a/server/routes/ordersByPubkey.ts b/server/routes/ordersByPubkey.ts new file mode 100644 index 0000000..0c10cfa --- /dev/null +++ b/server/routes/ordersByPubkey.ts @@ -0,0 +1,45 @@ +import { Router } from 'express' +import { getDb } from '../db/connection.js' +import type { OrderItem, OrderStatus } from '../../shared/types.js' + +export const ordersByPubkeyRouter = Router() + +interface OrderRow { + id: string + nostr_pubkey: string | null + email: string | null + btcpay_invoice_id: string | null + status: string + items: string + total_sats: number + note: string | null + created_at: string + updated_at: string +} + +ordersByPubkeyRouter.get('/:pubkey', (req, res) => { + const pubkey = req.params.pubkey + + if (!/^[0-9a-f]{64}$/.test(pubkey)) { + res.status(400).json({ error: { code: 'INVALID_PUBKEY', message: 'Invalid hex pubkey' } }) + return + } + + const db = getDb() + const rows = db.prepare( + 'SELECT * FROM orders WHERE nostr_pubkey = ? ORDER BY created_at DESC' + ).all(pubkey) 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, + }))) +}) diff --git a/src/components/SeoMeta.vue b/src/components/SeoMeta.vue new file mode 100644 index 0000000..de71aa7 --- /dev/null +++ b/src/components/SeoMeta.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/admin/AdminShell.vue b/src/components/admin/AdminShell.vue index efc315a..bfcaac6 100644 --- a/src/components/admin/AdminShell.vue +++ b/src/components/admin/AdminShell.vue @@ -23,6 +23,7 @@ async function handleLogout() {