← back to Jill Website
src/server.ts
285 lines
import express, { Request, Response, NextFunction, Application } from 'express';
import session from 'express-session';
import path from 'path';
import dotenv from 'dotenv';
import { PropertyModel } from './models/Property';
import propertiesRouter from './routes/properties';
import inquiriesRouter from './routes/inquiries';
import activitiesRouter from './routes/activities';
import adminRouter from './routes/admin';
import linkValidationRouter from './routes/linkValidation';
import { requireAuth } from './middleware/auth';
import { formatPhoneNumber, formatPhoneForTel } from './utils/phoneFormatter';
// Site-wide basic auth middleware
const siteAuth = (req: Request, res: Response, next: NextFunction): void => {
// Skip auth for health check
if (req.path === '/health') {
next();
return;
}
const authHeader = req.headers.authorization;
if (!authHeader) {
res.setHeader('WWW-Authenticate', 'Basic realm="Jill Website"');
res.status(401).send('Authentication required');
return;
}
const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
if (credentials[0] === SITE_USERNAME && credentials[1] === SITE_PASSWORD) {
next();
} else {
res.setHeader('WWW-Authenticate', 'Basic realm="Jill Website"');
res.status(401).send('Invalid credentials');
}
};
// Extend session data type
declare module 'express-session' {
interface SessionData {
adminUser?: {
username: string;
loginTime: number;
};
}
}
// Load environment variables
dotenv.config();
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const app: Application = express();
const PORT = process.env.PORT || 3000;
const SITE_USERNAME = requireEnv('SITE_USERNAME');
const SITE_PASSWORD = requireEnv('SITE_PASSWORD');
const SESSION_SECRET = requireEnv('SESSION_SECRET');
requireEnv('ADMIN_PASSWORD');
// Session middleware (must be before routes that use it)
app.use(session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Site-wide authentication (before other routes)
app.use(siteAuth);
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// SECURITY: 404-guard for snapshot/backup paths. Even if a .bak / .pre-* /
// .orig / .rej / .swp / ~ file ever slips into public/ via a sloppy edit,
// it must NEVER be servable over HTTP. Block before static handler runs.
app.use((req: Request, res: Response, next: NextFunction) => {
if (/(\.bak(\.[^/]+)?|\.pre-[^/]+|\.orig|\.rej|\.swp|~)$/i.test(req.path)) {
res.status(404).send('Not Found');
return;
}
next();
});
// Static files
app.use(express.static(path.join(__dirname, '../public')));
// View engine setup
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'ejs');
// Make phone formatter functions available in all views
app.use((req: Request, res: Response, next: NextFunction) => {
res.locals.formatPhoneNumber = formatPhoneNumber;
res.locals.formatPhoneForTel = formatPhoneForTel;
next();
});
// API Routes
app.use('/api', propertiesRouter);
app.use('/api', inquiriesRouter);
app.use('/api', activitiesRouter);
app.use('/api/link-validation', linkValidationRouter);
// Admin API Routes
app.use('/api/admin', adminRouter);
// Basic health check route
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Home route - Render homepage with properties
app.get('/', async (req: Request, res: Response) => {
try {
const propertyModel = new PropertyModel();
const properties = await propertyModel.findAll();
res.render('index', {
properties,
currentYear: new Date().getFullYear()
});
} catch (error) {
console.error('Error loading homepage:', error);
// Render with empty properties array on error
res.render('index', {
properties: [],
currentYear: new Date().getFullYear()
});
}
});
// Beaches route
app.get('/beaches', (req: Request, res: Response) => {
res.render('beaches', {
currentYear: new Date().getFullYear(),
currentPage: 'beaches'
});
});
// Surfing route
app.get('/surfing', (req: Request, res: Response) => {
res.render('surfing', {
currentYear: new Date().getFullYear(),
currentPage: 'surfing'
});
});
// Yoga & Wellness route
app.get('/yoga-wellness', (req: Request, res: Response) => {
res.render('yoga-wellness', {
currentYear: new Date().getFullYear(),
currentPage: 'yoga-wellness'
});
});
// Nature Adventures route
app.get('/nature-adventures', (req: Request, res: Response) => {
res.render('nature-adventures', {
currentYear: new Date().getFullYear(),
currentPage: 'nature-adventures'
});
});
// Culinary Delights route
app.get('/culinary-delights', (req: Request, res: Response) => {
res.render('culinary-delights', {
currentYear: new Date().getFullYear(),
currentPage: 'culinary-delights'
});
});
// Adventure Activities route
app.get('/adventure-activities', (req: Request, res: Response) => {
res.render('adventure-activities', {
currentYear: new Date().getFullYear(),
currentPage: 'adventure-activities'
});
});
// Gallery Demo route
app.get('/gallery-demo', (req: Request, res: Response) => {
res.render('gallery-demo', {
currentYear: new Date().getFullYear()
});
});
// Video demo route
app.get('/video-demo', (req: Request, res: Response) => {
res.render('video-demo', {
currentYear: new Date().getFullYear()
});
});
// Luna property page route
app.get('/properties/luna', (req: Request, res: Response) => {
res.render('properties-luna', {
currentYear: new Date().getFullYear()
});
});
// Sol property page route
app.get('/properties/sol', (req: Request, res: Response) => {
res.render('properties-sol', {
currentYear: new Date().getFullYear()
});
});
// Casita property page route
app.get('/properties/casita', (req: Request, res: Response) => {
res.render('properties-casita', {
currentYear: new Date().getFullYear()
});
});
// Main House property page route
app.get('/properties/main-house', (req: Request, res: Response) => {
res.render('properties-main-house', {
currentYear: new Date().getFullYear()
});
});
// Property detail route
app.get('/property/:id', async (req: Request, res: Response) => {
try {
const idParam = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
const propertyId = parseInt(idParam, 10);
if (isNaN(propertyId)) {
return res.status(400).render('error', {
message: 'Invalid property ID',
currentYear: new Date().getFullYear()
});
}
const propertyModel = new PropertyModel();
const property = await propertyModel.findById(propertyId);
if (!property) {
return res.status(404).render('error', {
message: 'Property not found',
currentYear: new Date().getFullYear()
});
}
res.render('property-detail', {
property,
currentYear: new Date().getFullYear()
});
} catch (error) {
console.error('Error loading property detail:', error);
res.status(500).render('error', {
message: 'Failed to load property details',
currentYear: new Date().getFullYear()
});
}
});
// 404 handler
app.use((req: Request, res: Response) => {
res.status(404).json({ error: 'Not Found' });
});
// Start server
if (require.main === module) {
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
console.log(`📍 Environment: ${process.env.NODE_ENV || 'development'}`);
});
}
export default app;