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.
@@ -0,0 +1,855 @@
1
+ import { getTypeOrmConfig, getTypeOrmEntity } from './sharedTemplate.js';
2
+ export function getHexagonalFiles(options) {
3
+ const files = {};
4
+ const userIdExpr = options.orm === 'prisma' ? 'user.id' : 'user._id.toString()';
5
+ // 1. Config & Env
6
+ files['src/config/env.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/config/data-source.ts'] = getTypeOrmConfig(options)
61
+ .replace('../entities/user.entity.js', '../adapters/outbound/db/user.entity.js');
62
+ }
63
+ // 2. Domain Models
64
+ files['src/domain/models/user.model.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. Ports
77
+ // Outbound Ports (Driven ports)
78
+ files['src/ports/outbound/user-repository.port.ts'] = `import { User } from '../../domain/models/user.model.js';
79
+
80
+ export interface IUserRepositoryPort {
81
+ findByEmail(email: string): Promise<User | null>;
82
+ findById(id: string): Promise<User | null>;
83
+ create(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
84
+ update(id: string, user: Partial<User>): Promise<User>;
85
+ delete(id: string): Promise<void>;
86
+ findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]>;
87
+ }
88
+ `;
89
+ files['src/ports/outbound/password-hasher.port.ts'] = `export interface IPasswordHasherPort {
90
+ hash(password: string): Promise<string>;
91
+ compare(raw: string, hashed: string): Promise<boolean>;
92
+ }
93
+ `;
94
+ files['src/ports/outbound/token-service.port.ts'] = `export interface ITokenServicePort {
95
+ generateTokens(payload: { id: string; email: string; role: string }): { accessToken: string; refreshToken: string };
96
+ verifyRefreshToken(token: string): any;
97
+ }
98
+ `;
99
+ // Inbound Ports (Driving ports / Usecase boundaries)
100
+ files['src/ports/inbound/user-usecase.port.ts'] = `import { User } from '../../domain/models/user.model.js';
101
+
102
+ export interface IRegisterUserPort {
103
+ execute(input: any): Promise<any>;
104
+ }
105
+
106
+ export interface ILoginUserPort {
107
+ execute(input: any): Promise<any>;
108
+ }
109
+
110
+ export interface IGetUserProfilePort {
111
+ execute(userId: string): Promise<any>;
112
+ }
113
+
114
+ export interface IRefreshTokenPort {
115
+ execute(token: string): Promise<any>;
116
+ }
117
+ `;
118
+ // 4. Application Services (Implements Inbound Ports, delegates to Outbound Ports)
119
+ files['src/application/use-cases/auth.use-cases.ts'] = `import { User } from '../../domain/models/user.model.js';
120
+ import { IUserRepositoryPort } from '../../ports/outbound/user-repository.port.js';
121
+ import { IPasswordHasherPort } from '../../ports/outbound/password-hasher.port.js';
122
+ import { ITokenServicePort } from '../../ports/outbound/token-service.port.js';
123
+ import {
124
+ IRegisterUserPort,
125
+ ILoginUserPort,
126
+ IGetUserProfilePort,
127
+ IRefreshTokenPort
128
+ } from '../../ports/inbound/user-usecase.port.js';
129
+
130
+ export class RegisterUserUseCase implements IRegisterUserPort {
131
+ constructor(
132
+ private userRepo: IUserRepositoryPort,
133
+ private hasher: IPasswordHasherPort,
134
+ private tokenService: ITokenServicePort
135
+ ) {}
136
+
137
+ async execute(input: any) {
138
+ const existing = await this.userRepo.findByEmail(input.email);
139
+ if (existing) throw new Error('Email already registered');
140
+
141
+ const hashedPassword = await this.hasher.hash(input.password);
142
+ const user = await this.userRepo.create({
143
+ email: input.email,
144
+ name: input.name || null,
145
+ password: hashedPassword,
146
+ role: input.role || 'user',
147
+ refreshToken: null
148
+ });
149
+
150
+ const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
151
+ await this.userRepo.update(user.id, { refreshToken: tokens.refreshToken });
152
+
153
+ return {
154
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
155
+ ...tokens
156
+ };
157
+ }
158
+ }
159
+
160
+ export class LoginUserUseCase implements ILoginUserPort {
161
+ constructor(
162
+ private userRepo: IUserRepositoryPort,
163
+ private hasher: IPasswordHasherPort,
164
+ private tokenService: ITokenServicePort
165
+ ) {}
166
+
167
+ async execute(input: any) {
168
+ const user = await this.userRepo.findByEmail(input.email);
169
+ if (!user) throw new Error('Invalid email or password');
170
+
171
+ const isMatch = await this.hasher.compare(input.password, user.password);
172
+ if (!isMatch) throw new Error('Invalid email or password');
173
+
174
+ const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
175
+ await this.userRepo.update(user.id, { refreshToken: tokens.refreshToken });
176
+
177
+ return {
178
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
179
+ ...tokens
180
+ };
181
+ }
182
+ }
183
+
184
+ export class GetUserProfileUseCase implements IGetUserProfilePort {
185
+ constructor(private userRepo: IUserRepositoryPort) {}
186
+
187
+ async execute(userId: string) {
188
+ const user = await this.userRepo.findById(userId);
189
+ if (!user) throw new Error('User not found');
190
+ return {
191
+ id: user.id,
192
+ email: user.email,
193
+ name: user.name,
194
+ role: user.role,
195
+ createdAt: user.createdAt
196
+ };
197
+ }
198
+ }
199
+
200
+ export class RefreshTokenUseCase implements IRefreshTokenPort {
201
+ constructor(
202
+ private userRepo: IUserRepositoryPort,
203
+ private tokenService: ITokenServicePort
204
+ ) {}
205
+
206
+ async execute(token: string) {
207
+ try {
208
+ const decoded = this.tokenService.verifyRefreshToken(token);
209
+ const user = await this.userRepo.findById(decoded.id);
210
+ if (!user || user.refreshToken !== token) {
211
+ throw new Error('Invalid or revoked refresh token');
212
+ }
213
+
214
+ const tokens = this.tokenService.generateTokens({ id: user.id, email: user.email, role: user.role });
215
+ await this.userRepo.update(user.id, { refreshToken: tokens.refreshToken });
216
+ return tokens;
217
+ } catch (err) {
218
+ throw new Error('Invalid or expired refresh token');
219
+ }
220
+ }
221
+ }
222
+ `;
223
+ // 5. Adapters
224
+ // Outbound Adapters (Security)
225
+ files['src/adapters/outbound/security/bcrypt.hasher.ts'] = `import bcrypt from 'bcryptjs';
226
+ import { IPasswordHasherPort } from '../../../ports/outbound/password-hasher.port.js';
227
+
228
+ export class BcryptPasswordHasher implements IPasswordHasherPort {
229
+ async hash(password: string): Promise<string> {
230
+ return bcrypt.hash(password, 10);
231
+ }
232
+ async compare(raw: string, hashed: string): Promise<boolean> {
233
+ return bcrypt.compare(raw, hashed);
234
+ }
235
+ }
236
+ `;
237
+ files['src/adapters/outbound/security/jwt.token-generator.ts'] = `import jwt from 'jsonwebtoken';
238
+ import { ITokenServicePort } from '../../../ports/outbound/token-service.port.js';
239
+ import { env } from '../../../config/env.js';
240
+
241
+ export class JwtTokenService implements ITokenServicePort {
242
+ generateTokens(payload: { id: string; email: string; role: string }) {
243
+ const accessToken = jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as any });
244
+ const refreshToken = jwt.sign(payload, env.JWT_REFRESH_SECRET, { expiresIn: env.JWT_REFRESH_EXPIRES_IN as any });
245
+ return { accessToken, refreshToken };
246
+ }
247
+ verifyRefreshToken(token: string) {
248
+ return jwt.verify(token, env.JWT_REFRESH_SECRET);
249
+ }
250
+ }
251
+ `;
252
+ // Outbound Adapters (Database entity if TypeORM)
253
+ if (options.orm === 'typeorm') {
254
+ files['src/adapters/outbound/db/user.entity.ts'] = getTypeOrmEntity();
255
+ }
256
+ else if (options.orm === 'mongoose') {
257
+ files['src/adapters/outbound/db/user.model.ts'] = `import mongoose, { Schema } from 'mongoose';
258
+
259
+ const UserSchema = new Schema({
260
+ email: { type: String, required: true, unique: true },
261
+ name: { type: String },
262
+ password: { type: String, required: true },
263
+ role: { type: String, default: 'user' },
264
+ refreshToken: { type: String },
265
+ }, { timestamps: true });
266
+
267
+ export const UserModel = mongoose.models.User || mongoose.model('User', UserSchema);
268
+ `;
269
+ }
270
+ // Outbound Adapters (Database repo implementing Outbound Port)
271
+ if (options.orm === 'prisma') {
272
+ files['src/adapters/outbound/db/orm-user.repository.ts'] = `import { PrismaClient } from '@prisma/client';
273
+ import { IUserRepositoryPort } from '../../../ports/outbound/user-repository.port.js';
274
+ import { User } from '../../../domain/models/user.model.js';
275
+
276
+ const prisma = new PrismaClient();
277
+
278
+ export class OrmUserRepository implements IUserRepositoryPort {
279
+ private mapToModel(u: any): User {
280
+ return new User(u.id, u.email, u.password, u.name, u.role, u.createdAt, u.refreshToken);
281
+ }
282
+
283
+ async findByEmail(email: string): Promise<User | null> {
284
+ const u = await prisma.user.findUnique({ where: { email } });
285
+ return u ? this.mapToModel(u) : null;
286
+ }
287
+ async findById(id: string): Promise<User | null> {
288
+ const u = await prisma.user.findUnique({ where: { id } });
289
+ return u ? this.mapToModel(u) : null;
290
+ }
291
+ async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
292
+ const created = await prisma.user.create({ data: user });
293
+ return this.mapToModel(created);
294
+ }
295
+ async update(id: string, user: Partial<User>): Promise<User> {
296
+ const updated = await prisma.user.update({ where: { id }, data: user });
297
+ return this.mapToModel(updated);
298
+ }
299
+ async delete(id: string): Promise<void> {
300
+ await prisma.user.delete({ where: { id } });
301
+ }
302
+ async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
303
+ const users = await prisma.user.findMany();
304
+ return users.map(u => ({ id: u.id, email: u.email, name: u.name, role: u.role, createdAt: u.createdAt }));
305
+ }
306
+ }
307
+ `;
308
+ }
309
+ else if (options.orm === 'typeorm') {
310
+ files['src/adapters/outbound/db/orm-user.repository.ts'] = `import { AppDataSource } from '../../../config/data-source.js';
311
+ import { UserEntity } from './user.entity.js';
312
+ import { IUserRepositoryPort } from '../../../ports/outbound/user-repository.port.js';
313
+ import { User } from '../../../domain/models/user.model.js';
314
+
315
+ export class OrmUserRepository implements IUserRepositoryPort {
316
+ private repo = AppDataSource.getRepository(UserEntity);
317
+
318
+ private mapToModel(u: UserEntity): User {
319
+ return new User(u.id, u.email, u.password, u.name ?? null, u.role, u.createdAt, u.refreshToken ?? null);
320
+ }
321
+
322
+ async findByEmail(email: string): Promise<User | null> {
323
+ const u = await this.repo.findOne({ where: { email } });
324
+ return u ? this.mapToModel(u) : null;
325
+ }
326
+ async findById(id: string): Promise<User | null> {
327
+ const u = await this.repo.findOne({ where: { id } });
328
+ return u ? this.mapToModel(u) : null;
329
+ }
330
+ async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
331
+ const created = this.repo.create({
332
+ email: user.email,
333
+ name: user.name ?? undefined,
334
+ password: user.password,
335
+ role: user.role,
336
+ refreshToken: user.refreshToken ?? undefined,
337
+ });
338
+ const saved = await this.repo.save(created);
339
+ return this.mapToModel(saved);
340
+ }
341
+ async update(id: string, user: Partial<User>): Promise<User> {
342
+ await this.repo.update(id, {
343
+ email: user.email,
344
+ name: user.name ?? undefined,
345
+ password: user.password,
346
+ role: user.role,
347
+ refreshToken: user.refreshToken ?? undefined,
348
+ });
349
+ const updated = await this.findById(id);
350
+ return updated!;
351
+ }
352
+ async delete(id: string): Promise<void> {
353
+ await this.repo.delete(id);
354
+ }
355
+ async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
356
+ const list = await this.repo.find();
357
+ return list.map(u => ({ id: u.id, email: u.email, name: u.name ?? null, role: u.role, createdAt: u.createdAt }));
358
+ }
359
+ }
360
+ `;
361
+ }
362
+ else {
363
+ // Mongoose
364
+ files['src/adapters/outbound/db/orm-user.repository.ts'] = `import { UserModel } from './user.model.js';
365
+ import { IUserRepositoryPort } from '../../../ports/outbound/user-repository.port.js';
366
+ import { User } from '../../../domain/models/user.model.js';
367
+
368
+ export class OrmUserRepository implements IUserRepositoryPort {
369
+ private mapToModel(doc: any): User {
370
+ return new User(doc._id.toString(), doc.email, doc.password, doc.name, doc.role, doc.createdAt, doc.refreshToken);
371
+ }
372
+
373
+ async findByEmail(email: string): Promise<User | null> {
374
+ const doc = await UserModel.findOne({ email });
375
+ return doc ? this.mapToModel(doc) : null;
376
+ }
377
+ async findById(id: string): Promise<User | null> {
378
+ const doc = await UserModel.findById(id);
379
+ return doc ? this.mapToModel(doc) : null;
380
+ }
381
+ async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
382
+ const doc = await UserModel.create(user);
383
+ return this.mapToModel(doc);
384
+ }
385
+ async update(id: string, user: Partial<User>): Promise<User> {
386
+ const doc = await UserModel.findByIdAndUpdate(id, { $set: user }, { new: true });
387
+ return this.mapToModel(doc);
388
+ }
389
+ async delete(id: string): Promise<void> {
390
+ await UserModel.findByIdAndDelete(id);
391
+ }
392
+ async findAll(): Promise<Omit<User, 'password' | 'refreshToken'>[]> {
393
+ const docs = await UserModel.find({});
394
+ return docs.map(d => ({ id: d._id.toString(), email: d.email, name: d.name, role: d.role, createdAt: d.createdAt }));
395
+ }
396
+ }
397
+ `;
398
+ }
399
+ // Inbound Adapters (Web Middlewares)
400
+ files['src/adapters/inbound/web/middlewares/error.middleware.ts'] = `import { Request, Response, NextFunction } from 'express';
401
+
402
+ export class AppError extends Error {
403
+ constructor(public statusCode: number, message: string) {
404
+ super(message);
405
+ Object.setPrototypeOf(this, new.target.prototype);
406
+ }
407
+ }
408
+
409
+ export const errorHandler = (err: Error | AppError, req: Request, res: Response, next: NextFunction) => {
410
+ const statusCode = err instanceof AppError ? err.statusCode : 500;
411
+ res.status(statusCode).json({
412
+ status: 'error',
413
+ statusCode,
414
+ message: err.message || 'Internal Server Error'
415
+ });
416
+ };
417
+ `;
418
+ files['src/adapters/inbound/web/middlewares/auth.middleware.ts'] = `import { Request, Response, NextFunction } from 'express';
419
+ import jwt from 'jsonwebtoken';
420
+ import { env } from '../../../../config/env.js';
421
+ import { AppError } from './error.middleware.js';
422
+
423
+ export interface AuthenticatedRequest extends Request {
424
+ user?: { id: string; email: string; role: string };
425
+ }
426
+
427
+ export const requireAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
428
+ const authHeader = req.headers.authorization;
429
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
430
+ return next(new AppError(401, 'Auth token missing'));
431
+ }
432
+ const token = authHeader.split(' ')[1];
433
+ try {
434
+ req.user = jwt.verify(token, env.JWT_SECRET) as any;
435
+ next();
436
+ } catch (error) {
437
+ next(new AppError(401, 'Token expired or corrupt'));
438
+ }
439
+ };
440
+ `;
441
+ if (options.rbac) {
442
+ files['src/adapters/inbound/web/middlewares/rbac.middleware.ts'] = `import { Response, NextFunction } from 'express';
443
+ import { AuthenticatedRequest } from './auth.middleware.js';
444
+ import { AppError } from './error.middleware.js';
445
+
446
+ export const authorize = (...roles: string[]) => {
447
+ return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
448
+ if (!req.user || !roles.includes(req.user.role)) {
449
+ return next(new AppError(403, 'Permission denied'));
450
+ }
451
+ next();
452
+ };
453
+ };
454
+ `;
455
+ }
456
+ if (options.rateLimiting) {
457
+ files['src/adapters/inbound/web/middlewares/rateLimit.middleware.ts'] = `import rateLimit from 'express-rate-limit';
458
+
459
+ export const apiLimiter = rateLimit({
460
+ windowMs: 15 * 60 * 1000,
461
+ max: 100,
462
+ message: { status: 'error', statusCode: 429, message: 'Too many requests' }
463
+ });
464
+ `;
465
+ }
466
+ // Request Validation Middleware & Schemas
467
+ files['src/adapters/inbound/web/middlewares/validate.middleware.ts'] = options.validation === 'zod'
468
+ ? `import { Request, Response, NextFunction } from 'express';
469
+ import { AnyZodObject, ZodError } from 'zod';
470
+
471
+ export const validate = (schema: AnyZodObject) => {
472
+ return async (req: Request, res: Response, next: NextFunction) => {
473
+ try {
474
+ const parsed = await schema.parseAsync({ body: req.body, query: req.query, params: req.params });
475
+ req.body = parsed.body;
476
+ next();
477
+ } catch (error) {
478
+ if (error instanceof ZodError) {
479
+ res.status(400).json({ status: 'error', statusCode: 400, message: 'Validation failed', errors: error.errors });
480
+ } else next(error);
481
+ }
482
+ };
483
+ };
484
+ `
485
+ : `import { Request, Response, NextFunction } from 'express';
486
+ import { Schema } from 'joi';
487
+
488
+ export const validate = (schema: Schema) => {
489
+ return async (req: Request, res: Response, next: NextFunction) => {
490
+ const { error, value } = schema.validate({ body: req.body, query: req.query, params: req.params }, { abortEarly: false });
491
+ if (error) {
492
+ return res.status(400).json({ status: 'error', statusCode: 400, message: 'Validation failed', errors: error.details });
493
+ }
494
+ req.body = value.body;
495
+ next();
496
+ };
497
+ };
498
+ `;
499
+ if (options.validation === 'zod') {
500
+ files['src/adapters/inbound/web/schemas/user.schema.ts'] = `import { z } from 'zod';
501
+
502
+ export const registerSchema = z.object({
503
+ body: z.object({
504
+ email: z.string().email(),
505
+ password: z.string().min(6),
506
+ name: z.string().optional(),
507
+ role: z.enum(['user', 'admin']).optional(),
508
+ }),
509
+ });
510
+
511
+ export const loginSchema = z.object({
512
+ body: z.object({
513
+ email: z.string().email(),
514
+ password: z.string().min(1),
515
+ }),
516
+ });
517
+
518
+ export const refreshSchema = z.object({
519
+ body: z.object({
520
+ refreshToken: z.string().min(1),
521
+ }),
522
+ });
523
+ `;
524
+ }
525
+ else {
526
+ files['src/adapters/inbound/web/schemas/user.schema.ts'] = `import Joi from 'joi';
527
+
528
+ export const registerSchema = Joi.object({
529
+ body: Joi.object({
530
+ email: Joi.string().email().required(),
531
+ password: Joi.string().min(6).required(),
532
+ name: Joi.string().optional(),
533
+ role: Joi.string().valid('user', 'admin').default('user'),
534
+ }).required(),
535
+ }).unknown(true);
536
+
537
+ export const loginSchema = Joi.object({
538
+ body: Joi.object({
539
+ email: Joi.string().email().required(),
540
+ password: Joi.string().required(),
541
+ }).required(),
542
+ }).unknown(true);
543
+
544
+ export const refreshSchema = Joi.object({
545
+ body: Joi.object({
546
+ refreshToken: Joi.string().required(),
547
+ }).required(),
548
+ }).unknown(true);
549
+ `;
550
+ }
551
+ // Web Controllers (HTTP driving adapter)
552
+ files['src/adapters/inbound/web/handlers/user.handler.ts'] = `import { Request, Response, NextFunction } from 'express';
553
+ import { RegisterUserUseCase, LoginUserUseCase, GetUserProfileUseCase, RefreshTokenUseCase } from '../../../../application/use-cases/auth.use-cases.js';
554
+ import { OrmUserRepository } from '../../../outbound/db/orm-user.repository.js';
555
+ import { BcryptPasswordHasher } from '../../../outbound/security/bcrypt.hasher.js';
556
+ import { JwtTokenService } from '../../../outbound/security/jwt.token-generator.js';
557
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware.js';
558
+
559
+ const userRepo = new OrmUserRepository();
560
+ const hasher = new BcryptPasswordHasher();
561
+ const tokenService = new JwtTokenService();
562
+
563
+ export class UserHandler {
564
+ async register(req: Request, res: Response, next: NextFunction) {
565
+ try {
566
+ const useCase = new RegisterUserUseCase(userRepo, hasher, tokenService);
567
+ const result = await useCase.execute(req.body);
568
+ res.status(201).json({ status: 'success', data: result });
569
+ } catch (err: any) {
570
+ res.status(400).json({ status: 'error', statusCode: 400, message: err.message });
571
+ }
572
+ }
573
+
574
+ async login(req: Request, res: Response, next: NextFunction) {
575
+ try {
576
+ const useCase = new LoginUserUseCase(userRepo, hasher, tokenService);
577
+ const result = await useCase.execute(req.body);
578
+ res.status(200).json({ status: 'success', data: result });
579
+ } catch (err: any) {
580
+ res.status(401).json({ status: 'error', statusCode: 401, message: err.message });
581
+ }
582
+ }
583
+
584
+ async refresh(req: Request, res: Response, next: NextFunction) {
585
+ try {
586
+ const useCase = new RefreshTokenUseCase(userRepo, tokenService);
587
+ const result = await useCase.execute(req.body.refreshToken);
588
+ res.status(200).json({ status: 'success', data: result });
589
+ } catch (err: any) {
590
+ res.status(401).json({ status: 'error', statusCode: 401, message: err.message });
591
+ }
592
+ }
593
+
594
+ async profile(req: AuthenticatedRequest, res: Response, next: NextFunction) {
595
+ try {
596
+ const useCase = new GetUserProfileUseCase(userRepo);
597
+ const user = await useCase.execute(req.user!.id);
598
+ res.status(200).json({ status: 'success', data: { user } });
599
+ } catch (err: any) {
600
+ res.status(404).json({ status: 'error', statusCode: 404, message: err.message });
601
+ }
602
+ }
603
+
604
+ async list(req: Request, res: Response, next: NextFunction) {
605
+ try {
606
+ const list = await userRepo.findAll();
607
+ res.status(200).json({ status: 'success', data: { users: list } });
608
+ } catch (err: any) {
609
+ next(err);
610
+ }
611
+ }
612
+
613
+ async show(req: Request, res: Response, next: NextFunction) {
614
+ try {
615
+ const user = await userRepo.findById(req.params.id);
616
+ if (!user) {
617
+ return res.status(404).json({ status: 'error', statusCode: 404, message: 'User not found' });
618
+ }
619
+ res.status(200).json({ status: 'success', data: { user } });
620
+ } catch (err: any) {
621
+ next(err);
622
+ }
623
+ }
624
+
625
+ async update(req: Request, res: Response, next: NextFunction) {
626
+ try {
627
+ const user = await userRepo.update(req.params.id, req.body);
628
+ res.status(200).json({ status: 'success', data: { user } });
629
+ } catch (err: any) {
630
+ next(err);
631
+ }
632
+ }
633
+
634
+ async remove(req: Request, res: Response, next: NextFunction) {
635
+ try {
636
+ await userRepo.delete(req.params.id);
637
+ res.status(204).send();
638
+ } catch (err: any) {
639
+ next(err);
640
+ }
641
+ }
642
+ }
643
+ `;
644
+ // Web Routes (HTTP driving adapter)
645
+ files['src/adapters/inbound/web/routes/user.routes.ts'] = `import { Router } from 'express';
646
+ import { UserHandler } from '../handlers/user.handler.js';
647
+ import { requireAuth } from '../middlewares/auth.middleware.js';
648
+ ${options.rbac ? `import { authorize } from '../middlewares/rbac.middleware.js';` : ''}
649
+ import { validate } from '../middlewares/validate.middleware.js';
650
+ import { registerSchema, loginSchema, refreshSchema } from '../schemas/user.schema.js';
651
+
652
+ const router = Router();
653
+ const handler = new UserHandler();
654
+
655
+ router.post('/register', validate(registerSchema), handler.register);
656
+ router.post('/login', validate(loginSchema), handler.login);
657
+ router.post('/refresh', validate(refreshSchema), handler.refresh);
658
+ router.get('/profile', requireAuth, handler.profile);
659
+ router.get('/', requireAuth${options.rbac ? `, authorize('admin')` : ''}, handler.list);
660
+ router.post('/', requireAuth${options.rbac ? `, authorize('admin')` : ''}, validate(registerSchema), handler.register);
661
+ router.get('/:id', requireAuth${options.rbac ? `, authorize('admin')` : ''}, handler.show);
662
+ router.put('/:id', requireAuth${options.rbac ? `, authorize('admin')` : ''}, handler.update);
663
+ router.delete('/:id', requireAuth${options.rbac ? `, authorize('admin')` : ''}, handler.remove);
664
+
665
+ export default router;
666
+ `;
667
+ files['src/adapters/inbound/web/routes/index.ts'] = `import { Router } from 'express';
668
+ import userRoutes from './user.routes.js';
669
+
670
+ const router = Router();
671
+ router.use('/users', userRoutes);
672
+
673
+ export default router;
674
+ `;
675
+ // 6. Application express setup and server start
676
+ let appMiddlewaresImport = '';
677
+ let appMiddlewares = '';
678
+ if (options.rateLimiting) {
679
+ appMiddlewaresImport += `import { apiLimiter } from './middlewares/rateLimit.middleware.js';\n`;
680
+ appMiddlewares += `app.use('/api', apiLimiter);\n`;
681
+ }
682
+ if (options.swagger) {
683
+ appMiddlewaresImport += `import swaggerUi from 'swagger-ui-express';\nimport { swaggerDocument } from './docs/swagger.js';\n`;
684
+ appMiddlewares += `
685
+ app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
686
+ `;
687
+ files['src/adapters/inbound/web/docs/swagger.ts'] = `const userSchema = {
688
+ type: "object",
689
+ properties: {
690
+ id: { type: "string" },
691
+ email: { type: "string" },
692
+ name: { type: "string", nullable: true },
693
+ role: { type: "string" },
694
+ createdAt: { type: "string", format: "date-time" }
695
+ }
696
+ };
697
+
698
+ const userInputSchema = {
699
+ type: "object",
700
+ properties: {
701
+ email: { type: "string" },
702
+ password: { type: "string" },
703
+ name: { type: "string" },
704
+ role: { type: "string", enum: ["user", "admin"] }
705
+ },
706
+ required: ["email"${options.auth ? ', "password"' : ''}]
707
+ };
708
+
709
+ export const swaggerDocument = {
710
+ openapi: "3.0.0",
711
+ info: { title: "${options.projectName} API (Hexagonal)", version: "1.0.0" },
712
+ servers: [{ url: "http://localhost:3000/api" }],
713
+ components: {
714
+ securitySchemes: {
715
+ bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" }
716
+ },
717
+ schemas: {
718
+ User: userSchema,
719
+ UserInput: userInputSchema
720
+ }
721
+ },
722
+ paths: {
723
+ ${options.auth ? `"/users/register": {
724
+ post: {
725
+ summary: "Register",
726
+ requestBody: {
727
+ required: true,
728
+ content: { "application/json": { schema: userInputSchema } }
729
+ },
730
+ responses: { "201": { description: "Created" } }
731
+ }
732
+ },
733
+ "/users/login": {
734
+ post: {
735
+ summary: "Login",
736
+ requestBody: {
737
+ required: true,
738
+ content: { "application/json": { schema: { type: "object", properties: { email: { type: "string" }, password: { type: "string" } }, required: ["email", "password"] } } }
739
+ },
740
+ responses: { "200": { description: "Authenticated" } }
741
+ }
742
+ },
743
+ "/users/refresh": {
744
+ post: {
745
+ summary: "Refresh token",
746
+ requestBody: {
747
+ required: true,
748
+ content: { "application/json": { schema: { type: "object", properties: { refreshToken: { type: "string" } }, required: ["refreshToken"] } } }
749
+ },
750
+ responses: { "200": { description: "Token refreshed" } }
751
+ }
752
+ },` : ''}
753
+ "/users": {
754
+ get: {
755
+ summary: "List users",
756
+ ${options.auth ? 'security: [{ bearerAuth: [] }],' : ''}
757
+ responses: { "200": { description: "Users listed" } }
758
+ },
759
+ post: {
760
+ summary: "Create user",
761
+ ${options.auth ? 'description: "For authenticated scaffolds, use /users/register for signup semantics.",' : ''}
762
+ requestBody: {
763
+ required: true,
764
+ content: { "application/json": { schema: userInputSchema } }
765
+ },
766
+ responses: { "201": { description: "User created" } }
767
+ }
768
+ },
769
+ "/users/{id}": {
770
+ get: {
771
+ summary: "Get user by id",
772
+ ${options.auth ? 'security: [{ bearerAuth: [] }],' : ''}
773
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
774
+ responses: { "200": { description: "User found" }, "404": { description: "User not found" } }
775
+ },
776
+ put: {
777
+ summary: "Update user",
778
+ ${options.auth ? 'security: [{ bearerAuth: [] }],' : ''}
779
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
780
+ requestBody: { required: true, content: { "application/json": { schema: userInputSchema } } },
781
+ responses: { "200": { description: "User updated" } }
782
+ },
783
+ delete: {
784
+ summary: "Delete user",
785
+ ${options.auth ? 'security: [{ bearerAuth: [] }],' : ''}
786
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
787
+ responses: { "204": { description: "User deleted" } }
788
+ }
789
+ }
790
+ }
791
+ };
792
+ `;
793
+ }
794
+ files['src/adapters/inbound/web/express.server.ts'] = `import express from 'express';
795
+ import cors from 'cors';
796
+ import helmet from 'helmet';
797
+ import routes from './routes/index.js';
798
+ import { errorHandler } from './middlewares/error.middleware.js';
799
+ ${appMiddlewaresImport}
800
+ const app = express();
801
+
802
+ app.use(helmet());
803
+ app.use(cors());
804
+ app.use(express.json());
805
+
806
+ ${appMiddlewares}
807
+ app.use('/api', routes);
808
+ app.use(errorHandler);
809
+
810
+ export default app;
811
+ `;
812
+ let dbConnectionCode = '';
813
+ if (options.orm === 'mongoose') {
814
+ dbConnectionCode = `import mongoose from 'mongoose';
815
+ async function connectDB() {
816
+ await mongoose.connect(env.DATABASE_URL);
817
+ console.log('🔌 Connected to MongoDB database successfully.');
818
+ }`;
819
+ }
820
+ else if (options.orm === 'typeorm') {
821
+ dbConnectionCode = `import { AppDataSource } from './config/data-source.js';
822
+ async function connectDB() {
823
+ await AppDataSource.initialize();
824
+ console.log('🔌 Connected to Database via TypeORM datasource.');
825
+ }`;
826
+ }
827
+ else {
828
+ dbConnectionCode = `import { PrismaClient } from '@prisma/client';
829
+ const prisma = new PrismaClient();
830
+ async function connectDB() {
831
+ await prisma.$connect();
832
+ console.log('🔌 Connected to Database via Prisma client.');
833
+ }`;
834
+ }
835
+ files['src/server.ts'] = `import app from './adapters/inbound/web/express.server.js';
836
+ import { env } from './config/env.js';
837
+ ${dbConnectionCode}
838
+
839
+ const PORT = env.PORT || 3000;
840
+
841
+ async function start() {
842
+ try {
843
+ await connectDB();
844
+ app.listen(PORT, () => {
845
+ console.log(\`🚀 Server running on port \${PORT} (Hexagonal Ports & Adapters)\`);
846
+ });
847
+ } catch (err) {
848
+ console.error('❌ Failed to start application:', err);
849
+ process.exit(1);
850
+ }
851
+ }
852
+ start();
853
+ `;
854
+ return files;
855
+ }