@plyaz/auth 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 (113) hide show
  1. package/.github/pull_request_template.md +71 -0
  2. package/.github/workflows/deploy.yml +9 -0
  3. package/.github/workflows/publish.yml +14 -0
  4. package/.github/workflows/security.yml +20 -0
  5. package/README.md +89 -0
  6. package/commits.txt +5 -0
  7. package/dist/common/index.cjs +48 -0
  8. package/dist/common/index.cjs.map +1 -0
  9. package/dist/common/index.mjs +43 -0
  10. package/dist/common/index.mjs.map +1 -0
  11. package/dist/index.cjs +20411 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +5139 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +13 -0
  16. package/index.html +13 -0
  17. package/package.json +141 -0
  18. package/src/adapters/auth-adapter-factory.ts +26 -0
  19. package/src/adapters/auth-adapter.mapper.ts +53 -0
  20. package/src/adapters/base-auth.adapter.ts +119 -0
  21. package/src/adapters/clerk/clerk.adapter.ts +204 -0
  22. package/src/adapters/custom/custom.adapter.ts +119 -0
  23. package/src/adapters/index.ts +4 -0
  24. package/src/adapters/next-auth/authOptions.ts +81 -0
  25. package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
  26. package/src/api/client.ts +37 -0
  27. package/src/audit/audit.logger.ts +52 -0
  28. package/src/client/components/ProtectedRoute.tsx +37 -0
  29. package/src/client/hooks/useAuth.ts +128 -0
  30. package/src/client/hooks/useConnectedAccounts.ts +108 -0
  31. package/src/client/hooks/usePermissions.ts +36 -0
  32. package/src/client/hooks/useRBAC.ts +36 -0
  33. package/src/client/hooks/useSession.ts +18 -0
  34. package/src/client/providers/AuthProvider.tsx +104 -0
  35. package/src/client/store/auth.store.ts +306 -0
  36. package/src/client/utils/storage.ts +70 -0
  37. package/src/common/constants/oauth-providers.ts +49 -0
  38. package/src/common/errors/auth.errors.ts +64 -0
  39. package/src/common/errors/specific-auth-errors.ts +201 -0
  40. package/src/common/index.ts +19 -0
  41. package/src/common/regex/index.ts +27 -0
  42. package/src/common/types/auth.types.ts +641 -0
  43. package/src/common/types/index.ts +297 -0
  44. package/src/common/utils/index.ts +84 -0
  45. package/src/core/blacklist/token.blacklist.ts +60 -0
  46. package/src/core/index.ts +2 -0
  47. package/src/core/jwt/jwt.manager.ts +131 -0
  48. package/src/core/session/session.manager.ts +56 -0
  49. package/src/db/repositories/connected-account.repository.ts +415 -0
  50. package/src/db/repositories/role.repository.ts +519 -0
  51. package/src/db/repositories/session.repository.ts +308 -0
  52. package/src/db/repositories/user.repository.ts +320 -0
  53. package/src/flows/index.ts +2 -0
  54. package/src/flows/sign-in.flow.ts +106 -0
  55. package/src/flows/sign-up.flow.ts +121 -0
  56. package/src/index.ts +54 -0
  57. package/src/libs/clerk.helper.ts +36 -0
  58. package/src/libs/supabase.helper.ts +255 -0
  59. package/src/libs/supabaseClient.ts +6 -0
  60. package/src/providers/base/auth-provider.interface.ts +42 -0
  61. package/src/providers/base/index.ts +1 -0
  62. package/src/providers/index.ts +2 -0
  63. package/src/providers/oauth/facebook.provider.ts +97 -0
  64. package/src/providers/oauth/github.provider.ts +148 -0
  65. package/src/providers/oauth/google.provider.ts +126 -0
  66. package/src/providers/oauth/index.ts +3 -0
  67. package/src/rbac/dynamic-roles.ts +552 -0
  68. package/src/rbac/index.ts +4 -0
  69. package/src/rbac/permission-checker.ts +464 -0
  70. package/src/rbac/role-hierarchy.ts +545 -0
  71. package/src/rbac/role.manager.ts +75 -0
  72. package/src/security/csrf/csrf.protection.ts +37 -0
  73. package/src/security/index.ts +3 -0
  74. package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
  75. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
  76. package/src/security/rate-limiting/auth.module.ts +32 -0
  77. package/src/server/auth.module.ts +158 -0
  78. package/src/server/decorators/auth.decorator.ts +43 -0
  79. package/src/server/decorators/auth.decorators.ts +31 -0
  80. package/src/server/decorators/current-user.decorator.ts +49 -0
  81. package/src/server/decorators/permission.decorator.ts +49 -0
  82. package/src/server/guards/auth.guard.ts +56 -0
  83. package/src/server/guards/custom-throttler.guard.ts +46 -0
  84. package/src/server/guards/permissions.guard.ts +115 -0
  85. package/src/server/guards/roles.guard.ts +31 -0
  86. package/src/server/middleware/auth.middleware.ts +46 -0
  87. package/src/server/middleware/index.ts +2 -0
  88. package/src/server/middleware/middleware.ts +11 -0
  89. package/src/server/middleware/session.middleware.ts +255 -0
  90. package/src/server/services/account.service.ts +269 -0
  91. package/src/server/services/auth.service.ts +79 -0
  92. package/src/server/services/brute-force.service.ts +98 -0
  93. package/src/server/services/index.ts +15 -0
  94. package/src/server/services/rate-limiter.service.ts +60 -0
  95. package/src/server/services/session.service.ts +287 -0
  96. package/src/server/services/token.service.ts +262 -0
  97. package/src/session/cookie-store.ts +255 -0
  98. package/src/session/enhanced-session-manager.ts +406 -0
  99. package/src/session/index.ts +14 -0
  100. package/src/session/memory-store.ts +320 -0
  101. package/src/session/redis-store.ts +443 -0
  102. package/src/strategies/oauth.strategy.ts +128 -0
  103. package/src/strategies/traditional-auth.strategy.ts +116 -0
  104. package/src/tokens/index.ts +4 -0
  105. package/src/tokens/refresh-token-manager.ts +448 -0
  106. package/src/tokens/token-validator.ts +311 -0
  107. package/tsconfig.build.json +28 -0
  108. package/tsconfig.json +38 -0
  109. package/tsup.config.mjs +28 -0
  110. package/vitest.config.mjs +16 -0
  111. package/vitest.setup.d.ts +2 -0
  112. package/vitest.setup.d.ts.map +1 -0
  113. package/vitest.setup.ts +1 -0
@@ -0,0 +1,308 @@
1
+ import type {
2
+ CreateSessionData,
3
+ Session,
4
+ SessionRepository as ISessionRepository,
5
+ } from "@plyaz/types";
6
+ import { createClient } from "@supabase/supabase-js";
7
+
8
+ /**
9
+ * Repository for managing user sessions
10
+ *
11
+ * @description
12
+ * Handles session lifecycle for both B2C (public) and B2B (backoffice) users.
13
+ * Tracks session expiration, activity, and device information.
14
+ * Schema-aware repository that works with public.sessions or backoffice.sessions.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const repo = new SessionRepository(url, key, 'public');
19
+ * const session = await repo.create({
20
+ * userId: 'uuid',
21
+ * provider: 'clerk',
22
+ * expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
23
+ * });
24
+ * ```
25
+ */
26
+ export class SessionRepository implements ISessionRepository {
27
+ private supabase;
28
+
29
+ /**
30
+ * STEP 1: Initialize repository with Supabase client
31
+ * @param supabaseUrl - Supabase project URL
32
+ * @param supabaseKey - Supabase service role key
33
+ * @param schema - Database schema ('public' for B2C, 'backoffice' for B2B)
34
+ */
35
+ constructor(
36
+ supabaseUrl: string,
37
+ supabaseKey: string,
38
+ private schema: "public" | "backoffice" = "public"
39
+ ) {
40
+ this.supabase = createClient(supabaseUrl, supabaseKey);
41
+ }
42
+
43
+ /**
44
+ * STEP 2: Create new session
45
+ * Automatically sets last_activity_at to current time
46
+ *
47
+ * @param data - Session creation data
48
+ * @returns Promise resolving to created Session
49
+ * @throws Error if creation fails
50
+ */
51
+ async create(data: CreateSessionData): Promise<Session> {
52
+ const insertData = this.transformToDbFormat(data);
53
+
54
+ const { data: sessionData, error } = await this.supabase
55
+ .from("sessions")
56
+ .insert(insertData)
57
+ .select()
58
+ .single();
59
+
60
+ if (error || !sessionData)
61
+ throw new Error(`Failed to create session: ${error?.message}`);
62
+ return this.mapToSession(sessionData);
63
+ }
64
+
65
+ /**
66
+ * STEP 3: Find session by ID
67
+ *
68
+ * @param id - Session UUID
69
+ * @returns Promise resolving to Session or null if not found
70
+ */
71
+ async findById(id: string): Promise<Session | null> {
72
+ const { data, error } = await this.supabase
73
+ .from("sessions")
74
+ .select("*")
75
+ .eq("id", id)
76
+ .single();
77
+
78
+ if (error || !data) return null;
79
+ return this.mapToSession(data);
80
+ }
81
+
82
+ /**
83
+ * STEP 4: Find all sessions for a user
84
+ * Returns sessions ordered by creation date (newest first)
85
+ *
86
+ * @param userId - User UUID
87
+ * @returns Promise resolving to array of Session
88
+ */
89
+ async findByUserId(userId: string): Promise<Session[]> {
90
+
91
+ this.schema === "backoffice" ? "backoffice_user_id" : "user_id";
92
+ const { data, error } = await this.supabase
93
+ .from("sessions")
94
+ .select("*")
95
+ .eq("user_id", userId)
96
+ .order("created_at", { ascending: false });
97
+
98
+ if (error || !data) return [];
99
+ return data.map(this.mapToSession);
100
+ }
101
+
102
+ /**
103
+ * STEP 5: Validate session
104
+ * Checks if session exists and hasn't expired
105
+ * Updates last_activity_at if valid
106
+ *
107
+ * @param sessionId - Session UUID
108
+ * @returns Promise resolving to Session or null if invalid/expired
109
+ */
110
+ async validate(sessionId: string): Promise<Session | null> {
111
+ const { data, error } = await this.supabase
112
+ .from("sessions")
113
+ .select("*")
114
+ .eq("id", sessionId)
115
+ .gt("expires_at", new Date().toISOString())
116
+ .single();
117
+
118
+ if (error || !data) return null;
119
+
120
+ await this.updateActivity(sessionId);
121
+ return this.mapToSession(data);
122
+ }
123
+
124
+ /**
125
+ * STEP 6: Invalidate single session (logout)
126
+ *
127
+ * @param sessionId - Session UUID
128
+ * @throws Error if invalidation fails
129
+ */
130
+ async invalidate(sessionId: string): Promise<void> {
131
+ const { error } = await this.supabase
132
+ .from("sessions")
133
+ .delete()
134
+ .eq("id", sessionId);
135
+
136
+ if (error)
137
+ throw new Error(`Failed to invalidate session: ${error.message}`);
138
+ }
139
+
140
+ /**
141
+ * STEP 7: Invalidate all sessions for a user (logout all devices)
142
+ *
143
+ * @param userId - User UUID
144
+ * @throws Error if invalidation fails
145
+ */
146
+ async invalidateAllForUser(userId: string): Promise<void> {
147
+
148
+ this.schema === "backoffice" ? "backoffice_user_id" : "user_id";
149
+ const { error } = await this.supabase
150
+ .from("sessions")
151
+ .delete()
152
+ .eq("user_id", userId);
153
+
154
+ if (error)
155
+ throw new Error(`Failed to invalidate user sessions: ${error.message}`);
156
+ }
157
+
158
+ /**
159
+ * STEP 8: Update session activity timestamp
160
+ * Called on each authenticated request to track user activity
161
+ *
162
+ * @param sessionId - Session UUID
163
+ */
164
+ async updateActivity(sessionId: string): Promise<void> {
165
+ await this.supabase
166
+ .from("sessions")
167
+ .update({ last_active_at: new Date() })
168
+ .eq("id", sessionId);
169
+ }
170
+
171
+ /**
172
+ * Update last active timestamp
173
+ */
174
+ async updateLastActive(sessionId: string): Promise<void> {
175
+ await this.updateActivity(sessionId);
176
+ }
177
+
178
+ /**
179
+ * Delete session by ID
180
+ */
181
+ async delete(sessionId: string): Promise<void> {
182
+ await this.invalidate(sessionId);
183
+ }
184
+
185
+ /**
186
+ * Delete all sessions for user
187
+ */
188
+ async deleteByUserId(userId: string): Promise<void> {
189
+ await this.invalidateAllForUser(userId);
190
+ }
191
+
192
+ /**
193
+ * Find active sessions by user
194
+ * Query WHERE user_id AND expires_at > NOW()
195
+ * @param userId - User identifier
196
+ * @returns Array of active sessions
197
+ */
198
+ async findActiveByUser(userId: string): Promise<Session[]> {
199
+
200
+ this.schema === "backoffice" ? "backoffice_user_id" : "user_id";
201
+ const { data, error } = await this.supabase
202
+ .from("sessions")
203
+ .select("*")
204
+ .eq("user_id", userId)
205
+ .gt("expires_at", new Date().toISOString())
206
+ .order("created_at", { ascending: false });
207
+
208
+ if (error || !data) return [];
209
+ return data.map(this.mapToSession);
210
+ }
211
+
212
+ /**
213
+ * Invalidate session (set is_valid = false)
214
+ * UPDATE is_valid = false and emit session.invalidated event
215
+ * @param sessionId - Session identifier
216
+ * @returns Invalidation result
217
+ */
218
+ async invalidateSession(sessionId: string): Promise<void> {
219
+ // For this implementation, we'll delete the session
220
+ // In a real implementation with is_valid column, this would update the flag
221
+ await this.invalidate(sessionId);
222
+
223
+ // Emit session invalidated event
224
+ this.emitSessionInvalidatedEvent(sessionId);
225
+ }
226
+
227
+ /**
228
+ * Invalidate all user sessions with count
229
+ * UPDATE all user sessions is_valid = false for "logout all devices" feature
230
+ * @param userId - User identifier
231
+ * @returns Object with invalidated count
232
+ */
233
+ async invalidateAllUserSessions(
234
+ userId: string
235
+ ): Promise<{ invalidatedCount: number }> {
236
+ const userSessions = await this.findByUserId(userId);
237
+ const invalidatedCount = userSessions.length;
238
+
239
+ await this.invalidateAllForUser(userId);
240
+
241
+ return { invalidatedCount };
242
+ }
243
+
244
+ /**
245
+ * Clean up expired sessions
246
+ * DELETE WHERE expires_at < NOW() - Background job: Daily at 03:00 UTC
247
+ * @returns Object with deleted count
248
+ */
249
+ async cleanupExpired(): Promise<{ deletedCount: number }> {
250
+ const { data, error } = await this.supabase
251
+ .from("sessions")
252
+ .delete()
253
+ .lt("expires_at", new Date().toISOString())
254
+ .select("id");
255
+
256
+ if (error) {
257
+ throw new Error(`Failed to cleanup expired sessions: ${error.message}`);
258
+ }
259
+
260
+ return { deletedCount: data?.length || 0 };
261
+ }
262
+
263
+ /**
264
+ * Emit session invalidated event
265
+ * @param sessionId - Session identifier
266
+ * @private
267
+ */
268
+ private emitSessionInvalidatedEvent(sessionId: string): void {
269
+ // Mock event emission - in real implementation would use event system
270
+ globalThis.console.log("Event: auth.session.invalidated", {
271
+ sessionId,
272
+ timestamp: new Date(),
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Transform camelCase DTO to snake_case database format
278
+ * @private
279
+ */
280
+ private transformToDbFormat(data: CreateSessionData): Record<string, Record<string, unknown> | string | Date > {
281
+ return {
282
+ user_id: data.userId,
283
+ token_hash: "temp-token-hash", // Required field
284
+ device_info: data.metadata ?? {},
285
+ expires_at: data.expiresAt,
286
+ last_active_at: new Date(),
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Map database row to Session interface
292
+ * @private
293
+ */
294
+ private mapToSession(data: Session): Session {
295
+ return {
296
+ id: data.id,
297
+ userId: data.userId,
298
+ provider: "test", // Default since not in schema
299
+ providerSessionId: undefined,
300
+ expiresAt: new Date(data.expiresAt),
301
+ createdAt: new Date(data.createdAt),
302
+ lastActivityAt: new Date(data.lastActivityAt),
303
+ ipAddress: undefined,
304
+ userAgent: undefined,
305
+ metadata: data.metadata,
306
+ };
307
+ }
308
+ }
@@ -0,0 +1,320 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import type { User, UserRepository as IUserRepository, CreateUserData, UpdateUserData } from '@/common/types/auth.types';
3
+ import type { AUTHPROVIDER, AuthUser } from '@plyaz/types';
4
+ /**
5
+ * Repository for managing user accounts
6
+ *
7
+ * @description
8
+ * Handles CRUD operations for both B2C (public) and B2B (backoffice) users.
9
+ * Schema-aware repository that works with public.users or backoffice.users.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // B2C users
14
+ * const publicRepo = new UserRepository(url, key, 'public');
15
+ * const user = await publicRepo.findByEmail('user@example.com');
16
+ *
17
+ * // B2B users
18
+ * const backofficeRepo = new UserRepository(url, key, 'backoffice');
19
+ * const admin = await backofficeRepo.findById('uuid');
20
+ * ```
21
+ */
22
+ export class UserRepository implements IUserRepository {
23
+ private supabase;
24
+ /**
25
+ * STEP 1: Initialize repository with Supabase client
26
+ * @param supabaseUrl - Supabase project URL
27
+ * @param supabaseKey - Supabase service role key
28
+ * @param schema - Database schema ('public' for B2C, 'backoffice' for B2B)
29
+ */
30
+ constructor(supabaseUrl: string, supabaseKey: string, private schema: 'public' | 'backoffice' = 'public') {
31
+ this.supabase = createClient(supabaseUrl, supabaseKey);
32
+ }
33
+ /**
34
+ * STEP 2: Find user by ID
35
+ *
36
+ * @param id - User UUID
37
+ * @returns Promise resolving to User or null if not found
38
+ */
39
+ async findById(id: string): Promise<User | null> {
40
+ const { data, error } = await this.supabase
41
+ .from('users')
42
+ .select('*')
43
+ .eq('id', id)
44
+ .single();
45
+ if (error || !data) return null;
46
+ return this.mapToUser(data);
47
+ }
48
+ /**
49
+ * STEP 3: Find user by email
50
+ *
51
+ * @param email - User email address
52
+ * @returns Promise resolving to User or null if not found
53
+ */
54
+ async findByEmail(email: string): Promise<User | null> {
55
+ const { data, error } = await this.supabase
56
+ .from('users')
57
+ .select('*')
58
+ .eq('email', email)
59
+ .single();
60
+ if (error || !data) return null;
61
+ return this.mapToUser(data);
62
+ }
63
+ /**
64
+ * STEP 4: Find user by provider account
65
+ * Looks up user via connected_accounts table
66
+ *
67
+ * @param provider - Provider name (e.g., 'clerk', 'google')
68
+ * @param providerAccountId - Provider's user ID
69
+ * @returns Promise resolving to User or null if not found
70
+ */
71
+ async findByProviderAccount(provider: string, providerAccountId: string): Promise<User | null> {
72
+ const { data: accountData, error: accountError } = await this.supabase
73
+ .from('connected_accounts')
74
+ .select('user_id')
75
+ .eq('provider', provider)
76
+ .eq('provider_account_id', providerAccountId)
77
+ .single();
78
+ if (accountError || !accountData) return null;
79
+ return this.findById(accountData.user_id);
80
+ }
81
+ /**
82
+ * STEP 5: Create new user
83
+ *
84
+ * @param data - User creation data
85
+ * @returns Promise resolving to created User
86
+ * @throws Error if creation fails
87
+ */
88
+ async create(data: CreateUserData): Promise<User> {
89
+ const insertData = this.transformToDbFormat(data);
90
+ const { data: userData, error } = await this.supabase
91
+ .from('users')
92
+ .insert(insertData)
93
+ .select()
94
+ .single();
95
+ if (error || !userData) throw new Error(`Failed to create user: ${error?.message}`);
96
+ return this.mapToUser(userData);
97
+ }
98
+ /**
99
+ * STEP 6: Update user
100
+ * Only updates provided fields, sets updated_at automatically
101
+ *
102
+ * @param id - User UUID
103
+ * @param data - Partial update data
104
+ * @returns Promise resolving to updated User
105
+ * @throws Error if update fails
106
+ */
107
+ async update(id: string, data: UpdateUserData): Promise<User> {
108
+ const updateData = this.buildUpdateData(data);
109
+ const { data: userData, error } = await this.supabase
110
+ .from('users')
111
+ .update(updateData)
112
+ .eq('id', id)
113
+ .select()
114
+ .single();
115
+ if (error || !userData) throw new Error(`Failed to update user: ${error?.message}`);
116
+ return this.mapToUser(userData);
117
+ }
118
+ /**
119
+ * STEP 7: Delete user
120
+ * Cascades to related records (sessions, roles, etc.)
121
+ *
122
+ * @param id - User UUID
123
+ * @throws Error if deletion fails
124
+ */
125
+ async delete(id: string): Promise<void> {
126
+ const { error } = await this.supabase
127
+ .from('users')
128
+ .delete()
129
+ .eq('id', id);
130
+ if (error) throw new Error(`Failed to delete user: ${error.message}`);
131
+ }
132
+ /**
133
+ * Find user by email and password for backoffice authentication
134
+ */
135
+ async findByCredentials(email: string, passwordHash: string): Promise<User | null> {
136
+ const { data, error } = await this.supabase
137
+ .from('users')
138
+ .select('*')
139
+ .eq('email', email)
140
+ .eq('password_hash', passwordHash)
141
+ .single();
142
+ if (error || !data) return null;
143
+ return this.mapToUser(data);
144
+ }
145
+ /**
146
+ * Assign role to user
147
+ */
148
+ async assignRole(userId: string, role: string, assignedBy?: string): Promise<void> {
149
+ const { error } = await this.supabase
150
+ .from('user_roles')
151
+ .insert({
152
+ user_id: userId,
153
+ role: role,
154
+ assigned_by: assignedBy,
155
+ status: 'ACTIVE'
156
+ });
157
+ if (error) throw new Error(`Failed to assign role: ${error.message}`);
158
+ }
159
+ /**
160
+ * Remove role from user
161
+ */
162
+ async removeRole(userId: string, role: string): Promise<void> {
163
+ const { error } = await this.supabase
164
+ .from('user_roles')
165
+ .delete()
166
+ .eq('user_id', userId)
167
+ .eq('role', role);
168
+ if (error) throw new Error(`Failed to remove role: ${error.message}`);
169
+ }
170
+ /**
171
+ * Get user roles
172
+ */
173
+ async getUserRoles(userId: string): Promise<string[]> {
174
+ const { data, error } = await this.supabase
175
+ .from('user_roles')
176
+ .select('role')
177
+ .eq('user_id', userId)
178
+ .eq('status', 'ACTIVE');
179
+ if (error || !data) return [];
180
+ return data.map((row: { role: string; }) => row.role);
181
+ }
182
+ /**
183
+ * Check permission for roles
184
+ */
185
+ async checkPermission(roles: string[], permission: string): Promise<boolean> {
186
+ // Implementation would check roles table for permissions
187
+ // For now, return basic role hierarchy
188
+ return roles.includes('ADMIN') || roles.includes(permission);
189
+ }
190
+ /**
191
+ * Update last login timestamp
192
+ */
193
+ async updateLastLogin(userId: string): Promise<void> {
194
+ const { error } = await this.supabase
195
+ .from('users')
196
+ .update({ last_login_at: new Date() })
197
+ .eq('id', userId);
198
+ if (error) throw new Error(`Failed to update last login: ${error.message}`);
199
+ }
200
+ /**
201
+ * Find user by Clerk ID
202
+ * Query users WHERE clerk_user_id = clerkId
203
+ * @param clerkId - Clerk user identifier
204
+ * @returns User or null if not found
205
+ */
206
+ async findByClerkId(clerkId: string): Promise<User | null> {
207
+ const { data, error } = await this.supabase
208
+ .from('users')
209
+ .select('*')
210
+ .eq('clerk_user_id', clerkId)
211
+ .single();
212
+ if (error || !data) return null;
213
+ return this.mapToUser(data);
214
+ }
215
+ /**
216
+ * Find user by wallet address
217
+ * Query via connected_accounts WHERE provider = 'web3'
218
+ * @param address - Wallet address
219
+ * @returns User or null if not found
220
+ */
221
+ async findByWalletAddress(address: string): Promise<User | null> {
222
+ const { data: accountData, error: accountError } = await this.supabase
223
+ .from('connected_accounts')
224
+ .select('user_id')
225
+ .eq('provider_type', 'WEB3')
226
+ .eq('wallet_address', address)
227
+ .eq('is_active', true)
228
+ .single();
229
+ if (accountError || !accountData) return null;
230
+ return this.findById(accountData.user_id);
231
+ }
232
+ /**
233
+ * Update onboarding status
234
+ * UPDATE onboarding_completed_at (if complete)
235
+ * @param userId - User identifier
236
+ * @param status - Onboarding status ('completed' | 'pending' | 'skipped')
237
+ * @returns Updated user
238
+ */
239
+ async updateOnboardingStatus(userId: string, status: string): Promise<User> {
240
+ const updateData: Partial<AuthUser> = {
241
+ updatedAt: new Date()
242
+ };
243
+ if (status === 'completed') {
244
+ updateData.updatedAt = new Date();
245
+ }
246
+ const { data: userData, error } = await this.supabase
247
+ .from('users')
248
+ .update(updateData)
249
+ .eq('id', userId)
250
+ .select()
251
+ .single();
252
+ if (error || !userData) throw new Error(`Failed to update onboarding status: ${error?.message}`);
253
+ return this.mapToUser(userData);
254
+ }
255
+ /**
256
+ * Transform camelCase DTO to snake_case database format
257
+ * @private
258
+ */
259
+ private transformToDbFormat(data: CreateUserData): Record<string, string | AUTHPROVIDER | undefined> {
260
+ return {
261
+ email: data.email,
262
+ clerk_user_id: data.clerkUserId,
263
+ auth_provider: data.authProvider ?? 'EMAIL',
264
+ first_name: data.firstName,
265
+ last_name: data.lastName,
266
+ display_name: data.displayName,
267
+ phone_number: data.phoneNumber,
268
+ };
269
+ }
270
+ /**
271
+ * Build update data with only defined fields
272
+ * @private
273
+ */
274
+ private buildUpdateData(data: UpdateUserData): Record<string, string> {
275
+ const result: Record<string, string> = { updated_at: new Date().toString() };
276
+ Object.entries(data).forEach(([key, value]) => {
277
+ if (value !== undefined) {
278
+ const dbKey = this.camelToSnake(key);
279
+ result[dbKey] = value;
280
+ }
281
+ });
282
+ return result;
283
+ }
284
+ /**
285
+ * Convert camelCase to snake_case
286
+ * @private
287
+ */
288
+ private camelToSnake(str: string): string {
289
+ return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
290
+ }
291
+ /**
292
+ * Map database row to User interface
293
+ * @private
294
+ */
295
+ private mapToUser(data:User): User {
296
+ return {
297
+ id: data.id,
298
+ email: data.email,
299
+ clerkUserId: data.clerkUserId,
300
+ authProvider: data.authProvider,
301
+ firstName: data.firstName,
302
+ lastName: data.lastName,
303
+ displayName: data.displayName,
304
+ avatarUrl: undefined,
305
+ phoneNumber: data.phoneNumber,
306
+ isActive: data.isActive ?? true,
307
+ isVerified: data.isVerified ?? false,
308
+ createdAt: new Date(data.createdAt),
309
+ updatedAt: new Date(data.updatedAt),
310
+ lastLoginAt: data.lastLoginAt ? new Date(data.lastLoginAt) : undefined,
311
+ };
312
+ }
313
+ }
314
+
315
+
316
+
317
+
318
+
319
+
320
+
@@ -0,0 +1,2 @@
1
+ export * from './sign-in.flow';
2
+ export * from "./sign-up.flow"