refresh
This commit is contained in:
481
backend/index.js
481
backend/index.js
@@ -1,18 +1,67 @@
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { body, param, validationResult } from 'express-validator';
|
||||
import winston from 'winston';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Logger setup
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: path.join(__dirname, 'error.log'), level: 'error' }),
|
||||
new winston.transports.File({ filename: path.join(__dirname, 'combined.log') }),
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const PORT = 4000;
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
// CORS Configuration
|
||||
const corsOptions = {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
optionsSuccessStatus: 200
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// Serve static frontend files in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const frontendPath = path.join(__dirname, '..', 'dist');
|
||||
app.use(express.static(frontendPath));
|
||||
}
|
||||
|
||||
// SQLite setup
|
||||
const dbPath = path.join(__dirname, 'awards.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'awards.db');
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
logger.error('Failed to connect to database:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
logger.info('Connected to SQLite database');
|
||||
});
|
||||
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE IF NOT EXISTS awards (
|
||||
@@ -28,27 +77,86 @@ db.serialize(() => {
|
||||
)`);
|
||||
});
|
||||
|
||||
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
||||
// Rate limiters
|
||||
const generalLimiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// Basic auth middleware for management endpoints
|
||||
const basicAuth = (req, res, next) => {
|
||||
const nominationLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: parseInt(process.env.RATE_LIMIT_NOMINATION_MAX) || 10,
|
||||
message: { error: 'Too many nominations, please try again later.' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
const emojiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: parseInt(process.env.RATE_LIMIT_EMOJI_MAX) || 50,
|
||||
message: { error: 'Too many emoji reactions, please try again later.' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
app.use('/awards', generalLimiter);
|
||||
|
||||
// Basic auth middleware with bcrypt
|
||||
const basicAuth = async (req, res, next) => {
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || !auth.startsWith('Basic ')) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="Management"');
|
||||
logger.warn('Authentication required - no credentials provided');
|
||||
return res.status(401).send('Authentication required.');
|
||||
}
|
||||
const credentials = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
|
||||
const [user, pass] = credentials;
|
||||
if (user === 'admin' && pass === 'password') return next();
|
||||
res.set('WWW-Authenticate', 'Basic realm="Management"');
|
||||
return res.status(401).send('Invalid credentials.');
|
||||
|
||||
try {
|
||||
const credentials = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
|
||||
const [user, pass] = credentials;
|
||||
|
||||
const expectedUser = process.env.ADMIN_USERNAME || 'admin';
|
||||
const expectedHash = process.env.ADMIN_PASSWORD_HASH;
|
||||
|
||||
if (!expectedHash) {
|
||||
logger.error('ADMIN_PASSWORD_HASH not configured');
|
||||
return res.status(500).json({ error: 'Server configuration error' });
|
||||
}
|
||||
|
||||
if (user === expectedUser && await bcrypt.compare(pass, expectedHash)) {
|
||||
logger.info('Successful admin authentication');
|
||||
return next();
|
||||
}
|
||||
|
||||
logger.warn('Failed admin authentication attempt');
|
||||
res.set('WWW-Authenticate', 'Basic realm="Management"');
|
||||
return res.status(401).send('Invalid credentials.');
|
||||
} catch (error) {
|
||||
logger.error('Error in authentication:', error);
|
||||
res.set('WWW-Authenticate', 'Basic realm="Management"');
|
||||
return res.status(401).send('Authentication error.');
|
||||
}
|
||||
};
|
||||
|
||||
// Validation middleware
|
||||
const validate = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
logger.warn('Validation errors:', errors.array());
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Get all awards
|
||||
app.get('/awards', (req, res) => {
|
||||
db.all('SELECT * FROM awards ORDER BY submitted_date DESC', [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
// Parse emoji_tally JSON for each row
|
||||
db.all('SELECT * FROM awards WHERE approved_date IS NOT NULL ORDER BY submitted_date DESC', [], (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('Error fetching awards:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch awards' });
|
||||
}
|
||||
const awards = rows.map(row => ({
|
||||
...row,
|
||||
emoji_tally: row.emoji_tally ? JSON.parse(row.emoji_tally) : {}
|
||||
@@ -58,121 +166,188 @@ app.get('/awards', (req, res) => {
|
||||
});
|
||||
|
||||
// Add a new award
|
||||
app.post('/awards', async (req, res) => {
|
||||
const { category, address, submitted_by } = req.body;
|
||||
if (!category || !address) {
|
||||
return res.status(400).json({ error: 'Category and address are required.' });
|
||||
}
|
||||
let lat = null, lng = null;
|
||||
let isPB = false;
|
||||
try {
|
||||
const resp = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`,
|
||||
{ headers: { 'User-Agent': 'react-awards-map/1.0 (contact@yourdomain.com)' } });
|
||||
const data = await resp.json();
|
||||
if (data && data[0]) {
|
||||
lat = parseFloat(data[0].lat);
|
||||
lng = parseFloat(data[0].lon);
|
||||
// Check if address is in Pacific Beach
|
||||
const displayName = data[0].display_name || '';
|
||||
const addressObj = data[0].address || {};
|
||||
// Accept if display_name contains 'Pacific Beach' or zip is 92109
|
||||
if (
|
||||
displayName.toLowerCase().includes('pacific beach') ||
|
||||
addressObj.suburb === 'Pacific Beach' ||
|
||||
addressObj.neighbourhood === 'Pacific Beach' ||
|
||||
addressObj.city === 'San Diego' && addressObj.postcode === '92109' ||
|
||||
addressObj.postcode === '92109'
|
||||
) {
|
||||
isPB = true;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore geocode errors */ }
|
||||
if (!isPB) {
|
||||
return res.status(400).json({ error: 'Address must be in Pacific Beach (92109, San Diego, CA).' });
|
||||
}
|
||||
db.run(
|
||||
'INSERT INTO awards (category, address, submitted_by, emoji_tally, submitted_date, lat, lng) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)',
|
||||
[category, address, submitted_by || null, '{}', lat, lng],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [this.lastID], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
row.emoji_tally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
res.status(201).json(row);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
app.post('/awards',
|
||||
nominationLimiter,
|
||||
body('category').trim().isLength({ min: 1, max: 200 }).withMessage('Category must be 1-200 characters'),
|
||||
body('address').trim().isLength({ min: 1, max: 500 }).withMessage('Address must be 1-500 characters'),
|
||||
body('submitted_by').optional().trim().isLength({ max: 255 }).withMessage('submitted_by must be max 255 characters'),
|
||||
validate,
|
||||
async (req, res) => {
|
||||
const { category, address, submitted_by } = req.body;
|
||||
|
||||
let lat = null, lng = null;
|
||||
let isPB = false;
|
||||
|
||||
try {
|
||||
const nominatimUrl = process.env.NOMINATIM_BASE_URL || 'https://nominatim.openstreetmap.org';
|
||||
const userAgent = process.env.NOMINATIM_USER_AGENT || 'BestOfPBAwardsApp/1.0';
|
||||
|
||||
const resp = await fetch(
|
||||
`${nominatimUrl}/search?format=json&q=${encodeURIComponent(address)}`,
|
||||
{ headers: { 'User-Agent': userAgent } }
|
||||
);
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (data && data[0]) {
|
||||
lat = parseFloat(data[0].lat);
|
||||
lng = parseFloat(data[0].lon);
|
||||
|
||||
const displayName = data[0].display_name || '';
|
||||
const addressObj = data[0].address || {};
|
||||
|
||||
if (
|
||||
displayName.toLowerCase().includes('pacific beach') ||
|
||||
addressObj.suburb === 'Pacific Beach' ||
|
||||
addressObj.neighbourhood === 'Pacific Beach' ||
|
||||
(addressObj.city === 'San Diego' && addressObj.postcode === '92109') ||
|
||||
addressObj.postcode === '92109'
|
||||
) {
|
||||
isPB = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Geocoding error:', e);
|
||||
}
|
||||
|
||||
if (!isPB) {
|
||||
logger.warn('Invalid address submitted:', address);
|
||||
return res.status(400).json({ error: 'Address must be in Pacific Beach (92109, San Diego, CA).' });
|
||||
}
|
||||
|
||||
// Update emoji tally for an award
|
||||
app.patch('/awards/:id/emojis', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { emoji, count } = req.body;
|
||||
if (!emoji || typeof count !== 'number') {
|
||||
return res.status(400).json({ error: 'Emoji and count are required.' });
|
||||
}
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => {
|
||||
if (err || !row) return res.status(404).json({ error: 'Award not found.' });
|
||||
let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
emojiTally[emoji] = count;
|
||||
db.run(
|
||||
'UPDATE awards SET emoji_tally = ? WHERE id = ?',
|
||||
[JSON.stringify(emojiTally), id],
|
||||
'INSERT INTO awards (category, address, submitted_by, emoji_tally, submitted_date, lat, lng) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)',
|
||||
[category, address, submitted_by || null, '{}', lat, lng],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json({ id, emoji_tally: emojiTally });
|
||||
if (err) {
|
||||
logger.error('Error inserting award:', err);
|
||||
return res.status(500).json({ error: 'Failed to create award' });
|
||||
}
|
||||
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [this.lastID], (err, row) => {
|
||||
if (err) {
|
||||
logger.error('Error fetching created award:', err);
|
||||
return res.status(500).json({ error: 'Award created but failed to fetch' });
|
||||
}
|
||||
row.emoji_tally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
logger.info('Award created:', { id: row.id, category, address });
|
||||
res.status(201).json(row);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove or decrement an emoji tally for an award
|
||||
app.patch('/awards/:id/emojis/remove', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { emoji } = req.body;
|
||||
if (!emoji) {
|
||||
return res.status(400).json({ error: 'Emoji is required.' });
|
||||
}
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => {
|
||||
if (err || !row) return res.status(404).json({ error: 'Award not found.' });
|
||||
let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
if (emojiTally[emoji]) {
|
||||
emojiTally[emoji] = emojiTally[emoji] - 1;
|
||||
if (emojiTally[emoji] <= 0) {
|
||||
delete emojiTally[emoji];
|
||||
);
|
||||
|
||||
// Update emoji tally for an award
|
||||
app.patch('/awards/:id/emojis',
|
||||
emojiLimiter,
|
||||
param('id').isInt().withMessage('Invalid award ID'),
|
||||
body('emoji').trim().isLength({ min: 1, max: 10 }).withMessage('Invalid emoji'),
|
||||
body('count').isInt({ min: 0, max: 1000 }).withMessage('Count must be between 0 and 1000'),
|
||||
validate,
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
const { emoji, count } = req.body;
|
||||
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => {
|
||||
if (err || !row) {
|
||||
logger.warn('Award not found for emoji update:', id);
|
||||
return res.status(404).json({ error: 'Award not found.' });
|
||||
}
|
||||
|
||||
let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
emojiTally[emoji] = count;
|
||||
|
||||
db.run(
|
||||
'UPDATE awards SET emoji_tally = ? WHERE id = ?',
|
||||
[JSON.stringify(emojiTally), id],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (err) {
|
||||
logger.error('Error updating emoji tally:', err);
|
||||
return res.status(500).json({ error: 'Failed to update emoji' });
|
||||
}
|
||||
res.json({ id, emoji_tally: emojiTally });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
res.json({ id, emoji_tally: emojiTally });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Remove or decrement an emoji tally for an award
|
||||
app.patch('/awards/:id/emojis/remove',
|
||||
emojiLimiter,
|
||||
param('id').isInt().withMessage('Invalid award ID'),
|
||||
body('emoji').trim().isLength({ min: 1, max: 10 }).withMessage('Invalid emoji'),
|
||||
validate,
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
const { emoji } = req.body;
|
||||
|
||||
db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => {
|
||||
if (err || !row) {
|
||||
logger.warn('Award not found for emoji removal:', id);
|
||||
return res.status(404).json({ error: 'Award not found.' });
|
||||
}
|
||||
|
||||
let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
|
||||
if (emojiTally[emoji]) {
|
||||
emojiTally[emoji] = emojiTally[emoji] - 1;
|
||||
if (emojiTally[emoji] <= 0) {
|
||||
delete emojiTally[emoji];
|
||||
}
|
||||
|
||||
db.run(
|
||||
'UPDATE awards SET emoji_tally = ? WHERE id = ?',
|
||||
[JSON.stringify(emojiTally), id],
|
||||
function (err) {
|
||||
if (err) {
|
||||
logger.error('Error removing emoji:', err);
|
||||
return res.status(500).json({ error: 'Failed to remove emoji' });
|
||||
}
|
||||
res.json({ id, emoji_tally: emojiTally });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
res.json({ id, emoji_tally: emojiTally });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Approve an award
|
||||
app.patch('/awards/:id/approve', basicAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
db.run(
|
||||
'UPDATE awards SET approved_date = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[id],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (this.changes === 0) return res.status(404).json({ error: 'Award not found.' });
|
||||
res.json({ id, approved_date: new Date().toISOString() });
|
||||
}
|
||||
);
|
||||
});
|
||||
app.patch('/awards/:id/approve',
|
||||
basicAuth,
|
||||
param('id').isInt().withMessage('Invalid award ID'),
|
||||
validate,
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
db.run(
|
||||
'UPDATE awards SET approved_date = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[id],
|
||||
function (err) {
|
||||
if (err) {
|
||||
logger.error('Error approving award:', err);
|
||||
return res.status(500).json({ error: 'Failed to approve award' });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'Award not found.' });
|
||||
}
|
||||
logger.info('Award approved:', id);
|
||||
res.json({ id, approved_date: new Date().toISOString() });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Get all pending (unapproved) awards
|
||||
app.get('/awards/pending', basicAuth, (req, res) => {
|
||||
db.all('SELECT * FROM awards WHERE approved_date IS NULL ORDER BY submitted_date DESC', [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (err) {
|
||||
logger.error('Error fetching pending awards:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch pending awards' });
|
||||
}
|
||||
const awards = rows.map(row => ({
|
||||
...row,
|
||||
emoji_tally: row.emoji_tally ? JSON.parse(row.emoji_tally) : {}
|
||||
@@ -182,39 +357,85 @@ app.get('/awards/pending', basicAuth, (req, res) => {
|
||||
});
|
||||
|
||||
// Reject (delete) a nomination
|
||||
app.delete('/awards/:id', basicAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
db.run('DELETE FROM awards WHERE id = ?', [id], function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (this.changes === 0) return res.status(404).json({ error: 'Award not found.' });
|
||||
res.json({ id, deleted: true });
|
||||
});
|
||||
});
|
||||
app.delete('/awards/:id',
|
||||
basicAuth,
|
||||
param('id').isInt().withMessage('Invalid award ID'),
|
||||
validate,
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
db.run('DELETE FROM awards WHERE id = ?', [id], function (err) {
|
||||
if (err) {
|
||||
logger.error('Error deleting award:', err);
|
||||
return res.status(500).json({ error: 'Failed to delete award' });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'Award not found.' });
|
||||
}
|
||||
logger.info('Award deleted:', id);
|
||||
res.json({ id, deleted: true });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Get top 5 awards approved, sorted by emoji tally (descending)
|
||||
app.get('/awards/top', (req, res) => {
|
||||
db.all(
|
||||
`SELECT * FROM awards WHERE approved_date IS NOT NULL`,
|
||||
'SELECT * FROM awards WHERE approved_date IS NOT NULL',
|
||||
[],
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
// Parse emoji_tally and calculate total emojis for sorting
|
||||
if (err) {
|
||||
logger.error('Error fetching top awards:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch top awards' });
|
||||
}
|
||||
|
||||
const awards = rows.map(row => {
|
||||
const emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {};
|
||||
const emojiTotal = Object.values(emojiTally).reduce((sum, n) => sum + Number(n), 0);
|
||||
return { ...row, emoji_tally: emojiTally, emojiTotal };
|
||||
});
|
||||
// Sort by emojiTotal descending, then by most recently approved
|
||||
|
||||
awards.sort((a, b) => {
|
||||
if (b.emojiTotal !== a.emojiTotal) return b.emojiTotal - a.emojiTotal;
|
||||
return new Date(b.approved_date) - new Date(a.approved_date);
|
||||
});
|
||||
|
||||
res.json(awards.slice(0, 5));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Serve frontend in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = () => {
|
||||
logger.info('Received shutdown signal, closing database connection...');
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
logger.error('Error closing database:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
logger.info('Database connection closed, exiting...');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Backend server running on http://localhost:${PORT}`);
|
||||
});
|
||||
logger.info(`Backend server running on http://localhost:${PORT}`);
|
||||
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
logger.info(`CORS origin: ${corsOptions.origin}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user