create-tigra 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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/bin/create-tigra.js +292 -0
  4. package/package.json +41 -0
  5. package/template/.agent/rules/client/01-project-structure.md +326 -0
  6. package/template/.agent/rules/client/02-component-patterns.md +249 -0
  7. package/template/.agent/rules/client/03-typescript-rules.md +226 -0
  8. package/template/.agent/rules/client/04-state-management.md +474 -0
  9. package/template/.agent/rules/client/05-api-integration.md +129 -0
  10. package/template/.agent/rules/client/06-forms-validation.md +129 -0
  11. package/template/.agent/rules/client/07-common-patterns.md +150 -0
  12. package/template/.agent/rules/client/08-color-system.md +93 -0
  13. package/template/.agent/rules/client/09-security-rules.md +97 -0
  14. package/template/.agent/rules/client/10-testing-strategy.md +370 -0
  15. package/template/.agent/rules/global/ai-edit-safety.md +38 -0
  16. package/template/.agent/rules/server/01-db-and-migrations.md +242 -0
  17. package/template/.agent/rules/server/02-general-rules.md +111 -0
  18. package/template/.agent/rules/server/03-migrations.md +20 -0
  19. package/template/.agent/rules/server/04-pagination.md +130 -0
  20. package/template/.agent/rules/server/05-project-conventions.md +71 -0
  21. package/template/.agent/rules/server/06-response-handling.md +173 -0
  22. package/template/.agent/rules/server/07-testing-strategy.md +506 -0
  23. package/template/.agent/rules/server/08-observability.md +180 -0
  24. package/template/.agent/rules/server/09-api-documentation-v2.md +168 -0
  25. package/template/.agent/rules/server/10-background-jobs-v2.md +185 -0
  26. package/template/.agent/rules/server/11-rate-limiting-v2.md +210 -0
  27. package/template/.agent/rules/server/12-performance-optimization.md +567 -0
  28. package/template/.claude/rules/client-01-project-structure.md +327 -0
  29. package/template/.claude/rules/client-02-component-patterns.md +250 -0
  30. package/template/.claude/rules/client-03-typescript-rules.md +227 -0
  31. package/template/.claude/rules/client-04-state-management.md +475 -0
  32. package/template/.claude/rules/client-05-api-integration.md +130 -0
  33. package/template/.claude/rules/client-06-forms-validation.md +130 -0
  34. package/template/.claude/rules/client-07-common-patterns.md +151 -0
  35. package/template/.claude/rules/client-08-color-system.md +94 -0
  36. package/template/.claude/rules/client-09-security-rules.md +98 -0
  37. package/template/.claude/rules/client-10-testing-strategy.md +371 -0
  38. package/template/.claude/rules/global-ai-edit-safety.md +39 -0
  39. package/template/.claude/rules/server-01-db-and-migrations.md +243 -0
  40. package/template/.claude/rules/server-02-general-rules.md +112 -0
  41. package/template/.claude/rules/server-03-migrations.md +21 -0
  42. package/template/.claude/rules/server-04-pagination.md +131 -0
  43. package/template/.claude/rules/server-05-project-conventions.md +72 -0
  44. package/template/.claude/rules/server-06-response-handling.md +174 -0
  45. package/template/.claude/rules/server-07-testing-strategy.md +507 -0
  46. package/template/.claude/rules/server-08-observability.md +181 -0
  47. package/template/.claude/rules/server-09-api-documentation-v2.md +169 -0
  48. package/template/.claude/rules/server-10-background-jobs-v2.md +186 -0
  49. package/template/.claude/rules/server-11-rate-limiting-v2.md +211 -0
  50. package/template/.claude/rules/server-12-performance-optimization.md +568 -0
  51. package/template/.cursor/rules/client-01-project-structure.mdc +327 -0
  52. package/template/.cursor/rules/client-02-component-patterns.mdc +250 -0
  53. package/template/.cursor/rules/client-03-typescript-rules.mdc +227 -0
  54. package/template/.cursor/rules/client-04-state-management.mdc +475 -0
  55. package/template/.cursor/rules/client-05-api-integration.mdc +130 -0
  56. package/template/.cursor/rules/client-06-forms-validation.mdc +130 -0
  57. package/template/.cursor/rules/client-07-common-patterns.mdc +151 -0
  58. package/template/.cursor/rules/client-08-color-system.mdc +94 -0
  59. package/template/.cursor/rules/client-09-security-rules.mdc +98 -0
  60. package/template/.cursor/rules/client-10-testing-strategy.mdc +371 -0
  61. package/template/.cursor/rules/global-ai-edit-safety.mdc +39 -0
  62. package/template/.cursor/rules/server-01-db-and-migrations.mdc +243 -0
  63. package/template/.cursor/rules/server-02-general-rules.mdc +112 -0
  64. package/template/.cursor/rules/server-03-migrations.mdc +21 -0
  65. package/template/.cursor/rules/server-04-pagination.mdc +131 -0
  66. package/template/.cursor/rules/server-05-project-conventions.mdc +72 -0
  67. package/template/.cursor/rules/server-06-response-handling.mdc +174 -0
  68. package/template/.cursor/rules/server-07-testing-strategy.mdc +507 -0
  69. package/template/.cursor/rules/server-08-observability.mdc +181 -0
  70. package/template/.cursor/rules/server-09-api-documentation-v2.mdc +169 -0
  71. package/template/.cursor/rules/server-10-background-jobs-v2.mdc +186 -0
  72. package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +211 -0
  73. package/template/.cursor/rules/server-12-performance-optimization.mdc +568 -0
  74. package/template/CLAUDE.md +207 -0
  75. package/template/server/.env.example +148 -0
  76. package/template/server/.tsc-aliasrc.json +12 -0
  77. package/template/server/README.md +175 -0
  78. package/template/server/SECURITY.md +190 -0
  79. package/template/server/biome.json +42 -0
  80. package/template/server/docker-compose.yml +111 -0
  81. package/template/server/package.json +83 -0
  82. package/template/server/postman_collection.json +733 -0
  83. package/template/server/prisma/schema.prisma +92 -0
  84. package/template/server/prisma/seed.ts +142 -0
  85. package/template/server/scripts/wait-for-db.js +60 -0
  86. package/template/server/src/app.ts +74 -0
  87. package/template/server/src/config/env.ts +101 -0
  88. package/template/server/src/hooks/request-timing.hook.ts +26 -0
  89. package/template/server/src/libs/auth/authenticate.middleware.ts +22 -0
  90. package/template/server/src/libs/auth/rbac.middleware.test.ts +134 -0
  91. package/template/server/src/libs/auth/rbac.middleware.ts +147 -0
  92. package/template/server/src/libs/db.ts +76 -0
  93. package/template/server/src/libs/error-handler.ts +89 -0
  94. package/template/server/src/libs/logger.ts +60 -0
  95. package/template/server/src/libs/queue.ts +79 -0
  96. package/template/server/src/libs/redis.ts +79 -0
  97. package/template/server/src/libs/swagger-schemas.ts +16 -0
  98. package/template/server/src/modules/admin/admin.controller.ts +122 -0
  99. package/template/server/src/modules/admin/admin.routes.ts +100 -0
  100. package/template/server/src/modules/admin/admin.schemas.ts +35 -0
  101. package/template/server/src/modules/admin/admin.service.ts +167 -0
  102. package/template/server/src/modules/auth/auth.controller.ts +141 -0
  103. package/template/server/src/modules/auth/auth.integration.test.ts +150 -0
  104. package/template/server/src/modules/auth/auth.repo.ts +218 -0
  105. package/template/server/src/modules/auth/auth.routes.ts +204 -0
  106. package/template/server/src/modules/auth/auth.schemas.ts +137 -0
  107. package/template/server/src/modules/auth/auth.service.test.ts +119 -0
  108. package/template/server/src/modules/auth/auth.service.ts +329 -0
  109. package/template/server/src/modules/auth/auth.types.ts +97 -0
  110. package/template/server/src/modules/resources/resources.controller.ts +218 -0
  111. package/template/server/src/modules/resources/resources.repo.ts +253 -0
  112. package/template/server/src/modules/resources/resources.routes.ts +355 -0
  113. package/template/server/src/modules/resources/resources.schemas.ts +146 -0
  114. package/template/server/src/modules/resources/resources.service.ts +218 -0
  115. package/template/server/src/modules/resources/resources.types.ts +73 -0
  116. package/template/server/src/plugins/rate-limit.plugin.ts +21 -0
  117. package/template/server/src/plugins/security.plugin.ts +21 -0
  118. package/template/server/src/plugins/swagger.plugin.ts +41 -0
  119. package/template/server/src/routes/health.routes.ts +31 -0
  120. package/template/server/src/server.ts +142 -0
  121. package/template/server/src/test/setup.ts +38 -0
  122. package/template/server/src/types/fastify.d.ts +36 -0
  123. package/template/server/src/utils/errors.ts +108 -0
  124. package/template/server/src/utils/pagination.ts +120 -0
  125. package/template/server/src/utils/response.ts +110 -0
  126. package/template/server/src/workers/file.worker.ts +106 -0
  127. package/template/server/tsconfig.build.json +30 -0
  128. package/template/server/tsconfig.build.tsbuildinfo +1 -0
  129. package/template/server/tsconfig.json +89 -0
  130. package/template/server/tsconfig.test.json +22 -0
  131. package/template/server/vitest.config.ts +98 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Authentication Repository
3
+ *
4
+ * Database operations for authentication module.
5
+ * Handles all Prisma queries related to users and sessions.
6
+ *
7
+ * @see /mnt/project/02-general-rules.md
8
+ */
9
+
10
+ import { prisma } from '@/libs/db';
11
+ import type { User as PrismaUser, Session as PrismaSession } from '@prisma/client';
12
+ import type { User } from './auth.types';
13
+
14
+ /**
15
+ * User with password (internal use only)
16
+ */
17
+ type UserWithPassword = PrismaUser;
18
+
19
+ /**
20
+ * Session with user relation
21
+ */
22
+ type SessionWithUser = PrismaSession & {
23
+ user: PrismaUser;
24
+ };
25
+
26
+ /**
27
+ * Create a new user
28
+ *
29
+ * @param data - User creation data
30
+ * @returns User object WITHOUT password field
31
+ */
32
+ export async function createUser(data: {
33
+ email: string;
34
+ password: string;
35
+ name: string;
36
+ }): Promise<User> {
37
+ const user = await prisma.user.create({
38
+ data: {
39
+ email: data.email,
40
+ password: data.password,
41
+ name: data.name,
42
+ role: 'USER',
43
+ emailVerified: false,
44
+ },
45
+ select: {
46
+ id: true,
47
+ email: true,
48
+ name: true,
49
+ role: true,
50
+ emailVerified: true,
51
+ createdAt: true,
52
+ updatedAt: true,
53
+ },
54
+ });
55
+
56
+ return user;
57
+ }
58
+
59
+ /**
60
+ * Find user by email (without password)
61
+ *
62
+ * @param email - User email
63
+ * @returns User object WITHOUT password field, or null if not found
64
+ */
65
+ export async function findUserByEmail(email: string): Promise<User | null> {
66
+ const user = await prisma.user.findUnique({
67
+ where: { email },
68
+ select: {
69
+ id: true,
70
+ email: true,
71
+ name: true,
72
+ role: true,
73
+ emailVerified: true,
74
+ createdAt: true,
75
+ updatedAt: true,
76
+ },
77
+ });
78
+
79
+ return user;
80
+ }
81
+
82
+ /**
83
+ * Find user by email WITH password
84
+ *
85
+ * Used for login verification only.
86
+ * NEVER return this to the client.
87
+ *
88
+ * @param email - User email
89
+ * @returns User object WITH password field, or null if not found
90
+ */
91
+ export async function findUserByEmailWithPassword(
92
+ email: string
93
+ ): Promise<UserWithPassword | null> {
94
+ const user = await prisma.user.findUnique({
95
+ where: { email },
96
+ });
97
+
98
+ return user;
99
+ }
100
+
101
+ /**
102
+ * Find user by ID (without password)
103
+ *
104
+ * @param id - User ID
105
+ * @returns User object WITHOUT password field, or null if not found
106
+ */
107
+ export async function findUserById(id: string): Promise<User | null> {
108
+ const user = await prisma.user.findUnique({
109
+ where: { id },
110
+ select: {
111
+ id: true,
112
+ email: true,
113
+ name: true,
114
+ role: true,
115
+ emailVerified: true,
116
+ createdAt: true,
117
+ updatedAt: true,
118
+ },
119
+ });
120
+
121
+ return user;
122
+ }
123
+
124
+ /**
125
+ * Create a new session
126
+ *
127
+ * @param data - Session creation data
128
+ * @returns Created session
129
+ */
130
+ export async function createSession(data: {
131
+ userId: string;
132
+ refreshToken: string;
133
+ expiresAt: Date;
134
+ }): Promise<PrismaSession> {
135
+ const session = await prisma.session.create({
136
+ data: {
137
+ userId: data.userId,
138
+ refreshToken: data.refreshToken,
139
+ expiresAt: data.expiresAt,
140
+ },
141
+ });
142
+
143
+ return session;
144
+ }
145
+
146
+ /**
147
+ * Find session by refresh token
148
+ *
149
+ * @param refreshToken - Refresh token
150
+ * @returns Session with user relation, or null if not found
151
+ */
152
+ export async function findSessionByToken(
153
+ refreshToken: string
154
+ ): Promise<SessionWithUser | null> {
155
+ const session = await prisma.session.findFirst({
156
+ where: { refreshToken },
157
+ include: {
158
+ user: true,
159
+ },
160
+ });
161
+
162
+ return session;
163
+ }
164
+
165
+ /**
166
+ * Update session with new refresh token
167
+ *
168
+ * @param oldToken - Current refresh token
169
+ * @param newToken - New refresh token
170
+ * @param expiresAt - New expiration date
171
+ * @returns Updated session
172
+ */
173
+ export async function updateSession(
174
+ oldToken: string,
175
+ newToken: string,
176
+ expiresAt: Date
177
+ ): Promise<PrismaSession> {
178
+ // Since refreshToken is no longer unique at DB level, we use updateMany
179
+ // In practice, it should only update one record
180
+ await prisma.session.updateMany({
181
+ where: { refreshToken: oldToken },
182
+ data: {
183
+ refreshToken: newToken,
184
+ expiresAt,
185
+ },
186
+ });
187
+
188
+ // Return the updated session (requires a fetch since updateMany returns a count)
189
+ const session = await prisma.session.findFirstOrThrow({
190
+ where: { refreshToken: newToken },
191
+ });
192
+
193
+ return session;
194
+ }
195
+
196
+ /**
197
+ * Delete session by refresh token
198
+ *
199
+ * @param refreshToken - Refresh token
200
+ */
201
+ export async function deleteSession(refreshToken: string): Promise<void> {
202
+ await prisma.session.deleteMany({
203
+ where: { refreshToken },
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Delete all sessions for a user
209
+ *
210
+ * Used for logout from all devices.
211
+ *
212
+ * @param userId - User ID
213
+ */
214
+ export async function deleteAllUserSessions(userId: string): Promise<void> {
215
+ await prisma.session.deleteMany({
216
+ where: { userId },
217
+ });
218
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Authentication Routes
3
+ *
4
+ * Fastify route definitions for authentication endpoints.
5
+ * Includes Swagger documentation and rate limiting.
6
+ *
7
+ * @see /mnt/project/09-api-documentation-v2.md
8
+ * @see /mnt/project/11-rate-limiting-v2.md
9
+ */
10
+
11
+ import type { FastifyInstance } from 'fastify';
12
+ import { toJsonSchema, getDefinition } from '@/libs/swagger-schemas';
13
+ import * as authController from './auth.controller';
14
+ import {
15
+ RegisterSchema,
16
+ LoginSchema,
17
+ RefreshTokenSchema,
18
+ AuthResponseSchema,
19
+ UserResponseSchema,
20
+ TokenResponseSchema,
21
+ ErrorResponseSchema,
22
+ } from './auth.schemas';
23
+
24
+ /**
25
+ * Register authentication routes
26
+ *
27
+ * @param fastify - Fastify instance
28
+ */
29
+ export async function authRoutes(fastify: FastifyInstance): Promise<void> {
30
+ /**
31
+ * POST /auth/register
32
+ *
33
+ * Register a new user account
34
+ */
35
+ fastify.post('/register', {
36
+ schema: {
37
+ description: 'Register a new user account',
38
+ tags: ['auth'],
39
+ summary: 'Register user',
40
+ body: toJsonSchema(RegisterSchema, 'RegisterRequest'),
41
+ response: {
42
+ 201: getDefinition(AuthResponseSchema, 'AuthResponse'),
43
+ 400: {
44
+ description: 'Validation error',
45
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
46
+ },
47
+ 409: {
48
+ description: 'Email already registered',
49
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
50
+ },
51
+ },
52
+ },
53
+ config: {
54
+ rateLimit: {
55
+ max: 3,
56
+ timeWindow: '1 hour',
57
+ },
58
+ },
59
+ handler: authController.register,
60
+ });
61
+
62
+ /**
63
+ * POST /auth/login
64
+ *
65
+ * Login with email and password
66
+ */
67
+ fastify.post('/login', {
68
+ schema: {
69
+ description: 'Login with email and password',
70
+ tags: ['auth'],
71
+ summary: 'Login user',
72
+ body: toJsonSchema(LoginSchema, 'LoginRequest'),
73
+ response: {
74
+ 200: getDefinition(AuthResponseSchema, 'AuthResponse'),
75
+ 401: {
76
+ description: 'Invalid credentials',
77
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
78
+ },
79
+ },
80
+ },
81
+ config: {
82
+ rateLimit: {
83
+ max: 5,
84
+ timeWindow: '15 minutes',
85
+ },
86
+ },
87
+ handler: authController.login,
88
+ });
89
+
90
+ /**
91
+ * POST /auth/refresh
92
+ *
93
+ * Refresh access token using refresh token
94
+ */
95
+ fastify.post('/refresh', {
96
+ schema: {
97
+ description: 'Refresh access token using refresh token',
98
+ tags: ['auth'],
99
+ summary: 'Refresh tokens',
100
+ body: toJsonSchema(RefreshTokenSchema, 'RefreshTokenRequest'),
101
+ response: {
102
+ 200: {
103
+ description: 'Tokens refreshed successfully',
104
+ type: 'object',
105
+ properties: {
106
+ success: { type: 'boolean', enum: [true] },
107
+ message: { type: 'string' },
108
+ data: {
109
+ type: 'object',
110
+ properties: {
111
+ accessToken: { type: 'string' },
112
+ refreshToken: { type: 'string' },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ 401: {
118
+ description: 'Invalid or expired refresh token',
119
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
120
+ },
121
+ },
122
+ },
123
+ config: {
124
+ rateLimit: {
125
+ max: 10,
126
+ timeWindow: '15 minutes',
127
+ },
128
+ },
129
+ handler: authController.refreshTokens,
130
+ });
131
+
132
+ /**
133
+ * POST /auth/logout
134
+ *
135
+ * Logout user by invalidating refresh token
136
+ */
137
+ fastify.post('/logout', {
138
+ schema: {
139
+ description: 'Logout user by invalidating refresh token',
140
+ tags: ['auth'],
141
+ summary: 'Logout user',
142
+ body: toJsonSchema(RefreshTokenSchema, 'RefreshTokenRequest'),
143
+ response: {
144
+ 200: {
145
+ description: 'Logout successful',
146
+ type: 'object',
147
+ properties: {
148
+ success: { type: 'boolean', enum: [true] },
149
+ message: { type: 'string' },
150
+ data: { type: 'null' },
151
+ },
152
+ },
153
+ },
154
+ },
155
+ handler: authController.logout,
156
+ });
157
+
158
+ /**
159
+ * GET /auth/me
160
+ *
161
+ * Get current authenticated user information
162
+ * Requires authentication
163
+ */
164
+ fastify.get('/me', {
165
+ schema: {
166
+ description: 'Get current authenticated user information',
167
+ tags: ['auth'],
168
+ summary: 'Get current user',
169
+ security: [{ bearerAuth: [] }],
170
+ response: {
171
+ 200: {
172
+ description: 'User retrieved successfully',
173
+ type: 'object',
174
+ properties: {
175
+ success: { type: 'boolean', enum: [true] },
176
+ message: { type: 'string' },
177
+ data: {
178
+ type: 'object',
179
+ properties: {
180
+ id: { type: 'string', format: 'uuid' },
181
+ email: { type: 'string', format: 'email' },
182
+ name: { type: 'string', nullable: true },
183
+ role: { type: 'string' },
184
+ emailVerified: { type: 'boolean' },
185
+ createdAt: { type: 'string', format: 'date-time' },
186
+ updatedAt: { type: 'string', format: 'date-time' },
187
+ },
188
+ },
189
+ },
190
+ },
191
+ 401: {
192
+ description: 'Unauthorized - Invalid or missing token',
193
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
194
+ },
195
+ 404: {
196
+ description: 'User not found',
197
+ ...getDefinition(ErrorResponseSchema, 'ErrorResponse'),
198
+ },
199
+ },
200
+ },
201
+ preHandler: [fastify.authenticate],
202
+ handler: authController.getMe,
203
+ });
204
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Authentication Schemas
3
+ *
4
+ * Zod schemas for request validation and Swagger documentation.
5
+ * These schemas ensure type safety and API documentation consistency.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+
10
+ /**
11
+ * Password Validation Regex
12
+ *
13
+ * Requirements:
14
+ * - At least 1 uppercase letter
15
+ * - At least 1 lowercase letter
16
+ * - At least 1 number
17
+ * - Minimum 8 characters
18
+ */
19
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/;
20
+
21
+ /**
22
+ * Register Schema
23
+ *
24
+ * Validates user registration requests.
25
+ */
26
+ export const RegisterSchema = z.object({
27
+ email: z
28
+ .string()
29
+ .email('Invalid email format')
30
+ .toLowerCase()
31
+ .trim(),
32
+ password: z
33
+ .string()
34
+ .min(8, 'Password must be at least 8 characters')
35
+ .regex(
36
+ passwordRegex,
37
+ 'Password must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number'
38
+ ),
39
+ name: z
40
+ .string()
41
+ .min(2, 'Name must be at least 2 characters')
42
+ .max(100, 'Name must not exceed 100 characters')
43
+ .trim(),
44
+ });
45
+
46
+ /**
47
+ * Login Schema
48
+ *
49
+ * Validates user login requests.
50
+ */
51
+ export const LoginSchema = z.object({
52
+ email: z
53
+ .string()
54
+ .email('Invalid email format')
55
+ .toLowerCase()
56
+ .trim(),
57
+ password: z
58
+ .string()
59
+ .min(8, 'Password must be at least 8 characters'),
60
+ });
61
+
62
+ /**
63
+ * Refresh Token Schema
64
+ *
65
+ * Validates refresh token requests.
66
+ */
67
+ export const RefreshTokenSchema = z.object({
68
+ refreshToken: z
69
+ .string()
70
+ .min(1, 'Refresh token is required'),
71
+ });
72
+
73
+ /**
74
+ * User Response Schema (for Swagger documentation)
75
+ *
76
+ * Defines the structure of user objects in API responses.
77
+ */
78
+ export const UserResponseSchema = z.object({
79
+ id: z.string().uuid(),
80
+ email: z.string().email(),
81
+ name: z.string().nullable(),
82
+ role: z.enum(['USER', 'ADMIN']),
83
+ emailVerified: z.boolean(),
84
+ createdAt: z.string().datetime(),
85
+ updatedAt: z.string().datetime(),
86
+ });
87
+
88
+ /**
89
+ * Token Response Schema (for Swagger documentation)
90
+ *
91
+ * Defines the structure of token objects in API responses.
92
+ */
93
+ export const TokenResponseSchema = z.object({
94
+ accessToken: z.string(),
95
+ refreshToken: z.string(),
96
+ expiresIn: z.number().int().positive(),
97
+ });
98
+
99
+ /**
100
+ * Auth Response Schema (for Swagger documentation)
101
+ *
102
+ * Complete authentication response structure.
103
+ * Used for login and register endpoints.
104
+ */
105
+ export const AuthResponseSchema = z.object({
106
+ success: z.literal(true),
107
+ message: z.string(),
108
+ data: z.object({
109
+ user: UserResponseSchema,
110
+ tokens: TokenResponseSchema,
111
+ }),
112
+ });
113
+
114
+ /**
115
+ * Error Response Schema (for Swagger documentation)
116
+ *
117
+ * Standard error response structure.
118
+ */
119
+ export const ErrorResponseSchema = z.object({
120
+ success: z.literal(false),
121
+ error: z.object({
122
+ code: z.string(),
123
+ message: z.string(),
124
+ }),
125
+ });
126
+
127
+ /**
128
+ * Type inference from schemas
129
+ *
130
+ * These types can be used in controllers and services.
131
+ */
132
+ export type RegisterInput = z.infer<typeof RegisterSchema>;
133
+ export type LoginInput = z.infer<typeof LoginSchema>;
134
+ export type RefreshTokenInput = z.infer<typeof RefreshTokenSchema>;
135
+ export type UserResponse = z.infer<typeof UserResponseSchema>;
136
+ export type TokenResponse = z.infer<typeof TokenResponseSchema>;
137
+ export type AuthResponse = z.infer<typeof AuthResponseSchema>;
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as authService from './auth.service';
3
+ import * as authRepo from './auth.repo';
4
+ import bcrypt from 'bcryptjs';
5
+ import jwt from 'jsonwebtoken';
6
+ import { env } from '@/config/env';
7
+ import { ConflictError, UnauthorizedError } from '@/utils/errors';
8
+
9
+ // Mock dependencies
10
+ vi.mock('./auth.repo');
11
+ vi.mock('bcryptjs');
12
+ vi.mock('jsonwebtoken');
13
+ vi.mock('@/libs/logger', () => ({
14
+ default: {
15
+ info: vi.fn(),
16
+ error: vi.fn(),
17
+ warn: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ describe('Auth Service', () => {
22
+ beforeEach(() => {
23
+ vi.resetAllMocks();
24
+ // Setup default env mocks if needed
25
+ env.JWT_SECRET = 'test-secret';
26
+ env.JWT_ACCESS_EXPIRATION = '15m';
27
+ env.JWT_REFRESH_EXPIRATION = '7d';
28
+ env.JWT_ISSUER = 'test-issuer';
29
+ });
30
+
31
+ describe('register', () => {
32
+ const registerData = {
33
+ email: 'test@example.com',
34
+ password: 'Password123!',
35
+ name: 'Test User',
36
+ };
37
+
38
+ it('should successfully register a new user', async () => {
39
+ // Mocks
40
+ vi.mocked(authRepo.findUserByEmail).mockResolvedValue(null);
41
+ vi.mocked(bcrypt.hash).mockResolvedValue('hashed_password' as any);
42
+ vi.mocked(authRepo.createUser).mockResolvedValue({
43
+ id: 'user-123',
44
+ email: registerData.email,
45
+ name: registerData.name,
46
+ role: 'USER',
47
+ emailVerified: false,
48
+ createdAt: new Date(),
49
+ updatedAt: new Date(),
50
+ });
51
+ vi.mocked(jwt.sign).mockReturnValue('mock_token' as any);
52
+ vi.mocked(authRepo.createSession).mockResolvedValue({} as any);
53
+
54
+ // Execute
55
+ const result = await authService.register(registerData);
56
+
57
+ // Assert
58
+ expect(result).toBeDefined();
59
+ expect(result.user.email).toBe(registerData.email);
60
+ expect(result.tokens.accessToken).toBe('mock_token');
61
+ expect(authRepo.createUser).toHaveBeenCalled();
62
+ expect(authRepo.createSession).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should throw ConflictError if email already exists', async () => {
66
+ // Mocks
67
+ vi.mocked(authRepo.findUserByEmail).mockResolvedValue({ id: 'existing' } as any);
68
+
69
+ // Execute & Assert
70
+ await expect(authService.register(registerData))
71
+ .rejects
72
+ .toThrow(ConflictError);
73
+ });
74
+ });
75
+
76
+ describe('login', () => {
77
+ const loginData = {
78
+ email: 'test@example.com',
79
+ password: 'Password123!',
80
+ };
81
+
82
+ it('should successfully login user', async () => {
83
+ // Mocks
84
+ vi.mocked(authRepo.findUserByEmailWithPassword).mockResolvedValue({
85
+ id: 'user-123',
86
+ email: loginData.email,
87
+ password: 'hashed_password', // Match what bcrypt.compare expects
88
+ role: 'USER',
89
+ } as any);
90
+ vi.mocked(bcrypt.compare).mockResolvedValue(true as any);
91
+ vi.mocked(authRepo.findUserById).mockResolvedValue({
92
+ id: 'user-123',
93
+ email: loginData.email,
94
+ role: 'USER',
95
+ } as any);
96
+ vi.mocked(jwt.sign).mockReturnValue('mock_token' as any);
97
+ vi.mocked(authRepo.createSession).mockResolvedValue({} as any);
98
+
99
+ const result = await authService.login(loginData);
100
+
101
+ expect(result).toBeDefined();
102
+ expect(result.user.id).toBe('user-123');
103
+ expect(result.tokens.accessToken).toBeDefined();
104
+ });
105
+
106
+ it('should throw UnauthorizedError on invalid password', async () => {
107
+ // Mocks
108
+ vi.mocked(authRepo.findUserByEmailWithPassword).mockResolvedValue({
109
+ id: 'user-123',
110
+ password: 'hashed_password',
111
+ } as any);
112
+ vi.mocked(bcrypt.compare).mockResolvedValue(false as any);
113
+
114
+ await expect(authService.login(loginData))
115
+ .rejects
116
+ .toThrow(UnauthorizedError);
117
+ });
118
+ });
119
+ });