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.
Files changed (51) hide show
  1. package/bin/cli.js +73 -0
  2. package/package.json +28 -0
  3. package/template/.env.example +15 -0
  4. package/template/.sequelizerc +11 -0
  5. package/template/_gitignore +3 -0
  6. package/template/nodemon.json +5 -0
  7. package/template/package.json +53 -0
  8. package/template/public/img/logo1.png +0 -0
  9. package/template/public/img/logo2.png +0 -0
  10. package/template/scripts/create-controller.ts +89 -0
  11. package/template/scripts/create-factory.ts +98 -0
  12. package/template/scripts/create-migration.ts +98 -0
  13. package/template/scripts/create-model.ts +105 -0
  14. package/template/scripts/maintenance.ts +73 -0
  15. package/template/scripts/migrate.ts +146 -0
  16. package/template/scripts/seed.ts +42 -0
  17. package/template/scripts/stubs/controller.stub +50 -0
  18. package/template/scripts/stubs/factory.stub +19 -0
  19. package/template/scripts/stubs/migration.stub +44 -0
  20. package/template/scripts/stubs/model.stub +39 -0
  21. package/template/server.ts +63 -0
  22. package/template/src/config/database.ts +39 -0
  23. package/template/src/controllers/AuthController.ts +21 -0
  24. package/template/src/controllers/UserController.ts +52 -0
  25. package/template/src/database/factories/Factory.ts +33 -0
  26. package/template/src/database/factories/UserFactory.ts +26 -0
  27. package/template/src/database/migrations/20260205084944-create_user.js +68 -0
  28. package/template/src/database/seeders/DatabaseSeeder.js +32 -0
  29. package/template/src/exceptions/Handler.ts +32 -0
  30. package/template/src/middlewares/Authentication.ts +34 -0
  31. package/template/src/middlewares/Limiter.ts +34 -0
  32. package/template/src/middlewares/Maintenance.ts +37 -0
  33. package/template/src/middlewares/Validator.ts +29 -0
  34. package/template/src/models/User.ts +91 -0
  35. package/template/src/models/index.ts +92 -0
  36. package/template/src/requests/UserRequest.ts +26 -0
  37. package/template/src/routes/api.ts +30 -0
  38. package/template/src/routes/web.ts +29 -0
  39. package/template/src/services/AuthService.ts +29 -0
  40. package/template/src/services/RouteService.ts +15 -0
  41. package/template/src/services/StorageService.ts +59 -0
  42. package/template/src/services/UserService.ts +31 -0
  43. package/template/src/types/Pagination.d.ts +13 -0
  44. package/template/src/types/express/index.d.ts +9 -0
  45. package/template/src/utils/ApiResponse.ts +27 -0
  46. package/template/src/utils/Hash.ts +21 -0
  47. package/template/src/utils/Logger.ts +79 -0
  48. package/template/src/utils/Paginator.ts +41 -0
  49. package/template/tsconfig.json +24 -0
  50. package/template/views/maintenance.html +42 -0
  51. 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,13 @@
1
+ export interface PaginationMeta {
2
+ total: number;
3
+ per_page: number;
4
+ current_page: number;
5
+ last_page: number;
6
+ from: number | null;
7
+ to: number | null;
8
+ }
9
+
10
+ export interface PaginatedResult<T> {
11
+ data: T[];
12
+ meta: PaginationMeta;
13
+ }
@@ -0,0 +1,9 @@
1
+ import { JwtPayload } from 'jsonwebtoken';
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ user?: string | JwtPayload | any;
7
+ }
8
+ }
9
+ }
@@ -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();