Files
alexa-amundson-portfolio/server.js
Claude 085dd1b186 feat: add real Stripe integration, E2E tests, and Pi deployment
- Express server with Stripe Checkout sessions, webhooks, and service catalog
- Frontend checkout page wired to Stripe API (consultation, audit, retainer)
- Playwright E2E tests covering health, services, checkout flow, and portfolio
- deploy-to-pi.sh: rsync + systemd deployment to Raspberry Pi(s)
- GitHub Actions workflow: run E2E tests then auto-deploy to Pi on merge
- Replace BlackRoad proprietary license with MIT
- Remove 11 bloated marketing/verification docs and fake metrics
- Clean up BlackRoad branding from homepage and workflows

https://claude.ai/code/session_01FmCd6rGDd2jS8JNzyL4e5G
2026-03-04 08:58:46 +00:00

168 lines
5.4 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const path = require('path');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
const PORT = process.env.PORT || 3000;
// Stripe webhook needs raw body — must be before express.json()
app.post(
'/api/webhooks/stripe',
express.raw({ type: 'application/json' }),
handleStripeWebhook
);
app.use(helmet({ contentSecurityPolicy: false }));
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname)));
// ─── Health check ───────────────────────────────────────────────
app.get('/api/health', (_req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
stripe: !!process.env.STRIPE_SECRET_KEY,
});
});
// ─── Stripe: get publishable key ────────────────────────────────
app.get('/api/stripe/config', (_req, res) => {
res.json({ publishableKey: process.env.STRIPE_PUBLISHABLE_KEY });
});
// ─── Stripe: create checkout session ────────────────────────────
const SERVICES = {
consultation: {
name: 'Technical Consultation (1 hr)',
amount: 15000, // $150.00
description: '1-hour technical consultation — architecture, infra, AI systems',
},
audit: {
name: 'Infrastructure Audit',
amount: 50000, // $500.00
description: 'Full infrastructure audit — security, performance, cost optimization',
},
retainer: {
name: 'Monthly Retainer',
amount: 300000, // $3,000.00
description: 'Monthly technical retainer — 20 hrs/month, priority support',
},
};
app.post('/api/stripe/checkout', async (req, res) => {
try {
const { serviceId, customerEmail } = req.body;
const service = SERVICES[serviceId];
if (!service) {
return res.status(400).json({ error: 'Invalid service' });
}
const baseUrl = process.env.BASE_URL || `http://localhost:${PORT}`;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
customer_email: customerEmail || undefined,
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: service.name,
description: service.description,
},
unit_amount: service.amount,
},
quantity: 1,
},
],
success_url: `${baseUrl}/pages/checkout.html?status=success&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/pages/checkout.html?status=cancelled`,
});
res.json({ sessionId: session.id, url: session.url });
} catch (err) {
console.error('Checkout error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── Stripe: retrieve session (for success page) ───────────────
app.get('/api/stripe/session/:id', async (req, res) => {
try {
const session = await stripe.checkout.sessions.retrieve(req.params.id);
res.json({
status: session.payment_status,
customerEmail: session.customer_details?.email,
amountTotal: session.amount_total,
});
} catch (err) {
res.status(404).json({ error: 'Session not found' });
}
});
// ─── Stripe: list services ──────────────────────────────────────
app.get('/api/services', (_req, res) => {
const services = Object.entries(SERVICES).map(([id, svc]) => ({
id,
...svc,
priceFormatted: `$${(svc.amount / 100).toFixed(2)}`,
}));
res.json(services);
});
// ─── Stripe webhook handler ────────────────────────────────────
async function handleStripeWebhook(req, res) {
const sig = req.headers['stripe-signature'];
let event;
try {
if (process.env.STRIPE_WEBHOOK_SECRET) {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} else {
event = JSON.parse(req.body);
}
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).json({ error: 'Webhook signature failed' });
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
console.log(
`Payment received: ${session.customer_details?.email}$${(session.amount_total / 100).toFixed(2)}`
);
break;
}
case 'payment_intent.succeeded': {
const intent = event.data.object;
console.log(`PaymentIntent succeeded: ${intent.id}`);
break;
}
default:
console.log(`Unhandled event: ${event.type}`);
}
res.json({ received: true });
}
// ─── Start ──────────────────────────────────────────────────────
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Portfolio server running on port ${PORT}`);
console.log(`Stripe: ${process.env.STRIPE_SECRET_KEY ? 'configured' : 'NOT configured — set STRIPE_SECRET_KEY'}`);
});
}
module.exports = { app, SERVICES };