import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import crypto from 'node:crypto' import express from 'express' import cors from 'cors' import Stripe from 'stripe' import dotenv from 'dotenv' import * as applications from './applicationsStore.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) dotenv.config({ path: path.join(__dirname, '..', '.env') }) const distDir = path.join(__dirname, '..', 'dist') const isProd = process.env.NODE_ENV === 'production' const PORT = Number(process.env.PORT) || 4242 const defaultClientUrl = isProd ? `http://127.0.0.1:${PORT}` : 'http://localhost:5173' const clientUrl = (process.env.CLIENT_URL || defaultClientUrl).replace(/\/$/, '') const allowedOrigins = new Set([ clientUrl, 'http://localhost:5173', 'http://127.0.0.1:5173', `http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`, 'http://home.thetempleofdoom.com', 'https://home.thetempleofdoom.com', ]) process.umask(0o077) const app = express() app.disable('x-powered-by') app.set('trust proxy', true) app.use( cors({ origin(origin, cb) { if (!origin || allowedOrigins.has(origin)) { cb(null, true) return } cb(null, false) }, }), ) app.use((_, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff') res.setHeader('X-Frame-Options', 'SAMEORIGIN') res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin') res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') next() }) function rateLimit({ windowMs, max }) { const buckets = new Map() return (req, res, next) => { const key = req.ip || req.socket.remoteAddress || 'unknown' const now = Date.now() const bucket = buckets.get(key) if (!bucket || bucket.resetAt <= now) { buckets.set(key, { count: 1, resetAt: now + windowMs }) next() return } bucket.count += 1 if (bucket.count > max) { res.status(429).json({ error: 'Too many requests. Please try again later.' }) return } next() } } function hasValidApplicationSecret(req, record) { const secret = req.get('x-application-secret') || req.body?.applicationSecret const storedHash = record?.applicationSecretHash if (!secret || !storedHash) return false const providedHash = applications.hashApplicationSecret(secret) return crypto.timingSafeEqual(Buffer.from(providedHash), Buffer.from(storedHash)) } const applicationInitLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 }) const checkoutLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 40 }) const stripeSecret = process.env.STRIPE_SECRET_KEY const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET /** Webhooks need the raw body for signature verification — must run before express.json(). */ app.post( '/api/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => { if (!stripeSecret) { return res.status(503).send('Stripe secret not configured.') } if (!webhookSecret) { return res.status(503).send('STRIPE_WEBHOOK_SECRET not set.') } const sig = req.headers['stripe-signature'] if (!sig || typeof sig !== 'string') { return res.status(400).send('Missing stripe-signature header.') } let event try { const stripe = new Stripe(stripeSecret) event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret) } catch (err) { const msg = err instanceof Error ? err.message : 'Invalid payload' console.error('Webhook signature verification failed:', msg) return res.status(400).send(`Webhook Error: ${msg}`) } switch (event.type) { case 'checkout.session.completed': { const session = event.data.object console.info( '[Stripe] checkout.session.completed', session.id, session.metadata?.house_name || '', ) { const ref = session.metadata?.application_ref if (ref && applications.isValidApplicationRef(ref)) { void applications.markPaidFromStripe(ref, session.id).catch((err) => { console.error('[applications] mark paid failed', err) }) } } break } default: break } res.json({ received: true }) }, ) app.use(express.json({ limit: '1mb' })) app.get('/api/health', (_, res) => { const payload = { ok: true, site: 'Sacred Villas', uptime_s: Math.round(process.uptime()), } if (!isProd) { payload.stripe = { secret_key_configured: Boolean(stripeSecret), publishable_key_configured: Boolean(process.env.VITE_STRIPE_PUBLISHABLE_KEY), webhook_secret_configured: Boolean(webhookSecret), } payload.client_url_configured = Boolean(process.env.CLIENT_URL) payload.applications_store = 'file' } res.json(payload) }) app.post('/api/applications/init', applicationInitLimiter, async (_, res) => { try { const record = await applications.createApplicationRecord({ payload: { version: 1 }, }) res.json({ applicationRef: record.applicationRef, applicationSecret: record.applicationSecret, createdAt: record.createdAt, }) } catch (err) { console.error(err) res.status(500).json({ error: 'Could not create application file.' }) } }) app.put('/api/applications/:ref', async (req, res) => { try { const { ref } = req.params if (!applications.isValidApplicationRef(ref)) { return res.status(400).json({ error: 'Invalid application reference.' }) } const existing = await applications.readApplication(ref) if (!existing) { return res.status(404).json({ error: 'Application not found.' }) } if (!hasValidApplicationSecret(req, existing)) { return res.status(401).json({ error: 'Invalid application credentials.' }) } if (existing.status === 'paid') { return res.status(409).json({ error: 'Paid applications cannot be changed.' }) } const payload = req.body?.payload if (!payload || typeof payload !== 'object') { return res.status(400).json({ error: 'payload object is required.' }) } const identity = payload.identity ?? {} const nextStatus = req.body?.status let status = existing.status if (nextStatus === 'submitted') status = 'submitted' else if (nextStatus === 'draft') status = 'draft' const updated = await applications.updateApplication(ref, { payload, applicantEmail: String(identity.email ?? '').slice(0, 256), applicantPhone: String(identity.primaryPhone ?? '').slice(0, 80), paymentType: payload.prefs?.paymentPath ? String(payload.prefs.paymentPath).slice(0, 32) : existing.paymentType, status, }) res.json({ ok: true, applicationRef: updated.applicationRef, updatedAt: updated.updatedAt, status: updated.status, }) } catch (err) { console.error(err) res.status(500).json({ error: 'Could not save application.' }) } }) app.post('/api/applications/sync-payment', checkoutLimiter, async (req, res) => { try { if (!stripeSecret) { return res.status(503).json({ error: 'Stripe is not configured on this host.' }) } const sessionId = req.body?.sessionId if (!sessionId || typeof sessionId !== 'string') { return res.status(400).json({ error: 'sessionId is required.' }) } const stripe = new Stripe(stripeSecret) const session = await stripe.checkout.sessions.retrieve(String(sessionId).slice(0, 128)) const ref = session.metadata?.application_ref if (!ref || !applications.isValidApplicationRef(ref)) { return res.json({ ok: false, reason: 'no_application_ref' }) } if (session.status === 'complete' && session.payment_status === 'paid') { await applications.markPaidFromStripe(ref, session.id) return res.json({ ok: true, applicationRef: ref }) } res.json({ ok: false, reason: 'not_paid', status: session.status, payment_status: session.payment_status, }) } catch (err) { console.error(err) res.status(500).json({ error: 'Unable to sync payment status.' }) } }) function assertApplicationsAdmin(req, res) { const token = process.env.APPLICATIONS_ADMIN_TOKEN if (!token) { res.status(503).json({ error: 'Admin listing is disabled. Set APPLICATIONS_ADMIN_TOKEN in the server environment to enable it.', }) return false } const auth = req.headers.authorization || '' const bearer = auth.startsWith('Bearer ') ? auth.slice(7).trim() : '' if (bearer !== token) { res.status(401).json({ error: 'Unauthorized.' }) return false } return true } app.get('/api/admin/applications', async (req, res) => { if (!assertApplicationsAdmin(req, res)) return const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 80)) const rows = await applications.listApplicationSummaries(limit) res.json({ applications: rows }) }) app.get('/api/admin/applications/:ref', async (req, res) => { if (!assertApplicationsAdmin(req, res)) return const { ref } = req.params if (!applications.isValidApplicationRef(ref)) { return res.status(400).json({ error: 'Invalid application reference.' }) } const doc = await applications.readApplication(ref) if (!doc) { return res.status(404).json({ error: 'Not found.' }) } res.json(doc) }) app.post('/api/create-checkout-session', checkoutLimiter, async (req, res) => { try { if (!stripeSecret) { return res.status(503).json({ error: 'Payments are not configured yet. Add STRIPE_SECRET_KEY on the server.', }) } const stripe = new Stripe(stripeSecret) const { applicationRef, paymentType, houseId, houseName, houseArea, customerEmail, fullName, phone, notes, } = req.body || {} if (!applicationRef || typeof applicationRef !== 'string') { return res.status(400).json({ error: 'applicationRef is required. Return to /apply and use the housing application wizard.', }) } if (!applications.isValidApplicationRef(applicationRef)) { return res.status(400).json({ error: 'Invalid application reference format.' }) } const appRow = await applications.readApplication(applicationRef) if (!appRow) { return res.status(400).json({ error: 'Unknown application reference. Please restart from the application page.', }) } if (!hasValidApplicationSecret(req, appRow)) { return res.status(401).json({ error: 'Invalid application credentials.' }) } if (paymentType !== 'deposit' && paymentType !== 'credit') { return res.status(400).json({ error: 'Invalid payment type.' }) } if (!customerEmail || typeof customerEmail !== 'string') { return res.status(400).json({ error: 'Email is required.' }) } if (!fullName || typeof fullName !== 'string') { return res.status(400).json({ error: 'Full name is required.' }) } const isDeposit = paymentType === 'deposit' const unitAmount = isDeposit ? 50000 : 7500 const productName = isDeposit ? 'Holding deposit — Sacred Villas' : 'Applicant credit screening — Sacred Villas' const propertyLine = houseName ? `Property: ${String(houseName).slice(0, 180)}` : 'Property: to be assigned with your concierge' const lineDescription = `${propertyLine}. Applicant: ${String(fullName).slice(0, 100)}. File: ${String(applicationRef).slice(0, 32)}`.slice( 0, 500, ) const session = await stripe.checkout.sessions.create({ ui_mode: 'embedded_page', mode: 'payment', customer_email: customerEmail.trim().slice(0, 256), line_items: [ { price_data: { currency: 'usd', unit_amount: unitAmount, product_data: { name: productName.slice(0, 120), description: lineDescription, }, }, quantity: 1, }, ], metadata: { application_ref: String(applicationRef).slice(0, 40), payment_type: paymentType, house_id: houseId ? String(houseId).slice(0, 120) : '', house_name: houseName ? String(houseName).slice(0, 120) : '', seattle_area: houseArea ? String(houseArea).slice(0, 120) : '', full_name: String(fullName).slice(0, 120), phone: phone ? String(phone).slice(0, 40) : '', notes: notes ? String(notes).slice(0, 500) : '', }, return_url: `${clientUrl}/apply/return?session_id={CHECKOUT_SESSION_ID}`, }) if (!session.client_secret) { return res.status(500).json({ error: 'Checkout session did not return a client secret.', }) } res.json({ clientSecret: session.client_secret }) } catch (err) { console.error(err) res.status(500).json({ error: 'Unable to start checkout.' }) } }) app.get('/api/checkout-session-status', checkoutLimiter, async (req, res) => { try { if (!stripeSecret) { return res.status(503).json({ error: 'Payments are not configured yet. Add STRIPE_SECRET_KEY on the server.', }) } const sessionId = req.query.session_id if (!sessionId || typeof sessionId !== 'string') { return res.status(400).json({ error: 'session_id query parameter is required.' }) } const stripe = new Stripe(stripeSecret) const session = await stripe.checkout.sessions.retrieve(String(sessionId).slice(0, 128)) res.json({ status: session.status, payment_status: session.payment_status, customer_email: session.customer_details?.email ?? null, application_ref: session.metadata?.application_ref ?? null, }) } catch (err) { console.error(err) res.status(500).json({ error: 'Unable to load session.' }) } }) if (isProd && fs.existsSync(distDir)) { app.use(express.static(distDir)) // Express 5 / path-to-regexp: named splat, not bare '*' app.get('/{*splat}', (req, res) => { res.sendFile(path.join(distDir, 'index.html')) }) } app.listen(PORT, '0.0.0.0', () => { console.log(`Sacred Villas API ${isProd ? '+ static' : ''} on http://0.0.0.0:${PORT}`) })