servcraft 0.1.0 → 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.
Files changed (216) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -1,153 +1,277 @@
1
- import { randomUUID } from 'crypto';
1
+ import { prisma } from '../../database/prisma.js';
2
2
  import type { PaginatedResult, PaginationParams } from '../../types/index.js';
3
3
  import { createPaginatedResult, getSkip } from '../../utils/pagination.js';
4
4
  import type { User, CreateUserData, UpdateUserData, UserFilters } from './types.js';
5
+ import { UserRole, UserStatus } from '@prisma/client';
5
6
 
6
- // In-memory storage for development (will be replaced by Prisma)
7
- const users = new Map<string, User>();
8
-
7
+ /**
8
+ * User Repository - Prisma Implementation
9
+ * Manages user data persistence using Prisma ORM
10
+ */
9
11
  export class UserRepository {
12
+ /**
13
+ * Find user by ID
14
+ */
10
15
  async findById(id: string): Promise<User | null> {
11
- return users.get(id) || null;
16
+ const user = await prisma.user.findUnique({
17
+ where: { id },
18
+ });
19
+
20
+ if (!user) return null;
21
+ return this.mapPrismaUserToUser(user);
12
22
  }
13
23
 
24
+ /**
25
+ * Find user by email (case-insensitive)
26
+ */
14
27
  async findByEmail(email: string): Promise<User | null> {
15
- for (const user of users.values()) {
16
- if (user.email.toLowerCase() === email.toLowerCase()) {
17
- return user;
18
- }
19
- }
20
- return null;
21
- }
22
-
23
- async findMany(
24
- params: PaginationParams,
25
- filters?: UserFilters
26
- ): Promise<PaginatedResult<User>> {
27
- let filteredUsers = Array.from(users.values());
28
-
29
- // Apply filters
30
- if (filters) {
31
- if (filters.status) {
32
- filteredUsers = filteredUsers.filter((u) => u.status === filters.status);
33
- }
34
- if (filters.role) {
35
- filteredUsers = filteredUsers.filter((u) => u.role === filters.role);
36
- }
37
- if (filters.emailVerified !== undefined) {
38
- filteredUsers = filteredUsers.filter((u) => u.emailVerified === filters.emailVerified);
39
- }
40
- if (filters.search) {
41
- const search = filters.search.toLowerCase();
42
- filteredUsers = filteredUsers.filter(
43
- (u) =>
44
- u.email.toLowerCase().includes(search) ||
45
- u.name?.toLowerCase().includes(search)
46
- );
47
- }
48
- }
28
+ const user = await prisma.user.findUnique({
29
+ where: { email: email.toLowerCase() },
30
+ });
49
31
 
50
- // Sort
51
- if (params.sortBy) {
52
- const sortKey = params.sortBy as keyof User;
53
- filteredUsers.sort((a, b) => {
54
- const aVal = a[sortKey];
55
- const bVal = b[sortKey];
56
- if (aVal === undefined || bVal === undefined) return 0;
57
- if (aVal < bVal) return params.sortOrder === 'desc' ? 1 : -1;
58
- if (aVal > bVal) return params.sortOrder === 'desc' ? -1 : 1;
59
- return 0;
60
- });
61
- }
32
+ if (!user) return null;
33
+ return this.mapPrismaUserToUser(user);
34
+ }
35
+
36
+ /**
37
+ * Find multiple users with pagination and filters
38
+ */
39
+ async findMany(params: PaginationParams, filters?: UserFilters): Promise<PaginatedResult<User>> {
40
+ const where = this.buildWhereClause(filters);
41
+ const orderBy = this.buildOrderBy(params);
42
+
43
+ const [data, total] = await Promise.all([
44
+ prisma.user.findMany({
45
+ where,
46
+ orderBy,
47
+ skip: getSkip(params),
48
+ take: params.limit,
49
+ }),
50
+ prisma.user.count({ where }),
51
+ ]);
62
52
 
63
- const total = filteredUsers.length;
64
- const skip = getSkip(params);
65
- const data = filteredUsers.slice(skip, skip + params.limit);
53
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
66
54
 
67
- return createPaginatedResult(data, total, params);
55
+ return createPaginatedResult(mappedUsers, total, params);
68
56
  }
69
57
 
58
+ /**
59
+ * Create a new user
60
+ */
70
61
  async create(data: CreateUserData): Promise<User> {
71
- const now = new Date();
72
- const user: User = {
73
- id: randomUUID(),
74
- email: data.email,
75
- password: data.password,
76
- name: data.name,
77
- role: data.role || 'user',
78
- status: 'active',
79
- emailVerified: false,
80
- createdAt: now,
81
- updatedAt: now,
82
- };
62
+ const user = await prisma.user.create({
63
+ data: {
64
+ email: data.email.toLowerCase(),
65
+ password: data.password,
66
+ name: data.name,
67
+ role: this.mapRoleToEnum(data.role || 'user'),
68
+ status: UserStatus.ACTIVE,
69
+ emailVerified: false,
70
+ },
71
+ });
83
72
 
84
- users.set(user.id, user);
85
- return user;
73
+ return this.mapPrismaUserToUser(user);
86
74
  }
87
75
 
76
+ /**
77
+ * Update user data
78
+ */
88
79
  async update(id: string, data: UpdateUserData): Promise<User | null> {
89
- const user = users.get(id);
90
- if (!user) return null;
91
-
92
- const updatedUser: User = {
93
- ...user,
94
- ...data,
95
- updatedAt: new Date(),
96
- };
80
+ try {
81
+ const user = await prisma.user.update({
82
+ where: { id },
83
+ data: {
84
+ ...(data.email && { email: data.email.toLowerCase() }),
85
+ ...(data.name !== undefined && { name: data.name }),
86
+ ...(data.role && { role: this.mapRoleToEnum(data.role) }),
87
+ ...(data.status && { status: this.mapStatusToEnum(data.status) }),
88
+ ...(data.emailVerified !== undefined && { emailVerified: data.emailVerified }),
89
+ ...(data.metadata && { metadata: data.metadata as object }),
90
+ },
91
+ });
97
92
 
98
- users.set(id, updatedUser);
99
- return updatedUser;
93
+ return this.mapPrismaUserToUser(user);
94
+ } catch {
95
+ // User not found
96
+ return null;
97
+ }
100
98
  }
101
99
 
100
+ /**
101
+ * Update user password
102
+ */
102
103
  async updatePassword(id: string, password: string): Promise<User | null> {
103
- const user = users.get(id);
104
- if (!user) return null;
105
-
106
- const updatedUser: User = {
107
- ...user,
108
- password,
109
- updatedAt: new Date(),
110
- };
104
+ try {
105
+ const user = await prisma.user.update({
106
+ where: { id },
107
+ data: { password },
108
+ });
111
109
 
112
- users.set(id, updatedUser);
113
- return updatedUser;
110
+ return this.mapPrismaUserToUser(user);
111
+ } catch {
112
+ return null;
113
+ }
114
114
  }
115
115
 
116
+ /**
117
+ * Update last login timestamp
118
+ */
116
119
  async updateLastLogin(id: string): Promise<User | null> {
117
- const user = users.get(id);
118
- if (!user) return null;
119
-
120
- const updatedUser: User = {
121
- ...user,
122
- lastLoginAt: new Date(),
123
- updatedAt: new Date(),
124
- };
120
+ try {
121
+ const user = await prisma.user.update({
122
+ where: { id },
123
+ data: { lastLoginAt: new Date() },
124
+ });
125
125
 
126
- users.set(id, updatedUser);
127
- return updatedUser;
126
+ return this.mapPrismaUserToUser(user);
127
+ } catch {
128
+ return null;
129
+ }
128
130
  }
129
131
 
132
+ /**
133
+ * Delete user by ID
134
+ */
130
135
  async delete(id: string): Promise<boolean> {
131
- return users.delete(id);
136
+ try {
137
+ await prisma.user.delete({
138
+ where: { id },
139
+ });
140
+ return true;
141
+ } catch {
142
+ return false;
143
+ }
132
144
  }
133
145
 
146
+ /**
147
+ * Count users with optional filters
148
+ */
134
149
  async count(filters?: UserFilters): Promise<number> {
135
- let count = 0;
136
- for (const user of users.values()) {
137
- if (filters) {
138
- if (filters.status && user.status !== filters.status) continue;
139
- if (filters.role && user.role !== filters.role) continue;
140
- if (filters.emailVerified !== undefined && user.emailVerified !== filters.emailVerified)
141
- continue;
142
- }
143
- count++;
144
- }
145
- return count;
150
+ const where = this.buildWhereClause(filters);
151
+ return prisma.user.count({ where });
146
152
  }
147
153
 
148
- // Helper to clear all users (for testing)
154
+ /**
155
+ * Helper to clear all users (for testing only)
156
+ * WARNING: This deletes all users from the database
157
+ */
149
158
  async clear(): Promise<void> {
150
- users.clear();
159
+ await prisma.user.deleteMany();
160
+ }
161
+
162
+ /**
163
+ * Build Prisma where clause from filters
164
+ */
165
+ private buildWhereClause(filters?: UserFilters): object {
166
+ if (!filters) return {};
167
+
168
+ return {
169
+ ...(filters.status && { status: this.mapStatusToEnum(filters.status) }),
170
+ ...(filters.role && { role: this.mapRoleToEnum(filters.role) }),
171
+ ...(filters.emailVerified !== undefined && { emailVerified: filters.emailVerified }),
172
+ ...(filters.search && {
173
+ OR: [
174
+ { email: { contains: filters.search, mode: 'insensitive' as const } },
175
+ { name: { contains: filters.search, mode: 'insensitive' as const } },
176
+ ],
177
+ }),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Build Prisma orderBy clause from pagination params
183
+ */
184
+ private buildOrderBy(params: PaginationParams): object {
185
+ if (!params.sortBy) {
186
+ return { createdAt: 'desc' as const };
187
+ }
188
+
189
+ return {
190
+ [params.sortBy]: params.sortOrder || 'asc',
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Map Prisma User to application User type
196
+ */
197
+ private mapPrismaUserToUser(prismaUser: {
198
+ id: string;
199
+ email: string;
200
+ password: string;
201
+ name: string | null;
202
+ role: UserRole;
203
+ status: UserStatus;
204
+ emailVerified: boolean;
205
+ lastLoginAt: Date | null;
206
+ metadata: unknown;
207
+ createdAt: Date;
208
+ updatedAt: Date;
209
+ }): User {
210
+ return {
211
+ id: prismaUser.id,
212
+ email: prismaUser.email,
213
+ password: prismaUser.password,
214
+ name: prismaUser.name ?? undefined,
215
+ role: this.mapEnumToRole(prismaUser.role),
216
+ status: this.mapEnumToStatus(prismaUser.status),
217
+ emailVerified: prismaUser.emailVerified,
218
+ lastLoginAt: prismaUser.lastLoginAt ?? undefined,
219
+ metadata: prismaUser.metadata as Record<string, unknown> | undefined,
220
+ createdAt: prismaUser.createdAt,
221
+ updatedAt: prismaUser.updatedAt,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Map application role to Prisma enum
227
+ */
228
+ private mapRoleToEnum(role: string): UserRole {
229
+ const roleMap: Record<string, UserRole> = {
230
+ user: UserRole.USER,
231
+ moderator: UserRole.MODERATOR,
232
+ admin: UserRole.ADMIN,
233
+ super_admin: UserRole.SUPER_ADMIN,
234
+ };
235
+ return roleMap[role] || UserRole.USER;
236
+ }
237
+
238
+ /**
239
+ * Map Prisma enum to application role
240
+ */
241
+ private mapEnumToRole(role: UserRole): User['role'] {
242
+ const roleMap: Record<UserRole, User['role']> = {
243
+ [UserRole.USER]: 'user',
244
+ [UserRole.MODERATOR]: 'moderator',
245
+ [UserRole.ADMIN]: 'admin',
246
+ [UserRole.SUPER_ADMIN]: 'super_admin',
247
+ };
248
+ return roleMap[role];
249
+ }
250
+
251
+ /**
252
+ * Map application status to Prisma enum
253
+ */
254
+ private mapStatusToEnum(status: string): UserStatus {
255
+ const statusMap: Record<string, UserStatus> = {
256
+ active: UserStatus.ACTIVE,
257
+ inactive: UserStatus.INACTIVE,
258
+ suspended: UserStatus.SUSPENDED,
259
+ banned: UserStatus.BANNED,
260
+ };
261
+ return statusMap[status] || UserStatus.ACTIVE;
262
+ }
263
+
264
+ /**
265
+ * Map Prisma enum to application status
266
+ */
267
+ private mapEnumToStatus(status: UserStatus): User['status'] {
268
+ const statusMap: Record<UserStatus, User['status']> = {
269
+ [UserStatus.ACTIVE]: 'active',
270
+ [UserStatus.INACTIVE]: 'inactive',
271
+ [UserStatus.SUSPENDED]: 'suspended',
272
+ [UserStatus.BANNED]: 'banned',
273
+ };
274
+ return statusMap[status];
151
275
  }
152
276
  }
153
277
 
@@ -1,17 +1,16 @@
1
- import type { FastifyInstance } from 'fastify';
1
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
2
  import type { UserController } from './user.controller.js';
3
3
  import type { AuthService } from '../auth/auth.service.js';
4
4
  import { createAuthMiddleware, createRoleMiddleware } from '../auth/auth.middleware.js';
5
- import { commonResponses, paginationQuery, idParam } from '../swagger/index.js';
6
5
 
7
- const userTag = 'Users';
8
- const userResponse = {
6
+ // Route params schema for Fastify
7
+ const idParamsSchema = {
9
8
  type: 'object',
10
9
  properties: {
11
- success: { type: 'boolean', example: true },
12
- data: { type: 'object' },
10
+ id: { type: 'string' },
13
11
  },
14
- };
12
+ required: ['id'],
13
+ } as const;
15
14
 
16
15
  export function registerUserRoutes(
17
16
  app: FastifyInstance,
@@ -23,177 +22,53 @@ export function registerUserRoutes(
23
22
  const isModerator = createRoleMiddleware(['moderator', 'admin', 'super_admin']);
24
23
 
25
24
  // Profile routes (authenticated users)
26
- app.get(
27
- '/profile',
28
- {
29
- preHandler: [authenticate],
30
- schema: {
31
- tags: [userTag],
32
- summary: 'Get current user profile',
33
- security: [{ bearerAuth: [] }],
34
- response: {
35
- 200: userResponse,
36
- 401: commonResponses.unauthorized,
37
- },
38
- },
39
- },
40
- controller.getProfile.bind(controller)
41
- );
42
- app.patch(
43
- '/profile',
44
- {
45
- preHandler: [authenticate],
46
- schema: {
47
- tags: [userTag],
48
- summary: 'Update current user profile',
49
- security: [{ bearerAuth: [] }],
50
- body: { type: 'object' },
51
- response: {
52
- 200: userResponse,
53
- 401: commonResponses.unauthorized,
54
- 400: commonResponses.error,
55
- },
56
- },
57
- },
58
- controller.updateProfile.bind(controller)
59
- );
25
+ app.get('/profile', { preHandler: [authenticate] }, controller.getProfile.bind(controller));
26
+ app.patch('/profile', { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
60
27
 
61
28
  // Admin routes
62
- app.get(
63
- '/users',
64
- {
65
- preHandler: [authenticate, isModerator],
66
- schema: {
67
- tags: [userTag],
68
- summary: 'List users',
69
- security: [{ bearerAuth: [] }],
70
- querystring: {
71
- ...paginationQuery,
72
- properties: {
73
- ...paginationQuery.properties,
74
- status: { type: 'string', enum: ['active', 'inactive', 'suspended', 'banned'] },
75
- role: { type: 'string', enum: ['user', 'admin', 'moderator', 'super_admin'] },
76
- search: { type: 'string' },
77
- emailVerified: { type: 'boolean' },
78
- },
79
- },
80
- response: {
81
- 200: commonResponses.paginated,
82
- 401: commonResponses.unauthorized,
83
- },
84
- },
85
- },
86
- controller.list.bind(controller)
87
- );
88
- app.get(
29
+ app.get('/users', { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
30
+ app.get<{ Params: { id: string } }>(
89
31
  '/users/:id',
90
- {
91
- preHandler: [authenticate, isModerator],
92
- schema: {
93
- tags: [userTag],
94
- summary: 'Get user by id',
95
- security: [{ bearerAuth: [] }],
96
- params: idParam,
97
- response: {
98
- 200: userResponse,
99
- 401: commonResponses.unauthorized,
100
- 404: commonResponses.notFound,
101
- },
102
- },
103
- },
104
- controller.getById.bind(controller)
32
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
33
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
34
+ return controller.getById(request, reply);
35
+ }
105
36
  );
106
- app.patch(
37
+ app.patch<{ Params: { id: string } }>(
107
38
  '/users/:id',
108
- {
109
- preHandler: [authenticate, isAdmin],
110
- schema: {
111
- tags: [userTag],
112
- summary: 'Update user',
113
- security: [{ bearerAuth: [] }],
114
- params: idParam,
115
- body: { type: 'object' },
116
- response: {
117
- 200: userResponse,
118
- 401: commonResponses.unauthorized,
119
- 404: commonResponses.notFound,
120
- },
121
- },
122
- },
123
- controller.update.bind(controller)
39
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
40
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
41
+ return controller.update(request, reply);
42
+ }
124
43
  );
125
- app.delete(
44
+ app.delete<{ Params: { id: string } }>(
126
45
  '/users/:id',
127
- {
128
- preHandler: [authenticate, isAdmin],
129
- schema: {
130
- tags: [userTag],
131
- summary: 'Delete user',
132
- security: [{ bearerAuth: [] }],
133
- params: idParam,
134
- response: {
135
- 204: { description: 'User deleted' },
136
- 401: commonResponses.unauthorized,
137
- 404: commonResponses.notFound,
138
- },
139
- },
140
- },
141
- controller.delete.bind(controller)
46
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
47
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
48
+ return controller.delete(request, reply);
49
+ }
142
50
  );
143
51
 
144
52
  // User status management
145
- app.post(
53
+ app.post<{ Params: { id: string } }>(
146
54
  '/users/:id/suspend',
147
- {
148
- preHandler: [authenticate, isAdmin],
149
- schema: {
150
- tags: [userTag],
151
- summary: 'Suspend user',
152
- security: [{ bearerAuth: [] }],
153
- params: idParam,
154
- response: {
155
- 200: userResponse,
156
- 401: commonResponses.unauthorized,
157
- 404: commonResponses.notFound,
158
- },
159
- },
160
- },
161
- controller.suspend.bind(controller)
55
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
56
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
57
+ return controller.suspend(request, reply);
58
+ }
162
59
  );
163
- app.post(
60
+ app.post<{ Params: { id: string } }>(
164
61
  '/users/:id/ban',
165
- {
166
- preHandler: [authenticate, isAdmin],
167
- schema: {
168
- tags: [userTag],
169
- summary: 'Ban user',
170
- security: [{ bearerAuth: [] }],
171
- params: idParam,
172
- response: {
173
- 200: userResponse,
174
- 401: commonResponses.unauthorized,
175
- 404: commonResponses.notFound,
176
- },
177
- },
178
- },
179
- controller.ban.bind(controller)
62
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
63
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
64
+ return controller.ban(request, reply);
65
+ }
180
66
  );
181
- app.post(
67
+ app.post<{ Params: { id: string } }>(
182
68
  '/users/:id/activate',
183
- {
184
- preHandler: [authenticate, isAdmin],
185
- schema: {
186
- tags: [userTag],
187
- summary: 'Activate user',
188
- security: [{ bearerAuth: [] }],
189
- params: idParam,
190
- response: {
191
- 200: userResponse,
192
- 401: commonResponses.unauthorized,
193
- 404: commonResponses.notFound,
194
- },
195
- },
196
- },
197
- controller.activate.bind(controller)
69
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
70
+ async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
71
+ return controller.activate(request, reply);
72
+ }
198
73
  );
199
74
  }
@@ -1,6 +1,7 @@
1
1
  import type { PaginatedResult, PaginationParams } from '../../types/index.js';
2
2
  import { NotFoundError, ConflictError } from '../../utils/errors.js';
3
- import { UserRepository, createUserRepository } from './user.repository.js';
3
+ import type { UserRepository } from './user.repository.js';
4
+ import { createUserRepository } from './user.repository.js';
4
5
  import type { User, CreateUserData, UpdateUserData, UserFilters, UserRole } from './types.js';
5
6
  import { DEFAULT_ROLE_PERMISSIONS } from './types.js';
6
7
  import { logger } from '../../core/logger.js';
@@ -25,7 +26,7 @@ export class UserService {
25
26
  // Remove passwords from results
26
27
  return {
27
28
  ...result,
28
- data: result.data.map(({ password, ...user }) => user) as Omit<User, 'password'>[],
29
+ data: result.data.map(({ password: _password, ...user }) => user) as Omit<User, 'password'>[],
29
30
  };
30
31
  }
31
32
 
@@ -126,7 +127,7 @@ export class UserService {
126
127
  }
127
128
 
128
129
  // Check wildcard match (e.g., "content:manage" matches "content:read")
129
- const [resource, action] = permission.split(':');
130
+ const [resource] = permission.split(':');
130
131
  const managePermission = `${resource}:manage`;
131
132
  if (permissions.includes(managePermission)) {
132
133
  return true;