create-lumina-project 1.0.1 → 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 CHANGED
@@ -3,70 +3,148 @@
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumina-project",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "author": "Glenson Ansin",
5
5
  "description": "Scaffold a modern Node.js API with Lumina architecture.",
6
6
  "type": "module",
@@ -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",
@@ -17,6 +17,7 @@ JWT_EXPIRES_IN=1d
17
17
  # Application
18
18
  NODE_ENV=development
19
19
  APP_PORT=3000
20
+ CORS_ORIGIN=http://localhost:3000
20
21
 
21
22
  # Logging
22
23
  LOG_LEVEL=info
@@ -1,55 +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
- "key:generate": "tsx scripts/generate-keys.ts"
22
- },
23
- "private": true,
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
- "pg": "^8.18.0",
39
- "pg-hstore": "^2.3.4",
40
- "sequelize": "^6.37.7",
41
- "sequelize-cli": "^6.6.5",
42
- "umzug": "^3.8.2",
43
- "winston": "^3.19.0",
44
- "zod": "^4.3.6"
45
- },
46
- "devDependencies": {
47
- "@types/bcrypt": "^6.0.0",
48
- "@types/cors": "^2.8.19",
49
- "@types/express": "^5.0.6",
50
- "@types/jsonwebtoken": "^9.0.10",
51
- "@types/multer": "^2.0.0",
52
- "tsx": "^4.21.0",
53
- "typescript": "^5.9.3"
54
- }
55
- }
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);
@@ -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();
@@ -1,8 +1,6 @@
1
1
  import { Options } from 'sequelize';
2
2
  import Logger from '../utils/Logger.js';
3
- import dotenv from 'dotenv';
4
-
5
- dotenv.config();
3
+ import env from './env.js';
6
4
 
7
5
  class DatabaseConfig {
8
6
  public development: Options;
@@ -19,18 +17,18 @@ class DatabaseConfig {
19
17
  * Helper to generate config based on the environment.
20
18
  * This reduces repetition.
21
19
  */
22
- private getEnvironmentConfig(env: 'development' | 'test' | 'production' = 'development'): Options {
23
- const isTest = env === 'test';
24
- const isProd = env === 'production';
25
- const useSSL = process.env.DB_SSL === 'true';
20
+ private getEnvironmentConfig(envName: 'development' | 'test' | 'production' = 'development'): Options {
21
+ const isTest = envName === 'test';
22
+ const isProd = envName === 'production';
23
+ const useSSL = env.DB_SSL === 'true';
26
24
 
27
25
  return {
28
- username: process.env.DB_USERNAME,
29
- password: process.env.DB_PASSWORD,
30
- database: isTest ? process.env.DB_DATABASE_TEST : process.env.DB_DATABASE,
31
- host: process.env.DB_HOST,
32
- port: Number(process.env.DB_PORT) || 3306,
33
- dialect: (process.env.DB_DIALECT as any) || 'mysql',
26
+ username: env.DB_USERNAME,
27
+ password: env.DB_PASSWORD,
28
+ database: isTest ? env.DB_DATABASE_TEST : env.DB_DATABASE,
29
+ host: env.DB_HOST,
30
+ port: env.DB_PORT,
31
+ dialect: env.DB_DIALECT as any,
34
32
  dialectOptions: useSSL ? {
35
33
  ssl: {
36
34
  require: true,
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ /**
7
+ * Centralized Environment Validation
8
+ *
9
+ * Validates all required environment variables at startup
10
+ * and exports a typed `env` object for use throughout the app.
11
+ */
12
+ const envSchema = z.object({
13
+ // Application
14
+ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
15
+ APP_PORT: z.coerce.number().default(3000),
16
+
17
+ // Database
18
+ DB_DIALECT: z.enum(['mysql', 'postgres', 'mariadb', 'sqlite']).default('mysql'),
19
+ DB_HOST: z.string().min(1, 'DB_HOST is required'),
20
+ DB_PORT: z.coerce.number().default(3306),
21
+ DB_DATABASE: z.string().min(1, 'DB_DATABASE is required'),
22
+ DB_DATABASE_TEST: z.string().optional(),
23
+ DB_USERNAME: z.string().min(1, 'DB_USERNAME is required'),
24
+ DB_PASSWORD: z.string().default(''),
25
+ DB_SSL: z.string().optional(),
26
+
27
+ // JWT
28
+ JWT_SECRET: z.string().min(16, 'JWT_SECRET must be at least 16 characters for security'),
29
+ JWT_EXPIRES_IN: z.string().default('15m'),
30
+ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
31
+
32
+ // CORS
33
+ CORS_ORIGIN: z.string().default('http://localhost:3000'),
34
+
35
+ // Logging
36
+ LOG_LEVEL: z.string().default('info'),
37
+
38
+ // Maintenance
39
+ MAINTENANCE_SECRET: z.string().optional(),
40
+ });
41
+
42
+ const parsed = envSchema.safeParse(process.env);
43
+
44
+ if (!parsed.success) {
45
+ console.error('āŒ Invalid environment variables:');
46
+ for (const issue of parsed.error.issues) {
47
+ console.error(` ${issue.path.join('.')}: ${issue.message}`);
48
+ }
49
+ process.exit(1);
50
+ }
51
+
52
+ const env = parsed.data;
53
+
54
+ export default env;
@@ -13,6 +13,36 @@ class AuthController {
13
13
  }
14
14
  }
15
15
 
16
+ public async refresh(req: Request, res: Response) {
17
+ try {
18
+ const { refreshToken } = req.body;
19
+
20
+ if (!refreshToken) {
21
+ return ApiResponse.error(res, 'Refresh token is required', 400);
22
+ }
23
+
24
+ const result = await AuthService.refresh(refreshToken);
25
+ return ApiResponse.success(res, result, 'Token refreshed successfully');
26
+ } catch (error: any) {
27
+ return ApiResponse.error(res, error.message, 401);
28
+ }
29
+ }
30
+
31
+ public async logout(req: Request, res: Response) {
32
+ try {
33
+ const { refreshToken } = req.body;
34
+
35
+ if (!refreshToken) {
36
+ return ApiResponse.error(res, 'Refresh token is required', 400);
37
+ }
38
+
39
+ await AuthService.logout(refreshToken);
40
+ return ApiResponse.success(res, null, 'Logged out successfully');
41
+ } catch (error: any) {
42
+ return ApiResponse.error(res, error.message, 500);
43
+ }
44
+ }
45
+
16
46
  public async me(req: Request, res: Response) {
17
47
  return ApiResponse.success(res, req.user);
18
48
  }
@@ -0,0 +1,63 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ class CreateRefreshTokensTable {
4
+ /**
5
+ * Run the migrations.
6
+ */
7
+ async up(queryInterface, Sequelize) {
8
+ await queryInterface.createTable('refresh_tokens', {
9
+ id: {
10
+ allowNull: false,
11
+ autoIncrement: true,
12
+ primaryKey: true,
13
+ type: DataTypes.BIGINT,
14
+ },
15
+ user_id: {
16
+ type: DataTypes.BIGINT,
17
+ allowNull: false,
18
+ references: {
19
+ model: 'users',
20
+ key: 'id',
21
+ },
22
+ onUpdate: 'CASCADE',
23
+ onDelete: 'CASCADE',
24
+ },
25
+ token: {
26
+ type: DataTypes.STRING(512),
27
+ allowNull: false,
28
+ unique: true,
29
+ },
30
+ expires_at: {
31
+ type: DataTypes.DATE,
32
+ allowNull: false,
33
+ },
34
+ revoked: {
35
+ type: DataTypes.BOOLEAN,
36
+ allowNull: false,
37
+ defaultValue: false,
38
+ },
39
+ created_at: {
40
+ allowNull: false,
41
+ type: DataTypes.DATE,
42
+ defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
43
+ },
44
+ updated_at: {
45
+ allowNull: false,
46
+ type: DataTypes.DATE,
47
+ defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
48
+ },
49
+ });
50
+
51
+ await queryInterface.addIndex('refresh_tokens', ['token']);
52
+ await queryInterface.addIndex('refresh_tokens', ['user_id']);
53
+ }
54
+
55
+ /**
56
+ * Reverse the migrations.
57
+ */
58
+ async down(queryInterface) {
59
+ await queryInterface.dropTable('refresh_tokens');
60
+ }
61
+ }
62
+
63
+ export default new CreateRefreshTokensTable();