@erwininteractive/mvc 0.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/README.md +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +61 -0
- package/dist/framework/App.d.ts +38 -0
- package/dist/framework/App.js +100 -0
- package/dist/framework/Auth.d.ts +27 -0
- package/dist/framework/Auth.js +67 -0
- package/dist/framework/Router.d.ts +29 -0
- package/dist/framework/Router.js +125 -0
- package/dist/framework/db.d.ts +13 -0
- package/dist/framework/db.js +41 -0
- package/dist/framework/index.d.ts +5 -0
- package/dist/framework/index.js +22 -0
- package/dist/generators/generateController.d.ts +7 -0
- package/dist/generators/generateController.js +110 -0
- package/dist/generators/generateModel.d.ts +7 -0
- package/dist/generators/generateModel.js +77 -0
- package/dist/generators/initApp.d.ts +8 -0
- package/dist/generators/initApp.js +113 -0
- package/dist/generators/paths.d.ts +17 -0
- package/dist/generators/paths.js +55 -0
- package/package.json +72 -0
- package/prisma/schema.prisma +19 -0
- package/templates/appScaffold/README.md +297 -0
- package/templates/appScaffold/package.json +23 -0
- package/templates/appScaffold/public/favicon.svg +16 -0
- package/templates/appScaffold/src/controllers/HomeController.ts +9 -0
- package/templates/appScaffold/src/middleware/auth.ts +8 -0
- package/templates/appScaffold/src/server.ts +24 -0
- package/templates/appScaffold/src/views/index.ejs +300 -0
- package/templates/appScaffold/tsconfig.json +16 -0
- package/templates/controller.ts.ejs +98 -0
- package/templates/model.prisma.ejs +7 -0
- package/templates/view.ejs.ejs +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @erwininteractive/mvc
|
|
2
|
+
|
|
3
|
+
A lightweight, full-featured MVC framework for Node.js 20+ built with TypeScript.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Express** - Fast, minimal web framework
|
|
8
|
+
- **Prisma** - Modern database ORM with PostgreSQL
|
|
9
|
+
- **EJS + Alpine.js** - Server-side templating with reactive client-side components
|
|
10
|
+
- **Redis Sessions** - Scalable session management
|
|
11
|
+
- **JWT Authentication** - Secure token-based auth with bcrypt password hashing
|
|
12
|
+
- **CLI Tools** - Scaffold apps and generate models/controllers
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Create a New Application
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @erwininteractive/mvc init myapp
|
|
20
|
+
cd myapp
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
# Edit .env with your database configuration
|
|
23
|
+
npx prisma migrate dev --name init
|
|
24
|
+
npm run dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Generate a Model
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx erwinmvc generate model Post
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This creates a Prisma model and runs migrations.
|
|
34
|
+
|
|
35
|
+
### Generate a Controller
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx erwinmvc generate controller Post
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This creates a CRUD controller with routes:
|
|
42
|
+
- `GET /posts` - List all posts
|
|
43
|
+
- `GET /posts/:id` - Show a single post
|
|
44
|
+
- `POST /posts` - Create a post
|
|
45
|
+
- `PUT /posts/:id` - Update a post
|
|
46
|
+
- `DELETE /posts/:id` - Delete a post
|
|
47
|
+
|
|
48
|
+
## CLI Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---------|-------------|
|
|
52
|
+
| `erwinmvc init <dir>` | Scaffold a new MVC application |
|
|
53
|
+
| `erwinmvc generate model <name>` | Generate a Prisma model |
|
|
54
|
+
| `erwinmvc generate controller <name>` | Generate a CRUD controller |
|
|
55
|
+
|
|
56
|
+
### Options
|
|
57
|
+
|
|
58
|
+
**init:**
|
|
59
|
+
- `--skip-install` - Skip running npm install
|
|
60
|
+
- `--skip-prisma` - Skip Prisma client generation
|
|
61
|
+
|
|
62
|
+
**generate model:**
|
|
63
|
+
- `--skip-migrate` - Skip running Prisma migrate
|
|
64
|
+
|
|
65
|
+
**generate controller:**
|
|
66
|
+
- `--no-views` - Skip generating EJS views
|
|
67
|
+
|
|
68
|
+
## Framework API
|
|
69
|
+
|
|
70
|
+
### App Factory
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { createMvcApp, startServer } from "@erwininteractive/mvc";
|
|
74
|
+
|
|
75
|
+
const { app, redisClient } = await createMvcApp({
|
|
76
|
+
viewsPath: "src/views",
|
|
77
|
+
publicPath: "public",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
startServer(app);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Database
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { getPrismaClient } from "@erwininteractive/mvc";
|
|
87
|
+
|
|
88
|
+
const prisma = getPrismaClient();
|
|
89
|
+
const users = await prisma.user.findMany();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Authentication
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import {
|
|
96
|
+
hashPassword,
|
|
97
|
+
verifyPassword,
|
|
98
|
+
signToken,
|
|
99
|
+
verifyToken,
|
|
100
|
+
authenticate,
|
|
101
|
+
} from "@erwininteractive/mvc";
|
|
102
|
+
|
|
103
|
+
// Hash a password
|
|
104
|
+
const hash = await hashPassword("secret123");
|
|
105
|
+
|
|
106
|
+
// Verify a password
|
|
107
|
+
const isValid = await verifyPassword("secret123", hash);
|
|
108
|
+
|
|
109
|
+
// Sign a JWT
|
|
110
|
+
const token = signToken({ userId: 1, email: "user@example.com" });
|
|
111
|
+
|
|
112
|
+
// Protect routes with middleware
|
|
113
|
+
app.get("/protected", authenticate, (req, res) => {
|
|
114
|
+
res.json({ user: req.user });
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Routing
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { registerControllers } from "@erwininteractive/mvc";
|
|
122
|
+
|
|
123
|
+
// Auto-register all *Controller.ts files
|
|
124
|
+
await registerControllers(app, path.resolve("src/controllers"));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Environment Variables
|
|
128
|
+
|
|
129
|
+
Create a `.env` file with:
|
|
130
|
+
|
|
131
|
+
```env
|
|
132
|
+
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/yourdb?schema=public"
|
|
133
|
+
REDIS_URL="redis://localhost:6379"
|
|
134
|
+
JWT_SECRET="your-secret-key"
|
|
135
|
+
SESSION_SECRET="your-session-secret"
|
|
136
|
+
PORT=3000
|
|
137
|
+
NODE_ENV=development
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Project Structure
|
|
141
|
+
|
|
142
|
+
Generated applications follow this structure:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
myapp/
|
|
146
|
+
├── src/
|
|
147
|
+
│ ├── controllers/ # Route controllers
|
|
148
|
+
│ ├── middleware/ # Express middleware
|
|
149
|
+
│ ├── views/ # EJS templates
|
|
150
|
+
│ └── server.ts # Entry point
|
|
151
|
+
├── prisma/
|
|
152
|
+
│ └── schema.prisma # Database schema
|
|
153
|
+
├── public/ # Static files
|
|
154
|
+
├── .env.example
|
|
155
|
+
├── package.json
|
|
156
|
+
└── tsconfig.json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Controller Convention
|
|
160
|
+
|
|
161
|
+
Controllers export named functions that map to routes:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// src/controllers/PostController.ts
|
|
165
|
+
export async function index(req, res) { /* GET /posts */ }
|
|
166
|
+
export async function show(req, res) { /* GET /posts/:id */ }
|
|
167
|
+
export async function store(req, res) { /* POST /posts */ }
|
|
168
|
+
export async function update(req, res) { /* PUT /posts/:id */ }
|
|
169
|
+
export async function destroy(req, res) { /* DELETE /posts/:id */ }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const initApp_1 = require("./generators/initApp");
|
|
6
|
+
const generateModel_1 = require("./generators/generateModel");
|
|
7
|
+
const generateController_1 = require("./generators/generateController");
|
|
8
|
+
const program = new commander_1.Command();
|
|
9
|
+
program
|
|
10
|
+
.name("erwinmvc")
|
|
11
|
+
.description("CLI for @erwininteractive/mvc framework")
|
|
12
|
+
.version("0.1.1");
|
|
13
|
+
// Init command - scaffold a new application
|
|
14
|
+
program
|
|
15
|
+
.command("init <dir>")
|
|
16
|
+
.description("Scaffold a new MVC application")
|
|
17
|
+
.option("--skip-install", "Skip npm install")
|
|
18
|
+
.option("--with-database", "Include database/Prisma setup")
|
|
19
|
+
.action(async (dir, options) => {
|
|
20
|
+
try {
|
|
21
|
+
await (0, initApp_1.initApp)(dir, options);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// Generate command group
|
|
29
|
+
const generate = program
|
|
30
|
+
.command("generate")
|
|
31
|
+
.alias("g")
|
|
32
|
+
.description("Generate models or controllers");
|
|
33
|
+
// Generate model
|
|
34
|
+
generate
|
|
35
|
+
.command("model <name>")
|
|
36
|
+
.description("Generate a Prisma model")
|
|
37
|
+
.option("--skip-migrate", "Skip running Prisma migrate")
|
|
38
|
+
.action(async (name, options) => {
|
|
39
|
+
try {
|
|
40
|
+
await (0, generateModel_1.generateModel)(name, options);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Generate controller
|
|
48
|
+
generate
|
|
49
|
+
.command("controller <name>")
|
|
50
|
+
.description("Generate a CRUD controller")
|
|
51
|
+
.option("--no-views", "Skip generating views")
|
|
52
|
+
.action(async (name, options) => {
|
|
53
|
+
try {
|
|
54
|
+
await (0, generateController_1.generateController)(name, options);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
program.parse();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Express } from "express";
|
|
2
|
+
import { RedisClientType } from "redis";
|
|
3
|
+
import helmet from "helmet";
|
|
4
|
+
import cors from "cors";
|
|
5
|
+
export interface MvcAppOptions {
|
|
6
|
+
/** Directory for EJS views (default: "src/views") */
|
|
7
|
+
viewsPath?: string;
|
|
8
|
+
/** Directory for static files (default: "public") */
|
|
9
|
+
publicPath?: string;
|
|
10
|
+
/** Directory for controllers (default: "src/controllers") */
|
|
11
|
+
controllersPath?: string;
|
|
12
|
+
/** Enable Redis sessions (default: true if REDIS_URL is set) */
|
|
13
|
+
enableRedis?: boolean;
|
|
14
|
+
/** Custom CORS options */
|
|
15
|
+
corsOptions?: cors.CorsOptions;
|
|
16
|
+
/** Custom Helmet options */
|
|
17
|
+
helmetOptions?: Parameters<typeof helmet>[0];
|
|
18
|
+
}
|
|
19
|
+
export interface MvcApp {
|
|
20
|
+
app: Express;
|
|
21
|
+
redisClient: RedisClientType | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create and configure an Express MVC application.
|
|
25
|
+
*
|
|
26
|
+
* Includes:
|
|
27
|
+
* - Helmet for security headers
|
|
28
|
+
* - CORS support
|
|
29
|
+
* - JSON and URL-encoded body parsing
|
|
30
|
+
* - Redis-backed sessions (if REDIS_URL is set)
|
|
31
|
+
* - EJS view engine
|
|
32
|
+
* - Static file serving
|
|
33
|
+
*/
|
|
34
|
+
export declare function createMvcApp(options?: MvcAppOptions): Promise<MvcApp>;
|
|
35
|
+
/**
|
|
36
|
+
* Start the Express server.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startServer(app: Express, port?: number): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createMvcApp = createMvcApp;
|
|
7
|
+
exports.startServer = startServer;
|
|
8
|
+
const express_1 = __importDefault(require("express"));
|
|
9
|
+
const express_session_1 = __importDefault(require("express-session"));
|
|
10
|
+
const redis_1 = require("redis");
|
|
11
|
+
const connect_redis_1 = __importDefault(require("connect-redis"));
|
|
12
|
+
const helmet_1 = __importDefault(require("helmet"));
|
|
13
|
+
const cors_1 = __importDefault(require("cors"));
|
|
14
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
// Load environment variables
|
|
17
|
+
dotenv_1.default.config();
|
|
18
|
+
/**
|
|
19
|
+
* Create and configure an Express MVC application.
|
|
20
|
+
*
|
|
21
|
+
* Includes:
|
|
22
|
+
* - Helmet for security headers
|
|
23
|
+
* - CORS support
|
|
24
|
+
* - JSON and URL-encoded body parsing
|
|
25
|
+
* - Redis-backed sessions (if REDIS_URL is set)
|
|
26
|
+
* - EJS view engine
|
|
27
|
+
* - Static file serving
|
|
28
|
+
*/
|
|
29
|
+
async function createMvcApp(options = {}) {
|
|
30
|
+
const { viewsPath = "src/views", publicPath = "public", enableRedis = !!process.env.REDIS_URL, corsOptions = {}, helmetOptions = {}, } = options;
|
|
31
|
+
const app = (0, express_1.default)();
|
|
32
|
+
// Security middleware
|
|
33
|
+
app.use((0, helmet_1.default)(helmetOptions));
|
|
34
|
+
app.use((0, cors_1.default)(corsOptions));
|
|
35
|
+
// Body parsing
|
|
36
|
+
app.use(express_1.default.json());
|
|
37
|
+
app.use(express_1.default.urlencoded({ extended: true }));
|
|
38
|
+
// Static files
|
|
39
|
+
app.use(express_1.default.static(path_1.default.resolve(publicPath)));
|
|
40
|
+
// Session store (Redis if available, memory otherwise)
|
|
41
|
+
let redisClient = null;
|
|
42
|
+
if (enableRedis && process.env.REDIS_URL) {
|
|
43
|
+
try {
|
|
44
|
+
redisClient = (0, redis_1.createClient)({ url: process.env.REDIS_URL });
|
|
45
|
+
await redisClient.connect();
|
|
46
|
+
const store = new connect_redis_1.default({
|
|
47
|
+
client: redisClient,
|
|
48
|
+
prefix: "mvc:",
|
|
49
|
+
});
|
|
50
|
+
app.use((0, express_session_1.default)({
|
|
51
|
+
store,
|
|
52
|
+
secret: process.env.SESSION_SECRET || "default-secret-change-me",
|
|
53
|
+
resave: false,
|
|
54
|
+
saveUninitialized: false,
|
|
55
|
+
cookie: {
|
|
56
|
+
secure: process.env.NODE_ENV === "production",
|
|
57
|
+
httpOnly: true,
|
|
58
|
+
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
console.log("Using Redis session store");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn("Failed to connect to Redis, using memory sessions:", err);
|
|
65
|
+
redisClient = null;
|
|
66
|
+
setupMemorySessions(app);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Use memory sessions when Redis is not configured
|
|
71
|
+
setupMemorySessions(app);
|
|
72
|
+
}
|
|
73
|
+
// View engine
|
|
74
|
+
app.set("view engine", "ejs");
|
|
75
|
+
app.set("views", path_1.default.resolve(viewsPath));
|
|
76
|
+
return { app, redisClient };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Setup memory-based sessions (for development without Redis).
|
|
80
|
+
*/
|
|
81
|
+
function setupMemorySessions(app) {
|
|
82
|
+
app.use((0, express_session_1.default)({
|
|
83
|
+
secret: process.env.SESSION_SECRET || "default-secret-change-me",
|
|
84
|
+
resave: false,
|
|
85
|
+
saveUninitialized: false,
|
|
86
|
+
cookie: {
|
|
87
|
+
secure: process.env.NODE_ENV === "production",
|
|
88
|
+
httpOnly: true,
|
|
89
|
+
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Start the Express server.
|
|
95
|
+
*/
|
|
96
|
+
function startServer(app, port = Number(process.env.PORT) || 3000) {
|
|
97
|
+
app.listen(port, () => {
|
|
98
|
+
console.log(`Server listening on port ${port}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import type { Request, Response, NextFunction } from "express";
|
|
3
|
+
/**
|
|
4
|
+
* Hash a plain text password using bcrypt.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hashPassword(plain: string): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Verify a plain text password against a bcrypt hash.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyPassword(plain: string, hash: string): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Sign a JWT token with the given payload.
|
|
13
|
+
* Token expires in 1 hour by default.
|
|
14
|
+
*/
|
|
15
|
+
export declare function signToken(payload: object, expiresIn?: string | number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Verify and decode a JWT token.
|
|
18
|
+
* Returns the decoded payload or throws an error if invalid.
|
|
19
|
+
*/
|
|
20
|
+
export declare function verifyToken(token: string): jwt.JwtPayload | string;
|
|
21
|
+
/**
|
|
22
|
+
* Express middleware to authenticate requests using JWT Bearer tokens.
|
|
23
|
+
* Attaches the decoded user payload to req.user on success.
|
|
24
|
+
*/
|
|
25
|
+
export declare function authenticate(req: Request & {
|
|
26
|
+
user?: jwt.JwtPayload | string;
|
|
27
|
+
}, res: Response, next: NextFunction): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hashPassword = hashPassword;
|
|
7
|
+
exports.verifyPassword = verifyPassword;
|
|
8
|
+
exports.signToken = signToken;
|
|
9
|
+
exports.verifyToken = verifyToken;
|
|
10
|
+
exports.authenticate = authenticate;
|
|
11
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
12
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
13
|
+
/**
|
|
14
|
+
* Hash a plain text password using bcrypt.
|
|
15
|
+
*/
|
|
16
|
+
async function hashPassword(plain) {
|
|
17
|
+
const salt = await bcryptjs_1.default.genSalt(10);
|
|
18
|
+
return bcryptjs_1.default.hash(plain, salt);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verify a plain text password against a bcrypt hash.
|
|
22
|
+
*/
|
|
23
|
+
async function verifyPassword(plain, hash) {
|
|
24
|
+
return bcryptjs_1.default.compare(plain, hash);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sign a JWT token with the given payload.
|
|
28
|
+
* Token expires in 1 hour by default.
|
|
29
|
+
*/
|
|
30
|
+
function signToken(payload, expiresIn = "1h") {
|
|
31
|
+
const secret = process.env.JWT_SECRET;
|
|
32
|
+
if (!secret) {
|
|
33
|
+
throw new Error("JWT_SECRET environment variable is not set");
|
|
34
|
+
}
|
|
35
|
+
return jsonwebtoken_1.default.sign(payload, secret, { expiresIn });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verify and decode a JWT token.
|
|
39
|
+
* Returns the decoded payload or throws an error if invalid.
|
|
40
|
+
*/
|
|
41
|
+
function verifyToken(token) {
|
|
42
|
+
const secret = process.env.JWT_SECRET;
|
|
43
|
+
if (!secret) {
|
|
44
|
+
throw new Error("JWT_SECRET environment variable is not set");
|
|
45
|
+
}
|
|
46
|
+
return jsonwebtoken_1.default.verify(token, secret);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Express middleware to authenticate requests using JWT Bearer tokens.
|
|
50
|
+
* Attaches the decoded user payload to req.user on success.
|
|
51
|
+
*/
|
|
52
|
+
function authenticate(req, res, next) {
|
|
53
|
+
const header = req.header("Authorization");
|
|
54
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7) : null;
|
|
55
|
+
if (!token) {
|
|
56
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const decoded = verifyToken(token);
|
|
61
|
+
req.user = decoded;
|
|
62
|
+
next();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
res.status(401).json({ error: "Invalid token" });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Express, Request, Response } from "express";
|
|
2
|
+
type RouteHandler = (req: Request, res: Response) => Promise<void> | void;
|
|
3
|
+
interface ControllerModule {
|
|
4
|
+
index?: RouteHandler;
|
|
5
|
+
show?: RouteHandler;
|
|
6
|
+
store?: RouteHandler;
|
|
7
|
+
update?: RouteHandler;
|
|
8
|
+
destroy?: RouteHandler;
|
|
9
|
+
[key: string]: RouteHandler | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Register controllers from a directory with convention-based routing.
|
|
13
|
+
*
|
|
14
|
+
* Convention:
|
|
15
|
+
* - GET /<resource> -> index
|
|
16
|
+
* - GET /<resource>/:id -> show
|
|
17
|
+
* - POST /<resource> -> store
|
|
18
|
+
* - PUT /<resource>/:id -> update
|
|
19
|
+
* - DELETE /<resource>/:id -> destroy
|
|
20
|
+
*
|
|
21
|
+
* @param app - Express application instance
|
|
22
|
+
* @param controllersDir - Absolute path to the controllers directory
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerControllers(app: Express, controllersDir: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Register a single controller with custom base path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registerController(app: Express, basePath: string, controller: ControllerModule): void;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.registerControllers = registerControllers;
|
|
40
|
+
exports.registerController = registerController;
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const fs_1 = __importDefault(require("fs"));
|
|
43
|
+
/**
|
|
44
|
+
* Convert a controller name to a resource path.
|
|
45
|
+
* e.g., "UserController" -> "users", "PostController" -> "posts"
|
|
46
|
+
*/
|
|
47
|
+
function controllerNameToResource(name) {
|
|
48
|
+
// Remove "Controller" suffix
|
|
49
|
+
const baseName = name.replace(/Controller$/, "");
|
|
50
|
+
// Convert to lowercase and simple pluralization
|
|
51
|
+
const lower = baseName.toLowerCase();
|
|
52
|
+
// Simple pluralization: add 's' if not already ending in 's'
|
|
53
|
+
return lower.endsWith("s") ? lower : lower + "s";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register controllers from a directory with convention-based routing.
|
|
57
|
+
*
|
|
58
|
+
* Convention:
|
|
59
|
+
* - GET /<resource> -> index
|
|
60
|
+
* - GET /<resource>/:id -> show
|
|
61
|
+
* - POST /<resource> -> store
|
|
62
|
+
* - PUT /<resource>/:id -> update
|
|
63
|
+
* - DELETE /<resource>/:id -> destroy
|
|
64
|
+
*
|
|
65
|
+
* @param app - Express application instance
|
|
66
|
+
* @param controllersDir - Absolute path to the controllers directory
|
|
67
|
+
*/
|
|
68
|
+
async function registerControllers(app, controllersDir) {
|
|
69
|
+
if (!fs_1.default.existsSync(controllersDir)) {
|
|
70
|
+
console.warn(`Controllers directory not found: ${controllersDir}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const files = fs_1.default.readdirSync(controllersDir);
|
|
74
|
+
const controllerFiles = files.filter((f) => f.endsWith("Controller.ts") || f.endsWith("Controller.js"));
|
|
75
|
+
for (const file of controllerFiles) {
|
|
76
|
+
const controllerPath = path_1.default.join(controllersDir, file);
|
|
77
|
+
const controllerName = path_1.default.basename(file, path_1.default.extname(file));
|
|
78
|
+
const resource = controllerNameToResource(controllerName);
|
|
79
|
+
try {
|
|
80
|
+
// Dynamic import for ES modules
|
|
81
|
+
const controller = await Promise.resolve(`${controllerPath}`).then(s => __importStar(require(s)));
|
|
82
|
+
// Register routes based on exported functions
|
|
83
|
+
if (controller.index) {
|
|
84
|
+
app.get(`/${resource}`, controller.index);
|
|
85
|
+
}
|
|
86
|
+
if (controller.show) {
|
|
87
|
+
app.get(`/${resource}/:id`, controller.show);
|
|
88
|
+
}
|
|
89
|
+
if (controller.store) {
|
|
90
|
+
app.post(`/${resource}`, controller.store);
|
|
91
|
+
}
|
|
92
|
+
if (controller.update) {
|
|
93
|
+
app.put(`/${resource}/:id`, controller.update);
|
|
94
|
+
}
|
|
95
|
+
if (controller.destroy) {
|
|
96
|
+
app.delete(`/${resource}/:id`, controller.destroy);
|
|
97
|
+
}
|
|
98
|
+
console.log(`Registered controller: ${controllerName} -> /${resource}`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error(`Failed to load controller ${file}:`, err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Register a single controller with custom base path.
|
|
107
|
+
*/
|
|
108
|
+
function registerController(app, basePath, controller) {
|
|
109
|
+
const resource = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
110
|
+
if (controller.index) {
|
|
111
|
+
app.get(resource, controller.index);
|
|
112
|
+
}
|
|
113
|
+
if (controller.show) {
|
|
114
|
+
app.get(`${resource}/:id`, controller.show);
|
|
115
|
+
}
|
|
116
|
+
if (controller.store) {
|
|
117
|
+
app.post(resource, controller.store);
|
|
118
|
+
}
|
|
119
|
+
if (controller.update) {
|
|
120
|
+
app.put(`${resource}/:id`, controller.update);
|
|
121
|
+
}
|
|
122
|
+
if (controller.destroy) {
|
|
123
|
+
app.delete(`${resource}/:id`, controller.destroy);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get or create a singleton PrismaClient instance.
|
|
3
|
+
* Safe for use in long-running Node processes.
|
|
4
|
+
*
|
|
5
|
+
* Note: Only call this if your app uses a database.
|
|
6
|
+
* Throws an error if @prisma/client is not installed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getPrismaClient(): any;
|
|
9
|
+
/**
|
|
10
|
+
* Disconnect the Prisma client.
|
|
11
|
+
* Useful for graceful shutdown.
|
|
12
|
+
*/
|
|
13
|
+
export declare function disconnectPrisma(): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPrismaClient = getPrismaClient;
|
|
4
|
+
exports.disconnectPrisma = disconnectPrisma;
|
|
5
|
+
// Prisma is optional - only loaded when needed
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
let prisma = null;
|
|
8
|
+
let PrismaClient = null;
|
|
9
|
+
/**
|
|
10
|
+
* Get or create a singleton PrismaClient instance.
|
|
11
|
+
* Safe for use in long-running Node processes.
|
|
12
|
+
*
|
|
13
|
+
* Note: Only call this if your app uses a database.
|
|
14
|
+
* Throws an error if @prisma/client is not installed.
|
|
15
|
+
*/
|
|
16
|
+
function getPrismaClient() {
|
|
17
|
+
if (!prisma) {
|
|
18
|
+
if (!PrismaClient) {
|
|
19
|
+
try {
|
|
20
|
+
// Dynamic import to make Prisma optional
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
PrismaClient = require("@prisma/client").PrismaClient;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("Prisma is not installed. Run 'npm install @prisma/client prisma' to use database features.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
prisma = new PrismaClient();
|
|
29
|
+
}
|
|
30
|
+
return prisma;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Disconnect the Prisma client.
|
|
34
|
+
* Useful for graceful shutdown.
|
|
35
|
+
*/
|
|
36
|
+
async function disconnectPrisma() {
|
|
37
|
+
if (prisma) {
|
|
38
|
+
await prisma.$disconnect();
|
|
39
|
+
prisma = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createMvcApp, startServer } from "./App";
|
|
2
|
+
export type { MvcAppOptions, MvcApp } from "./App";
|
|
3
|
+
export { getPrismaClient, disconnectPrisma } from "./db";
|
|
4
|
+
export { hashPassword, verifyPassword, signToken, verifyToken, authenticate, } from "./Auth";
|
|
5
|
+
export { registerControllers, registerController } from "./Router";
|