Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ JWT_SECRET=your_jwt_secret_key_change_in_production
JWT_EXPIRES_IN=7d
NODE_ENV=development
# comma separated list of allowed origins for *production*
ALLOWED_ORIGINS=https://scheduleapp.com,https://www.scheduleapp.com
ALLOWED_ORIGINS=https://scheduleapp.com,https://www.scheduleapp.com
# Email Configuration
EMAIL_SERVICE=gmail
[email protected]
EMAIL_PASS=your-password
8 changes: 6 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ PORT=8123
MONGODB_URI=mongodb://localhost:27017/scheduleapp
JWT_SECRET=your_jwt_secret_key_change_in_production
JWT_EXPIRES_IN=7d
NODE_ENV=development
NODE_ENV=development # development, production, test
# comma-separated list of allowed origins for cors in production
ALLOWED_ORIGINS=https://your-app.com,https://www.your-app.com
ALLOWED_ORIGINS=https://your-app.com,https://www.your-app.com
# Email Configuration
EMAIL_SERVICE=gmail
[email protected]
EMAIL_PASS=example_password
9 changes: 3 additions & 6 deletions backend/src/config/db.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';

// load environment variables
dotenv.config();
import { config } from './env.config';

// connection function
const connectDB = async (): Promise<void> => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI as string);
const conn = await mongoose.connect(config.mongodb.uri);
console.log(`mongodb connected: ${conn.connection.host}`);
} catch (error: any) {
console.error(`error: ${error.message}`);
process.exit(1);
}
};

export default connectDB;
export default connectDB;
99 changes: 99 additions & 0 deletions backend/src/config/env.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import dotenv from 'dotenv';
import path from 'path';

// Load .env file from backend/.env
dotenv.config({ path: path.resolve(__dirname, "../../.env") });

/*
* Config interface for env variables
*/
interface Config {
port: number;
nodeEnv: string;
mongodb: {
uri: string;
};
jwt: {
secret: string;
expiresIn: string;
};
cors: {
allowedOrigins: string[];
};
email: {
service: string;
user: string;
pass: string;
};
}

/**
* Validates that all required env variables are present
* Throws an error if any are missing
*/
function validateConfig(): Config {
// List of required environment variables
const requiredEnvVars = [
"PORT",
"MONGODB_URI",
"JWT_SECRET",
"JWT_EXPIRES_IN",
"NODE_ENV",
"EMAIL_SERVICE",
"EMAIL_USER",
"EMAIL_PASS",
];

// Check for missing required variables
const missingVars = requiredEnvVars.filter(
(varName) => !process.env[varName]
);

// Throw error if atleast one env variable is missing
if (missingVars.length > 0) {
throw new Error(
`Missing required environment variables: ${missingVars.join(", ")}\n`
);
}

// Parse and validate PORT
const port = parseInt(process.env.PORT!, 10);
if (isNaN(port) || port < 0 || port > 65535) {
throw new Error(
`Invalid PORT value: ${process.env.PORT}. Must be a number between 0 and 65535.`
);
}

// Build the configuration object
const config: Config = {
port,
nodeEnv: process.env.NODE_ENV!.trim(),
mongodb: {
uri: process.env.MONGODB_URI!.trim(),
},
jwt: {
secret: process.env.JWT_SECRET!.trim(),
expiresIn: process.env.JWT_EXPIRES_IN!.trim(),
},
cors: {
allowedOrigins: process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(",").map((origin) => origin.trim())
: [],
},
email: {
service: process.env.EMAIL_SERVICE!.trim(),
user: process.env.EMAIL_USER!.trim(),
pass: process.env.EMAIL_PASS!.trim(),
},
};

return config;
}

// Validate and export configuration
export const config = validateConfig();

// Export helper functions for environment checks
export const isDevelopment = (): boolean => config.nodeEnv === "development";
export const isProduction = (): boolean => config.nodeEnv === "production";
export const isTest = (): boolean => config.nodeEnv === "test";
49 changes: 28 additions & 21 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import connectDB from './config/db';
import routes from './routes';
import { notFound, errorHandler } from './middleware/errorMiddleware';

// load environment variables
dotenv.config();
import { config, isProduction } from './config/env.config';

// create express app
const app = express();
const port = process.env.PORT || 5000;

// connect to mongodb
connectDB();

// cors configiuration
const isProduction = process.env.NODE_ENV === 'production';
const corsOptions = {
// in development, allow all localhost and 127.0.0.1 requests
// in production, this would be replaced with specific allowed origins
origin: isProduction
? process.env.ALLOWED_ORIGINS?.split(',') || []
: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
// allow requests with no origin (like mobile apps, curl, postman)
if (!origin) return callback(null, true);

// allow all localhost and 127.0.0.1 requests in development
origin: isProduction()
? config.cors.allowedOrigins
: (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
) => {
// allow requests with no origin (like mobile apps, curl, postman)
if (!origin) return callback(null, true);

// allow all localhost and 127.0.0.1 requests in development
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
return callback(null, true);
}
return callback(null, true);
}

callback(new Error('Not allowed by CORS'));
},
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
Expand Down Expand Up @@ -62,7 +60,16 @@ app.use(notFound);
app.use(errorHandler);

// start server
app.listen(port, () => {
console.log(`server running on port ${port} (http://localhost:${port})`);
console.log(`CORS enabled for: ${isProduction ? process.env.ALLOWED_ORIGINS || 'specific origins' : 'all localhost origins'}`);
});
app.listen(config.port, () => {
console.log(
`server running on port ${config.port} (http://localhost:${config.port})`
);
console.log(`Environment: ${config.nodeEnv}`);
console.log(
`CORS enabled for: ${
isProduction()
? config.cors.allowedOrigins.join(", ") || "specific origins"
: "all localhost origins"
}`
);
});
7 changes: 4 additions & 3 deletions backend/src/middleware/errorMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { isProduction } from "../config/env.config";

// not found error handler
export const notFound = (req: Request, res: Response, next: NextFunction) => {
Expand All @@ -10,9 +11,9 @@ export const notFound = (req: Request, res: Response, next: NextFunction) => {
// general error handler
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;

res.status(statusCode).json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
stack: isProduction() ? null : err.stack,
});
};
};
48 changes: 25 additions & 23 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { Router } from 'express';
import { config } from "../config/env.config";

// importing status routes
import statusRoutes from './statusRoutes';
Expand All @@ -10,7 +10,7 @@ const router = Router();

// api root route - returns general api info
router.get('/', (req, res) => {
res.status(200).json({
res.status(200).json({
message: 'api is running',
version: '1.0.0',
endpoints: {
Expand All @@ -31,7 +31,7 @@ router.get('/health', (req, res) => {
// mongodb status check
router.get('/db-status', (req, res) => {
console.log('mongodb status check endpoint hit');

try {
const state = mongoose.connection.readyState;
/*
Expand All @@ -40,7 +40,7 @@ router.get('/db-status', (req, res) => {
2 = connecting
3 = disconnecting
*/

let status: {
connected: boolean;
state: string;
Expand All @@ -50,7 +50,7 @@ router.get('/db-status', (req, res) => {
connected: false,
state: 'unknown',
};

switch(state) {
case 0:
status.state = 'disconnected';
Expand All @@ -72,7 +72,7 @@ router.get('/db-status', (req, res) => {
status.error = 'Disconnecting from MongoDB';
break;
}

res.status(200).json(status);
} catch (error: any) {
res.status(500).json({
Expand All @@ -84,23 +84,25 @@ router.get('/db-status', (req, res) => {

// env variables check
router.get('/env-check', (req, res) => {
console.log('environment variables check endpoint hit');

// required env variables
const requiredVars = [
'PORT',
'MONGODB_URI',
'JWT_SECRET',
'JWT_EXPIRES_IN'
];

const missingVars = requiredVars.filter(varName => !process.env[varName]);

// Config is already validated at startup
res.status(200).json({
valid: missingVars.length === 0,
missing: missingVars.length > 0 ? missingVars : undefined,
total: requiredVars.length,
present: requiredVars.length - missingVars.length
valid: true,
message: "All environment variables are valid",
config: {
port: config.port,
nodeEnv: config.nodeEnv,
mongodb: {
// finds "//username:password@" and replaces with "//<credentials>@"
uri: config.mongodb.uri.replace(/\/\/.*@/, "//<credentials>@"),
}, // Hide credentials
jwt: { expiresIn: config.jwt.expiresIn },
cors: { allowedOrigins: config.cors.allowedOrigins },
email: {
service: config.email.service,
user: config.email.user,
pass: "***hidden***",
},
},
});
});

Expand All @@ -112,4 +114,4 @@ router.get('/test', (req, res) => {
// mounting status routes
router.use('/status', statusRoutes);

export default router;
export default router;
Loading