servcraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,192 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { logger } from '../../core/logger.js';
3
+ import type { AuditLogEntry, AuditLogQuery } from './types.js';
4
+ import type { PaginatedResult } from '../../types/index.js';
5
+ import { createPaginatedResult } from '../../utils/pagination.js';
6
+
7
+ // In-memory storage (will be replaced by Prisma in production)
8
+ const auditLogs: Map<string, AuditLogEntry & { id: string; createdAt: Date }> = new Map();
9
+
10
+ export class AuditService {
11
+ async log(entry: AuditLogEntry): Promise<void> {
12
+ const id = randomUUID();
13
+ const auditEntry = {
14
+ ...entry,
15
+ id,
16
+ createdAt: new Date(),
17
+ };
18
+
19
+ auditLogs.set(id, auditEntry);
20
+
21
+ // Also log to structured logger
22
+ logger.info(
23
+ {
24
+ audit: true,
25
+ userId: entry.userId,
26
+ action: entry.action,
27
+ resource: entry.resource,
28
+ resourceId: entry.resourceId,
29
+ ipAddress: entry.ipAddress,
30
+ },
31
+ `Audit: ${entry.action} on ${entry.resource}`
32
+ );
33
+ }
34
+
35
+ async query(
36
+ params: AuditLogQuery
37
+ ): Promise<PaginatedResult<AuditLogEntry & { id: string; createdAt: Date }>> {
38
+ const { page = 1, limit = 20 } = params;
39
+ let logs = Array.from(auditLogs.values());
40
+
41
+ // Apply filters
42
+ if (params.userId) {
43
+ logs = logs.filter((log) => log.userId === params.userId);
44
+ }
45
+ if (params.action) {
46
+ logs = logs.filter((log) => log.action === params.action);
47
+ }
48
+ if (params.resource) {
49
+ logs = logs.filter((log) => log.resource === params.resource);
50
+ }
51
+ if (params.resourceId) {
52
+ logs = logs.filter((log) => log.resourceId === params.resourceId);
53
+ }
54
+ if (params.startDate) {
55
+ logs = logs.filter((log) => log.createdAt >= params.startDate!);
56
+ }
57
+ if (params.endDate) {
58
+ logs = logs.filter((log) => log.createdAt <= params.endDate!);
59
+ }
60
+
61
+ // Sort by date descending
62
+ logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
63
+
64
+ const total = logs.length;
65
+ const skip = (page - 1) * limit;
66
+ const data = logs.slice(skip, skip + limit);
67
+
68
+ return createPaginatedResult(data, total, { page, limit });
69
+ }
70
+
71
+ async findByUser(userId: string, limit = 50): Promise<(AuditLogEntry & { id: string; createdAt: Date })[]> {
72
+ const result = await this.query({ userId, limit });
73
+ return result.data;
74
+ }
75
+
76
+ async findByResource(
77
+ resource: string,
78
+ resourceId: string,
79
+ limit = 50
80
+ ): Promise<(AuditLogEntry & { id: string; createdAt: Date })[]> {
81
+ const result = await this.query({ resource, resourceId, limit });
82
+ return result.data;
83
+ }
84
+
85
+ // Shortcut methods for common audit events
86
+ async logCreate(
87
+ resource: string,
88
+ resourceId: string,
89
+ userId?: string,
90
+ newValue?: Record<string, unknown>,
91
+ meta?: { ipAddress?: string; userAgent?: string }
92
+ ): Promise<void> {
93
+ await this.log({
94
+ action: 'create',
95
+ resource,
96
+ resourceId,
97
+ userId,
98
+ newValue,
99
+ ...meta,
100
+ });
101
+ }
102
+
103
+ async logUpdate(
104
+ resource: string,
105
+ resourceId: string,
106
+ userId?: string,
107
+ oldValue?: Record<string, unknown>,
108
+ newValue?: Record<string, unknown>,
109
+ meta?: { ipAddress?: string; userAgent?: string }
110
+ ): Promise<void> {
111
+ await this.log({
112
+ action: 'update',
113
+ resource,
114
+ resourceId,
115
+ userId,
116
+ oldValue,
117
+ newValue,
118
+ ...meta,
119
+ });
120
+ }
121
+
122
+ async logDelete(
123
+ resource: string,
124
+ resourceId: string,
125
+ userId?: string,
126
+ oldValue?: Record<string, unknown>,
127
+ meta?: { ipAddress?: string; userAgent?: string }
128
+ ): Promise<void> {
129
+ await this.log({
130
+ action: 'delete',
131
+ resource,
132
+ resourceId,
133
+ userId,
134
+ oldValue,
135
+ ...meta,
136
+ });
137
+ }
138
+
139
+ async logLogin(
140
+ userId: string,
141
+ meta?: { ipAddress?: string; userAgent?: string }
142
+ ): Promise<void> {
143
+ await this.log({
144
+ action: 'login',
145
+ resource: 'auth',
146
+ userId,
147
+ ...meta,
148
+ });
149
+ }
150
+
151
+ async logLogout(
152
+ userId: string,
153
+ meta?: { ipAddress?: string; userAgent?: string }
154
+ ): Promise<void> {
155
+ await this.log({
156
+ action: 'logout',
157
+ resource: 'auth',
158
+ userId,
159
+ ...meta,
160
+ });
161
+ }
162
+
163
+ async logPasswordChange(
164
+ userId: string,
165
+ meta?: { ipAddress?: string; userAgent?: string }
166
+ ): Promise<void> {
167
+ await this.log({
168
+ action: 'password_change',
169
+ resource: 'auth',
170
+ userId,
171
+ ...meta,
172
+ });
173
+ }
174
+
175
+ // Clear all logs (for testing)
176
+ async clear(): Promise<void> {
177
+ auditLogs.clear();
178
+ }
179
+ }
180
+
181
+ let auditService: AuditService | null = null;
182
+
183
+ export function getAuditService(): AuditService {
184
+ if (!auditService) {
185
+ auditService = new AuditService();
186
+ }
187
+ return auditService;
188
+ }
189
+
190
+ export function createAuditService(): AuditService {
191
+ return new AuditService();
192
+ }
@@ -0,0 +1,2 @@
1
+ export { AuditService, getAuditService, createAuditService } from './audit.service.js';
2
+ export type { AuditLogEntry, AuditLogQuery, AuditAction } from './types.js';
@@ -0,0 +1,37 @@
1
+ export type AuditAction =
2
+ | 'create'
3
+ | 'read'
4
+ | 'update'
5
+ | 'delete'
6
+ | 'login'
7
+ | 'logout'
8
+ | 'register'
9
+ | 'password_change'
10
+ | 'password_reset'
11
+ | 'email_verify'
12
+ | 'role_change'
13
+ | 'status_change'
14
+ | 'settings_change';
15
+
16
+ export interface AuditLogEntry {
17
+ userId?: string;
18
+ action: AuditAction | string;
19
+ resource: string;
20
+ resourceId?: string;
21
+ oldValue?: Record<string, unknown>;
22
+ newValue?: Record<string, unknown>;
23
+ ipAddress?: string;
24
+ userAgent?: string;
25
+ metadata?: Record<string, unknown>;
26
+ }
27
+
28
+ export interface AuditLogQuery {
29
+ userId?: string;
30
+ action?: string;
31
+ resource?: string;
32
+ resourceId?: string;
33
+ startDate?: Date;
34
+ endDate?: Date;
35
+ page?: number;
36
+ limit?: number;
37
+ }
@@ -0,0 +1,182 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+ import type { AuthService } from './auth.service.js';
3
+ import type { UserService } from '../user/user.service.js';
4
+ import {
5
+ loginSchema,
6
+ registerSchema,
7
+ refreshTokenSchema,
8
+ changePasswordSchema,
9
+ } from './schemas.js';
10
+ import { success, created } from '../../utils/response.js';
11
+ import { BadRequestError, UnauthorizedError } from '../../utils/errors.js';
12
+ import { validateBody } from '../validation/validator.js';
13
+ import type { AuthenticatedRequest } from './types.js';
14
+
15
+ export class AuthController {
16
+ constructor(
17
+ private authService: AuthService,
18
+ private userService: UserService
19
+ ) {}
20
+
21
+ async register(request: FastifyRequest, reply: FastifyReply): Promise<void> {
22
+ const data = validateBody(registerSchema, request.body);
23
+
24
+ // Check if user already exists
25
+ const existingUser = await this.userService.findByEmail(data.email);
26
+ if (existingUser) {
27
+ throw new BadRequestError('Email already registered');
28
+ }
29
+
30
+ // Hash password and create user
31
+ const hashedPassword = await this.authService.hashPassword(data.password);
32
+ const user = await this.userService.create({
33
+ email: data.email,
34
+ password: hashedPassword,
35
+ name: data.name,
36
+ });
37
+
38
+ // Generate tokens
39
+ const tokens = this.authService.generateTokenPair({
40
+ id: user.id,
41
+ email: user.email,
42
+ role: user.role,
43
+ });
44
+
45
+ created(reply, {
46
+ user: {
47
+ id: user.id,
48
+ email: user.email,
49
+ name: user.name,
50
+ role: user.role,
51
+ },
52
+ ...tokens,
53
+ });
54
+ }
55
+
56
+ async login(request: FastifyRequest, reply: FastifyReply): Promise<void> {
57
+ const data = validateBody(loginSchema, request.body);
58
+
59
+ // Find user
60
+ const user = await this.userService.findByEmail(data.email);
61
+ if (!user) {
62
+ throw new UnauthorizedError('Invalid credentials');
63
+ }
64
+
65
+ // Check if user is active
66
+ if (user.status !== 'active') {
67
+ throw new UnauthorizedError('Account is not active');
68
+ }
69
+
70
+ // Verify password
71
+ const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
72
+ if (!isValidPassword) {
73
+ throw new UnauthorizedError('Invalid credentials');
74
+ }
75
+
76
+ // Update last login
77
+ await this.userService.updateLastLogin(user.id);
78
+
79
+ // Generate tokens
80
+ const tokens = this.authService.generateTokenPair({
81
+ id: user.id,
82
+ email: user.email,
83
+ role: user.role,
84
+ });
85
+
86
+ success(reply, {
87
+ user: {
88
+ id: user.id,
89
+ email: user.email,
90
+ name: user.name,
91
+ role: user.role,
92
+ },
93
+ ...tokens,
94
+ });
95
+ }
96
+
97
+ async refresh(request: FastifyRequest, reply: FastifyReply): Promise<void> {
98
+ const data = validateBody(refreshTokenSchema, request.body);
99
+
100
+ // Verify refresh token
101
+ const payload = await this.authService.verifyRefreshToken(data.refreshToken);
102
+
103
+ // Get fresh user data
104
+ const user = await this.userService.findById(payload.sub);
105
+ if (!user || user.status !== 'active') {
106
+ throw new UnauthorizedError('User not found or inactive');
107
+ }
108
+
109
+ // Blacklist old refresh token (token rotation)
110
+ this.authService.blacklistToken(data.refreshToken);
111
+
112
+ // Generate new tokens
113
+ const tokens = this.authService.generateTokenPair({
114
+ id: user.id,
115
+ email: user.email,
116
+ role: user.role,
117
+ });
118
+
119
+ success(reply, tokens);
120
+ }
121
+
122
+ async logout(request: FastifyRequest, reply: FastifyReply): Promise<void> {
123
+ const authHeader = request.headers.authorization;
124
+ if (authHeader?.startsWith('Bearer ')) {
125
+ const token = authHeader.substring(7);
126
+ this.authService.blacklistToken(token);
127
+ }
128
+
129
+ success(reply, { message: 'Logged out successfully' });
130
+ }
131
+
132
+ async me(request: FastifyRequest, reply: FastifyReply): Promise<void> {
133
+ const authRequest = request as AuthenticatedRequest;
134
+ const user = await this.userService.findById(authRequest.user.id);
135
+
136
+ if (!user) {
137
+ throw new UnauthorizedError('User not found');
138
+ }
139
+
140
+ success(reply, {
141
+ id: user.id,
142
+ email: user.email,
143
+ name: user.name,
144
+ role: user.role,
145
+ status: user.status,
146
+ createdAt: user.createdAt,
147
+ });
148
+ }
149
+
150
+ async changePassword(request: FastifyRequest, reply: FastifyReply): Promise<void> {
151
+ const authRequest = request as AuthenticatedRequest;
152
+ const data = validateBody(changePasswordSchema, request.body);
153
+
154
+ // Get current user
155
+ const user = await this.userService.findById(authRequest.user.id);
156
+ if (!user) {
157
+ throw new UnauthorizedError('User not found');
158
+ }
159
+
160
+ // Verify current password
161
+ const isValidPassword = await this.authService.verifyPassword(
162
+ data.currentPassword,
163
+ user.password
164
+ );
165
+ if (!isValidPassword) {
166
+ throw new BadRequestError('Current password is incorrect');
167
+ }
168
+
169
+ // Hash and update password
170
+ const hashedPassword = await this.authService.hashPassword(data.newPassword);
171
+ await this.userService.updatePassword(user.id, hashedPassword);
172
+
173
+ success(reply, { message: 'Password changed successfully' });
174
+ }
175
+ }
176
+
177
+ export function createAuthController(
178
+ authService: AuthService,
179
+ userService: UserService
180
+ ): AuthController {
181
+ return new AuthController(authService, userService);
182
+ }
@@ -0,0 +1,87 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+ import { UnauthorizedError, ForbiddenError } from '../../utils/errors.js';
3
+ import type { AuthService } from './auth.service.js';
4
+ import type { AuthUser } from './types.js';
5
+
6
+ export function createAuthMiddleware(authService: AuthService) {
7
+ return async function authenticate(
8
+ request: FastifyRequest,
9
+ reply: FastifyReply
10
+ ): Promise<void> {
11
+ const authHeader = request.headers.authorization;
12
+
13
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
14
+ throw new UnauthorizedError('Missing or invalid authorization header');
15
+ }
16
+
17
+ const token = authHeader.substring(7);
18
+ const payload = await authService.verifyAccessToken(token);
19
+
20
+ request.user = {
21
+ id: payload.sub,
22
+ email: payload.email,
23
+ role: payload.role,
24
+ };
25
+ };
26
+ }
27
+
28
+ export function createRoleMiddleware(allowedRoles: string[]) {
29
+ return async function authorize(
30
+ request: FastifyRequest,
31
+ _reply: FastifyReply
32
+ ): Promise<void> {
33
+ const user = request.user as AuthUser | undefined;
34
+
35
+ if (!user) {
36
+ throw new UnauthorizedError('Authentication required');
37
+ }
38
+
39
+ if (!allowedRoles.includes(user.role)) {
40
+ throw new ForbiddenError('Insufficient permissions');
41
+ }
42
+ };
43
+ }
44
+
45
+ export function createPermissionMiddleware(requiredPermissions: string[]) {
46
+ return async function checkPermissions(
47
+ request: FastifyRequest,
48
+ _reply: FastifyReply
49
+ ): Promise<void> {
50
+ const user = request.user as AuthUser | undefined;
51
+
52
+ if (!user) {
53
+ throw new UnauthorizedError('Authentication required');
54
+ }
55
+
56
+ // This would check against a permissions system
57
+ // For now, we'll implement a basic role-based check
58
+ // In a full implementation, you'd query the user's permissions from the database
59
+ };
60
+ }
61
+
62
+ // Optional authentication - doesn't throw if no token
63
+ export function createOptionalAuthMiddleware(authService: AuthService) {
64
+ return async function optionalAuthenticate(
65
+ request: FastifyRequest,
66
+ _reply: FastifyReply
67
+ ): Promise<void> {
68
+ const authHeader = request.headers.authorization;
69
+
70
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
71
+ return;
72
+ }
73
+
74
+ try {
75
+ const token = authHeader.substring(7);
76
+ const payload = await authService.verifyAccessToken(token);
77
+
78
+ request.user = {
79
+ id: payload.sub,
80
+ email: payload.email,
81
+ role: payload.role,
82
+ };
83
+ } catch {
84
+ // Silently ignore auth errors for optional auth
85
+ }
86
+ };
87
+ }
@@ -0,0 +1,123 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AuthController } from './auth.controller.js';
3
+ import type { AuthService } from './auth.service.js';
4
+ import { createAuthMiddleware } from './auth.middleware.js';
5
+ import { commonResponses } from '../swagger/index.js';
6
+
7
+ const credentialsBody = {
8
+ type: 'object',
9
+ required: ['email', 'password'],
10
+ properties: {
11
+ email: { type: 'string', format: 'email' },
12
+ password: { type: 'string', minLength: 8 },
13
+ },
14
+ };
15
+
16
+ const changePasswordBody = {
17
+ type: 'object',
18
+ required: ['currentPassword', 'newPassword'],
19
+ properties: {
20
+ currentPassword: { type: 'string', minLength: 8 },
21
+ newPassword: { type: 'string', minLength: 8 },
22
+ },
23
+ };
24
+
25
+ export function registerAuthRoutes(
26
+ app: FastifyInstance,
27
+ controller: AuthController,
28
+ authService: AuthService
29
+ ): void {
30
+ const authenticate = createAuthMiddleware(authService);
31
+
32
+ // Public routes
33
+ app.post('/auth/register', {
34
+ schema: {
35
+ tags: ['Auth'],
36
+ summary: 'Register a new user',
37
+ body: credentialsBody,
38
+ response: {
39
+ 201: commonResponses.success,
40
+ 400: commonResponses.error,
41
+ 409: commonResponses.error,
42
+ },
43
+ },
44
+ handler: controller.register.bind(controller),
45
+ });
46
+
47
+ app.post('/auth/login', {
48
+ schema: {
49
+ tags: ['Auth'],
50
+ summary: 'Login and obtain tokens',
51
+ body: credentialsBody,
52
+ response: {
53
+ 200: commonResponses.success,
54
+ 400: commonResponses.error,
55
+ 401: commonResponses.unauthorized,
56
+ },
57
+ },
58
+ handler: controller.login.bind(controller),
59
+ });
60
+
61
+ app.post('/auth/refresh', {
62
+ schema: {
63
+ tags: ['Auth'],
64
+ summary: 'Refresh access token',
65
+ body: {
66
+ type: 'object',
67
+ required: ['refreshToken'],
68
+ properties: {
69
+ refreshToken: { type: 'string' },
70
+ },
71
+ },
72
+ response: {
73
+ 200: commonResponses.success,
74
+ 401: commonResponses.unauthorized,
75
+ },
76
+ },
77
+ handler: controller.refresh.bind(controller),
78
+ });
79
+
80
+ // Protected routes
81
+ app.post('/auth/logout', {
82
+ preHandler: [authenticate],
83
+ schema: {
84
+ tags: ['Auth'],
85
+ summary: 'Logout current user',
86
+ security: [{ bearerAuth: [] }],
87
+ response: {
88
+ 200: commonResponses.success,
89
+ 401: commonResponses.unauthorized,
90
+ },
91
+ },
92
+ handler: controller.logout.bind(controller),
93
+ });
94
+ app.get('/auth/me', {
95
+ preHandler: [authenticate],
96
+ schema: {
97
+ tags: ['Auth'],
98
+ summary: 'Get current user profile',
99
+ security: [{ bearerAuth: [] }],
100
+ response: {
101
+ 200: commonResponses.success,
102
+ 401: commonResponses.unauthorized,
103
+ },
104
+ },
105
+ handler: controller.me.bind(controller),
106
+ });
107
+
108
+ app.post('/auth/change-password', {
109
+ preHandler: [authenticate],
110
+ schema: {
111
+ tags: ['Auth'],
112
+ summary: 'Change current user password',
113
+ security: [{ bearerAuth: [] }],
114
+ body: changePasswordBody,
115
+ response: {
116
+ 200: commonResponses.success,
117
+ 400: commonResponses.error,
118
+ 401: commonResponses.unauthorized,
119
+ },
120
+ },
121
+ handler: controller.changePassword.bind(controller),
122
+ });
123
+ }