The server code
Server code lives in a _worker.js file at the top of the bundle. This is the part that touches Brod, trimmed to the essentials:
// _worker.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
const email = request.headers.get('x-brod-user-email');
const role = request.headers.get('x-brod-role');
if (!email) return new Response('Sign in first', { status: 401 });
await env.DB.run(`CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY, by TEXT, cents INTEGER, note TEXT,
receipt TEXT, status TEXT DEFAULT 'pending', decided_by TEXT)`);
// Submit an expense with its receipt
if (request.method === 'POST' && url.pathname === '/api/expenses') {
const form = await request.formData();
const file = form.get('receipt');
if (!file || !/^(image\/|application\/pdf)/.test(file.type)) {
return new Response('Receipt must be an image or a PDF', { status: 400 });
}
const key = 'receipts/' + crypto.randomUUID();
await env.FILES.put(key, await file.arrayBuffer(), file.type);
await env.DB.run(
'INSERT INTO expenses (by, cents, note, receipt) VALUES (?, ?, ?, ?)',
[email, Math.round(Number(form.get('amount')) * 100), form.get('note'), key]);
return Response.json({ ok: true });
}
// Approve or reject: approvers only, never their own
if (request.method === 'POST' && url.pathname === '/api/decide') {
if (role !== 'approver') return new Response('Approvers only', { status: 403 });
const { id, approve } = await request.json();
const { changes } = await env.DB.run(
"UPDATE expenses SET status = ?, decided_by = ? WHERE id = ? AND status = 'pending' AND by != ?",
[approve ? 'approved' : 'rejected', email, id, email]);
return Response.json({ ok: changes === 1 });
}
// A receipt: the person who submitted it, or an approver
if (url.pathname.startsWith('/api/receipts/')) {
const id = Number(url.pathname.split('/').pop());
const [row] = await env.DB.query('SELECT by, receipt FROM expenses WHERE id = ?', [id]);
if (!row || (row.by !== email && role !== 'approver')) return new Response('Not found', { status: 404 });
return (await env.FILES.get(row.receipt)) ?? new Response('Not found', { status: 404 });
}
// The list: approvers see what is pending, submitters see their own
if (url.pathname === '/api/expenses') {
const rows = role === 'approver'
? await env.DB.query("SELECT * FROM expenses WHERE status = 'pending'")
: await env.DB.query('SELECT * FROM expenses WHERE by = ?', [email]);
return Response.json({ expenses: rows });
}
return new Response('Not found', { status: 404 });
},
};
The receipt check matters: only accepting images and PDFs keeps someone from uploading an HTML file and having it served back to a manager from the app's own address.