Harden dev environment configuration
This commit is contained in:
		| @@ -1,10 +1,15 @@ | |||||||
| # Global defaults | # Global defaults | ||||||
| NODE_ENV=development | NODE_ENV=development | ||||||
|  | LOG_LEVEL=info | ||||||
|  |  | ||||||
| # Backend service | # Backend service | ||||||
|  | BACKEND_HOST=0.0.0.0 | ||||||
|  | BACKEND_PORT=3001 | ||||||
| DATABASE_URL=postgresql://merchantsofhope_user:merchantsofhope_password@merchantsofhope-supplyanddemandportal-database:5432/merchantsofhope_supplyanddemandportal | DATABASE_URL=postgresql://merchantsofhope_user:merchantsofhope_password@merchantsofhope-supplyanddemandportal-database:5432/merchantsofhope_supplyanddemandportal | ||||||
| JWT_SECRET=merchantsofhope_jwt_secret_key_2024 | JWT_SECRET=merchantsofhope_jwt_secret_key_2024 | ||||||
| PORT=3001 | CORS_ORIGIN=http://localhost:12000 | ||||||
|  |  | ||||||
| # Frontend service | # Frontend service | ||||||
|  | FRONTEND_HOST=0.0.0.0 | ||||||
|  | FRONTEND_PORT=12000 | ||||||
| REACT_APP_API_URL=http://localhost:3001 | REACT_APP_API_URL=http://localhost:3001 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,6 +2,7 @@ node_modules/ | |||||||
| dist/ | dist/ | ||||||
| .env | .env | ||||||
| .env.* | .env.* | ||||||
|  | !.env.development | ||||||
| !.env.example | !.env.example | ||||||
| *.log | *.log | ||||||
| tmp/ | tmp/ | ||||||
|   | |||||||
| @@ -76,9 +76,9 @@ A comprehensive SAAS application for managing recruiter workflows, built with mo | |||||||
|    ``` |    ``` | ||||||
|  |  | ||||||
| 5. **Access the application** | 5. **Access the application** | ||||||
|    - Frontend: http://localhost:3000 |    - Frontend: http://localhost:12000 | ||||||
|    - Backend API: http://localhost:3001 |    - Backend API: http://merchantsofhope-supplyanddemandportal-backend:3001 (inside Docker network) or http://localhost:3001 when running natively | ||||||
|    - Database: localhost:5432 |    - Database: merchantsofhope-supplyanddemandportal-database:5432 (inside Docker network) | ||||||
|  |  | ||||||
| ### Alternative: Native Node.js workflow | ### Alternative: Native Node.js workflow | ||||||
|  |  | ||||||
| @@ -98,7 +98,7 @@ cd ../frontend | |||||||
| npm start | npm start | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Ensure a PostgreSQL instance is running and the `DATABASE_URL` in `.env` points to it. | Ensure a PostgreSQL instance is running and the `DATABASE_URL` in `.env` points to it. The frontend `.env.development` file pins the dev server to `0.0.0.0:12000` so it matches the Docker behaviour. | ||||||
|  |  | ||||||
| ### Demo Accounts | ### Demo Accounts | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,10 +3,13 @@ FROM node:18-alpine | |||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
| COPY package*.json ./ | COPY package*.json ./ | ||||||
| RUN npm install | RUN npm ci | ||||||
|  |  | ||||||
| COPY . . | COPY . . | ||||||
|  |  | ||||||
|  | ENV HOST=0.0.0.0 \ | ||||||
|  |     PORT=3001 | ||||||
|  |  | ||||||
| EXPOSE 3001 | EXPOSE 3001 | ||||||
|  |  | ||||||
| CMD ["npm", "run", "dev"] | CMD ["npm", "run", "dev"] | ||||||
|   | |||||||
							
								
								
									
										5563
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5563
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -4,8 +4,8 @@ | |||||||
|   "description": "Backend for MerchantsOfHope-SupplyANdDemandPortal recruiter workflow SAAS", |   "description": "Backend for MerchantsOfHope-SupplyANdDemandPortal recruiter workflow SAAS", | ||||||
|   "main": "src/server.js", |   "main": "src/server.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "node src/server.js", |     "start": "node src/index.js", | ||||||
|     "dev": "nodemon src/server.js", |     "dev": "nodemon src/index.js", | ||||||
|     "test": "jest", |     "test": "jest", | ||||||
|     "test:watch": "jest --watch", |     "test:watch": "jest --watch", | ||||||
|     "migrate": "node src/database/migrate.js", |     "migrate": "node src/database/migrate.js", | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								backend/src/config/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/config/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | const dotenv = require('dotenv'); | ||||||
|  |  | ||||||
|  | dotenv.config(); | ||||||
|  |  | ||||||
|  | const optionalNumber = (value, fallback) => { | ||||||
|  |   const parsed = parseInt(value, 10); | ||||||
|  |   return Number.isFinite(parsed) ? parsed : fallback; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const config = { | ||||||
|  |   env: process.env.NODE_ENV || 'development', | ||||||
|  |   host: process.env.HOST || process.env.BACKEND_HOST || '0.0.0.0', | ||||||
|  |   port: optionalNumber(process.env.PORT || process.env.BACKEND_PORT, 3001), | ||||||
|  |   databaseUrl: process.env.DATABASE_URL, | ||||||
|  |   jwtSecret: process.env.JWT_SECRET, | ||||||
|  |   corsOrigin: process.env.CORS_ORIGIN || '*', | ||||||
|  |   logLevel: process.env.LOG_LEVEL || 'info' | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const required = ['databaseUrl', 'jwtSecret']; | ||||||
|  | const missingKeys = required.filter((key) => !config[key]); | ||||||
|  |  | ||||||
|  | if (missingKeys.length > 0) { | ||||||
|  |   throw new Error(`Missing required environment variables: ${missingKeys.join(', ')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = config; | ||||||
| @@ -1,14 +1,16 @@ | |||||||
| const { Pool } = require('pg'); | const { Pool } = require('pg'); | ||||||
| require('dotenv').config(); | const config = require('../config'); | ||||||
|  |  | ||||||
| const pool = new Pool({ | const pool = new Pool({ | ||||||
|   connectionString: process.env.DATABASE_URL, |   connectionString: config.databaseUrl, | ||||||
|   ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false |   ssl: config.env === 'production' ? { rejectUnauthorized: false } : false | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test database connection | // Test database connection | ||||||
| pool.on('connect', () => { | pool.on('connect', () => { | ||||||
|   console.log('Connected to MerchantsOfHope-SupplyANdDemandPortal database'); |   if (config.logLevel !== 'silent') { | ||||||
|  |     console.log('Connected to MerchantsOfHope-SupplyANdDemandPortal database'); | ||||||
|  |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| pool.on('error', (err) => { | pool.on('error', (err) => { | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								backend/src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/src/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | const app = require('./server'); | ||||||
|  | const config = require('./config'); | ||||||
|  |  | ||||||
|  | app.listen(config.port, config.host, () => { | ||||||
|  |   console.log(`MerchantsOfHope-SupplyANdDemandPortal backend server running on ${config.host}:${config.port}`); | ||||||
|  | }); | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| const jwt = require('jsonwebtoken'); | const jwt = require('jsonwebtoken'); | ||||||
| const pool = require('../database/connection'); | const pool = require('../database/connection'); | ||||||
|  | const config = require('../config'); | ||||||
|  |  | ||||||
| const authenticateToken = async (req, res, next) => { | const authenticateToken = async (req, res, next) => { | ||||||
|   const authHeader = req.headers['authorization']; |   const authHeader = req.headers['authorization']; | ||||||
| @@ -10,7 +11,7 @@ const authenticateToken = async (req, res, next) => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     const decoded = jwt.verify(token, process.env.JWT_SECRET); |     const decoded = jwt.verify(token, config.jwtSecret); | ||||||
|      |      | ||||||
|     // Get user details from database |     // Get user details from database | ||||||
|     const userResult = await pool.query( |     const userResult = await pool.query( | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken'); | |||||||
| const { body, validationResult } = require('express-validator'); | const { body, validationResult } = require('express-validator'); | ||||||
| const pool = require('../database/connection'); | const pool = require('../database/connection'); | ||||||
| const { authenticateToken } = require('../middleware/auth'); | const { authenticateToken } = require('../middleware/auth'); | ||||||
|  | const config = require('../config'); | ||||||
|  |  | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
|  |  | ||||||
| @@ -47,7 +48,7 @@ router.post('/register', [ | |||||||
|     // Generate JWT token |     // Generate JWT token | ||||||
|     const token = jwt.sign( |     const token = jwt.sign( | ||||||
|       { userId: user.id, email: user.email, role: user.role }, |       { userId: user.id, email: user.email, role: user.role }, | ||||||
|       process.env.JWT_SECRET, |       config.jwtSecret, | ||||||
|       { expiresIn: '24h' } |       { expiresIn: '24h' } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -106,7 +107,7 @@ router.post('/login', [ | |||||||
|     // Generate JWT token |     // Generate JWT token | ||||||
|     const token = jwt.sign( |     const token = jwt.sign( | ||||||
|       { userId: user.id, email: user.email, role: user.role }, |       { userId: user.id, email: user.email, role: user.role }, | ||||||
|       process.env.JWT_SECRET, |       config.jwtSecret, | ||||||
|       { expiresIn: '24h' } |       { expiresIn: '24h' } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ const express = require('express'); | |||||||
| const cors = require('cors'); | const cors = require('cors'); | ||||||
| const helmet = require('helmet'); | const helmet = require('helmet'); | ||||||
| const morgan = require('morgan'); | const morgan = require('morgan'); | ||||||
| require('dotenv').config(); | const config = require('./config'); | ||||||
|  |  | ||||||
| const authRoutes = require('./routes/auth'); | const authRoutes = require('./routes/auth'); | ||||||
| const userRoutes = require('./routes/users'); | const userRoutes = require('./routes/users'); | ||||||
| @@ -13,16 +13,19 @@ const applicationRoutes = require('./routes/applications'); | |||||||
| const resumeRoutes = require('./routes/resumes'); | const resumeRoutes = require('./routes/resumes'); | ||||||
|  |  | ||||||
| const app = express(); | const app = express(); | ||||||
| const PORT = process.env.PORT || 3001; |  | ||||||
|  |  | ||||||
| // Middleware | app.disable('x-powered-by'); | ||||||
|  |  | ||||||
|  | const corsOrigins = config.corsOrigin === '*' | ||||||
|  |   ? undefined | ||||||
|  |   : config.corsOrigin.split(',').map((origin) => origin.trim()); | ||||||
|  |  | ||||||
| app.use(helmet()); | app.use(helmet()); | ||||||
| app.use(cors()); | app.use(cors(corsOrigins ? { origin: corsOrigins, credentials: true } : {})); | ||||||
| app.use(morgan('combined')); | app.use(morgan(config.env === 'production' ? 'combined' : 'dev')); | ||||||
| app.use(express.json({ limit: '10mb' })); | app.use(express.json({ limit: '10mb' })); | ||||||
| app.use(express.urlencoded({ extended: true })); | app.use(express.urlencoded({ extended: true })); | ||||||
|  |  | ||||||
| // Routes |  | ||||||
| app.use('/api/auth', authRoutes); | app.use('/api/auth', authRoutes); | ||||||
| app.use('/api/users', userRoutes); | app.use('/api/users', userRoutes); | ||||||
| app.use('/api/employers', employerRoutes); | app.use('/api/employers', employerRoutes); | ||||||
| @@ -31,27 +34,21 @@ app.use('/api/jobs', jobRoutes); | |||||||
| app.use('/api/applications', applicationRoutes); | app.use('/api/applications', applicationRoutes); | ||||||
| app.use('/api/resumes', resumeRoutes); | app.use('/api/resumes', resumeRoutes); | ||||||
|  |  | ||||||
| // Health check |  | ||||||
| app.get('/api/health', (req, res) => { | app.get('/api/health', (req, res) => { | ||||||
|   res.json({ status: 'OK', timestamp: new Date().toISOString() }); |   res.json({ status: 'OK', timestamp: new Date().toISOString() }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Error handling middleware | // eslint-disable-next-line no-unused-vars | ||||||
| app.use((err, req, res, next) => { | app.use((err, req, res, next) => { | ||||||
|   console.error(err.stack); |   console.error(err.stack); | ||||||
|   res.status(500).json({ |   res.status(500).json({ | ||||||
|     error: 'Something went wrong!', |     error: 'Something went wrong!', | ||||||
|     message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error' |     message: config.env === 'development' ? err.message : 'Internal server error' | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // 404 handler |  | ||||||
| app.use('*', (req, res) => { | app.use('*', (req, res) => { | ||||||
|   res.status(404).json({ error: 'Route not found' }); |   res.status(404).json({ error: 'Route not found' }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| app.listen(PORT, () => { |  | ||||||
|   console.log(`MerchantsOfHope-SupplyANdDemandPortal backend server running on port ${PORT}`); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| module.exports = app; | module.exports = app; | ||||||
|   | |||||||
| @@ -36,10 +36,11 @@ services: | |||||||
|     depends_on: |     depends_on: | ||||||
|       - merchantsofhope-supplyanddemandportal-backend |       - merchantsofhope-supplyanddemandportal-backend | ||||||
|     environment: |     environment: | ||||||
|       PORT: 3000 |       HOST: 0.0.0.0 | ||||||
|  |       PORT: 12000 | ||||||
|       REACT_APP_API_URL: ${REACT_APP_API_URL:-http://merchantsofhope-supplyanddemandportal-backend:3001} |       REACT_APP_API_URL: ${REACT_APP_API_URL:-http://merchantsofhope-supplyanddemandportal-backend:3001} | ||||||
|     ports: |     ports: | ||||||
|       - "0.0.0.0:3000:3000" |       - "0.0.0.0:12000:12000" | ||||||
|  |  | ||||||
| volumes: | volumes: | ||||||
|   merchantsofhope-supplyanddemandportal-postgres-data: |   merchantsofhope-supplyanddemandportal-postgres-data: | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ services: | |||||||
|       POSTGRES_DB: merchantsofhope_supplyanddemandportal |       POSTGRES_DB: merchantsofhope_supplyanddemandportal | ||||||
|       POSTGRES_USER: merchantsofhope_user |       POSTGRES_USER: merchantsofhope_user | ||||||
|       POSTGRES_PASSWORD: merchantsofhope_password |       POSTGRES_PASSWORD: merchantsofhope_password | ||||||
|     ports: |     expose: | ||||||
|       - "0.0.0.0:5432:5432" |       - "5432" | ||||||
|     volumes: |     volumes: | ||||||
|       - merchantsofhope-supplyanddemandportal-postgres-data:/var/lib/postgresql/data |       - merchantsofhope-supplyanddemandportal-postgres-data:/var/lib/postgresql/data | ||||||
|     networks: |     networks: | ||||||
| @@ -22,9 +22,10 @@ services: | |||||||
|       NODE_ENV: development |       NODE_ENV: development | ||||||
|       DATABASE_URL: postgresql://merchantsofhope_user:merchantsofhope_password@merchantsofhope-supplyanddemandportal-database:5432/merchantsofhope_supplyanddemandportal |       DATABASE_URL: postgresql://merchantsofhope_user:merchantsofhope_password@merchantsofhope-supplyanddemandportal-database:5432/merchantsofhope_supplyanddemandportal | ||||||
|       JWT_SECRET: merchantsofhope_jwt_secret_key_2024 |       JWT_SECRET: merchantsofhope_jwt_secret_key_2024 | ||||||
|       PORT: 3001 |       HOST: ${BACKEND_HOST:-0.0.0.0} | ||||||
|     ports: |       PORT: ${BACKEND_PORT:-3001} | ||||||
|       - "0.0.0.0:3001:3001" |     expose: | ||||||
|  |       - "3001" | ||||||
|     depends_on: |     depends_on: | ||||||
|       - merchantsofhope-supplyanddemandportal-database |       - merchantsofhope-supplyanddemandportal-database | ||||||
|     volumes: |     volumes: | ||||||
| @@ -39,9 +40,11 @@ services: | |||||||
|       dockerfile: Dockerfile |       dockerfile: Dockerfile | ||||||
|     container_name: merchantsofhope-supplyanddemandportal-frontend |     container_name: merchantsofhope-supplyanddemandportal-frontend | ||||||
|     environment: |     environment: | ||||||
|       REACT_APP_API_URL: http://localhost:3001 |       HOST: ${FRONTEND_HOST:-0.0.0.0} | ||||||
|  |       PORT: ${FRONTEND_PORT:-12000} | ||||||
|  |       REACT_APP_API_URL: http://merchantsofhope-supplyanddemandportal-backend:3001 | ||||||
|     ports: |     ports: | ||||||
|       - "0.0.0.0:12000:3000" |       - "0.0.0.0:12000:12000" | ||||||
|     depends_on: |     depends_on: | ||||||
|       - merchantsofhope-supplyanddemandportal-backend |       - merchantsofhope-supplyanddemandportal-backend | ||||||
|     volumes: |     volumes: | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ Expose the tag you want Coolify to deploy by either: | |||||||
|    Configure any additional secrets used by your environment (mail providers, analytics, etc.). |    Configure any additional secrets used by your environment (mail providers, analytics, etc.). | ||||||
|  |  | ||||||
| 3. **Networking and ports** | 3. **Networking and ports** | ||||||
|    - Expose port `3000` externally for the frontend. |    - Expose port `12000` externally for the frontend (or map to 80/443 via Coolify's proxy). | ||||||
|    - Optionally expose `3001` if you want direct API access; otherwise rely on the frontend or internal services. |    - Optionally expose `3001` if you want direct API access; otherwise rely on the frontend or internal services. | ||||||
|    - Attach the application to an HTTPS domain using Coolify's built-in proxy configuration. |    - Attach the application to an HTTPS domain using Coolify's built-in proxy configuration. | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								frontend/.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.env.development
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | HOST=0.0.0.0 | ||||||
|  | PORT=12000 | ||||||
|  | REACT_APP_API_URL=http://localhost:3001 | ||||||
| @@ -3,10 +3,13 @@ FROM node:18-alpine | |||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
| COPY package*.json ./ | COPY package*.json ./ | ||||||
| RUN npm install | RUN npm ci | ||||||
|  |  | ||||||
| COPY . . | COPY . . | ||||||
|  |  | ||||||
| EXPOSE 3000 | ENV HOST=0.0.0.0 \ | ||||||
|  |     PORT=12000 | ||||||
|  |  | ||||||
|  | EXPOSE 12000 | ||||||
|  |  | ||||||
| CMD ["npm", "start"] | CMD ["npm", "start"] | ||||||
|   | |||||||
							
								
								
									
										20671
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										20671
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import ReactDOM from 'react-dom/client'; | import ReactDOM from 'react-dom/client'; | ||||||
| import './index.css'; | import './index.css'; | ||||||
|  | import './lib/configureAxios'; | ||||||
| import App from './App'; | import App from './App'; | ||||||
|  |  | ||||||
| const root = ReactDOM.createRoot(document.getElementById('root')); | const root = ReactDOM.createRoot(document.getElementById('root')); | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								frontend/src/lib/configureAxios.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/lib/configureAxios.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import axios from 'axios'; | ||||||
|  |  | ||||||
|  | const baseURL = process.env.REACT_APP_API_URL || ''; | ||||||
|  |  | ||||||
|  | if (baseURL) { | ||||||
|  |   axios.defaults.baseURL = baseURL; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | axios.defaults.headers.common['Content-Type'] = 'application/json'; | ||||||
|  |  | ||||||
|  | export default axios; | ||||||
		Reference in New Issue
	
	Block a user