create-lumina-project 1.0.0
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 +73 -0
- package/package.json +28 -0
- package/template/.env.example +15 -0
- package/template/.sequelizerc +11 -0
- package/template/_gitignore +3 -0
- package/template/nodemon.json +5 -0
- package/template/package.json +53 -0
- package/template/public/img/logo1.png +0 -0
- package/template/public/img/logo2.png +0 -0
- package/template/scripts/create-controller.ts +89 -0
- package/template/scripts/create-factory.ts +98 -0
- package/template/scripts/create-migration.ts +98 -0
- package/template/scripts/create-model.ts +105 -0
- package/template/scripts/maintenance.ts +73 -0
- package/template/scripts/migrate.ts +146 -0
- package/template/scripts/seed.ts +42 -0
- package/template/scripts/stubs/controller.stub +50 -0
- package/template/scripts/stubs/factory.stub +19 -0
- package/template/scripts/stubs/migration.stub +44 -0
- package/template/scripts/stubs/model.stub +39 -0
- package/template/server.ts +63 -0
- package/template/src/config/database.ts +39 -0
- package/template/src/controllers/AuthController.ts +21 -0
- package/template/src/controllers/UserController.ts +52 -0
- package/template/src/database/factories/Factory.ts +33 -0
- package/template/src/database/factories/UserFactory.ts +26 -0
- package/template/src/database/migrations/20260205084944-create_user.js +68 -0
- package/template/src/database/seeders/DatabaseSeeder.js +32 -0
- package/template/src/exceptions/Handler.ts +32 -0
- package/template/src/middlewares/Authentication.ts +34 -0
- package/template/src/middlewares/Limiter.ts +34 -0
- package/template/src/middlewares/Maintenance.ts +37 -0
- package/template/src/middlewares/Validator.ts +29 -0
- package/template/src/models/User.ts +91 -0
- package/template/src/models/index.ts +92 -0
- package/template/src/requests/UserRequest.ts +26 -0
- package/template/src/routes/api.ts +30 -0
- package/template/src/routes/web.ts +29 -0
- package/template/src/services/AuthService.ts +29 -0
- package/template/src/services/RouteService.ts +15 -0
- package/template/src/services/StorageService.ts +59 -0
- package/template/src/services/UserService.ts +31 -0
- package/template/src/types/Pagination.d.ts +13 -0
- package/template/src/types/express/index.d.ts +9 -0
- package/template/src/utils/ApiResponse.ts +27 -0
- package/template/src/utils/Hash.ts +21 -0
- package/template/src/utils/Logger.ts +79 -0
- package/template/src/utils/Paginator.ts +41 -0
- package/template/tsconfig.json +24 -0
- package/template/views/maintenance.html +42 -0
- package/template/views/welcome.html +62 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Umzug, SequelizeStorage } from 'umzug';
|
|
2
|
+
import { Sequelize, QueryInterface } from 'sequelize';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import db from '../src/models/index.js';
|
|
5
|
+
import configList from '../src/config/database.js';
|
|
6
|
+
import Logger from '../src/utils/Logger.js';
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
class MigrationRunner {
|
|
12
|
+
/**
|
|
13
|
+
* The command line argument (e.g., 'up', 'down', 'reset')
|
|
14
|
+
*/
|
|
15
|
+
private command: string;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
// Get the command from process arguments
|
|
19
|
+
this.command = process.argv[2] || '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main entry point to execute the migration logic.
|
|
24
|
+
*/
|
|
25
|
+
public async run(): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
// 1. Ensure Database Exists
|
|
28
|
+
await this.ensureDatabaseExists();
|
|
29
|
+
|
|
30
|
+
// 2. Connect to the App Database
|
|
31
|
+
await this.connectDatabase();
|
|
32
|
+
|
|
33
|
+
// 3. Configure Umzug (The Migration Engine)
|
|
34
|
+
const umzug = this.getUmzugInstance();
|
|
35
|
+
|
|
36
|
+
// 4. Execute the requested command
|
|
37
|
+
await this.executeCommand(umzug);
|
|
38
|
+
|
|
39
|
+
process.exit(0);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
Logger.error('Migration failed:', error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if the database exists and create it if not.
|
|
48
|
+
*/
|
|
49
|
+
private async ensureDatabaseExists(): Promise<void> {
|
|
50
|
+
const env = process.env.NODE_ENV || 'development';
|
|
51
|
+
const config = (configList as any)[env];
|
|
52
|
+
const dbName = config.database;
|
|
53
|
+
|
|
54
|
+
// Connect to the server, not the specific DB
|
|
55
|
+
const tempSequelize = new Sequelize('', config.username, config.password, {
|
|
56
|
+
host: config.host,
|
|
57
|
+
dialect: config.dialect,
|
|
58
|
+
logging: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await tempSequelize.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\`;`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
Logger.error('Failed to create database:', error);
|
|
65
|
+
throw error;
|
|
66
|
+
} finally {
|
|
67
|
+
await tempSequelize.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Verify the database connection.
|
|
73
|
+
*/
|
|
74
|
+
private async connectDatabase(): Promise<void> {
|
|
75
|
+
await db.connect();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize and configure Umzug.
|
|
80
|
+
*/
|
|
81
|
+
private getUmzugInstance(): Umzug<QueryInterface> {
|
|
82
|
+
const sequelize = db.sequelize;
|
|
83
|
+
|
|
84
|
+
return new Umzug<QueryInterface>({
|
|
85
|
+
migrations: {
|
|
86
|
+
glob: 'src/database/migrations/*.js',
|
|
87
|
+
resolve: ({ name, path, context }) => {
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
up: async () => {
|
|
91
|
+
// Windows Fix: Convert path to URL
|
|
92
|
+
const migration = await import(pathToFileURL(path as string).href);
|
|
93
|
+
return migration.default.up(context, sequelize.Sequelize);
|
|
94
|
+
},
|
|
95
|
+
down: async () => {
|
|
96
|
+
// Windows Fix: Convert path to URL
|
|
97
|
+
const migration = await import(pathToFileURL(path as string).href);
|
|
98
|
+
return migration.default.down(context, sequelize.Sequelize);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
context: sequelize.getQueryInterface(),
|
|
104
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
105
|
+
logger: console,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handle the specific CLI command logic.
|
|
111
|
+
*/
|
|
112
|
+
private async executeCommand(umzug: Umzug<QueryInterface>): Promise<void> {
|
|
113
|
+
switch (this.command) {
|
|
114
|
+
case 'up':
|
|
115
|
+
Logger.info('Running Migrations...');
|
|
116
|
+
await umzug.up();
|
|
117
|
+
Logger.info('Migrations executed successfully.');
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'down':
|
|
121
|
+
Logger.info('Rolling back last migration...');
|
|
122
|
+
await umzug.down();
|
|
123
|
+
Logger.info('Rollback complete.');
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'reset':
|
|
127
|
+
Logger.info('Resetting Database...');
|
|
128
|
+
await umzug.down({ to: 0 });
|
|
129
|
+
Logger.info('Database reset complete.');
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
Logger.info(`
|
|
134
|
+
Unknown command: "${this.command}"
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
npm run migrate -> Run pending migrations
|
|
138
|
+
npm run migrate:undo -> Undo last migration
|
|
139
|
+
npm run migrate:reset -> Undo all migrations
|
|
140
|
+
`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
new MigrationRunner().run();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import db from '../src/models/index.js';
|
|
2
|
+
import DatabaseSeeder from '../src/database/seeders/DatabaseSeeder.js';
|
|
3
|
+
import Logger from '../src/utils/Logger.js';
|
|
4
|
+
|
|
5
|
+
class SeederRunner {
|
|
6
|
+
/**
|
|
7
|
+
* Main entry point to execute the seeding logic.
|
|
8
|
+
*/
|
|
9
|
+
public async run(): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
// 1. Connect to the Database
|
|
12
|
+
await this.connectDatabase();
|
|
13
|
+
|
|
14
|
+
// 2. Execute the Main Seeder
|
|
15
|
+
await this.executeSeeding();
|
|
16
|
+
|
|
17
|
+
Logger.info('Database seeding completed successfully.');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
Logger.error('Seeding failed:', error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Verify the database connection.
|
|
27
|
+
*/
|
|
28
|
+
private async connectDatabase(): Promise<void> {
|
|
29
|
+
await db.connect();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Instantiate and run the main DatabaseSeeder.
|
|
34
|
+
*/
|
|
35
|
+
private async executeSeeding(): Promise<void> {
|
|
36
|
+
Logger.info('Starting Database Seeder...');
|
|
37
|
+
|
|
38
|
+
await DatabaseSeeder.run();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
new SeederRunner().run();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
3
|
+
|
|
4
|
+
class {{ControllerName}} {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Display a listing of the resource.
|
|
8
|
+
*/
|
|
9
|
+
public async index(req: Request, res: Response) {
|
|
10
|
+
// const items = await Model.findAll();
|
|
11
|
+
return ApiResponse.success(res, []);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Store a newly created resource in storage.
|
|
16
|
+
*/
|
|
17
|
+
public async store(req: Request, res: Response) {
|
|
18
|
+
// const item = await Model.create(req.body);
|
|
19
|
+
return ApiResponse.success(res, {}, 'Resource created successfully');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Display the specified resource.
|
|
24
|
+
*/
|
|
25
|
+
public async show(req: Request, res: Response) {
|
|
26
|
+
const { id } = req.params;
|
|
27
|
+
// const item = await Model.findByPk(id);
|
|
28
|
+
return ApiResponse.success(res, { id });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the specified resource in storage.
|
|
33
|
+
*/
|
|
34
|
+
public async update(req: Request, res: Response) {
|
|
35
|
+
const { id } = req.params;
|
|
36
|
+
// await Model.update(req.body, { where: { id } });
|
|
37
|
+
return ApiResponse.success(res, { id }, 'Resource updated successfully');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remove the specified resource from storage.
|
|
42
|
+
*/
|
|
43
|
+
public async destroy(req: Request, res: Response) {
|
|
44
|
+
const { id } = req.params;
|
|
45
|
+
// await Model.destroy({ where: { id } });
|
|
46
|
+
return ApiResponse.success(res, null, 'Resource deleted successfully');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default new {{ControllerName}}();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import {{ModelName}} from '../../models/{{ModelName}}.js';
|
|
3
|
+
import Factory from './Factory.js';
|
|
4
|
+
|
|
5
|
+
class {{FactoryName}} extends Factory<{{ModelName}}> {
|
|
6
|
+
protected model = {{ModelName}};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Define the model's default state.
|
|
10
|
+
*/
|
|
11
|
+
protected definition() {
|
|
12
|
+
return {
|
|
13
|
+
// name: faker.person.fullName(),
|
|
14
|
+
// email: faker.internet.email(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default new {{FactoryName}}();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
class Create{{ClassName}} {
|
|
4
|
+
/**
|
|
5
|
+
* Run the migrations.
|
|
6
|
+
*/
|
|
7
|
+
async up(queryInterface, sequelize) {
|
|
8
|
+
await queryInterface.createTable('{{TableName}}', {
|
|
9
|
+
id: {
|
|
10
|
+
allowNull: false,
|
|
11
|
+
autoIncrement: true,
|
|
12
|
+
primaryKey: true,
|
|
13
|
+
type: DataTypes.BIGINT
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Add your columns here...
|
|
17
|
+
|
|
18
|
+
created_at: {
|
|
19
|
+
type: DataTypes.DATE,
|
|
20
|
+
allowNull: false,
|
|
21
|
+
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
|
|
22
|
+
},
|
|
23
|
+
updated_at: {
|
|
24
|
+
type: DataTypes.DATE,
|
|
25
|
+
allowNull: false,
|
|
26
|
+
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
|
|
27
|
+
},
|
|
28
|
+
// Uncomment for paranoid mode
|
|
29
|
+
// deleted_at: {
|
|
30
|
+
// type: DataTypes.DATE,
|
|
31
|
+
// allowNull: true,
|
|
32
|
+
// }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reverse the migrations.
|
|
38
|
+
*/
|
|
39
|
+
async down(queryInterface, sequelize) {
|
|
40
|
+
await queryInterface.dropTable('{{TableName}}');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default new Create{{ClassName}}();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Model, DataTypes, Sequelize, Optional } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
interface {{ModelName}}Attributes {
|
|
4
|
+
id: number;
|
|
5
|
+
created_at?: Date;
|
|
6
|
+
updated_at?: Date;
|
|
7
|
+
deleted_at?: Date | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface {{ModelName}}CreationAttributes extends Optional<{{ModelName}}Attributes, 'id' | 'created_at' | 'updated_at' | 'deleted_at'> {}
|
|
11
|
+
|
|
12
|
+
class {{ModelName}} extends Model<{{ModelName}}Attributes, {{ModelName}}CreationAttributes> implements {{ModelName}}Attributes {
|
|
13
|
+
declare id: number;
|
|
14
|
+
declare created_at: Date;
|
|
15
|
+
declare updated_at: Date;
|
|
16
|
+
declare deleted_at: Date | null;
|
|
17
|
+
|
|
18
|
+
static initModel(sequelize: Sequelize) {
|
|
19
|
+
{{ModelName}}.init(
|
|
20
|
+
{
|
|
21
|
+
id: {
|
|
22
|
+
type: DataTypes.INTEGER,
|
|
23
|
+
autoIncrement: true,
|
|
24
|
+
primaryKey: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
sequelize,
|
|
29
|
+
modelName: '{{ModelName}}',
|
|
30
|
+
tableName: '{{TableName}}', // e.g. 'products'
|
|
31
|
+
paranoid: true,
|
|
32
|
+
timestamps: true,
|
|
33
|
+
underscored: true,
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default {{ModelName}};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import express, { Application } from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import helmet from 'helmet';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import db from './src/models/index.js';
|
|
6
|
+
import RouteService from './src/services/RouteService.js';
|
|
7
|
+
import ExceptionHandler from './src/exceptions/Handler.js';
|
|
8
|
+
import Logger from './src/utils/Logger.js';
|
|
9
|
+
import Limiter from './src/middlewares/Limiter.js';
|
|
10
|
+
import Maintenance from './src/middlewares/Maintenance.js';
|
|
11
|
+
import dotenv from 'dotenv';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
const app: Application = express();
|
|
16
|
+
const PORT = process.env.APP_PORT || 3000;
|
|
17
|
+
|
|
18
|
+
// ==========================
|
|
19
|
+
// Global Middleware
|
|
20
|
+
// ==========================
|
|
21
|
+
app.use(Maintenance.handle);
|
|
22
|
+
app.use(helmet({ crossOriginResourcePolicy: false }));
|
|
23
|
+
app.use(cors());
|
|
24
|
+
app.use(express.json());
|
|
25
|
+
app.use(express.urlencoded({ extended: true }));
|
|
26
|
+
app.use(express.static(path.join(process.cwd(), 'public')));
|
|
27
|
+
app.use(Limiter.global);
|
|
28
|
+
|
|
29
|
+
// ==========================
|
|
30
|
+
// Register Routes
|
|
31
|
+
// ==========================
|
|
32
|
+
// This loads both your API and Web routes
|
|
33
|
+
RouteService.boot(app);
|
|
34
|
+
|
|
35
|
+
// ==========================
|
|
36
|
+
// Error Handling
|
|
37
|
+
// ==========================
|
|
38
|
+
|
|
39
|
+
// 404 Not Found Handler
|
|
40
|
+
app.use(ExceptionHandler.notFound);
|
|
41
|
+
|
|
42
|
+
// Global Error Handler
|
|
43
|
+
app.use(ExceptionHandler.handle);
|
|
44
|
+
|
|
45
|
+
// ==========================
|
|
46
|
+
// Start Server
|
|
47
|
+
// ==========================
|
|
48
|
+
const start = async () => {
|
|
49
|
+
try {
|
|
50
|
+
// Test Database Connection
|
|
51
|
+
await db.connect();
|
|
52
|
+
|
|
53
|
+
// Start Listening
|
|
54
|
+
app.listen(PORT, () => {
|
|
55
|
+
Logger.info(`Server running on http://localhost:${PORT}`);
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
Logger.error('Unable to connect to the database:', error);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
start();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Options } from 'sequelize';
|
|
2
|
+
import Logger from '../utils/Logger.js';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
class DatabaseConfig {
|
|
8
|
+
public development: Options;
|
|
9
|
+
public test: Options;
|
|
10
|
+
public production: Options;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.development = this.getEnvironmentConfig();
|
|
14
|
+
this.test = this.getEnvironmentConfig('test');
|
|
15
|
+
this.production = this.getEnvironmentConfig('production');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper to generate config based on the environment.
|
|
20
|
+
* This reduces repetition.
|
|
21
|
+
*/
|
|
22
|
+
private getEnvironmentConfig(env: 'development' | 'test' | 'production' = 'development'): Options {
|
|
23
|
+
const isTest = env === 'test';
|
|
24
|
+
const isProd = env === 'production';
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
username: process.env.DB_USERNAME,
|
|
28
|
+
password: process.env.DB_PASSWORD,
|
|
29
|
+
database: isTest ? process.env.DB_DATABASE_TEST : process.env.DB_DATABASE,
|
|
30
|
+
host: process.env.DB_HOST,
|
|
31
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
32
|
+
dialect: (process.env.DB_DIALECT as any) || 'mysql',
|
|
33
|
+
// Disable logging in test/production to keep logs clean
|
|
34
|
+
logging: isProd || isTest ? false : (msg) => Logger.info(`[SQL] ${msg}`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default new DatabaseConfig();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import AuthService from '../services/AuthService.js';
|
|
3
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
4
|
+
|
|
5
|
+
class AuthController {
|
|
6
|
+
public async login(req: Request, res: Response) {
|
|
7
|
+
try {
|
|
8
|
+
const { email, password } = req.body;
|
|
9
|
+
const result = await AuthService.login(email, password);
|
|
10
|
+
return ApiResponse.success(res, result, 'Login successful');
|
|
11
|
+
} catch (error: any) {
|
|
12
|
+
return ApiResponse.error(res, error.message, 401);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async me(req: Request, res: Response) {
|
|
17
|
+
return ApiResponse.success(res, req.user);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default new AuthController();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import UserService from '../services/UserService.js';
|
|
3
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
4
|
+
|
|
5
|
+
class UserController {
|
|
6
|
+
/**
|
|
7
|
+
* Get paginated list of users.
|
|
8
|
+
*/
|
|
9
|
+
public async index(req: Request, res: Response, next: NextFunction) {
|
|
10
|
+
try {
|
|
11
|
+
const page = Number(req.query.page) || 1;
|
|
12
|
+
const limit = Number(req.query.limit) || 15;
|
|
13
|
+
const users = await UserService.getAllUsers(page, limit);
|
|
14
|
+
return ApiResponse.success(res, users, 'Users retrieved successfully');
|
|
15
|
+
} catch (error) {
|
|
16
|
+
next(error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async store(req: Request, res: Response, next: NextFunction) {
|
|
21
|
+
try {
|
|
22
|
+
const user = await UserService.createUser(req.body);
|
|
23
|
+
return ApiResponse.success(res, user, 'User created successfully', 201);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
next(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async uploadAvatar(req: Request, res: Response, next: NextFunction) {
|
|
30
|
+
try {
|
|
31
|
+
if (!req.file) {
|
|
32
|
+
return ApiResponse.error(res, 'No file uploaded', 400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 1. Get the file path relative to your server
|
|
36
|
+
const filePath = `uploads/${req.file.filename}`;
|
|
37
|
+
|
|
38
|
+
// 2. Update the user in the DB (assuming you have req.user from AuthMiddleware)
|
|
39
|
+
await UserService.updateAvatar(req.user.id, filePath);
|
|
40
|
+
|
|
41
|
+
// 3. Return success
|
|
42
|
+
return ApiResponse.success(res, {
|
|
43
|
+
path: filePath,
|
|
44
|
+
url: `${req.protocol}://${req.get('host')}/${filePath}`
|
|
45
|
+
}, 'Avatar uploaded successfully');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
next(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default new UserController();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Model, ModelStatic } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
export default abstract class Factory<T extends Model> {
|
|
4
|
+
protected abstract model: ModelStatic<T>;
|
|
5
|
+
|
|
6
|
+
// abstract method where you define the default state
|
|
7
|
+
protected abstract definition(): Record<string, any>;
|
|
8
|
+
|
|
9
|
+
// Make a single object (not saved to DB)
|
|
10
|
+
public make(overrides: Record<string, any> = {}): Record<string, any> {
|
|
11
|
+
return {
|
|
12
|
+
...this.definition(),
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Make multiple objects (not saved to DB)
|
|
18
|
+
public makeMany(count: number, overrides: Record<string, any> = {}): Record<string, any>[] {
|
|
19
|
+
return Array.from({ length: count }).map(() => this.make(overrides));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create single record in DB
|
|
23
|
+
public async create(overrides: Record<string, any> = {}): Promise<T> {
|
|
24
|
+
const data = this.make(overrides);
|
|
25
|
+
return await this.model.create(data as any);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create multiple records in DB
|
|
29
|
+
public async createMany(count: number, overrides: Record<string, any> = {}): Promise<T[]> {
|
|
30
|
+
const data = this.makeMany(count, overrides);
|
|
31
|
+
return await this.model.bulkCreate(data as any);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import User from '../../models/User.js'; // Import your Model
|
|
3
|
+
import Factory from './Factory.js';
|
|
4
|
+
|
|
5
|
+
class UserFactory extends Factory<User> {
|
|
6
|
+
protected model = User;
|
|
7
|
+
|
|
8
|
+
protected definition() {
|
|
9
|
+
return {
|
|
10
|
+
firstname: faker.person.firstName(),
|
|
11
|
+
lastname: faker.person.lastName(),
|
|
12
|
+
email: faker.internet.email(),
|
|
13
|
+
password: '$2b$10$YourHashedPasswordHere',
|
|
14
|
+
role: 'user',
|
|
15
|
+
avatar: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public admin() {
|
|
20
|
+
return {
|
|
21
|
+
role: 'admin',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default new UserFactory();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
class CreateUserTable {
|
|
4
|
+
/**
|
|
5
|
+
* Run the migrations.
|
|
6
|
+
*/
|
|
7
|
+
async up(queryInterface, Sequelize) {
|
|
8
|
+
await queryInterface.createTable('users', {
|
|
9
|
+
id: {
|
|
10
|
+
allowNull: false,
|
|
11
|
+
autoIncrement: true,
|
|
12
|
+
primaryKey: true,
|
|
13
|
+
type: DataTypes.BIGINT
|
|
14
|
+
},
|
|
15
|
+
firstname: {
|
|
16
|
+
type: DataTypes.STRING(50),
|
|
17
|
+
allowNull: false
|
|
18
|
+
},
|
|
19
|
+
lastname: {
|
|
20
|
+
type: DataTypes.STRING(50),
|
|
21
|
+
allowNull: false
|
|
22
|
+
},
|
|
23
|
+
email: {
|
|
24
|
+
type: DataTypes.STRING(100),
|
|
25
|
+
unique: true,
|
|
26
|
+
allowNull: false
|
|
27
|
+
},
|
|
28
|
+
password: {
|
|
29
|
+
type: DataTypes.STRING(255),
|
|
30
|
+
allowNull: false
|
|
31
|
+
},
|
|
32
|
+
role: {
|
|
33
|
+
type: DataTypes.STRING(20),
|
|
34
|
+
allowNull: false,
|
|
35
|
+
defaultValue: 'user'
|
|
36
|
+
},
|
|
37
|
+
avatar: {
|
|
38
|
+
type: DataTypes.TEXT,
|
|
39
|
+
allowNull: true,
|
|
40
|
+
defaultValue: null,
|
|
41
|
+
},
|
|
42
|
+
created_at: {
|
|
43
|
+
type: DataTypes.DATE,
|
|
44
|
+
allowNull: false,
|
|
45
|
+
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
|
46
|
+
},
|
|
47
|
+
updated_at: {
|
|
48
|
+
type: DataTypes.DATE,
|
|
49
|
+
allowNull: false,
|
|
50
|
+
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
|
51
|
+
},
|
|
52
|
+
deleted_at: {
|
|
53
|
+
type: DataTypes.DATE,
|
|
54
|
+
allowNull: true,
|
|
55
|
+
defaultValue: null,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reverse the migrations.
|
|
62
|
+
*/
|
|
63
|
+
async down(queryInterface, Sequelize) {
|
|
64
|
+
await queryInterface.dropTable('users');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default new CreateUserTable();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import UserFactory from '../factories/UserFactory.js';
|
|
2
|
+
import Logger from '../../utils/Logger.js';
|
|
3
|
+
|
|
4
|
+
class DatabaseSeeder {
|
|
5
|
+
/**
|
|
6
|
+
* Run the database seeds.
|
|
7
|
+
*/
|
|
8
|
+
async run() {
|
|
9
|
+
Logger.info('Seeding database...');
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// 1. Create a specific Admin User
|
|
13
|
+
await UserFactory.create({
|
|
14
|
+
firstname: 'Admin',
|
|
15
|
+
lastname: 'Lumina',
|
|
16
|
+
email: 'admin@lumina.com',
|
|
17
|
+
password: "lumina123",
|
|
18
|
+
role: 'admin',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 2. Create random users
|
|
22
|
+
await UserFactory.createMany(20);
|
|
23
|
+
|
|
24
|
+
Logger.info('Seeding complete!');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
Logger.error('Seeding failed:', error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default new DatabaseSeeder();
|