create-flex-stack 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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.js +330 -0
- package/dist/templates/cleanTemplate.js +697 -0
- package/dist/templates/frontendTemplate.js +855 -0
- package/dist/templates/hexagonalTemplate.js +855 -0
- package/dist/templates/layeredTemplate.js +745 -0
- package/dist/templates/modularTemplate.js +691 -0
- package/dist/templates/sharedTemplate.js +654 -0
- package/dist/templates/vmcTemplate.js +26 -0
- package/dist/types.js +1 -0
- package/dist/utils/generator.js +1120 -0
- package/package.json +46 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { getTypeOrmConfig, getTypeOrmEntity } from './sharedTemplate.js';
|
|
2
|
+
export function getCleanFiles(options) {
|
|
3
|
+
const files = {};
|
|
4
|
+
const userIdExpr = options.orm === 'prisma' ? 'user.id' : 'user._id.toString()';
|
|
5
|
+
// 1. Config & Env
|
|
6
|
+
files['src/infrastructure/config/env.config.ts'] = options.validation === 'zod'
|
|
7
|
+
? `import dotenv from 'dotenv';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
const envSchema = z.object({
|
|
13
|
+
PORT: z.coerce.number().default(3000),
|
|
14
|
+
DATABASE_URL: z.string().min(1),
|
|
15
|
+
${options.auth ? `JWT_SECRET: z.string().min(8),
|
|
16
|
+
JWT_REFRESH_SECRET: z.string().min(8),
|
|
17
|
+
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
18
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),` : ''}
|
|
19
|
+
${options.caching ? `REDIS_URL: z.string().min(1),` : ''}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const parsed = envSchema.safeParse(process.env);
|
|
23
|
+
if (!parsed.success) {
|
|
24
|
+
console.error('❌ Invalid environment variables:', parsed.error.format());
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
export const env = parsed.data;
|
|
28
|
+
`
|
|
29
|
+
: `import dotenv from 'dotenv';
|
|
30
|
+
import Joi from 'joi';
|
|
31
|
+
|
|
32
|
+
dotenv.config();
|
|
33
|
+
|
|
34
|
+
const envSchema = Joi.object({
|
|
35
|
+
PORT: Joi.number().default(3000),
|
|
36
|
+
DATABASE_URL: Joi.string().required(),
|
|
37
|
+
${options.auth ? `JWT_SECRET: Joi.string().min(8).required(),
|
|
38
|
+
JWT_REFRESH_SECRET: Joi.string().min(8).required(),
|
|
39
|
+
JWT_EXPIRES_IN: Joi.string().default('15m'),
|
|
40
|
+
JWT_REFRESH_EXPIRES_IN: Joi.string().default('7d'),` : ''}
|
|
41
|
+
${options.caching ? `REDIS_URL: Joi.string().required(),` : ''}
|
|
42
|
+
}).unknown().required();
|
|
43
|
+
|
|
44
|
+
const { error, value } = envSchema.validate(process.env);
|
|
45
|
+
if (error) {
|
|
46
|
+
console.error('❌ Invalid environment variables:', error.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
export const env = {
|
|
50
|
+
PORT: Number(value.PORT),
|
|
51
|
+
DATABASE_URL: value.DATABASE_URL,
|
|
52
|
+
${options.auth ? `JWT_SECRET: value.JWT_SECRET,
|
|
53
|
+
JWT_REFRESH_SECRET: value.JWT_REFRESH_SECRET,
|
|
54
|
+
JWT_EXPIRES_IN: value.JWT_EXPIRES_IN,
|
|
55
|
+
JWT_REFRESH_EXPIRES_IN: value.JWT_REFRESH_EXPIRES_IN,` : ''}
|
|
56
|
+
${options.caching ? `REDIS_URL: value.REDIS_URL,` : ''}
|
|
57
|
+
};
|
|
58
|
+
`;
|
|
59
|
+
if (options.orm === 'typeorm') {
|
|
60
|
+
files['src/infrastructure/database/connection.ts'] = getTypeOrmConfig(options)
|
|
61
|
+
.replace('../entities/user.entity.js', '../repositories/user.entity.js');
|
|
62
|
+
}
|
|
63
|
+
// 2. Domain Layer
|
|
64
|
+
files['src/domain/entities/user.entity.ts'] = `export class User {
|
|
65
|
+
constructor(
|
|
66
|
+
public readonly id: string,
|
|
67
|
+
public readonly email: string,
|
|
68
|
+
public readonly password: string,
|
|
69
|
+
public readonly name: string | null,
|
|
70
|
+
public readonly role: string,
|
|
71
|
+
public readonly createdAt: Date,
|
|
72
|
+
public readonly refreshToken: string | null = null
|
|
73
|
+
) {}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
// 3. Application Layer
|
|
77
|
+
files['src/application/interfaces/user-repository.interface.ts'] = `import { User } from '../../domain/entities/user.entity.js';
|
|
78
|
+
|
|
79
|
+
export interface IUserRepository {
|
|
80
|
+
findByEmail(email: string): Promise<User | null>;
|
|
81
|
+
findById(id: string): Promise<User | null>;
|
|
82
|
+
create(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
|
|
83
|
+
update(id: string, user: Partial<User>): Promise<User>;
|
|
84
|
+
findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]>;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
files['src/application/use-cases/register-user.use-case.ts'] = `import { IUserRepository } from '../interfaces/user-repository.interface.js';
|
|
88
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
89
|
+
|
|
90
|
+
export class RegisterUserUseCase {
|
|
91
|
+
constructor(
|
|
92
|
+
private userRepository: IUserRepository,
|
|
93
|
+
private passwordHasher: { hash(password: string): Promise<string> },
|
|
94
|
+
private tokenService: { generateTokens(payload: any): { accessToken: string; refreshToken: string } }
|
|
95
|
+
) {}
|
|
96
|
+
|
|
97
|
+
async execute(input: any) {
|
|
98
|
+
const existing = await this.userRepository.findByEmail(input.email);
|
|
99
|
+
if (existing) throw new Error('Email already registered');
|
|
100
|
+
|
|
101
|
+
const hashedPassword = await this.passwordHasher.hash(input.password);
|
|
102
|
+
const user = await this.userRepository.create({
|
|
103
|
+
email: input.email,
|
|
104
|
+
name: input.name || null,
|
|
105
|
+
password: hashedPassword,
|
|
106
|
+
role: input.role || 'user',
|
|
107
|
+
refreshToken: null
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
|
|
111
|
+
await this.userRepository.update(user.id, { refreshToken: tokens.refreshToken });
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
115
|
+
...tokens
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
files['src/application/use-cases/login-user.use-case.ts'] = `import { IUserRepository } from '../interfaces/user-repository.interface.js';
|
|
121
|
+
|
|
122
|
+
export class LoginUserUseCase {
|
|
123
|
+
constructor(
|
|
124
|
+
private userRepository: IUserRepository,
|
|
125
|
+
private passwordHasher: { compare(raw: string, hashed: string): Promise<boolean> },
|
|
126
|
+
private tokenService: { generateTokens(payload: any): { accessToken: string; refreshToken: string } }
|
|
127
|
+
) {}
|
|
128
|
+
|
|
129
|
+
async execute(input: any) {
|
|
130
|
+
const user = await this.userRepository.findByEmail(input.email);
|
|
131
|
+
if (!user) throw new Error('Invalid email or password');
|
|
132
|
+
|
|
133
|
+
const isMatch = await this.passwordHasher.compare(input.password, user.password);
|
|
134
|
+
if (!isMatch) throw new Error('Invalid email or password');
|
|
135
|
+
|
|
136
|
+
const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
|
|
137
|
+
await this.userRepository.update(user.id, { refreshToken: tokens.refreshToken });
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
141
|
+
...tokens
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
files['src/application/use-cases/get-user-profile.use-case.ts'] = `import { IUserRepository } from '../interfaces/user-repository.interface.js';
|
|
147
|
+
|
|
148
|
+
export class GetUserProfileUseCase {
|
|
149
|
+
constructor(private userRepository: IUserRepository) {}
|
|
150
|
+
|
|
151
|
+
async execute(userId: string) {
|
|
152
|
+
const user = await this.userRepository.findById(userId);
|
|
153
|
+
if (!user) throw new Error('User not found');
|
|
154
|
+
return {
|
|
155
|
+
id: user.id,
|
|
156
|
+
email: user.email,
|
|
157
|
+
name: user.name,
|
|
158
|
+
role: user.role,
|
|
159
|
+
createdAt: user.createdAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
files['src/application/use-cases/refresh-token.use-case.ts'] = `import { IUserRepository } from '../interfaces/user-repository.interface.js';
|
|
165
|
+
|
|
166
|
+
export class RefreshTokenUseCase {
|
|
167
|
+
constructor(
|
|
168
|
+
private userRepository: IUserRepository,
|
|
169
|
+
private tokenService: {
|
|
170
|
+
verifyRefreshToken(token: string): any;
|
|
171
|
+
generateTokens(payload: any): { accessToken: string; refreshToken: string };
|
|
172
|
+
}
|
|
173
|
+
) {}
|
|
174
|
+
|
|
175
|
+
async execute(token: string) {
|
|
176
|
+
try {
|
|
177
|
+
const decoded = this.tokenService.verifyRefreshToken(token);
|
|
178
|
+
const user = await this.userRepository.findById(decoded.id);
|
|
179
|
+
if (!user || user.refreshToken !== token) throw new Error('Invalid or revoked refresh token');
|
|
180
|
+
|
|
181
|
+
const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
|
|
182
|
+
await this.userRepository.update(user.id, { refreshToken: tokens.refreshToken });
|
|
183
|
+
return tokens;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
throw new Error('Invalid or expired refresh token');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
// 4. Presentation Layer
|
|
191
|
+
files['src/presentation/middlewares/error.middleware.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
192
|
+
|
|
193
|
+
export class AppError extends Error {
|
|
194
|
+
constructor(public statusCode: number, message: string) {
|
|
195
|
+
super(message);
|
|
196
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const errorHandler = (err: Error | AppError, req: Request, res: Response, next: NextFunction) => {
|
|
201
|
+
const statusCode = err instanceof AppError ? err.statusCode : 500;
|
|
202
|
+
res.status(statusCode).json({
|
|
203
|
+
status: 'error',
|
|
204
|
+
statusCode,
|
|
205
|
+
message: err.message || 'Internal Server Error'
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
`;
|
|
209
|
+
files['src/presentation/middlewares/auth.middleware.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
210
|
+
import jwt from 'jsonwebtoken';
|
|
211
|
+
import { env } from '../../infrastructure/config/env.config.js';
|
|
212
|
+
import { AppError } from './error.middleware.js';
|
|
213
|
+
|
|
214
|
+
export interface AuthenticatedRequest extends Request {
|
|
215
|
+
user?: { id: string; email: string; role: string };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const requireAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
219
|
+
const authHeader = req.headers.authorization;
|
|
220
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
221
|
+
return next(new AppError(401, 'Auth token missing'));
|
|
222
|
+
}
|
|
223
|
+
const token = authHeader.split(' ')[1];
|
|
224
|
+
try {
|
|
225
|
+
req.user = jwt.verify(token, env.JWT_SECRET) as any;
|
|
226
|
+
next();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
next(new AppError(401, 'Token expired or corrupt'));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
`;
|
|
232
|
+
if (options.rbac) {
|
|
233
|
+
files['src/presentation/middlewares/rbac.middleware.ts'] = `import { Response, NextFunction } from 'express';
|
|
234
|
+
import { AuthenticatedRequest } from './auth.middleware.js';
|
|
235
|
+
import { AppError } from './error.middleware.js';
|
|
236
|
+
|
|
237
|
+
export const authorize = (...roles: string[]) => {
|
|
238
|
+
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
239
|
+
if (!req.user || !roles.includes(req.user.role)) {
|
|
240
|
+
return next(new AppError(403, 'Permission denied'));
|
|
241
|
+
}
|
|
242
|
+
next();
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
if (options.rateLimiting) {
|
|
248
|
+
files['src/presentation/middlewares/rateLimit.middleware.ts'] = `import rateLimit from 'express-rate-limit';
|
|
249
|
+
|
|
250
|
+
export const apiLimiter = rateLimit({
|
|
251
|
+
windowMs: 15 * 60 * 1000,
|
|
252
|
+
max: 100,
|
|
253
|
+
standardHeaders: true,
|
|
254
|
+
legacyHeaders: false,
|
|
255
|
+
message: {
|
|
256
|
+
status: 'error',
|
|
257
|
+
statusCode: 429,
|
|
258
|
+
message: 'Too many requests, please try again later.'
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
files['src/presentation/middlewares/validate.middleware.ts'] = options.validation === 'zod'
|
|
264
|
+
? `import { Request, Response, NextFunction } from 'express';
|
|
265
|
+
import { AnyZodObject, ZodError } from 'zod';
|
|
266
|
+
|
|
267
|
+
export const validate = (schema: AnyZodObject) => {
|
|
268
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = await schema.parseAsync({ body: req.body, query: req.query, params: req.params });
|
|
271
|
+
req.body = parsed.body;
|
|
272
|
+
next();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof ZodError) {
|
|
275
|
+
res.status(400).json({ status: 'error', statusCode: 400, message: 'Validation failed', errors: error.errors });
|
|
276
|
+
} else next(error);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
`
|
|
281
|
+
: `import { Request, Response, NextFunction } from 'express';
|
|
282
|
+
import { Schema } from 'joi';
|
|
283
|
+
|
|
284
|
+
export const validate = (schema: Schema) => {
|
|
285
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
286
|
+
const { error, value } = schema.validate({ body: req.body, query: req.query, params: req.params }, { abortEarly: false });
|
|
287
|
+
if (error) {
|
|
288
|
+
return res.status(400).json({ status: 'error', statusCode: 400, message: 'Validation failed', errors: error.details });
|
|
289
|
+
}
|
|
290
|
+
req.body = value.body;
|
|
291
|
+
next();
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
`;
|
|
295
|
+
if (options.validation === 'zod') {
|
|
296
|
+
files['src/presentation/schemas/user.schema.ts'] = `import { z } from 'zod';
|
|
297
|
+
|
|
298
|
+
export const registerSchema = z.object({
|
|
299
|
+
body: z.object({
|
|
300
|
+
email: z.string().email(),
|
|
301
|
+
password: z.string().min(6),
|
|
302
|
+
name: z.string().optional(),
|
|
303
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export const loginSchema = z.object({
|
|
308
|
+
body: z.object({
|
|
309
|
+
email: z.string().email(),
|
|
310
|
+
password: z.string().min(1),
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
export const refreshSchema = z.object({
|
|
315
|
+
body: z.object({
|
|
316
|
+
refreshToken: z.string().min(1),
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
files['src/presentation/schemas/user.schema.ts'] = `import Joi from 'joi';
|
|
323
|
+
|
|
324
|
+
export const registerSchema = Joi.object({
|
|
325
|
+
body: Joi.object({
|
|
326
|
+
email: Joi.string().email().required(),
|
|
327
|
+
password: Joi.string().min(6).required(),
|
|
328
|
+
name: Joi.string().optional(),
|
|
329
|
+
role: Joi.string().valid('user', 'admin').default('user'),
|
|
330
|
+
}).required(),
|
|
331
|
+
}).unknown(true);
|
|
332
|
+
|
|
333
|
+
export const loginSchema = Joi.object({
|
|
334
|
+
body: Joi.object({
|
|
335
|
+
email: Joi.string().email().required(),
|
|
336
|
+
password: Joi.string().required(),
|
|
337
|
+
}).required(),
|
|
338
|
+
}).unknown(true);
|
|
339
|
+
|
|
340
|
+
export const refreshSchema = Joi.object({
|
|
341
|
+
body: Joi.object({
|
|
342
|
+
refreshToken: Joi.string().required(),
|
|
343
|
+
}).required(),
|
|
344
|
+
}).unknown(true);
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
// Presentation Controllers & Routes
|
|
348
|
+
const userRepoClass = options.orm === 'prisma' ? 'PrismaUserRepository' : options.orm === 'typeorm' ? 'TypeOrmUserRepository' : 'MongooseUserRepository';
|
|
349
|
+
const userRepoFile = options.orm === 'prisma' ? 'prisma-user.repository.impl.js' : options.orm === 'typeorm' ? 'typeorm-user.repository.impl.js' : 'mongoose-user.repository.impl.js';
|
|
350
|
+
files['src/presentation/controllers/user.controller.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
351
|
+
import { RegisterUserUseCase } from '../../application/use-cases/register-user.use-case.js';
|
|
352
|
+
import { LoginUserUseCase } from '../../application/use-cases/login-user.use-case.js';
|
|
353
|
+
import { GetUserProfileUseCase } from '../../application/use-cases/get-user-profile.use-case.js';
|
|
354
|
+
import { RefreshTokenUseCase } from '../../application/use-cases/refresh-token.use-case.js';
|
|
355
|
+
import { ${userRepoClass} } from '../../infrastructure/repositories/${userRepoFile}';
|
|
356
|
+
import { BcryptPasswordHasher } from '../../infrastructure/security/bcrypt.hasher.js';
|
|
357
|
+
import { JwtTokenService } from '../../infrastructure/security/jwt.service.js';
|
|
358
|
+
import { AuthenticatedRequest } from '../middlewares/auth.middleware.js';
|
|
359
|
+
|
|
360
|
+
const userRepository = new ${userRepoClass}();
|
|
361
|
+
const passwordHasher = new BcryptPasswordHasher();
|
|
362
|
+
const tokenService = new JwtTokenService();
|
|
363
|
+
|
|
364
|
+
export class UserController {
|
|
365
|
+
async register(req: Request, res: Response, next: NextFunction) {
|
|
366
|
+
try {
|
|
367
|
+
const useCase = new RegisterUserUseCase(userRepository, passwordHasher, tokenService);
|
|
368
|
+
const result = await useCase.execute(req.body);
|
|
369
|
+
res.status(201).json({ status: 'success', data: result });
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
res.status(400).json({ status: 'error', statusCode: 400, message: err.message });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async login(req: Request, res: Response, next: NextFunction) {
|
|
376
|
+
try {
|
|
377
|
+
const useCase = new LoginUserUseCase(userRepository, passwordHasher, tokenService);
|
|
378
|
+
const result = await useCase.execute(req.body);
|
|
379
|
+
res.status(200).json({ status: 'success', data: result });
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
res.status(401).json({ status: 'error', statusCode: 401, message: err.message });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async refresh(req: Request, res: Response, next: NextFunction) {
|
|
386
|
+
try {
|
|
387
|
+
const useCase = new RefreshTokenUseCase(userRepository, tokenService);
|
|
388
|
+
const result = await useCase.execute(req.body.refreshToken);
|
|
389
|
+
res.status(200).json({ status: 'success', data: result });
|
|
390
|
+
} catch (err: any) {
|
|
391
|
+
res.status(401).json({ status: 'error', statusCode: 401, message: err.message });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async profile(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
|
396
|
+
try {
|
|
397
|
+
const useCase = new GetUserProfileUseCase(userRepository);
|
|
398
|
+
const user = await useCase.execute(req.user!.id);
|
|
399
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
res.status(404).json({ status: 'error', statusCode: 404, message: err.message });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
406
|
+
try {
|
|
407
|
+
const list = await userRepository.findAll();
|
|
408
|
+
res.status(200).json({ status: 'success', data: { users: list } });
|
|
409
|
+
} catch (err: any) {
|
|
410
|
+
next(err);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
`;
|
|
415
|
+
files['src/presentation/routes/user.routes.ts'] = `import { Router } from 'express';
|
|
416
|
+
import { UserController } from '../controllers/user.controller.js';
|
|
417
|
+
import { requireAuth } from '../middlewares/auth.middleware.js';
|
|
418
|
+
${options.rbac ? `import { authorize } from '../middlewares/rbac.middleware.js';` : ''}
|
|
419
|
+
import { validate } from '../middlewares/validate.middleware.js';
|
|
420
|
+
import { registerSchema, loginSchema, refreshSchema } from '../schemas/user.schema.js';
|
|
421
|
+
|
|
422
|
+
const router = Router();
|
|
423
|
+
const controller = new UserController();
|
|
424
|
+
|
|
425
|
+
router.post('/register', validate(registerSchema), controller.register);
|
|
426
|
+
router.post('/login', validate(loginSchema), controller.login);
|
|
427
|
+
router.post('/refresh', validate(refreshSchema), controller.refresh);
|
|
428
|
+
router.get('/profile', requireAuth, controller.profile);
|
|
429
|
+
router.get('/', requireAuth${options.rbac ? `, authorize('admin')` : ''}, controller.list);
|
|
430
|
+
|
|
431
|
+
export default router;
|
|
432
|
+
`;
|
|
433
|
+
files['src/presentation/routes/index.ts'] = `import { Router } from 'express';
|
|
434
|
+
import userRoutes from './user.routes.js';
|
|
435
|
+
|
|
436
|
+
const router = Router();
|
|
437
|
+
router.use('/users', userRoutes);
|
|
438
|
+
|
|
439
|
+
export default router;
|
|
440
|
+
`;
|
|
441
|
+
// 5. Infrastructure Layer - Repositories impl
|
|
442
|
+
if (options.orm === 'prisma') {
|
|
443
|
+
files['src/infrastructure/repositories/prisma-user.repository.impl.ts'] = `import { PrismaClient } from '@prisma/client';
|
|
444
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
445
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
446
|
+
|
|
447
|
+
const prisma = new PrismaClient();
|
|
448
|
+
|
|
449
|
+
export class PrismaUserRepository implements IUserRepository {
|
|
450
|
+
private mapToEntity(dbUser: any): User {
|
|
451
|
+
return new User(
|
|
452
|
+
dbUser.id,
|
|
453
|
+
dbUser.email,
|
|
454
|
+
dbUser.password,
|
|
455
|
+
dbUser.name,
|
|
456
|
+
dbUser.role,
|
|
457
|
+
dbUser.createdAt,
|
|
458
|
+
dbUser.refreshToken
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
463
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
464
|
+
return user ? this.mapToEntity(user) : null;
|
|
465
|
+
}
|
|
466
|
+
async findById(id: string): Promise<User | null> {
|
|
467
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
468
|
+
return user ? this.mapToEntity(user) : null;
|
|
469
|
+
}
|
|
470
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
471
|
+
const created = await prisma.user.create({ data: user });
|
|
472
|
+
return this.mapToEntity(created);
|
|
473
|
+
}
|
|
474
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
475
|
+
const updated = await prisma.user.update({ where: { id }, data: user });
|
|
476
|
+
return this.mapToEntity(updated);
|
|
477
|
+
}
|
|
478
|
+
async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
|
|
479
|
+
const list = await prisma.user.findMany();
|
|
480
|
+
return list.map(u => ({ id: u.id, email: u.email, name: u.name, role: u.role, createdAt: u.createdAt }));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
else if (options.orm === 'typeorm') {
|
|
486
|
+
files['src/infrastructure/repositories/user.entity.ts'] = getTypeOrmEntity();
|
|
487
|
+
files['src/infrastructure/repositories/typeorm-user.repository.impl.ts'] = `import { AppDataSource } from '../database/connection.js';
|
|
488
|
+
import { UserEntity } from './user.entity.js';
|
|
489
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
490
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
491
|
+
|
|
492
|
+
export class TypeOrmUserRepository implements IUserRepository {
|
|
493
|
+
private repo = AppDataSource.getRepository(UserEntity);
|
|
494
|
+
|
|
495
|
+
private mapToEntity(u: UserEntity): User {
|
|
496
|
+
return new User(u.id, u.email, u.password, u.name, u.role, u.createdAt, u.refreshToken);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
500
|
+
const u = await this.repo.findOne({ where: { email } });
|
|
501
|
+
return u ? this.mapToEntity(u) : null;
|
|
502
|
+
}
|
|
503
|
+
async findById(id: string): Promise<User | null> {
|
|
504
|
+
const u = await this.repo.findOne({ where: { id } });
|
|
505
|
+
return u ? this.mapToEntity(u) : null;
|
|
506
|
+
}
|
|
507
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
508
|
+
const created = this.repo.create(user);
|
|
509
|
+
const saved = await this.repo.save(created);
|
|
510
|
+
return this.mapToEntity(saved);
|
|
511
|
+
}
|
|
512
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
513
|
+
await this.repo.update(id, user);
|
|
514
|
+
const updated = await this.findById(id);
|
|
515
|
+
return updated!;
|
|
516
|
+
}
|
|
517
|
+
async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
|
|
518
|
+
const list = await this.repo.find();
|
|
519
|
+
return list.map(u => ({ id: u.id, email: u.email, name: u.name, role: u.role, createdAt: u.createdAt }));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// Mongoose
|
|
526
|
+
files['src/infrastructure/repositories/user.model.ts'] = `import mongoose, { Schema } from 'mongoose';
|
|
527
|
+
|
|
528
|
+
const UserSchema = new Schema({
|
|
529
|
+
email: { type: String, required: true, unique: true },
|
|
530
|
+
name: { type: String },
|
|
531
|
+
password: { type: String, required: true },
|
|
532
|
+
role: { type: String, default: 'user' },
|
|
533
|
+
refreshToken: { type: String },
|
|
534
|
+
}, { timestamps: true });
|
|
535
|
+
|
|
536
|
+
export const UserModel = mongoose.models.User || mongoose.model('User', UserSchema);
|
|
537
|
+
`;
|
|
538
|
+
files['src/infrastructure/repositories/mongoose-user.repository.impl.ts'] = `import { UserModel } from './user.model.js';
|
|
539
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
540
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
541
|
+
|
|
542
|
+
export class MongooseUserRepository implements IUserRepository {
|
|
543
|
+
private mapToEntity(doc: any): User {
|
|
544
|
+
return new User(
|
|
545
|
+
doc._id.toString(),
|
|
546
|
+
doc.email,
|
|
547
|
+
doc.password,
|
|
548
|
+
doc.name,
|
|
549
|
+
doc.role,
|
|
550
|
+
doc.createdAt,
|
|
551
|
+
doc.refreshToken
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
556
|
+
const user = await UserModel.findOne({ email });
|
|
557
|
+
return user ? this.mapToEntity(user) : null;
|
|
558
|
+
}
|
|
559
|
+
async findById(id: string): Promise<User | null> {
|
|
560
|
+
const user = await UserModel.findById(id);
|
|
561
|
+
return user ? this.mapToEntity(user) : null;
|
|
562
|
+
}
|
|
563
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
564
|
+
const created = await UserModel.create(user);
|
|
565
|
+
return this.mapToEntity(created);
|
|
566
|
+
}
|
|
567
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
568
|
+
const updated = await UserModel.findByIdAndUpdate(id, { $set: user }, { new: true });
|
|
569
|
+
return this.mapToEntity(updated);
|
|
570
|
+
}
|
|
571
|
+
async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
|
|
572
|
+
const docs = await UserModel.find({});
|
|
573
|
+
return docs.map(d => ({ id: d._id.toString(), email: d.email, name: d.name, role: d.role, createdAt: d.createdAt }));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
// Infrastructure Security
|
|
579
|
+
files['src/infrastructure/security/bcrypt.hasher.ts'] = `import bcrypt from 'bcryptjs';
|
|
580
|
+
|
|
581
|
+
export class BcryptPasswordHasher {
|
|
582
|
+
async hash(password: string): Promise<string> {
|
|
583
|
+
return bcrypt.hash(password, 10);
|
|
584
|
+
}
|
|
585
|
+
async compare(raw: string, hashed: string): Promise<boolean> {
|
|
586
|
+
return bcrypt.compare(raw, hashed);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
`;
|
|
590
|
+
files['src/infrastructure/security/jwt.service.ts'] = `import jwt from 'jsonwebtoken';
|
|
591
|
+
import { env } from '../config/env.config.js';
|
|
592
|
+
|
|
593
|
+
export class JwtTokenService {
|
|
594
|
+
generateTokens(payload: { id: string; email: string; role: string }) {
|
|
595
|
+
const accessToken = jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as any });
|
|
596
|
+
const refreshToken = jwt.sign(payload, env.JWT_REFRESH_SECRET, { expiresIn: env.JWT_REFRESH_EXPIRES_IN as any });
|
|
597
|
+
return { accessToken, refreshToken };
|
|
598
|
+
}
|
|
599
|
+
verifyRefreshToken(token: string) {
|
|
600
|
+
return jwt.verify(token, env.JWT_REFRESH_SECRET);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
`;
|
|
604
|
+
// Shared types
|
|
605
|
+
files['src/shared/types/index.ts'] = `export type UserRole = 'user' | 'admin';`;
|
|
606
|
+
// 6. Presentation server/bootstrap
|
|
607
|
+
let appMiddlewaresImport = '';
|
|
608
|
+
let appMiddlewares = '';
|
|
609
|
+
if (options.rateLimiting) {
|
|
610
|
+
appMiddlewaresImport += `import { apiLimiter } from './middlewares/rateLimit.middleware.js';\n`;
|
|
611
|
+
appMiddlewares += `app.use('/api', apiLimiter);\n`;
|
|
612
|
+
}
|
|
613
|
+
if (options.swagger) {
|
|
614
|
+
appMiddlewaresImport += `import swaggerUi from 'swagger-ui-express';\n`;
|
|
615
|
+
appMiddlewares += `
|
|
616
|
+
const swaggerDocument = {
|
|
617
|
+
openapi: "3.0.0",
|
|
618
|
+
info: { title: "${options.projectName} API (Clean)", version: "1.0.0" },
|
|
619
|
+
servers: [{ url: "http://localhost:3000/api" }],
|
|
620
|
+
paths: {
|
|
621
|
+
"/users/register": {
|
|
622
|
+
post: {
|
|
623
|
+
summary: "Register",
|
|
624
|
+
requestBody: {
|
|
625
|
+
required: true,
|
|
626
|
+
content: { "application/json": { schema: { type: "object", properties: { email: { type: "string" }, password: { type: "string" } } } } }
|
|
627
|
+
},
|
|
628
|
+
responses: { "201": { description: "Created" } }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
files['src/presentation/express.server.ts'] = `import express from 'express';
|
|
637
|
+
import cors from 'cors';
|
|
638
|
+
import helmet from 'helmet';
|
|
639
|
+
import routes from './routes/index.js';
|
|
640
|
+
import { errorHandler } from './middlewares/error.middleware.js';
|
|
641
|
+
${appMiddlewaresImport}
|
|
642
|
+
const app = express();
|
|
643
|
+
|
|
644
|
+
app.use(helmet());
|
|
645
|
+
app.use(cors());
|
|
646
|
+
app.use(express.json());
|
|
647
|
+
|
|
648
|
+
${appMiddlewares}
|
|
649
|
+
app.use('/api', routes);
|
|
650
|
+
app.use(errorHandler);
|
|
651
|
+
|
|
652
|
+
export default app;
|
|
653
|
+
`;
|
|
654
|
+
let dbConnectionCode = '';
|
|
655
|
+
if (options.orm === 'mongoose') {
|
|
656
|
+
dbConnectionCode = `import mongoose from 'mongoose';
|
|
657
|
+
async function connectDB() {
|
|
658
|
+
await mongoose.connect(env.DATABASE_URL);
|
|
659
|
+
console.log('🔌 Connected to MongoDB database successfully.');
|
|
660
|
+
}`;
|
|
661
|
+
}
|
|
662
|
+
else if (options.orm === 'typeorm') {
|
|
663
|
+
dbConnectionCode = `import { AppDataSource } from './infrastructure/database/connection.js';
|
|
664
|
+
async function connectDB() {
|
|
665
|
+
await AppDataSource.initialize();
|
|
666
|
+
console.log('🔌 Connected to Database via TypeORM datasource.');
|
|
667
|
+
}`;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
dbConnectionCode = `import { PrismaClient } from '@prisma/client';
|
|
671
|
+
const prisma = new PrismaClient();
|
|
672
|
+
async function connectDB() {
|
|
673
|
+
await prisma.$connect();
|
|
674
|
+
console.log('🔌 Connected to Database via Prisma client.');
|
|
675
|
+
}`;
|
|
676
|
+
}
|
|
677
|
+
files['src/server.ts'] = `import app from './presentation/express.server.js';
|
|
678
|
+
import { env } from './infrastructure/config/env.config.js';
|
|
679
|
+
${dbConnectionCode}
|
|
680
|
+
|
|
681
|
+
const PORT = env.PORT || 3000;
|
|
682
|
+
|
|
683
|
+
async function start() {
|
|
684
|
+
try {
|
|
685
|
+
await connectDB();
|
|
686
|
+
app.listen(PORT, () => {
|
|
687
|
+
console.log(\`🚀 Server running on port \${PORT} (Clean Architecture)\`);
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error('❌ Failed to start application:', err);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
start();
|
|
695
|
+
`;
|
|
696
|
+
return files;
|
|
697
|
+
}
|