← back to Petitionyour
server.js
155 lines
const express = require('express');
const path = require('path');
const petitions = require('./lib/petitions');
const reps = require('./lib/reps');
const app = express();
const PORT = process.env.PORT || 4000;
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.set('trust proxy', true); // behind Cloudflare + nginx in prod
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Minimal, honest security headers — no framework dependency needed for this.
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
function siteUrl(req) {
const proto = req.headers['x-forwarded-proto'] || req.protocol;
return `${proto}://${req.get('host')}`;
}
// ---- health (smoke test target) ----
app.get('/health', (req, res) => {
res.status(200).type('text/plain').send('ok');
});
// ---- browse petitions ----
app.get('/', (req, res) => {
const list = petitions.listPetitions().sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt)
);
const totalSignatures = list.reduce((sum, p) => sum + p.signatureCount, 0);
res.render('index', {
petitions: list,
categories: petitions.CATEGORIES,
totalSignatures,
siteUrl: siteUrl(req),
});
});
// ---- create petition ----
app.get('/petitions/new', (req, res) => {
res.render('new', { categories: petitions.CATEGORIES, error: null, form: {} });
});
app.post('/petitions', (req, res) => {
try {
const p = petitions.createPetition(req.body);
res.redirect(`/petitions/${p.slug}?created=1`);
} catch (err) {
res.status(400).render('new', {
categories: petitions.CATEGORIES,
error: err.message,
form: req.body,
});
}
});
// ---- petition detail + sign ----
app.get('/petitions/:slug', (req, res) => {
const p = petitions.getPetitionBySlug(req.params.slug);
if (!p) return res.status(404).render('404', {});
const supporters = petitions.listSignaturesForPetition(p.id, { publicOnly: true }).slice(0, 25);
res.render('petition', {
p,
supporters,
reach: petitions.repReachForPetition(p.id),
siteUrl: siteUrl(req),
created: req.query.created === '1',
signed: req.query.signed === '1',
error: null,
form: {},
});
});
app.post('/petitions/:slug/sign', (req, res) => {
const p = petitions.getPetitionBySlug(req.params.slug);
if (!p) return res.status(404).render('404', {});
try {
petitions.addSignature(req.params.slug, req.body);
res.redirect(`/petitions/${req.params.slug}?signed=1#top`);
} catch (err) {
const supporters = petitions.listSignaturesForPetition(p.id, { publicOnly: true }).slice(0, 25);
res.status(400).render('petition', {
p,
supporters,
reach: petitions.repReachForPetition(p.id),
siteUrl: siteUrl(req),
created: false,
signed: false,
error: err.message,
form: req.body,
});
}
});
// ---- find & contact your representatives ----
// Standalone page. The rich resolution (actual members + contact forms) is
// rendered client-side by the reps panel via /api/reps; this page just seeds
// the ZIP and provides the always-current official .gov links as a backstop.
app.get('/find-reps', (req, res) => {
const zip = req.query.zip || '';
const hit = zip ? reps.resolveZip(zip) : null;
const links = hit ? reps.officialLinks(hit.stateAbbr, hit.zip) : null;
res.render('find-reps', { zip, hit, links, error: zip && !hit ? 'Could not match that ZIP code.' : null });
});
// Legacy JSON endpoint (state + official links). Kept for backward compat.
app.get('/api/find-reps', (req, res) => {
const zip = req.query.zip || '';
const hit = reps.resolveZip(zip);
if (!hit) {
return res.status(404).json({ ok: false, error: 'Could not match that ZIP code to a state.' });
}
res.json({ ok: true, ...hit, links: reps.officialLinks(hit.stateAbbr, hit.zip) });
});
// Rich resolution: ZIP (+ optional district) -> actual senators + house rep(s)
// with their official contact-form URLs and phones.
app.get('/api/reps', (req, res) => {
const zip = req.query.zip || '';
const district = req.query.district;
const resolved = reps.resolveReps({ zip, district });
if (!resolved) {
return res.status(404).json({ ok: false, error: 'Could not match that ZIP code to a state.' });
}
res.json({ ok: true, ...resolved, links: reps.officialLinks(resolved.stateAbbr, resolved.zip) });
});
// Precise House district from a full street address (free U.S. Census
// geocoder, no key). Returns the district so the client can re-call /api/reps.
app.get('/api/geocode-district', async (req, res) => {
const address = req.query.address || '';
const geo = await reps.geocodeAddressToDistrict(address);
if (!geo.ok) return res.status(422).json(geo);
res.json(geo);
});
// ---- 404 ----
app.use((req, res) => {
res.status(404).render('404', {});
});
app.listen(PORT, () => {
console.log(`petitionyour listening on http://127.0.0.1:${PORT}`);
});