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.
- package/bin/cli.js +107 -29
- package/package.json +3 -2
- package/template/.env.example +16 -5
- package/template/package.json +60 -53
- package/template/public/js/status.js +53 -0
- package/template/scripts/generate-keys.ts +78 -0
- package/template/scripts/migrate.ts +33 -3
- package/template/server.ts +50 -8
- package/template/src/config/database.ts +17 -12
- package/template/src/config/env.ts +54 -0
- package/template/src/controllers/AuthController.ts +30 -0
- package/template/src/database/migrations/20260222151300-create_refresh_tokens.js +63 -0
- package/template/src/exceptions/Handler.ts +10 -3
- package/template/src/middlewares/Authentication.ts +2 -5
- package/template/src/middlewares/Csrf.ts +52 -0
- package/template/src/middlewares/Maintenance.ts +2 -4
- package/template/src/middlewares/RequestLogger.ts +31 -0
- package/template/src/models/RefreshToken.ts +69 -0
- package/template/src/models/index.ts +4 -6
- package/template/src/routes/api.ts +5 -0
- package/template/src/routes/web.ts +34 -5
- package/template/src/services/AuthService.ts +101 -9
- package/template/src/services/StorageService.ts +36 -9
- package/template/src/tests/apiResponse.test.ts +66 -0
- package/template/src/tests/env.test.ts +99 -0
- package/template/src/tests/hash.test.ts +30 -0
- package/template/src/utils/Logger.ts +3 -5
- package/template/views/404.html +233 -0
- package/template/views/maintenance.html +445 -23
- package/template/views/status.html +545 -0
- package/template/views/welcome.html +472 -39
- 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('
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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(
|
|
82
|
+
console.log(''); // Blank space for readability
|
|
43
83
|
|
|
44
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
98
|
+
// 6. Update package.json name
|
|
57
99
|
const pkgPath = path.join(projectPath, 'package.json');
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"author": "
|
|
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",
|
package/template/.env.example
CHANGED
|
@@ -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
|
-
|
|
13
|
+
# JWT Configuration
|
|
14
|
+
JWT_SECRET=your_super_secret_key_here
|
|
9
15
|
JWT_EXPIRES_IN=1d
|
|
10
16
|
|
|
11
|
-
|
|
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
|
-
|
|
25
|
+
# Maintenance Mode
|
|
26
|
+
MAINTENANCE_SECRET=secret_key_for_bypass
|
package/template/package.json
CHANGED
|
@@ -1,53 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "lumina",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "server.ts",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"migrate
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"create:
|
|
20
|
-
"create:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"express
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"@
|
|
49
|
-
"@types/
|
|
50
|
-
"
|
|
51
|
-
"
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/template/server.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
25
|
-
|
|
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();
|