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,32 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
3
|
+
import Logger from '../utils/Logger.js';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
class ExceptionHandler {
|
|
9
|
+
/**
|
|
10
|
+
* Handle 404 Not Found errors.
|
|
11
|
+
*/
|
|
12
|
+
notFound(req: Request, res: Response, next: NextFunction) {
|
|
13
|
+
return ApiResponse.error(res, 'Route not found', 404);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handle global application errors.
|
|
18
|
+
* Matches Express (err, req, res, next) signature.
|
|
19
|
+
*/
|
|
20
|
+
handle(err: any, req: Request, res: Response, next: NextFunction) {
|
|
21
|
+
// Log the error
|
|
22
|
+
Logger.error('Exception:', err.stack || err.message);
|
|
23
|
+
|
|
24
|
+
const status = err.status || 500;
|
|
25
|
+
const message = err.message || 'Internal Server Error';
|
|
26
|
+
const errorDetails = process.env.NODE_ENV === 'development' ? err : {};
|
|
27
|
+
|
|
28
|
+
return ApiResponse.error(res, message, status, errorDetails);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default new ExceptionHandler();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
class Authentication {
|
|
9
|
+
/**
|
|
10
|
+
* Handle an incoming request.
|
|
11
|
+
*/
|
|
12
|
+
handle(req: Request, res: Response, next: NextFunction) {
|
|
13
|
+
const authHeader = req.headers.authorization;
|
|
14
|
+
|
|
15
|
+
if (!authHeader || !authHeader.startsWith('Bearer')) {
|
|
16
|
+
return ApiResponse.error(res, 'Unauthorized: No token provided', 401);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const token = authHeader.split(' ')[1];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const secret = process.env.JWT_SECRET || 'default_secret';
|
|
23
|
+
const decoded = jwt.verify(token, secret);
|
|
24
|
+
|
|
25
|
+
req.user = decoded;
|
|
26
|
+
|
|
27
|
+
next();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return ApiResponse.error(res, 'Unauthorized: Invalid token', 401);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default new Authentication();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import rateLimit from 'express-rate-limit';
|
|
2
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
3
|
+
|
|
4
|
+
class Limiter {
|
|
5
|
+
/**
|
|
6
|
+
* General limiter for the entire API.
|
|
7
|
+
* Allows 100 requests per 15 minutes.
|
|
8
|
+
*/
|
|
9
|
+
public static global = rateLimit({
|
|
10
|
+
windowMs: 15 * 60 * 1000,
|
|
11
|
+
max: 100,
|
|
12
|
+
standardHeaders: true,
|
|
13
|
+
legacyHeaders: false,
|
|
14
|
+
handler: (req, res) => {
|
|
15
|
+
return ApiResponse.error(res, 'Too many requests, please try again later.', 429);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Strict limiter for sensitive routes (Login/Register).
|
|
21
|
+
* Allows 5 attempts per hour.
|
|
22
|
+
*/
|
|
23
|
+
public static auth = rateLimit({
|
|
24
|
+
windowMs: 60 * 60 * 1000,
|
|
25
|
+
max: 5,
|
|
26
|
+
standardHeaders: true,
|
|
27
|
+
legacyHeaders: false,
|
|
28
|
+
handler: (req, res) => {
|
|
29
|
+
return ApiResponse.error(res, 'Too many login attempts, please try again after an hour.', 429);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Limiter;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
class Maintenance {
|
|
10
|
+
private lockFile = path.join(process.cwd(), 'maintenance.lock');
|
|
11
|
+
|
|
12
|
+
public handle = (req: Request, res: Response, next: NextFunction) => {
|
|
13
|
+
if (fs.existsSync(this.lockFile)) {
|
|
14
|
+
const bypassSecret = req.headers['x-bypass-maintenance'];
|
|
15
|
+
|
|
16
|
+
if (bypassSecret === process.env.MAINTENANCE_SECRET) {
|
|
17
|
+
return next();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
res.status(503);
|
|
21
|
+
res.setHeader('Retry-After', '60');
|
|
22
|
+
|
|
23
|
+
if (req.accepts('html')) {
|
|
24
|
+
const viewPath = path.join(process.cwd(), 'views', 'maintenance.html');
|
|
25
|
+
if (fs.existsSync(viewPath)) {
|
|
26
|
+
return res.sendFile(viewPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return ApiResponse.error(res, 'System is currently under maintenance. Please try again later.', 503);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
next();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default new Maintenance();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { ZodType, ZodError } from 'zod';
|
|
3
|
+
import ApiResponse from '../utils/ApiResponse.js';
|
|
4
|
+
|
|
5
|
+
class Validator {
|
|
6
|
+
/**
|
|
7
|
+
* Validates the request body against a Zod schema.
|
|
8
|
+
*/
|
|
9
|
+
public validate(schema: ZodType<any>) {
|
|
10
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
11
|
+
try {
|
|
12
|
+
schema.parse(req.body);
|
|
13
|
+
next();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof ZodError) {
|
|
16
|
+
const formattedErrors = error.issues.map((err) => ({
|
|
17
|
+
field: err.path.join('.'),
|
|
18
|
+
message: err.message,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
return ApiResponse.error(res, 'Validation Failed', 422, formattedErrors);
|
|
22
|
+
}
|
|
23
|
+
next(error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default new Validator();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Model, DataTypes, Sequelize, Optional } from 'sequelize';
|
|
2
|
+
import Hash from '../utils/Hash.js';
|
|
3
|
+
|
|
4
|
+
interface UserAttributes {
|
|
5
|
+
id: number;
|
|
6
|
+
firstname: string;
|
|
7
|
+
lastname: string;
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
role: string;
|
|
11
|
+
avatar: string | null;
|
|
12
|
+
created_at?: Date;
|
|
13
|
+
updated_at?: Date;
|
|
14
|
+
deleted_at?: Date | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UserCreationAttributes extends Optional<UserAttributes, 'id' | 'created_at' | 'updated_at' | 'deleted_at' | 'role' | 'avatar'> {}
|
|
18
|
+
|
|
19
|
+
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
|
20
|
+
declare id: number;
|
|
21
|
+
declare firstname: string;
|
|
22
|
+
declare lastname: string;
|
|
23
|
+
declare email: string;
|
|
24
|
+
declare password: string;
|
|
25
|
+
declare role: string;
|
|
26
|
+
declare avatar: string | null;
|
|
27
|
+
declare created_at: Date;
|
|
28
|
+
declare updated_at: Date;
|
|
29
|
+
declare deleted_at?: Date | null;
|
|
30
|
+
|
|
31
|
+
static initModel(sequelize: Sequelize) {
|
|
32
|
+
User.init(
|
|
33
|
+
{
|
|
34
|
+
id: {
|
|
35
|
+
type: DataTypes.INTEGER,
|
|
36
|
+
autoIncrement: true,
|
|
37
|
+
primaryKey: true,
|
|
38
|
+
},
|
|
39
|
+
firstname: {
|
|
40
|
+
type: DataTypes.STRING,
|
|
41
|
+
allowNull: false,
|
|
42
|
+
},
|
|
43
|
+
lastname: {
|
|
44
|
+
type: DataTypes.STRING,
|
|
45
|
+
allowNull: false,
|
|
46
|
+
},
|
|
47
|
+
email: {
|
|
48
|
+
type: DataTypes.STRING,
|
|
49
|
+
unique: true,
|
|
50
|
+
allowNull: false,
|
|
51
|
+
},
|
|
52
|
+
password: {
|
|
53
|
+
type: DataTypes.STRING,
|
|
54
|
+
allowNull: false,
|
|
55
|
+
},
|
|
56
|
+
role: {
|
|
57
|
+
type: DataTypes.STRING,
|
|
58
|
+
allowNull: false,
|
|
59
|
+
defaultValue: 'user',
|
|
60
|
+
},
|
|
61
|
+
avatar: {
|
|
62
|
+
type: DataTypes.TEXT,
|
|
63
|
+
allowNull: true,
|
|
64
|
+
defaultValue: null,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
sequelize,
|
|
69
|
+
modelName: 'User',
|
|
70
|
+
tableName: 'users',
|
|
71
|
+
paranoid: true,
|
|
72
|
+
timestamps: true,
|
|
73
|
+
underscored: true,
|
|
74
|
+
hooks: {
|
|
75
|
+
beforeCreate: async (user: User) => {
|
|
76
|
+
if (user.password) {
|
|
77
|
+
user.password = await Hash.make(user.password);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
beforeUpdate: async (user: User) => {
|
|
81
|
+
if (user.changed('password')) {
|
|
82
|
+
user.password = await Hash.make(user.password);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default User;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Sequelize } from 'sequelize';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
|
+
import configList from '../config/database.js';
|
|
6
|
+
import Logger from '../utils/Logger.js';
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
class Database {
|
|
15
|
+
public sequelize: Sequelize;
|
|
16
|
+
public models: any = {};
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
const env = process.env.NODE_ENV || 'development';
|
|
20
|
+
const config = (configList as any)[env];
|
|
21
|
+
|
|
22
|
+
if (!config) {
|
|
23
|
+
Logger.error(`Fatal Error: Database config for "${env}" not found.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Initialize Sequelize
|
|
28
|
+
this.sequelize = new Sequelize(
|
|
29
|
+
config.database,
|
|
30
|
+
config.username,
|
|
31
|
+
config.password,
|
|
32
|
+
config
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Connect to DB and Load Models
|
|
38
|
+
*/
|
|
39
|
+
public async connect() {
|
|
40
|
+
try {
|
|
41
|
+
// 1. Authenticate Connection
|
|
42
|
+
await this.sequelize.authenticate();
|
|
43
|
+
Logger.info('Database connection established.');
|
|
44
|
+
|
|
45
|
+
// 2. Load Models Dynamically
|
|
46
|
+
await this.loadModels();
|
|
47
|
+
|
|
48
|
+
// 3. Setup Associations
|
|
49
|
+
this.associateModels();
|
|
50
|
+
|
|
51
|
+
} catch (error) {
|
|
52
|
+
Logger.error('Unable to connect to the database:', error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async loadModels() {
|
|
58
|
+
const basename = path.basename(__filename);
|
|
59
|
+
|
|
60
|
+
// Read all files in this directory
|
|
61
|
+
const files = fs.readdirSync(__dirname).filter((file) => {
|
|
62
|
+
return (
|
|
63
|
+
file.indexOf('.') !== 0 &&
|
|
64
|
+
file !== basename &&
|
|
65
|
+
(file.slice(-3) === '.ts' || file.slice(-3) === '.js')
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Import and Init each model
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const modelPath = pathToFileURL(path.join(__dirname, file)).href;
|
|
72
|
+
const modelModule = await import(modelPath);
|
|
73
|
+
const model = modelModule.default;
|
|
74
|
+
|
|
75
|
+
if (model && model.initModel) {
|
|
76
|
+
model.initModel(this.sequelize);
|
|
77
|
+
// Store in models object instead of root
|
|
78
|
+
this.models[model.name] = model;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private associateModels() {
|
|
84
|
+
Object.keys(this.models).forEach((modelName) => {
|
|
85
|
+
if (this.models[modelName].associate) {
|
|
86
|
+
this.models[modelName].associate(this.models);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default new Database();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
class UserRequest {
|
|
4
|
+
/**
|
|
5
|
+
* Rules for creating a user.
|
|
6
|
+
*/
|
|
7
|
+
public static store = z.object({
|
|
8
|
+
firstname: z.string().min(2, 'First name must be at least 2 characters'),
|
|
9
|
+
lastname: z.string().min(2, 'Last name must be at least 2 characters'),
|
|
10
|
+
email: z.email('Invalid email address'),
|
|
11
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
12
|
+
// Optional: role is not required here if you default it in the database
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rules for updating a user.
|
|
17
|
+
*/
|
|
18
|
+
public static update = z.object({
|
|
19
|
+
// fields optional for updates
|
|
20
|
+
firstname: z.string().min(2).optional(),
|
|
21
|
+
lastname: z.string().min(2).optional(),
|
|
22
|
+
email: z.string().email().optional(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default UserRequest;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import UserController from '../controllers/UserController.js';
|
|
3
|
+
import Authentication from '../middlewares/Authentication.js';
|
|
4
|
+
import Validator from '../middlewares/Validator.js';
|
|
5
|
+
import UserRequest from '../requests/UserRequest.js';
|
|
6
|
+
import AuthController from '../controllers/AuthController.js';
|
|
7
|
+
import Limiter from '../middlewares/Limiter.js';
|
|
8
|
+
import StorageService from '../services/StorageService.js';
|
|
9
|
+
|
|
10
|
+
class ApiRoutes {
|
|
11
|
+
public router: Router;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.router = Router();
|
|
15
|
+
this.initializeRoutes();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Define all API routes here.
|
|
20
|
+
*/
|
|
21
|
+
protected initializeRoutes(): void {
|
|
22
|
+
this.router.post('/login', Limiter.auth, AuthController.login);
|
|
23
|
+
this.router.get('/me', Authentication.handle, AuthController.me);
|
|
24
|
+
this.router.get('/users', Authentication.handle, UserController.index);
|
|
25
|
+
this.router.post('/users', Validator.validate(UserRequest.store), UserController.store);
|
|
26
|
+
this.router.post('/users/avatar', Authentication.handle, StorageService.uploader.single('avatar'), UserController.uploadAvatar);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default new ApiRoutes().router;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
class WebRoutes {
|
|
8
|
+
public router: Router;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.router = Router();
|
|
12
|
+
this.initializeRoutes();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected initializeRoutes(): void {
|
|
16
|
+
// Home Route
|
|
17
|
+
this.router.get('/', (req: Request, res: Response) => {
|
|
18
|
+
const viewPath = path.join(process.cwd(), 'views', 'welcome.html');
|
|
19
|
+
res.sendFile(viewPath);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Status Route
|
|
23
|
+
this.router.get('/status', (req: Request, res: Response) => {
|
|
24
|
+
res.json({ status: 'UP', environment: process.env.NODE_ENV });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default new WebRoutes().router;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import User from '../models/User.js';
|
|
3
|
+
import Hash from '../utils/Hash.js';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
class AuthService {
|
|
9
|
+
public async login(email: string, password: string) {
|
|
10
|
+
const user = await User.findOne({ where: { email } });
|
|
11
|
+
|
|
12
|
+
if (!user || !(await Hash.check(password, user.password))) {
|
|
13
|
+
throw new Error('Invalid credentials');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = jwt.sign(
|
|
17
|
+
{ id: user.id, email: user.email, role: user.role },
|
|
18
|
+
process.env.JWT_SECRET as string,
|
|
19
|
+
{ expiresIn: process.env.JWT_EXPIRES_IN || '1d' } as jwt.SignOptions
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const userResponse = user.toJSON();
|
|
23
|
+
const { password: _, ...userWithoutPassword } = userResponse;
|
|
24
|
+
|
|
25
|
+
return { user: userWithoutPassword, token };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default new AuthService();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import apiRoutes from '../routes/api.js';
|
|
3
|
+
import webRoutes from '../routes/web.js';
|
|
4
|
+
|
|
5
|
+
class RouteService {
|
|
6
|
+
/**
|
|
7
|
+
* Register all routes for the application.
|
|
8
|
+
*/
|
|
9
|
+
public boot(app: Application): void {
|
|
10
|
+
app.use('/api', apiRoutes);
|
|
11
|
+
app.use('/', webRoutes);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default new RouteService();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import multer, { StorageEngine } from 'multer';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
|
|
6
|
+
class StorageService {
|
|
7
|
+
private storage: StorageEngine;
|
|
8
|
+
private uploadDir: string;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
// 1. Define where files go
|
|
12
|
+
this.uploadDir = path.join(process.cwd(), 'public', 'uploads');
|
|
13
|
+
|
|
14
|
+
// Ensure directory exists
|
|
15
|
+
if (!fs.existsSync(this.uploadDir)) {
|
|
16
|
+
fs.mkdirSync(this.uploadDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Configure Disk Storage
|
|
20
|
+
this.storage = multer.diskStorage({
|
|
21
|
+
destination: (req, file, cb) => {
|
|
22
|
+
cb(null, this.uploadDir);
|
|
23
|
+
},
|
|
24
|
+
filename: (req, file, cb) => {
|
|
25
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
26
|
+
const ext = path.extname(file.originalname);
|
|
27
|
+
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the Multer instance with validation rules.
|
|
34
|
+
*/
|
|
35
|
+
public get uploader() {
|
|
36
|
+
return multer({
|
|
37
|
+
storage: this.storage,
|
|
38
|
+
limits: {
|
|
39
|
+
fileSize: 5 * 1024 * 1024,
|
|
40
|
+
},
|
|
41
|
+
fileFilter: this.fileFilter,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate file types (e.g., Images only).
|
|
47
|
+
*/
|
|
48
|
+
private fileFilter(req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) {
|
|
49
|
+
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
|
|
50
|
+
|
|
51
|
+
if (allowedMimeTypes.includes(file.mimetype)) {
|
|
52
|
+
cb(null, true);
|
|
53
|
+
} else {
|
|
54
|
+
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default new StorageService();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import User, { UserCreationAttributes } from '../models/User.js';
|
|
2
|
+
import Paginator from '../utils/Paginator.js';
|
|
3
|
+
|
|
4
|
+
class UserService {
|
|
5
|
+
/**
|
|
6
|
+
* Get all users with pagination.
|
|
7
|
+
*/
|
|
8
|
+
public async getAllUsers(page: number, limit: number) {
|
|
9
|
+
return await Paginator.paginate(User, page, limit, {
|
|
10
|
+
attributes: { exclude: ['password'] },
|
|
11
|
+
order: [['id', 'DESC']]
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async createUser(data: UserCreationAttributes) {
|
|
16
|
+
return await User.create(data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async updateAvatar(userId: number, avatarPath: string) {
|
|
20
|
+
const user = await User.findByPk(userId);
|
|
21
|
+
|
|
22
|
+
if (!user) {
|
|
23
|
+
throw new Error('User not found');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
user.avatar = avatarPath;
|
|
27
|
+
return await user.save();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default new UserService();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
|
|
3
|
+
class ApiResponse {
|
|
4
|
+
/**
|
|
5
|
+
* Send a standard success response.
|
|
6
|
+
*/
|
|
7
|
+
public success(res: Response, data: any, message: string = 'Success', code: number = 200) {
|
|
8
|
+
return res.status(code).json({
|
|
9
|
+
success: true,
|
|
10
|
+
message,
|
|
11
|
+
data,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a standard error response.
|
|
17
|
+
*/
|
|
18
|
+
public error(res: Response, message: string = 'Error', code: number = 400, errors: any = null) {
|
|
19
|
+
return res.status(code).json({
|
|
20
|
+
success: false,
|
|
21
|
+
message,
|
|
22
|
+
errors,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default new ApiResponse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import bcrypt from 'bcrypt';
|
|
2
|
+
|
|
3
|
+
class Hash {
|
|
4
|
+
/**
|
|
5
|
+
* Hash a plain text string.
|
|
6
|
+
* Usage: await Hash.make('password123');
|
|
7
|
+
*/
|
|
8
|
+
public async make(plainText: string): Promise<string> {
|
|
9
|
+
return await bcrypt.hash(plainText, 10);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Verify a plain text string against a hash.
|
|
14
|
+
* Usage: await Hash.check('password123', user.password);
|
|
15
|
+
*/
|
|
16
|
+
public async check(plainText: string, hashedValue: string): Promise<boolean> {
|
|
17
|
+
return await bcrypt.compare(plainText, hashedValue);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default new Hash();
|