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