import { pbkdf2Sync, randomBytes, timingSafeEqual } from "node:crypto"; import { createServer } from "node:http"; import { db } from "./db.js"; const PORT = Number(process.env.API_PORT || 4174); const SESSION_DAYS = 30; const now = () => new Date().toISOString(); const addDays = (days) => new Date(Date.now() + days * 86400000).toISOString(); const moneyId = (value) => value; const json = (res, status, payload) => { res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }); res.end(JSON.stringify(payload)); }; const readBody = async (req) => new Promise((resolve, reject) => { let raw = ""; req.on("data", (chunk) => { raw += chunk; if (raw.length > 1_000_000) { reject(new Error("Request body too large")); req.destroy(); } }); req.on("end", () => { if (!raw) { resolve({}); return; } try { resolve(JSON.parse(raw)); } catch { reject(new Error("Invalid JSON")); } }); req.on("error", reject); }); const hashPassword = (password, salt = randomBytes(16).toString("hex")) => ({ salt, hash: pbkdf2Sync(password, salt, 120000, 64, "sha512").toString("hex"), }); const verifyPassword = (password, salt, expectedHash) => { const { hash } = hashPassword(password, salt); const actual = Buffer.from(hash, "hex"); const expected = Buffer.from(expectedHash, "hex"); return actual.length === expected.length && timingSafeEqual(actual, expected); }; const getBearerToken = (req) => { const auth = req.headers.authorization || ""; const match = auth.match(/^Bearer\s+(.+)$/i); return match ? match[1] : null; }; const composeAddress = ({ street_name, house_number, zip_code, city }) => [street_name, house_number].filter(Boolean).join(" ").trim() + (zip_code || city ? `, ${[zip_code, city].filter(Boolean).join(" ")}` : ""); const normalizeEmail = (email) => String(email || "").trim().toLowerCase(); const notificationForUser = (userId) => { const existing = db .prepare("SELECT * FROM notification_preferences WHERE user_id = ?") .get(userId); if (existing) return existing; db.prepare( `INSERT INTO notification_preferences ( user_id, drops_enabled, restocks_enabled, small_batch_enabled, discovery_enabled, updated_at ) VALUES (?, 0, 0, 0, 0, ?)` ).run(userId, now()); return db.prepare("SELECT * FROM notification_preferences WHERE user_id = ?").get(userId); }; const rowToUser = (row) => { if (!row) return null; return { id: row.id, email: row.email, name: row.name, first_name: row.first_name, surname: row.surname, address: row.address, street_name: row.street_name || "", house_number: row.house_number || "", zip_code: row.zip_code || "", city: row.city || "", birthdate: row.birthdate || "", created_at: row.created_at, notifications: prefsToJson(notificationForUser(row.id)), productSubscriptions: getProductSubscriptions(row.id), discoveryStatus: getDiscoveryStatus(row.id), sampleCredits: getSampleCreditStatus(row.id), loyaltyStatus: getLoyaltyStatus(row.id), }; }; const prefsToJson = (prefs) => ({ drops_enabled: Boolean(prefs.drops_enabled), restocks_enabled: Boolean(prefs.restocks_enabled), small_batch_enabled: Boolean(prefs.small_batch_enabled), discovery_enabled: Boolean(prefs.discovery_enabled), updated_at: prefs.updated_at, }); const authenticate = (req) => { const token = getBearerToken(req); if (!token) return null; const session = db .prepare("SELECT * FROM sessions WHERE token = ?") .get(token); if (!session) return null; if (new Date(session.expires_at).getTime() <= Date.now()) { db.prepare("DELETE FROM sessions WHERE token = ?").run(token); return null; } const user = db.prepare("SELECT * FROM users WHERE id = ?").get(session.user_id); return user ? { token, user } : null; }; const requireAuth = (req, res) => { const auth = authenticate(req); if (!auth) { json(res, 401, { error: "Please log in to continue." }); return null; } return auth; }; const createSession = (userId) => { const token = randomBytes(32).toString("hex"); db.prepare( "INSERT INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)" ).run(token, userId, now(), addDays(SESSION_DAYS)); return token; }; const getCartRows = (userId) => db.prepare( `SELECT c.product_id, c.quantity, p.slug, p.name, p.kind, p.size_label, p.price_cents, p.discovery_credit_cents FROM cart_items c JOIN products p ON p.id = c.product_id WHERE c.user_id = ? ORDER BY c.created_at ASC` ).all(userId); const getAvailableDiscounts = (userId, rows) => { const discounts = []; const fullRows = rows.filter((row) => row.kind === "full_size"); const fullTotal = fullRows.reduce( (sum, row) => sum + row.price_cents * row.quantity, 0 ); if (fullTotal > 0) { const discovery = db .prepare( `SELECT * FROM discovery_credits WHERE user_id = ? AND redeemed_order_id IS NULL ORDER BY id ASC LIMIT 1` ) .get(userId); if (discovery) { discounts.push({ type: "discovery", creditId: discovery.id, amount_cents: Math.min(discovery.amount_cents, fullTotal), label: "Discovery Set credit", }); } for (const row of fullRows) { const sample = db .prepare( `SELECT * FROM sample_credits WHERE user_id = ? AND slug = ? AND redeemed_order_id IS NULL ORDER BY id ASC LIMIT 1` ) .get(userId, row.slug); if (sample) { discounts.push({ type: "sample", creditId: sample.id, slug: row.slug, product_id: row.product_id, amount_cents: Math.min(sample.amount_cents, row.price_cents * row.quantity), label: `${row.name.replace(" Full Size", "")} sample credit`, }); } } } return discounts.filter((discount) => discount.amount_cents > 0); }; const getCart = (userId) => { const rows = getCartRows(userId); const items = rows.map((row) => ({ product_id: row.product_id, quantity: row.quantity, line_total_cents: row.price_cents * row.quantity, product: { id: row.product_id, slug: row.slug, name: row.name, kind: row.kind, size_label: row.size_label, price_cents: row.price_cents, discovery_credit_cents: row.discovery_credit_cents, }, })); const subtotal_cents = items.reduce((sum, item) => sum + item.line_total_cents, 0); const discounts = getAvailableDiscounts(userId, rows); const discount_cents = discounts.reduce((sum, item) => sum + item.amount_cents, 0); return { items, subtotal_cents, discount_cents, total_cents: Math.max(0, subtotal_cents - discount_cents), total_quantity: items.reduce((sum, item) => sum + item.quantity, 0), discounts, }; }; const getOrders = (userId) => { const orders = db .prepare("SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC") .all(userId); const itemStmt = db.prepare( `SELECT oi.*, p.name, p.slug, p.kind, p.size_label FROM order_items oi JOIN products p ON p.id = oi.product_id WHERE oi.order_id = ? ORDER BY oi.id ASC` ); return orders.map((order) => ({ ...order, items: itemStmt.all(order.id).map((item) => ({ id: item.id, product_id: item.product_id, quantity: item.quantity, unit_price_cents: item.unit_price_cents, line_total_cents: item.line_total_cents, product: { id: item.product_id, name: item.name, slug: item.slug, kind: item.kind, size_label: item.size_label, }, })), })); }; function getDiscoveryStatus(userId) { const credit = db .prepare("SELECT * FROM discovery_credits WHERE user_id = ? ORDER BY id ASC LIMIT 1") .get(userId); if (!credit) return "No Discount atm"; return credit.redeemed_order_id ? "Discount already used" : "Discount available"; } function getSampleCreditStatus(userId) { return db .prepare( `SELECT slug, amount_cents, redeemed_order_id, created_at, redeemed_at FROM sample_credits WHERE user_id = ? ORDER BY created_at DESC` ) .all(userId) .map((credit) => ({ ...credit, status: credit.redeemed_order_id ? "used" : "available", })); } function getProductSubscriptions(userId) { return db .prepare( `SELECT ps.id, ps.product_id, ps.type, ps.created_at, p.slug, p.name, p.kind, p.size_label FROM product_subscriptions ps JOIN products p ON p.id = ps.product_id WHERE ps.user_id = ? ORDER BY ps.created_at DESC, ps.id DESC` ) .all(userId); } function getLoyaltyStatus(userId) { const orderStats = db .prepare( `SELECT COUNT(*) AS purchases, COALESCE(SUM(total_cents), 0) AS spent_cents FROM orders WHERE user_id = ?` ) .get(userId); const discovery = db .prepare( `SELECT 1 FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.user_id = ? AND oi.product_id = 'discovery-set' LIMIT 1` ) .get(userId); const full = db .prepare( `SELECT 1 FROM order_items oi JOIN orders o ON o.id = oi.order_id JOIN products p ON p.id = oi.product_id WHERE o.user_id = ? AND p.kind = 'full_size' LIMIT 1` ) .get(userId); const status = { hasDiscoverySet: Boolean(discovery), hasFullSize: Boolean(full), purchases: Number(orderStats.purchases || 0), spent_cents: Number(orderStats.spent_cents || 0), }; status.unlocked = status.hasDiscoverySet && status.hasFullSize && status.purchases >= 3 && status.spent_cents > 50000; return status; } const stateForUser = (userRow, token) => ({ token, user: rowToUser(userRow), cart: getCart(userRow.id), orders: getOrders(userRow.id), }); const register = async (req, res) => { const body = await readBody(req); const email = normalizeEmail(body.email); const password = String(body.password || ""); const firstName = String(body.first_name || body.firstName || "").trim(); const surname = String(body.surname || "").trim(); if (!firstName || !surname || !email || !password) { json(res, 400, { error: "First name, surname, email, and password are required." }); return; } const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email); if (existing) { json(res, 409, { error: "An account with this email already exists." }); return; } const { salt, hash } = hashPassword(password); const fullName = `${firstName} ${surname}`.trim(); const created = now(); const result = db .prepare( `INSERT INTO users ( email, password_hash, password_salt, name, first_name, surname, address, street_name, house_number, zip_code, city, birthdate, created_at ) VALUES (?, ?, ?, ?, ?, ?, '', '', '', '', '', '', ?)` ) .run(email, hash, salt, fullName, firstName, surname, created); notificationForUser(result.lastInsertRowid); const user = db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid); json(res, 201, stateForUser(user, createSession(user.id))); }; const login = async (req, res) => { const body = await readBody(req); const email = normalizeEmail(body.email); const password = String(body.password || ""); const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email); if (!user || !verifyPassword(password, user.password_salt, user.password_hash)) { json(res, 401, { error: "Invalid email or password." }); return; } json(res, 200, stateForUser(user, createSession(user.id))); }; const patchProfile = async (req, res, user) => { const body = await readBody(req); const current = db.prepare("SELECT * FROM users WHERE id = ?").get(user.id); const firstName = String(body.first_name ?? current.first_name ?? "").trim(); const surname = String(body.surname ?? current.surname ?? "").trim(); const streetName = String(body.street_name ?? current.street_name ?? "").trim(); const houseNumber = String(body.house_number ?? current.house_number ?? "").trim(); const zipCode = String(body.zip_code ?? current.zip_code ?? "").trim(); const city = String(body.city ?? current.city ?? "").trim(); const birthdate = String(body.birthdate ?? current.birthdate ?? "").trim(); const name = `${firstName} ${surname}`.trim(); const address = composeAddress({ street_name: streetName, house_number: houseNumber, zip_code: zipCode, city, }); if (!firstName || !surname) { json(res, 400, { error: "First name and surname are required." }); return; } db.prepare( `UPDATE users SET name = ?, first_name = ?, surname = ?, address = ?, street_name = ?, house_number = ?, zip_code = ?, city = ?, birthdate = ? WHERE id = ?` ).run(name, firstName, surname, address, streetName, houseNumber, zipCode, city, birthdate, user.id); const updated = db.prepare("SELECT * FROM users WHERE id = ?").get(user.id); json(res, 200, { user: rowToUser(updated) }); }; const patchNotifications = async (req, res, user) => { const body = await readBody(req); db.prepare( `INSERT INTO notification_preferences ( user_id, drops_enabled, restocks_enabled, small_batch_enabled, discovery_enabled, updated_at ) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET drops_enabled = excluded.drops_enabled, restocks_enabled = excluded.restocks_enabled, small_batch_enabled = excluded.small_batch_enabled, discovery_enabled = excluded.discovery_enabled, updated_at = excluded.updated_at` ).run( user.id, body.drops_enabled ? 1 : 0, body.restocks_enabled ? 1 : 0, body.small_batch_enabled ? 1 : 0, body.discovery_enabled ? 1 : 0, now() ); json(res, 200, { notifications: prefsToJson(notificationForUser(user.id)) }); }; const addCartItem = async (req, res, user) => { const body = await readBody(req); const productId = moneyId(String(body.productId || body.product_id || "")); const quantity = Math.max(1, Number(body.quantity || 1)); const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId); if (!product) { json(res, 404, { error: "Product not found." }); return; } const timestamp = now(); db.prepare( `INSERT INTO cart_items (user_id, product_id, quantity, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(user_id, product_id) DO UPDATE SET quantity = cart_items.quantity + excluded.quantity, updated_at = excluded.updated_at` ).run(user.id, productId, quantity, timestamp, timestamp); json(res, 200, { cart: getCart(user.id), message: `${quantity} x ${product.name} added.`, }); }; const patchCartItem = async (req, res, user, productId) => { const body = await readBody(req); const quantity = Number(body.quantity); if (!Number.isFinite(quantity)) { json(res, 400, { error: "Quantity is required." }); return; } if (quantity <= 0) { db.prepare("DELETE FROM cart_items WHERE user_id = ? AND product_id = ?").run(user.id, productId); } else { db.prepare( "UPDATE cart_items SET quantity = ?, updated_at = ? WHERE user_id = ? AND product_id = ?" ).run(Math.floor(quantity), now(), user.id, productId); } json(res, 200, { cart: getCart(user.id) }); }; const deleteCartItem = (res, user, productId) => { db.prepare("DELETE FROM cart_items WHERE user_id = ? AND product_id = ?").run(user.id, productId); json(res, 200, { cart: getCart(user.id) }); }; const checkout = async (req, res, user) => { const body = await readBody(req); const rows = getCartRows(user.id); if (rows.length === 0) { json(res, 400, { error: "Your cart is empty." }); return; } const addressFields = { street_name: String(body.street_name || "").trim(), house_number: String(body.house_number || "").trim(), zip_code: String(body.zip_code || "").trim(), city: String(body.city || "").trim(), }; const paymentMethod = String(body.payment_method || body.paymentMethod || "").trim(); if (!addressFields.street_name || !addressFields.house_number || !addressFields.zip_code || !addressFields.city) { json(res, 400, { error: "Street name, house number, ZIP code, and city are required." }); return; } if (!["Bill", "Card", "Twint", "PayPal"].includes(paymentMethod)) { json(res, 400, { error: "Choose a payment method." }); return; } const subtotal = rows.reduce((sum, row) => sum + row.price_cents * row.quantity, 0); const discounts = getAvailableDiscounts(user.id, rows); const discountTotal = discounts.reduce((sum, discount) => sum + discount.amount_cents, 0); const total = Math.max(0, subtotal - discountTotal); const shippingAddress = composeAddress(addressFields); const timestamp = now(); try { db.exec("BEGIN"); const orderResult = db .prepare( `INSERT INTO orders ( user_id, subtotal_cents, discount_cents, total_cents, shipping_address, payment_method, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?)` ) .run(user.id, subtotal, discountTotal, total, shippingAddress, paymentMethod, timestamp); const orderId = orderResult.lastInsertRowid; const insertItem = db.prepare( `INSERT INTO order_items ( order_id, product_id, quantity, unit_price_cents, line_total_cents ) VALUES (?, ?, ?, ?, ?)` ); for (const row of rows) { insertItem.run(orderId, row.product_id, row.quantity, row.price_cents, row.price_cents * row.quantity); } for (const discount of discounts) { if (discount.type === "discovery") { db.prepare( "UPDATE discovery_credits SET redeemed_order_id = ?, redeemed_at = ? WHERE id = ?" ).run(orderId, timestamp, discount.creditId); } if (discount.type === "sample") { db.prepare( "UPDATE sample_credits SET redeemed_order_id = ?, redeemed_at = ? WHERE id = ?" ).run(orderId, timestamp, discount.creditId); } } const hasDiscoverySet = rows.some((row) => row.product_id === "discovery-set"); const hadDiscoveryCredit = db .prepare("SELECT id FROM discovery_credits WHERE user_id = ? LIMIT 1") .get(user.id); if (hasDiscoverySet && !hadDiscoveryCredit) { db.prepare( `INSERT INTO discovery_credits ( user_id, order_id, amount_cents, redeemed_order_id, created_at, redeemed_at ) VALUES (?, ?, 4800, NULL, ?, NULL)` ).run(user.id, orderId, timestamp); } const sampleRows = rows.filter((row) => row.kind === "sample"); for (const row of sampleRows) { const existing = db .prepare("SELECT id FROM sample_credits WHERE user_id = ? AND slug = ? LIMIT 1") .get(user.id, row.slug); if (!existing) { db.prepare( `INSERT INTO sample_credits ( user_id, slug, order_id, amount_cents, redeemed_order_id, created_at, redeemed_at ) VALUES (?, ?, ?, ?, NULL, ?, NULL)` ).run(user.id, row.slug, orderId, row.price_cents, timestamp); } } db.prepare("DELETE FROM cart_items WHERE user_id = ?").run(user.id); db.exec("COMMIT"); const updatedUser = db.prepare("SELECT * FROM users WHERE id = ?").get(user.id); json(res, 200, { order: getOrders(user.id).find((order) => order.id === orderId), cart: getCart(user.id), orders: getOrders(user.id), user: rowToUser(updatedUser), }); } catch (error) { db.exec("ROLLBACK"); throw error; } }; const subscribeProduct = async (req, res, user) => { const body = await readBody(req); const productId = String(body.product_id || body.productId || "").trim(); const type = String(body.type || "restock").trim(); const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId); if (!product) { json(res, 404, { error: "Product not found." }); return; } db.prepare( `INSERT OR IGNORE INTO product_subscriptions (user_id, product_id, type, created_at) VALUES (?, ?, ?, ?)` ).run(user.id, productId, type, now()); json(res, 200, { ok: true, message: `${product.name} ${type} subscription saved.`, subscriptions: getProductSubscriptions(user.id), }); }; const deleteProductSubscription = (res, user, id) => { db.prepare("DELETE FROM product_subscriptions WHERE id = ? AND user_id = ?").run( id, user.id ); json(res, 200, { ok: true, subscriptions: getProductSubscriptions(user.id), }); }; const smallBatch = (res, user) => { const loyaltyStatus = getLoyaltyStatus(user.id); const releases = loyaltyStatus.unlocked ? [ { type: "Archive Batch", name: "KALTER BETON Archive Batch 01", note: "A colder iris-heavy return from the first concrete accord trials.", }, { type: "Prototype", name: "NASSER MARMOR Fog Prototype", note: "A misted marble study with softened aldehydes and mineral musk.", }, { type: "Small Batch", name: "SCHWARZES BENZIN Night Run", note: "Low-light petrol, birch smoke, and leather in a numbered run.", }, ] : []; json(res, 200, { loyaltyStatus, releases }); }; const route = async (req, res) => { if (req.method === "OPTIONS") { json(res, 204, {}); return; } const url = new URL(req.url, `http://${req.headers.host}`); const path = url.pathname; if (req.method === "GET" && path === "/api/catalog") { json(res, 200, { products: db.prepare("SELECT * FROM products ORDER BY id ASC").all() }); return; } if (req.method === "POST" && path === "/api/auth/register") return register(req, res); if (req.method === "POST" && path === "/api/auth/login") return login(req, res); if (req.method === "POST" && path === "/api/auth/logout") { const token = getBearerToken(req); if (token) db.prepare("DELETE FROM sessions WHERE token = ?").run(token); json(res, 200, { ok: true }); return; } if (req.method === "GET" && path === "/api/auth/session") { const auth = requireAuth(req, res); if (!auth) return; json(res, 200, stateForUser(auth.user, auth.token)); return; } const auth = requireAuth(req, res); if (!auth) return; const { user } = auth; if (req.method === "PATCH" && path === "/api/profile") return patchProfile(req, res, user); if (req.method === "PATCH" && path === "/api/notifications") return patchNotifications(req, res, user); if (req.method === "GET" && path === "/api/cart") { json(res, 200, { cart: getCart(user.id) }); return; } if (req.method === "POST" && path === "/api/cart/items") return addCartItem(req, res, user); if (req.method === "POST" && path === "/api/cart/checkout") return checkout(req, res, user); if (req.method === "POST" && path === "/api/product-subscriptions") return subscribeProduct(req, res, user); if (req.method === "GET" && path === "/api/small-batch") return smallBatch(res, user); const subscriptionMatch = path.match(/^\/api\/product-subscriptions\/(\d+)$/); if (subscriptionMatch && req.method === "DELETE") { return deleteProductSubscription(res, user, Number(subscriptionMatch[1])); } const itemMatch = path.match(/^\/api\/cart\/items\/([^/]+)$/); if (itemMatch && req.method === "PATCH") { return patchCartItem(req, res, user, decodeURIComponent(itemMatch[1])); } if (itemMatch && req.method === "DELETE") { return deleteCartItem(res, user, decodeURIComponent(itemMatch[1])); } json(res, 404, { error: "Route not found." }); }; createServer((req, res) => { route(req, res).catch((error) => { console.error(error); json(res, 500, { error: "Server error." }); }); }).listen(PORT, () => { console.log(`Shop API listening on http://localhost:${PORT}`); });