← back to Hospitalitydesignreps
server.js
139 lines
const express = require('express');
const path = require('path');
const fs = require('fs');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 9927;
const GA_ID = process.env.GA_ID || 'G-M79CJBLRFH';
const reps = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'reps.json'), 'utf8'));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}));
app.use(compression());
app.use(morgan('tiny'));
// HTTP Basic Auth gate (Steve: admin / DW2024!). Env-overridable. /healthz stays open so the
// fleet uptime/canary probes still work without credentials.
const BASIC_USER = process.env.BASIC_AUTH_USER || 'admin';
const BASIC_PASS = process.env.BASIC_AUTH_PASS || 'DW2024!';
app.use((req, res, next) => {
if (req.path === '/healthz') return next();
const hdr = req.headers.authorization || '';
const [scheme, b64] = hdr.split(' ');
if (scheme === 'Basic' && b64) {
const decoded = Buffer.from(b64, 'base64').toString();
const idx = decoded.indexOf(':');
const u = decoded.slice(0, idx);
const p = decoded.slice(idx + 1); // rejoin rest so a ':' in the password is preserved
if (u === BASIC_USER && p === BASIC_PASS) return next();
}
res.set('WWW-Authenticate', 'Basic realm="Hospitality Design Reps"');
return res.status(401).send('Authentication required.');
});
app.use((req, res, next) => {
res.set('Cache-Control', 'no-store, must-revalidate');
next();
});
// 404-guard: never serve snapshot files (.bak / .bak.* / .pre-*) from static root
app.use((req, res, next) => {
if (/\.(bak|pre-[^/]*)($|\.)|\.bak\./i.test(req.path)) {
return res.status(404).render('404', { site: SITE });
}
next();
});
app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
const allTerritories = Array.from(new Set(reps.flatMap(r => r.territories))).sort();
const allLines = Array.from(new Set(reps.flatMap(r => r.lines))).sort();
const allVerticals = Array.from(new Set(reps.flatMap(r => r.vertical))).sort();
const allCities = Array.from(new Set(reps.map(r => r.city))).sort();
const SITE = {
domain: 'hospitalitydesignreps.com',
name: 'Hospitality Design Reps',
tagline: 'The U.S. hospitality FF&E rep map — who carries what, where, and for which brand standard',
description: 'An independent, free index of hospitality FF&E rep agencies across the United States. Filter sales reps and showroom partners by state territory, by hospitality segment (boutique hotel, hotel brand, resort, casino, cruise, F&B, spa, hospitality healthcare), or by manufacturer line carried.',
email: 'info@hospitalitydesignreps.com',
phone: '888-373-4564',
address: 'Designer Wallcoverings, 15442 Ventura Bl #102, Sherman Oaks CA 91403',
ga_id: GA_ID,
};
function sortReps(list, sort) {
const copy = list.slice();
switch (sort) {
case 'name':
return copy.sort((a, b) => a.name.localeCompare(b.name));
case 'agency':
return copy.sort((a, b) => a.agency.localeCompare(b.agency));
case 'city':
return copy.sort((a, b) => a.city.localeCompare(b.city));
case 'territory':
return copy.sort((a, b) => (a.territories[0] || '').localeCompare(b.territories[0] || ''));
case 'newest':
default:
return copy.sort((a, b) => b.id - a.id);
}
}
app.get('/', (req, res) => {
const { q, territory, vertical, line, sort } = req.query;
let filtered = reps.slice();
if (q) {
const needle = String(q).toLowerCase();
filtered = filtered.filter(r =>
r.name.toLowerCase().includes(needle) ||
r.agency.toLowerCase().includes(needle) ||
r.city.toLowerCase().includes(needle) ||
r.lines.some(l => l.toLowerCase().includes(needle))
);
}
if (territory) filtered = filtered.filter(r => r.territories.includes(String(territory)));
if (vertical) filtered = filtered.filter(r => r.vertical.includes(String(vertical)));
if (line) filtered = filtered.filter(r => r.lines.includes(String(line)));
filtered = sortReps(filtered, sort);
res.render('index', {
site: SITE,
reps: filtered,
total: reps.length,
matched: filtered.length,
filters: { q: q || '', territory: territory || '', vertical: vertical || '', line: line || '', sort: sort || 'newest' },
facets: { territories: allTerritories, lines: allLines, verticals: allVerticals, cities: allCities },
});
});
app.get('/rep/:id', (req, res) => {
const id = Number(req.params.id);
const rep = reps.find(r => r.id === id);
if (!rep) return res.status(404).render('404', { site: SITE });
const related = reps.filter(r => r.id !== rep.id && r.vertical.some(v => rep.vertical.includes(v))).slice(0, 4);
res.render('rep', { site: SITE, rep, related });
});
app.get('/about', (req, res) => {
res.render('about', { site: SITE });
});
app.get('/healthz', (req, res) => res.json({ ok: true, port: PORT, reps: reps.length }));
app.use((req, res) => res.status(404).render('404', { site: SITE }));
app.listen(PORT, () => {
console.log(`hospitalitydesignreps listening on :${PORT} (${reps.length} reps loaded, GA ${GA_ID})`);
});