create-lumina-project 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/bin/cli.js +107 -29
  2. package/package.json +3 -2
  3. package/template/.env.example +16 -5
  4. package/template/package.json +60 -53
  5. package/template/public/js/status.js +53 -0
  6. package/template/scripts/generate-keys.ts +78 -0
  7. package/template/scripts/migrate.ts +33 -3
  8. package/template/server.ts +50 -8
  9. package/template/src/config/database.ts +17 -12
  10. package/template/src/config/env.ts +54 -0
  11. package/template/src/controllers/AuthController.ts +30 -0
  12. package/template/src/database/migrations/20260222151300-create_refresh_tokens.js +63 -0
  13. package/template/src/exceptions/Handler.ts +10 -3
  14. package/template/src/middlewares/Authentication.ts +2 -5
  15. package/template/src/middlewares/Csrf.ts +52 -0
  16. package/template/src/middlewares/Maintenance.ts +2 -4
  17. package/template/src/middlewares/RequestLogger.ts +31 -0
  18. package/template/src/models/RefreshToken.ts +69 -0
  19. package/template/src/models/index.ts +4 -6
  20. package/template/src/routes/api.ts +5 -0
  21. package/template/src/routes/web.ts +34 -5
  22. package/template/src/services/AuthService.ts +101 -9
  23. package/template/src/services/StorageService.ts +36 -9
  24. package/template/src/tests/apiResponse.test.ts +66 -0
  25. package/template/src/tests/env.test.ts +99 -0
  26. package/template/src/tests/hash.test.ts +30 -0
  27. package/template/src/utils/Logger.ts +3 -5
  28. package/template/views/404.html +233 -0
  29. package/template/views/maintenance.html +445 -23
  30. package/template/views/status.html +545 -0
  31. package/template/views/welcome.html +472 -39
  32. package/template/vitest.config.ts +10 -0
package/bin/cli.js CHANGED
@@ -1,72 +1,150 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
5
5
  import prompts from 'prompts';
6
- import { bold, green, cyan, red } from 'kleur/colors';
6
+ import { bold, green, cyan, red, yellow } from 'kleur/colors';
7
7
  import { fileURLToPath } from 'url';
8
+ import { exec } from 'child_process';
9
+ import util from 'util';
10
+ import ora from 'ora';
11
+
12
+ const execAsync = util.promisify(exec);
8
13
 
9
14
  // Get current directory of the script
10
15
  const __filename = fileURLToPath(import.meta.url);
11
16
  const __dirname = path.dirname(__filename);
12
17
 
13
18
  async function init() {
14
- console.log(bold(cyan('\nšŸš€ Welcome to the Lumina Installer!')));
19
+ console.log(`\n${bold(cyan('šŸš€ Welcome to the Lumina Installer!'))}\n`);
15
20
 
16
21
  // 1. Ask for Project Name
17
22
  let targetDir = process.argv[2];
18
23
 
19
24
  if (!targetDir) {
20
- const response = await prompts({
21
- type: 'text',
22
- name: 'projectName',
23
- message: 'What is your project name?',
24
- initial: 'my-lumina-app'
25
- });
26
-
27
- if (!response.projectName) {
28
- console.log(red('āœ– Operation cancelled'));
29
- process.exit(1);
30
- }
31
- targetDir = response.projectName;
25
+ const response = await prompts(
26
+ {
27
+ type: 'text',
28
+ name: 'projectName',
29
+ message: 'What is your project name?',
30
+ initial: 'my-lumina-app',
31
+ validate: (name) => {
32
+ if (!name.trim()) return 'Project name cannot be empty.';
33
+ if (!/^[a-z0-9-_]+$/.test(name)) {
34
+ return 'Project name can only contain lowercase letters, numbers, dashes, and underscores.';
35
+ }
36
+ return true;
37
+ }
38
+ },
39
+ {
40
+ onCancel: () => {
41
+ console.log(red('āœ– Operation cancelled'));
42
+ process.exit(1);
43
+ }
44
+ }
45
+ );
46
+
47
+ targetDir = response.projectName.trim();
32
48
  }
33
49
 
34
- const projectPath = path.join(process.cwd(), targetDir);
50
+ const projectPath = path.resolve(process.cwd(), targetDir);
35
51
 
36
52
  // 2. Check if folder exists
37
53
  if (fs.existsSync(projectPath)) {
38
54
  console.log(red(`\nāœ– Error: Directory "${targetDir}" already exists.`));
39
55
  process.exit(1);
40
56
  }
57
+
58
+ // 3. Prompt for extra setups (git and npm install)
59
+ const setupPreferences = await prompts(
60
+ [
61
+ {
62
+ type: 'confirm',
63
+ name: 'initGit',
64
+ message: 'Initialize a new git repository?',
65
+ initial: true
66
+ },
67
+ {
68
+ type: 'confirm',
69
+ name: 'installDeps',
70
+ message: 'Install dependencies automatically?',
71
+ initial: true
72
+ }
73
+ ],
74
+ {
75
+ onCancel: () => {
76
+ console.log(red('āœ– Operation cancelled'));
77
+ process.exit(1);
78
+ }
79
+ }
80
+ );
41
81
 
42
- console.log(`\nšŸ“‚ Creating project in ${green(projectPath)}...`);
82
+ console.log(''); // Blank space for readability
43
83
 
44
- // 3. Copy Template
84
+ const copySpinner = ora(`šŸ“‚ Creating project in ${green(projectPath)}...`).start();
85
+
86
+ // 4. Copy Template
45
87
  const templateDir = path.resolve(__dirname, '../template');
46
88
 
47
89
  try {
48
90
  await fs.copy(templateDir, projectPath);
49
91
 
50
- // 4. Rename _gitignore to .gitignore (npm usually strips .gitignore from published packages)
92
+ // 5. Rename _gitignore to .gitignore
51
93
  const gitignorePath = path.join(projectPath, '_gitignore');
52
94
  if (fs.existsSync(gitignorePath)) {
53
95
  fs.renameSync(gitignorePath, path.join(projectPath, '.gitignore'));
54
96
  }
55
97
 
56
- // 5. Update package.json name
98
+ // 6. Update package.json name
57
99
  const pkgPath = path.join(projectPath, 'package.json');
58
- const pkg = await fs.readJson(pkgPath);
59
- pkg.name = targetDir;
60
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
100
+ if (fs.existsSync(pkgPath)) {
101
+ const pkg = await fs.readJson(pkgPath);
102
+ pkg.name = path.basename(projectPath);
103
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
104
+ }
61
105
 
62
- console.log(green('\nāœ… Project setup complete!'));
63
- console.log('\nTo get started run:');
64
- console.log(cyan(` cd ${targetDir}`));
65
- console.log(cyan(' npm install'));
66
- console.log(cyan(' npm run dev'));
106
+ copySpinner.succeed(green('Project template copied successfully!'));
107
+
108
+ // 7. Initialize Git
109
+ if (setupPreferences.initGit) {
110
+ const gitSpinner = ora('Initializing git repository...').start();
111
+ try {
112
+ await execAsync('git init', { cwd: projectPath });
113
+ // Optional: add initial commit but usually people prefer to do it themselves or standard tooling just inits
114
+ gitSpinner.succeed(green('Git repository initialized.'));
115
+ } catch (error) {
116
+ gitSpinner.fail(yellow('Failed to initialize git repository (is git installed?)'));
117
+ }
118
+ }
119
+
120
+ // 8. Install Dependencies
121
+ if (setupPreferences.installDeps) {
122
+ const depsSpinner = ora('Installing dependencies (this might take a few minutes)...').start();
123
+ try {
124
+ await execAsync('npm install', { cwd: projectPath });
125
+ depsSpinner.succeed(green('Dependencies installed successfully!'));
126
+ } catch (error) {
127
+ depsSpinner.fail(red('Failed to install dependencies. You can run "npm install" manually later.'));
128
+ }
129
+ }
130
+
131
+ // 9. Success message & Next steps
132
+ console.log(`\n${bold(green('āœ… All done! Project setup complete.'))}`);
133
+ console.log('\nTo get started run:\n');
134
+
135
+ const cdCmd = projectPath === process.cwd() ? '' : `cd ${targetDir}`;
136
+ if (cdCmd) console.log(cyan(` ${cdCmd}`));
137
+
138
+ if (!setupPreferences.installDeps) {
139
+ console.log(cyan(' npm install'));
140
+ }
141
+
142
+ console.log(cyan(' npm run dev\n'));
67
143
 
68
144
  } catch (error) {
69
- console.error(red('Error copying files:'), error);
145
+ copySpinner.fail(red('Error copying files:'));
146
+ console.error(error);
147
+ process.exit(1);
70
148
  }
71
149
  }
72
150
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-lumina-project",
3
- "version": "1.0.0",
4
- "author": "Ghost",
3
+ "version": "1.1.1",
4
+ "author": "Glenson Ansin",
5
5
  "description": "Scaffold a modern Node.js API with Lumina architecture.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "dependencies": {
15
15
  "fs-extra": "^11.3.3",
16
16
  "kleur": "^4.1.5",
17
+ "ora": "^9.3.0",
17
18
  "prompts": "^2.4.2"
18
19
  },
19
20
  "license": "MIT",
@@ -1,15 +1,26 @@
1
+ # Database Configuration
2
+ # Supported dialects: mysql, postgres, mariadb, sqlite
1
3
  DB_DIALECT=mysql
2
4
  DB_HOST=127.0.0.1
3
5
  DB_PORT=3306
4
6
  DB_DATABASE=lumina
7
+ DB_DATABASE_TEST=lumina_test
5
8
  DB_USERNAME=root
6
- DB_PASSWORD=
9
+ DB_PASSWORD=your_password
10
+ #true or false
11
+ DB_SSL=
7
12
 
8
- JWT_SECRET=secret_key
13
+ # JWT Configuration
14
+ JWT_SECRET=your_super_secret_key_here
9
15
  JWT_EXPIRES_IN=1d
10
16
 
11
- LOG_LEVEL=
12
-
17
+ # Application
13
18
  NODE_ENV=development
19
+ APP_PORT=3000
20
+ CORS_ORIGIN=http://localhost:3000
21
+
22
+ # Logging
23
+ LOG_LEVEL=info
14
24
 
15
- MAINTENANCE_SECRET=secret_key
25
+ # Maintenance Mode
26
+ MAINTENANCE_SECRET=secret_key_for_bypass
@@ -1,53 +1,60 @@
1
- {
2
- "name": "lumina",
3
- "version": "1.0.0",
4
- "description": "",
5
- "main": "server.ts",
6
- "scripts": {
7
- "dev": "nodemon --exec tsx server.ts",
8
- "build": "tsc",
9
- "start": "node dist/server.js",
10
- "test": "echo \"Error: no test specified\" && exit 1",
11
- "migrate": "tsx scripts/migrate.ts up",
12
- "migrate:undo": "tsx scripts/migrate.ts down",
13
- "migrate:reset": "tsx scripts/migrate.ts reset",
14
- "db:seed": "tsx scripts/seed.ts",
15
- "down": "tsx scripts/maintenance.ts down",
16
- "up": "tsx scripts/maintenance.ts up",
17
- "create:model": "tsx scripts/create-model.ts",
18
- "create:migration": "tsx scripts/create-migration.ts",
19
- "create:controller": "tsx scripts/create-controller.ts",
20
- "create:factory": "tsx scripts/create-factory.ts"
21
- },
22
- "author": "Ghost",
23
- "license": "ISC",
24
- "type": "module",
25
- "dependencies": {
26
- "@faker-js/faker": "^10.2.0",
27
- "bcrypt": "^6.0.0",
28
- "bcryptjs": "^3.0.3",
29
- "cors": "^2.8.6",
30
- "dotenv": "^17.2.3",
31
- "express": "^5.2.1",
32
- "express-rate-limit": "^8.2.1",
33
- "helmet": "^8.1.0",
34
- "jsonwebtoken": "^9.0.3",
35
- "multer": "^2.0.2",
36
- "mysql2": "^3.16.2",
37
- "nodemon": "^3.1.11",
38
- "sequelize": "^6.37.7",
39
- "sequelize-cli": "^6.6.5",
40
- "umzug": "^3.8.2",
41
- "winston": "^3.19.0",
42
- "zod": "^4.3.6"
43
- },
44
- "devDependencies": {
45
- "@types/bcrypt": "^6.0.0",
46
- "@types/cors": "^2.8.19",
47
- "@types/express": "^5.0.6",
48
- "@types/jsonwebtoken": "^9.0.10",
49
- "@types/multer": "^2.0.0",
50
- "tsx": "^4.21.0",
51
- "typescript": "^5.9.3"
52
- }
53
- }
1
+ {
2
+ "name": "lumina",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "server.ts",
6
+ "private": true,
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "nodemon --exec tsx server.ts",
10
+ "build": "tsc",
11
+ "start": "node dist/server.js",
12
+ "test": "vitest run",
13
+ "migrate": "tsx scripts/migrate.ts up",
14
+ "migrate:undo": "tsx scripts/migrate.ts down",
15
+ "migrate:reset": "tsx scripts/migrate.ts reset",
16
+ "db:seed": "tsx scripts/seed.ts",
17
+ "down": "tsx scripts/maintenance.ts down",
18
+ "up": "tsx scripts/maintenance.ts up",
19
+ "create:model": "tsx scripts/create-model.ts",
20
+ "create:migration": "tsx scripts/create-migration.ts",
21
+ "create:controller": "tsx scripts/create-controller.ts",
22
+ "create:factory": "tsx scripts/create-factory.ts",
23
+ "key:generate": "tsx scripts/generate-keys.ts"
24
+ },
25
+ "dependencies": {
26
+ "bcrypt": "^6.0.0",
27
+ "bcryptjs": "^3.0.3",
28
+ "compression": "^1.8.1",
29
+ "cookie-parser": "^1.4.7",
30
+ "cors": "^2.8.6",
31
+ "dotenv": "^17.2.3",
32
+ "express": "^5.2.1",
33
+ "express-rate-limit": "^8.2.1",
34
+ "helmet": "^8.1.0",
35
+ "jsonwebtoken": "^9.0.3",
36
+ "multer": "^2.0.2",
37
+ "mysql2": "^3.16.2",
38
+ "nodemon": "^3.1.11",
39
+ "pg": "^8.18.0",
40
+ "pg-hstore": "^2.3.4",
41
+ "sequelize": "^6.37.7",
42
+ "sequelize-cli": "^6.6.5",
43
+ "umzug": "^3.8.2",
44
+ "winston": "^3.19.0",
45
+ "zod": "^4.3.6"
46
+ },
47
+ "devDependencies": {
48
+ "@faker-js/faker": "^10.3.0",
49
+ "@types/bcrypt": "^6.0.0",
50
+ "@types/compression": "^1.8.1",
51
+ "@types/cookie-parser": "^1.4.10",
52
+ "@types/cors": "^2.8.19",
53
+ "@types/express": "^5.0.6",
54
+ "@types/jsonwebtoken": "^9.0.10",
55
+ "@types/multer": "^2.0.0",
56
+ "tsx": "^4.21.0",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.18"
59
+ }
60
+ }
@@ -0,0 +1,53 @@
1
+ async function fetchStatus() {
2
+ try {
3
+ const res = await fetch('/status/json');
4
+ const data = await res.json();
5
+
6
+ // Status
7
+ const heroEl = document.getElementById('statusHero');
8
+ const heroText = document.getElementById('statusHeroText');
9
+ const appStatus = document.getElementById('appStatus');
10
+ const isUp = data.status === 'UP';
11
+
12
+ heroEl.className = 'status-hero ' + (isUp ? 'up' : 'down');
13
+ heroText.textContent = isUp ? 'All Systems Operational' : 'System Degraded';
14
+ appStatus.textContent = data.status;
15
+ appStatus.className = 'info-value ' + (isUp ? 'status-up' : 'status-down');
16
+
17
+ // Environment
18
+ document.getElementById('appEnv').textContent = data.environment || 'unknown';
19
+
20
+ // Uptime
21
+ if (data.uptime !== undefined) {
22
+ document.getElementById('appUptime').textContent = formatUptime(data.uptime);
23
+ }
24
+
25
+ // Memory
26
+ if (data.memoryMB !== undefined) {
27
+ document.getElementById('appMemory').textContent = data.memoryMB + ' MB';
28
+ }
29
+
30
+ // Timestamp
31
+ document.getElementById('lastChecked').textContent = 'Checked ' + new Date().toLocaleTimeString();
32
+ } catch (err) {
33
+ document.getElementById('statusHero').className = 'status-hero down';
34
+ document.getElementById('statusHeroText').textContent = 'Unable to Reach Server';
35
+ }
36
+ }
37
+
38
+ function formatUptime(seconds) {
39
+ const d = Math.floor(seconds / 86400);
40
+ const h = Math.floor((seconds % 86400) / 3600);
41
+ const m = Math.floor((seconds % 3600) / 60);
42
+ const s = Math.floor(seconds % 60);
43
+ const parts = [];
44
+ if (d > 0) parts.push(d + 'd');
45
+ if (h > 0) parts.push(h + 'h');
46
+ if (m > 0) parts.push(m + 'm');
47
+ parts.push(s + 's');
48
+ return parts.join(' ');
49
+ }
50
+
51
+ // Fetch immediately, then refresh every 30s
52
+ fetchStatus();
53
+ setInterval(fetchStatus, 30000);
@@ -0,0 +1,78 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import Logger from '../src/utils/Logger.js';
5
+
6
+ class KeyGenerator {
7
+ private envPath: string;
8
+ private examplePath: string;
9
+
10
+ constructor() {
11
+ this.envPath = path.join(process.cwd(), '.env');
12
+ this.examplePath = path.join(process.cwd(), '.env.example');
13
+ }
14
+
15
+ public run(): void {
16
+ try {
17
+ this.ensureEnvExists();
18
+ this.updateKeys();
19
+ Logger.info('Application keys generated and saved to .env successfully.');
20
+ } catch (error) {
21
+ Logger.error('Failed to generate keys:', error);
22
+ process.exit(1);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Generates a random 64-character hex string.
28
+ */
29
+ private generateSecret(): string {
30
+ return crypto.randomBytes(32).toString('hex');
31
+ }
32
+
33
+ /**
34
+ * Checks if .env exists. If not, copies .env.example.
35
+ */
36
+ private ensureEnvExists(): void {
37
+ if (!fs.existsSync(this.envPath)) {
38
+ if (fs.existsSync(this.examplePath)) {
39
+ fs.copyFileSync(this.examplePath, this.envPath);
40
+ Logger.info('Created .env file from .env.example');
41
+ } else {
42
+ throw new Error('.env.example file not found. Cannot generate .env.');
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Reads .env, replaces specific keys, and writes it back.
49
+ */
50
+ private updateKeys(): void {
51
+ let envContent = fs.readFileSync(this.envPath, 'utf-8');
52
+
53
+ // 1. Update JWT_SECRET
54
+ envContent = this.replaceOrAppend(envContent, 'JWT_SECRET', this.generateSecret());
55
+
56
+ // 2. Update MAINTENANCE_SECRET
57
+ envContent = this.replaceOrAppend(envContent, 'MAINTENANCE_SECRET', this.generateSecret());
58
+
59
+ fs.writeFileSync(this.envPath, envContent);
60
+ }
61
+
62
+ /**
63
+ * Helper to replace an existing key's value or append if missing.
64
+ */
65
+ private replaceOrAppend(content: string, key: string, newValue: string): string {
66
+ const regex = new RegExp(`^${key}=.*`, 'm');
67
+
68
+ if (regex.test(content)) {
69
+ // Key exists, replace it
70
+ return content.replace(regex, `${key}=${newValue}`);
71
+ } else {
72
+ // Key missing, append it
73
+ return content + `\n${key}=${newValue}`;
74
+ }
75
+ }
76
+ }
77
+
78
+ new KeyGenerator().run();
@@ -43,6 +43,19 @@ class MigrationRunner {
43
43
  }
44
44
  }
45
45
 
46
+ /**
47
+ * Helper to get SSL config for the temp connection
48
+ */
49
+ private getSSLConfig() {
50
+ const useSSL = process.env.DB_SSL === 'true';
51
+ return useSSL ? {
52
+ ssl: {
53
+ require: true,
54
+ rejectUnauthorized: false
55
+ }
56
+ } : {};
57
+ }
58
+
46
59
  /**
47
60
  * Check if the database exists and create it if not.
48
61
  */
@@ -50,19 +63,36 @@ class MigrationRunner {
50
63
  const env = process.env.NODE_ENV || 'development';
51
64
  const config = (configList as any)[env];
52
65
  const dbName = config.database;
66
+ const isPostgres = config.dialect === 'postgres';
67
+
68
+ const connectionDb = isPostgres ? 'postgres' : '';
53
69
 
54
70
  // Connect to the server, not the specific DB
55
- const tempSequelize = new Sequelize('', config.username, config.password, {
71
+ const tempSequelize = new Sequelize(connectionDb, config.username, config.password, {
56
72
  host: config.host,
73
+ port: config.port,
57
74
  dialect: config.dialect,
58
75
  logging: false,
76
+ dialectOptions: this.getSSLConfig(),
59
77
  });
60
78
 
61
79
  try {
62
- await tempSequelize.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\`;`);
80
+ if (isPostgres) {
81
+ const result = await tempSequelize.query(
82
+ `SELECT 1 FROM pg_database WHERE datname = '${dbName}'`
83
+ );
84
+
85
+ if ((result[0] as any[]).length === 0) {
86
+ Logger.info(`Database "${dbName}" not found. Creating...`);
87
+ await tempSequelize.query(`CREATE DATABASE "${dbName}"`);
88
+ Logger.info(`Database "${dbName}" created.`);
89
+ }
90
+ } else {
91
+ // MySQL fallback
92
+ await tempSequelize.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\`;`);
93
+ }
63
94
  } catch (error) {
64
95
  Logger.error('Failed to create database:', error);
65
- throw error;
66
96
  } finally {
67
97
  await tempSequelize.close();
68
98
  }
@@ -1,6 +1,8 @@
1
1
  import express, { Application } from 'express';
2
2
  import cors from 'cors';
3
3
  import helmet from 'helmet';
4
+ import compression from 'compression';
5
+ import cookieParser from 'cookie-parser';
4
6
  import path from 'path';
5
7
  import db from './src/models/index.js';
6
8
  import RouteService from './src/services/RouteService.js';
@@ -8,23 +10,28 @@ import ExceptionHandler from './src/exceptions/Handler.js';
8
10
  import Logger from './src/utils/Logger.js';
9
11
  import Limiter from './src/middlewares/Limiter.js';
10
12
  import Maintenance from './src/middlewares/Maintenance.js';
11
- import dotenv from 'dotenv';
12
-
13
- dotenv.config();
13
+ import RequestLogger from './src/middlewares/RequestLogger.js';
14
+ import env from './src/config/env.js';
14
15
 
15
16
  const app: Application = express();
16
- const PORT = process.env.APP_PORT || 3000;
17
+ const PORT = env.APP_PORT;
17
18
 
18
19
  // ==========================
19
20
  // Global Middleware
20
21
  // ==========================
21
22
  app.use(Maintenance.handle);
22
23
  app.use(helmet({ crossOriginResourcePolicy: false }));
23
- app.use(cors());
24
- app.use(express.json());
25
- app.use(express.urlencoded({ extended: true }));
24
+ app.use(cors({
25
+ origin: env.CORS_ORIGIN,
26
+ credentials: true,
27
+ }));
28
+ app.use(compression());
29
+ app.use(cookieParser());
30
+ app.use(express.json({ limit: '10kb' }));
31
+ app.use(express.urlencoded({ extended: true, limit: '10kb' }));
26
32
  app.use(express.static(path.join(process.cwd(), 'public')));
27
33
  app.use(Limiter.global);
34
+ app.use(RequestLogger.handle);
28
35
 
29
36
  // ==========================
30
37
  // Register Routes
@@ -45,13 +52,15 @@ app.use(ExceptionHandler.handle);
45
52
  // ==========================
46
53
  // Start Server
47
54
  // ==========================
55
+ let server: ReturnType<typeof app.listen>;
56
+
48
57
  const start = async () => {
49
58
  try {
50
59
  // Test Database Connection
51
60
  await db.connect();
52
61
 
53
62
  // Start Listening
54
- app.listen(PORT, () => {
63
+ server = app.listen(PORT, () => {
55
64
  Logger.info(`Server running on http://localhost:${PORT}`);
56
65
  });
57
66
  } catch (error) {
@@ -60,4 +69,37 @@ const start = async () => {
60
69
  }
61
70
  };
62
71
 
72
+ // ==========================
73
+ // Graceful Shutdown
74
+ // ==========================
75
+ const shutdown = async (signal: string) => {
76
+ Logger.info(`${signal} received. Shutting down gracefully...`);
77
+
78
+ if (server) {
79
+ server.close(async () => {
80
+ Logger.info('HTTP server closed.');
81
+
82
+ try {
83
+ await db.sequelize.close();
84
+ Logger.info('Database connection closed.');
85
+ } catch (error) {
86
+ Logger.error('Error closing database connection:', error);
87
+ }
88
+
89
+ process.exit(0);
90
+ });
91
+
92
+ // Force shutdown after 10 seconds
93
+ setTimeout(() => {
94
+ Logger.error('Forced shutdown after timeout.');
95
+ process.exit(1);
96
+ }, 10000);
97
+ } else {
98
+ process.exit(0);
99
+ }
100
+ };
101
+
102
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
103
+ process.on('SIGINT', () => shutdown('SIGINT'));
104
+
63
105
  start();