Harden dev environment configuration
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / Build Docker Images (push) Has been cancelled

This commit is contained in:
2025-10-16 17:48:26 -05:00
parent 75c7430d01
commit 9355d5c7c2
20 changed files with 26341 additions and 42 deletions

View File

@@ -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
View File

@@ -2,6 +2,7 @@ node_modules/
dist/ dist/
.env .env
.env.* .env.*
!.env.development
!.env.example !.env.example
*.log *.log
tmp/ tmp/

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View 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;

View File

@@ -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
View 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}`);
});

View File

@@ -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(

View File

@@ -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' }
); );

View File

@@ -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;

View File

@@ -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:

View File

@@ -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:

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
HOST=0.0.0.0
PORT=12000
REACT_APP_API_URL=http://localhost:3001

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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'));

View 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;