Spaces:
Running
Running
| Here's a complete production-ready Node.js Express server for a gym website: | |
| ```javascript | |
| // server.js | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const helmet = require('helmet'); | |
| const rateLimit = require('express-rate-limit'); | |
| const morgan = require('morgan'); | |
| const jwt = require('jsonwebtoken'); | |
| const bcrypt = require('bcryptjs'); | |
| const { Pool } = require('pg'); | |
| const path = require('path'); | |
| require('dotenv').config(); | |
| const app = express(); | |
| const PORT = process.env.PORT || 3000; | |
| // Database connection | |
| const pool = new Pool({ | |
| user: process.env.DB_USER || 'postgres', | |
| host: process.env.DB_HOST || 'localhost', | |
| database: process.env.DB_NAME || 'gym_db', | |
| password: process.env.DB_PASSWORD || 'password', | |
| port: process.env.DB_PORT || 5432, | |
| max: 20, | |
| idleTimeoutMillis: 30000, | |
| connectionTimeoutMillis: 2000, | |
| }); | |
| // Test database connection | |
| pool.on('connect', () => { | |
| console.log('✅ Connected to PostgreSQL database'); | |
| }); | |
| pool.on('error', (err) => { | |
| console.error('❌ Database connection error:', err); | |
| }); | |
| // Security middleware | |
| app.use(helmet({ | |
| contentSecurityPolicy: { | |
| directives: { | |
| defaultSrc: ["'self'"], | |
| styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], | |
| scriptSrc: ["'self'"], | |
| imgSrc: ["'self'", "data:", "https:"], | |
| connectSrc: ["'self'"], | |
| fontSrc: ["'self'", "https://fonts.gstatic.com"], | |
| }, | |
| }, | |
| })); | |
| // CORS configuration | |
| const corsOptions = { | |
| origin: process.env.NODE_ENV === 'production' | |
| ? [process.env.FRONTEND_URL || 'https://yourgym.com'] | |
| : ['http://localhost:3000', 'http://localhost:3001'], | |
| methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], | |
| allowedHeaders: ['Content-Type', 'Authorization'], | |
| credentials: true, | |
| optionsSuccessStatus: 200 | |
| }; | |
| app.use(cors(corsOptions)); | |
| // Rate limiting | |
| const limiter = rateLimit({ | |
| windowMs: 15 * 60 * 1000, // 15 minutes | |
| max: 100, // Limit each IP to 100 requests per windowMs | |
| message: { | |
| error: 'Too many requests from this IP, please try again later.', | |
| }, | |
| standardHeaders: true, | |
| legacyHeaders: false, | |
| }); | |
| const authLimiter = rateLimit({ | |
| windowMs: 15 * 60 * 1000, | |
| max: 5, // Limit auth attempts | |
| message: { | |
| error: 'Too many authentication attempts, please try again later.', | |
| }, | |
| }); | |
| app.use('/api/', limiter); | |
| app.use('/api/auth/', authLimiter); | |
| // Logging | |
| app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); | |
| // Body parser | |
| app.use(express.json({ limit: '10mb' })); | |
| app.use(express.urlencoded({ extended: true, limit: '10mb' })); | |
| // Static files | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| // JWT verification middleware | |
| const verifyToken = async (req, res, next) => { | |
| try { | |
| const token = req.headers.authorization?.split(' ')[1]; | |
| if (!token) { | |
| return res.status(401).json({ error: 'Access token required' }); | |
| } | |
| const decoded = jwt.verify(token, process.env.JWT_SECRET || 'gym_secret_key'); | |
| // Get user from database | |
| const result = await pool.query( | |
| 'SELECT id, email, first_name, last_name, role FROM users WHERE id = $1', | |
| [decoded.userId] | |
| ); | |
| if (result.rows.length === 0) { | |
| return res.status(401).json({ error: 'Invalid token' }); | |
| } | |
| req.user = result.rows[0]; | |
| next(); | |
| } catch (error) { | |
| console.error('Token verification error:', error); | |
| res.status(401).json({ error: 'Invalid token' }); | |
| } | |
| }; | |
| // Health check endpoint | |
| app.get('/health', async (req, res) => { | |
| try { | |
| await pool.query('SELECT 1'); | |
| res.status(200).json({ | |
| status: 'OK', | |
| timestamp: new Date().toISOString(), | |
| uptime: process.uptime(), | |
| database: 'Connected', | |
| environment: process.env.NODE_ENV || 'development' | |
| }); | |
| } catch (error) { | |
| res.status(503).json({ | |
| status: 'ERROR', | |
| timestamp: new Date().toISOString(), | |
| database: 'Disconnected', | |
| error: error.message | |
| }); | |
| } | |
| }); | |
| // Authentication routes | |
| app.post('/api/auth/register', async (req, res) => { | |
| try { | |
| const { email, password, firstName, lastName, phone } = req.body; | |
| // Validation | |
| if (!email || !password || !firstName || !lastName) { | |
| return res.status(400).json({ error: 'All fields are required' }); | |
| } | |
| // Check if user exists | |
| const existingUser = await pool.query( | |
| 'SELECT id FROM users WHERE email = $1', | |
| [email] | |
| ); | |
| if (existingUser.rows.length > 0) { | |
| return res.status(409).json({ error: 'User already exists' }); | |
| } | |
| // Hash password | |
| const saltRounds = 12; | |
| const hashedPassword = await bcrypt.hash(password, saltRounds); | |
| // Create user | |
| const result = await pool.query( | |
| `INSERT INTO users (email, password, first_name, last_name, phone, role, created_at) | |
| VALUES ($1, $2, $3, $4, $5, $6, NOW()) | |
| RETURNING id, email, first_name, last_name, role`, | |
| [email, hashedPassword, firstName, lastName, phone, 'member'] | |
| ); | |
| const user = result.rows[0]; | |
| // Generate JWT | |
| const token = jwt.sign( | |
| { userId: user.id, email: user.email }, | |
| process.env.JWT_SECRET || 'gym_secret_key', | |
| { expiresIn: '7d' } | |
| ); | |
| res.status(201).json({ | |
| message: 'User registered successfully', | |
| user: { | |
| id: user.id, | |
| email: user.email, | |
| firstName: user.first_name, | |
| lastName: user.last_name, | |
| role: user.role | |
| }, | |
| token | |
| }); | |
| } catch (error) { | |
| console.error('Registration error:', error); | |
| res.status(500).json({ error: 'Internal server error' }); | |
| } | |
| }); | |
| app.post('/api/auth/login', async (req, res) => { | |
| try { | |
| const { email, password } = req.body; | |
| if (!email || !password) { | |
| return res.status(400).json({ error: 'Email and password are required' }); | |
| } | |
| // Get user | |
| const result = await pool.query( | |
| 'SELECT id, email, password, first_name, last_name, role FROM users WHERE email = $1', | |
| [email] | |
| ); | |
| if (result.rows.length === 0) { | |
| return res.status(401).json({ error: 'Invalid credentials' }); | |
| } | |
| const user = result.rows[0]; | |
| // Verify password | |
| const isValidPassword = await bcrypt.compare(password, user.password); | |
| if (!isValidPassword) { | |
| return res.status(401).json({ error: 'Invalid credentials' }); | |
| } | |
| // Generate JWT | |
| const token = jwt.sign( | |
| { userId: user.id, email: user.email }, | |
| process.env.JWT_SECRET || 'gym_secret_key', | |
| { expiresIn: '7d' } | |
| ); | |
| res.json({ | |
| message: 'Login successful', | |
| user: { | |
| id: user.id, | |
| email: user.email, | |
| firstName: user.first_name, | |
| lastName: user.last_name, | |
| role: user.role | |
| }, | |
| token | |
| }); | |
| } catch (error) { | |
| console.error('Login error:', error); | |
| res.status(500).json({ error: 'Internal server error' }); | |
| } | |
| }); | |
| // Booking routes | |
| app.get('/api/booking/classes', async (req, res) => { | |
| try { | |
| const result = await pool.query(` | |
| SELECT | |
| c.id, c.name, c.description, c.duration, c.capacity, c.price, | |
| cs.id as schedule_id, cs.class_date, cs.start_time, cs.end_time, | |
| cs.available_spots, | |
| t.first_name as trainer_first_name, t.last_name as trainer_last_name, | |
| t.specialization, t.image_url as trainer_image | |
| FROM classes c | |
| JOIN class_schedules cs ON c.id = cs.class_id | |
| JOIN trainers t ON cs.trainer_id = t.id | |
| WHERE cs.class_date >= CURRENT_DATE | |
| ORDER BY cs.class_date, cs.start_time | |
| `); | |
| const classes = result.rows.map(row => ({ | |
| id: row.id, | |
| name: row.name, | |
| description: row.description, | |
| duration: row.duration, | |
| capacity: row.capacity, | |
| price: parseFloat(row.price), | |
| scheduleId: row.schedule_id, | |
| date: row.class_date, | |
| startTime: row.start_time, | |
| endTime: row.end_time, | |
| availableSpots: row.available_spots, | |
| trainer: { | |
| name: `${row.trainer_first_name} ${row.trainer_last_name}`, | |
| specialization: row.specialization, | |
| image: row.trainer_image | |
| } | |
| })); | |
| res.json({ classes }); | |
| } catch (error) { | |
| console.error('Get classes error:', error); | |
| res.status(500).json({ error: 'Internal server error' }); | |
| } | |
| }); | |
| app.post('/api/booking/book', verifyToken, async (req, res) => { | |
| const client = await pool.connect(); | |
| try { | |
| await client.query('BEGIN'); | |
| const { scheduleId } = req.body; | |
| const userId = req.user.id; | |
| // Check if user already booked this class | |
| const existingBooking = await client.query( | |
| 'SELECT id FROM bookings WHERE user_id = $1 AND schedule_id = $2', | |
| [userId, scheduleId] | |
| ); | |
| if (existingBooking.rows.length > 0) { | |
| await client.query('ROLLBACK'); | |
| return res.status(409).json({ error: 'You have already booked this class' }); | |
| } | |
| // Check available spots | |
| const scheduleResult = await client.query( | |
| 'SELECT available_spots FROM class_schedules WHERE id = $1', | |
| [scheduleId] | |
| ); | |
| if (scheduleResult.rows.length === 0) { | |
| await client.query('ROLLBACK'); | |
| return res.status(404).json({ error: 'Class not found' }); | |
| } | |
| if (scheduleResult.rows[0].available_spots <= 0) { | |
| await client.query('ROLLBACK'); | |
| return res.status(400).json({ error: 'No available spots' }); | |
| } | |
| // Create booking |