create-lumina-project 1.2.0 → 2.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 (37) hide show
  1. package/package.json +3 -3
  2. package/template/.env.example +2 -1
  3. package/template/nodemon.json +1 -1
  4. package/template/package.json +26 -15
  5. package/template/resources/css/app.css +491 -0
  6. package/template/resources/js/Pages/Errors/404.tsx +32 -0
  7. package/template/resources/js/Pages/Maintenance.tsx +34 -0
  8. package/template/resources/js/Pages/Status.tsx +103 -0
  9. package/template/resources/js/Pages/Welcome.tsx +82 -0
  10. package/template/resources/js/app.tsx +43 -0
  11. package/template/resources/js/tsconfig.json +20 -0
  12. package/template/resources/js/vite-env.d.ts +1 -0
  13. package/template/resources/views/app.html +14 -0
  14. package/template/src/controllers/UserController.ts +5 -1
  15. package/template/src/controllers/WebController.ts +94 -0
  16. package/template/src/database/migrations/20260420050855-create_refresh_tokens.js +5 -0
  17. package/template/src/exceptions/Handler.ts +4 -4
  18. package/template/src/middlewares/ApiAuth.ts +67 -0
  19. package/template/src/middlewares/Csrf.ts +5 -4
  20. package/template/src/middlewares/InertiaMiddleware.ts +95 -0
  21. package/template/src/middlewares/Maintenance.ts +5 -5
  22. package/template/src/middlewares/RoleGuard.ts +20 -0
  23. package/template/src/middlewares/Validator.ts +17 -1
  24. package/template/src/middlewares/WebAuth.ts +122 -0
  25. package/template/src/models/BaseModel.ts +24 -0
  26. package/template/src/models/RefreshToken.ts +6 -5
  27. package/template/src/models/User.ts +3 -5
  28. package/template/src/routes/api.ts +16 -8
  29. package/template/src/routes/web.ts +10 -43
  30. package/template/src/server.ts +115 -0
  31. package/template/src/types/express/index.d.ts +8 -1
  32. package/template/src/types/express-inertia.d.ts +28 -0
  33. package/template/vite.config.ts +20 -0
  34. package/template/views/404.html +0 -233
  35. package/template/views/maintenance.html +0 -464
  36. package/template/views/status.html +0 -545
  37. package/template/views/welcome.html +0 -495
@@ -0,0 +1,122 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import jwt from 'jsonwebtoken';
3
+ import env from '../config/env.js';
4
+ import AuthService from '../services/AuthService.js';
5
+ import Logger from '../utils/Logger.js';
6
+ import { UserPayload } from '../types/express/index.js';
7
+
8
+
9
+ class WebAuth {
10
+ /**
11
+ * Middleware to protect web routes and redirect to login if not authenticated.
12
+ */
13
+ public async handle(req: Request, res: Response, next: NextFunction) {
14
+ const accessToken = req.cookies?.access_token;
15
+ const refreshToken = req.cookies?.refresh_token;
16
+
17
+ if (!accessToken) {
18
+ // If no access token, try to refresh
19
+ if (refreshToken) {
20
+ return this.silentRefresh(req, res, next, refreshToken);
21
+ }
22
+ return res.redirect('/login');
23
+ }
24
+
25
+ try {
26
+ // Verify access token
27
+ const decoded = jwt.verify(accessToken, env.JWT_SECRET) as UserPayload;
28
+ req.user = decoded;
29
+ next();
30
+ } catch (error) {
31
+ // Access token invalid/expired, try refresh
32
+ if (refreshToken) {
33
+ return this.silentRefresh(req, res, next, refreshToken);
34
+ }
35
+ res.clearCookie('access_token');
36
+ return res.redirect('/login');
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Middleware to detect the user but allow the request to continue.
42
+ * Useful for public pages that need to know if a user is logged in.
43
+ */
44
+ public async detect(req: Request, res: Response, next: NextFunction) {
45
+ const accessToken = req.cookies?.access_token;
46
+ const refreshToken = req.cookies?.refresh_token;
47
+
48
+ if (!accessToken) {
49
+ if (refreshToken) {
50
+ try {
51
+ const { accessToken: newToken } = await AuthService.refresh(refreshToken);
52
+ res.cookie('access_token', newToken, {
53
+ httpOnly: true,
54
+ secure: process.env.NODE_ENV === 'production',
55
+ sameSite: 'strict',
56
+ maxAge: 15 * 60 * 1000,
57
+ });
58
+ const decoded = jwt.verify(newToken, env.JWT_SECRET) as UserPayload;
59
+ req.user = decoded;
60
+ } catch (error) {
61
+ // Silent failure
62
+ Logger.debug('Silent refresh failed in detect middleware');
63
+ }
64
+ }
65
+ return next();
66
+ }
67
+
68
+ try {
69
+ const decoded = jwt.verify(accessToken, env.JWT_SECRET) as UserPayload;
70
+ req.user = decoded;
71
+ next();
72
+ } catch (error) {
73
+ // Access token invalid/expired, try silent refresh
74
+ if (refreshToken) {
75
+ try {
76
+ const { accessToken: newToken } = await AuthService.refresh(refreshToken);
77
+ res.cookie('access_token', newToken, {
78
+ httpOnly: true,
79
+ secure: process.env.NODE_ENV === 'production',
80
+ sameSite: 'strict',
81
+ maxAge: 15 * 60 * 1000,
82
+ });
83
+ const decoded = jwt.verify(newToken, env.JWT_SECRET) as UserPayload;
84
+ req.user = decoded;
85
+ } catch (err) {
86
+ Logger.debug('Silent refresh failed in detect middleware after verify error');
87
+ }
88
+ }
89
+ next();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Attempt to refresh the access token silently.
95
+ */
96
+ private async silentRefresh(req: Request, res: Response, next: NextFunction, token: string) {
97
+ try {
98
+ const { accessToken } = await AuthService.refresh(token);
99
+
100
+ // Set new access token cookie
101
+ res.cookie('access_token', accessToken, {
102
+ httpOnly: true,
103
+ secure: process.env.NODE_ENV === 'production',
104
+ sameSite: 'strict',
105
+ maxAge: 15 * 60 * 1000,
106
+ });
107
+
108
+ // Verify the new token to get user data
109
+ const decoded = jwt.verify(accessToken, env.JWT_SECRET) as UserPayload;
110
+ req.user = decoded;
111
+
112
+ next();
113
+ } catch (error) {
114
+ res.clearCookie('access_token');
115
+ res.clearCookie('refresh_token');
116
+ return res.redirect('/login');
117
+ }
118
+ }
119
+ }
120
+
121
+
122
+ export default new WebAuth();
@@ -0,0 +1,24 @@
1
+ import { Model, ModelAttributes, InitOptions } from 'sequelize';
2
+
3
+ /**
4
+ * BaseModel provides shared configuration for all models.
5
+ * It automatically enables Soft Deletes (paranoid), Timestamps, and Underscored naming.
6
+ */
7
+ export default class BaseModel<TAttributes extends {} = any, TCreationAttributes extends {} = any>
8
+ extends Model<TAttributes, TCreationAttributes> {
9
+
10
+ /**
11
+ * Helper to initialize the model with default options.
12
+ */
13
+ public static init<MS extends Model, M extends typeof Model>(
14
+ attributes: ModelAttributes<MS, any>,
15
+ options: InitOptions<MS>
16
+ ): M {
17
+ return super.init(attributes, {
18
+ paranoid: true,
19
+ timestamps: true,
20
+ underscored: true,
21
+ ...options,
22
+ }) as unknown as M;
23
+ }
24
+ }
@@ -1,4 +1,5 @@
1
- import { Model, DataTypes, Sequelize, Optional } from 'sequelize';
1
+ import { DataTypes, Sequelize, Optional } from 'sequelize';
2
+ import BaseModel from './BaseModel.js';
2
3
 
3
4
  interface RefreshTokenAttributes {
4
5
  id: number;
@@ -8,11 +9,12 @@ interface RefreshTokenAttributes {
8
9
  revoked: boolean;
9
10
  created_at?: Date;
10
11
  updated_at?: Date;
12
+ deleted_at?: Date | null;
11
13
  }
12
14
 
13
- export interface RefreshTokenCreationAttributes extends Optional<RefreshTokenAttributes, 'id' | 'revoked' | 'created_at' | 'updated_at'> {}
15
+ export interface RefreshTokenCreationAttributes extends Optional<RefreshTokenAttributes, 'id' | 'revoked' | 'created_at' | 'updated_at' | 'deleted_at'> {}
14
16
 
15
- class RefreshToken extends Model<RefreshTokenAttributes, RefreshTokenCreationAttributes> implements RefreshTokenAttributes {
17
+ class RefreshToken extends BaseModel<RefreshTokenAttributes, RefreshTokenCreationAttributes> implements RefreshTokenAttributes {
16
18
  declare id: number;
17
19
  declare user_id: number;
18
20
  declare token: string;
@@ -20,6 +22,7 @@ class RefreshToken extends Model<RefreshTokenAttributes, RefreshTokenCreationAtt
20
22
  declare revoked: boolean;
21
23
  declare created_at: Date;
22
24
  declare updated_at: Date;
25
+ declare deleted_at: Date | null;
23
26
 
24
27
  static initModel(sequelize: Sequelize) {
25
28
  RefreshToken.init(
@@ -52,8 +55,6 @@ class RefreshToken extends Model<RefreshTokenAttributes, RefreshTokenCreationAtt
52
55
  sequelize,
53
56
  modelName: 'RefreshToken',
54
57
  tableName: 'refresh_tokens',
55
- timestamps: true,
56
- underscored: true,
57
58
  }
58
59
  );
59
60
  }
@@ -1,4 +1,5 @@
1
- import { Model, DataTypes, Sequelize, Optional } from 'sequelize';
1
+ import { DataTypes, Sequelize, Optional } from 'sequelize';
2
+ import BaseModel from './BaseModel.js';
2
3
  import Hash from '../utils/Hash.js';
3
4
 
4
5
  interface UserAttributes {
@@ -16,7 +17,7 @@ interface UserAttributes {
16
17
 
17
18
  export interface UserCreationAttributes extends Optional<UserAttributes, 'id' | 'created_at' | 'updated_at' | 'deleted_at' | 'role' | 'avatar'> {}
18
19
 
19
- class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
20
+ class User extends BaseModel<UserAttributes, UserCreationAttributes> implements UserAttributes {
20
21
  declare id: number;
21
22
  declare firstname: string;
22
23
  declare lastname: string;
@@ -68,9 +69,6 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
68
69
  sequelize,
69
70
  modelName: 'User',
70
71
  tableName: 'users',
71
- paranoid: true,
72
- timestamps: true,
73
- underscored: true,
74
72
  hooks: {
75
73
  beforeCreate: async (user: User) => {
76
74
  if (user.password) {
@@ -1,6 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import UserController from '../controllers/UserController.js';
3
- import Authentication from '../middlewares/Authentication.js';
3
+ import ApiAuth from '../middlewares/ApiAuth.js';
4
+ import RoleGuard from '../middlewares/RoleGuard.js';
4
5
  import Validator from '../middlewares/Validator.js';
5
6
  import UserRequest from '../requests/UserRequest.js';
6
7
  import AuthController from '../controllers/AuthController.js';
@@ -19,17 +20,24 @@ class ApiRoutes {
19
20
  * Define all API routes here.
20
21
  */
21
22
  protected initializeRoutes(): void {
22
- // Auth
23
+ // --- Public Routes ---
23
24
  this.router.post('/login', Limiter.auth, AuthController.login);
24
25
  this.router.post('/refresh', AuthController.refresh);
25
- this.router.post('/logout', Authentication.handle, AuthController.logout);
26
26
 
27
- // Protected
28
- this.router.get('/me', Authentication.handle, AuthController.me);
29
- this.router.get('/users', Authentication.handle, UserController.index);
30
- this.router.post('/users', Validator.validate(UserRequest.store), UserController.store);
31
- this.router.post('/users/avatar', Authentication.handle, StorageService.uploader.single('avatar'), UserController.uploadAvatar);
27
+ // --- Protected Routes Group ---
28
+ const protectedRouter = Router();
29
+ protectedRouter.use(ApiAuth.handle);
30
+
31
+ protectedRouter.post('/logout', AuthController.logout);
32
+ protectedRouter.get('/me', AuthController.me);
33
+ protectedRouter.get('/users', RoleGuard.allow('admin'), UserController.index);
34
+ protectedRouter.post('/users', RoleGuard.allow('admin'), Validator.validate(UserRequest.store), UserController.store);
35
+ protectedRouter.post('/users/avatar', StorageService.uploader.single('avatar'), UserController.uploadAvatar);
36
+
37
+ // Mount the group
38
+ this.router.use(protectedRouter);
32
39
  }
33
40
  }
34
41
 
42
+
35
43
  export default new ApiRoutes().router;
@@ -1,6 +1,6 @@
1
- import { Router, Request, Response } from 'express';
2
- import path from 'path';
3
- import db from '../models/index.js';
1
+ import { Router } from 'express';
2
+ import WebController from '../controllers/WebController.js';
3
+ import Csrf from '../middlewares/Csrf.js';
4
4
 
5
5
  class WebRoutes {
6
6
  public router: Router;
@@ -11,48 +11,15 @@ class WebRoutes {
11
11
  }
12
12
 
13
13
  protected initializeRoutes(): void {
14
- // Home Route
15
- this.router.get('/', (req: Request, res: Response) => {
16
- const viewPath = path.join(process.cwd(), 'views', 'welcome.html');
17
- res.sendFile(viewPath);
18
- });
14
+ // Apply CSRF protection to all web routes
15
+ this.router.use(Csrf.handle);
19
16
 
20
- // Status Route (HTML view)
21
- this.router.get('/status', (req: Request, res: Response) => {
22
- const viewPath = path.join(process.cwd(), 'views', 'status.html');
23
- res.sendFile(viewPath);
24
- });
17
+ // --- Public Routes ---
18
+ this.router.get('/', WebController.index);
19
+ this.router.get('/status', WebController.status);
20
+ this.router.get('/health', WebController.health);
25
21
 
26
- // Status JSON API (used by the status view)
27
- this.router.get('/status/json', (req: Request, res: Response) => {
28
- res.json({
29
- status: 'UP',
30
- environment: process.env.NODE_ENV,
31
- uptime: process.uptime(),
32
- memoryMB: (process.memoryUsage().rss / 1024 / 1024).toFixed(1),
33
- });
34
- });
35
-
36
- // Health Check (for container/orchestration probes)
37
- this.router.get('/health', async (req: Request, res: Response) => {
38
- try {
39
- await db.sequelize.authenticate();
40
- res.json({
41
- status: 'healthy',
42
- uptime: process.uptime(),
43
- database: 'connected',
44
- timestamp: new Date().toISOString(),
45
- });
46
- } catch {
47
- res.status(503).json({
48
- status: 'unhealthy',
49
- uptime: process.uptime(),
50
- database: 'disconnected',
51
- timestamp: new Date().toISOString(),
52
- });
53
- }
54
- });
55
22
  }
56
23
  }
57
24
 
58
- export default new WebRoutes().router;
25
+ export default new WebRoutes().router;
@@ -0,0 +1,115 @@
1
+ import express, { Application } from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import compression from 'compression';
5
+ import cookieParser from 'cookie-parser';
6
+ import path from 'path';
7
+ import db from './models/index.js';
8
+ import RouteService from './services/RouteService.js';
9
+ import ExceptionHandler from './exceptions/Handler.js';
10
+ import Logger from './utils/Logger.js';
11
+ import Limiter from './middlewares/Limiter.js';
12
+ import Maintenance from './middlewares/Maintenance.js';
13
+ import RequestLogger from './middlewares/RequestLogger.js';
14
+ import env from './config/env.js';
15
+ import InertiaMiddleware from './middlewares/InertiaMiddleware.js';
16
+ import WebAuth from './middlewares/WebAuth.js';
17
+
18
+ const app: Application = express();
19
+ const PORT = env.APP_PORT;
20
+
21
+ // ==========================
22
+ // Global Middleware
23
+ // ==========================
24
+ app.use(Maintenance.handle);
25
+ app.use(helmet({
26
+ crossOriginResourcePolicy: false,
27
+ contentSecurityPolicy: env.NODE_ENV === 'production' ? undefined : false,
28
+ }));
29
+ app.use(cors({
30
+ origin: env.CORS_ORIGIN,
31
+ credentials: true,
32
+ }));
33
+ app.use(compression());
34
+ app.use(cookieParser());
35
+ app.use(express.json({ limit: '10kb' }));
36
+ app.use(express.urlencoded({ extended: true, limit: '10kb' }));
37
+
38
+ // User detection and Inertia
39
+ app.use(WebAuth.detect.bind(WebAuth));
40
+ app.use(InertiaMiddleware.handle);
41
+
42
+ app.use(express.static(path.join(process.cwd(), 'public')));
43
+ app.use(Limiter.global);
44
+ app.use(RequestLogger.handle);
45
+
46
+ // ==========================
47
+ // Register Routes
48
+ // ==========================
49
+ // This loads both your API and Web routes
50
+ RouteService.boot(app);
51
+
52
+ // ==========================
53
+ // Error Handling
54
+ // ==========================
55
+
56
+ // 404 Not Found Handler
57
+ app.use(ExceptionHandler.notFound);
58
+
59
+ // Global Error Handler
60
+ app.use(ExceptionHandler.handle);
61
+
62
+ // ==========================
63
+ // Start Server
64
+ // ==========================
65
+ let server: ReturnType<typeof app.listen>;
66
+
67
+ const start = async () => {
68
+ try {
69
+ // Test Database Connection
70
+ await db.connect();
71
+
72
+ // Start Listening
73
+ server = app.listen(PORT, () => {
74
+ Logger.info(`Server running on http://localhost:${PORT}`);
75
+ });
76
+ } catch (error) {
77
+ Logger.error('Unable to connect to the database:', error);
78
+ process.exit(1);
79
+ }
80
+ };
81
+
82
+ // ==========================
83
+ // Graceful Shutdown
84
+ // ==========================
85
+ const shutdown = async (signal: string) => {
86
+ Logger.info(`${signal} received. Shutting down gracefully...`);
87
+
88
+ if (server) {
89
+ server.close(async () => {
90
+ Logger.info('HTTP server closed.');
91
+
92
+ try {
93
+ await db.sequelize.close();
94
+ Logger.info('Database connection closed.');
95
+ } catch (error) {
96
+ Logger.error('Error closing database connection:', error);
97
+ }
98
+
99
+ process.exit(0);
100
+ });
101
+
102
+ // Force shutdown after 10 seconds
103
+ setTimeout(() => {
104
+ Logger.error('Forced shutdown after timeout.');
105
+ process.exit(1);
106
+ }, 10000);
107
+ } else {
108
+ process.exit(0);
109
+ }
110
+ };
111
+
112
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
113
+ process.on('SIGINT', () => shutdown('SIGINT'));
114
+
115
+ start();
@@ -1,9 +1,16 @@
1
1
  import { JwtPayload } from 'jsonwebtoken';
2
2
 
3
+
4
+ export interface UserPayload extends JwtPayload {
5
+ id: number;
6
+ email: string;
7
+ role: string;
8
+ }
9
+
3
10
  declare global {
4
11
  namespace Express {
5
12
  interface Request {
6
- user?: string | JwtPayload | any;
13
+ user?: UserPayload;
7
14
  }
8
15
  }
9
16
  }
@@ -0,0 +1,28 @@
1
+ declare module 'inertia-node' {
2
+ import { Request, Response, NextFunction } from 'express';
3
+
4
+ export interface Inertia {
5
+ render(page: { component: string; props?: Record<string, any> }): void;
6
+ shareProps(props: Record<string, any>): void;
7
+ setViewData(data: Record<string, any>): void;
8
+ redirect(url?: string): void;
9
+ }
10
+
11
+ function inertia(
12
+ html: (page: string) => string,
13
+ version?: string | (() => string)
14
+ ): (req: Request, res: Response, next: NextFunction) => void;
15
+
16
+ export default inertia;
17
+ }
18
+
19
+ declare namespace Express {
20
+ export interface Request {
21
+ Inertia: import('inertia-node').Inertia;
22
+ }
23
+
24
+ export interface Response {
25
+ inertia(component: string, props?: Record<string, any>): void;
26
+ flash(type: 'success' | 'error' | 'info' | 'warning' | string, message: string): void;
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ outDir: 'public/build',
8
+ manifest: true,
9
+ rollupOptions: {
10
+ input: 'resources/js/app.tsx',
11
+ },
12
+ },
13
+ server: {
14
+ strictPort: true,
15
+ port: 5173,
16
+ hmr: {
17
+ host: 'localhost',
18
+ },
19
+ },
20
+ });