@lukoweb/apitogo 0.2.35 → 0.2.37

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.
@@ -1,412 +1,412 @@
1
- import {
2
- createClient,
3
- type Provider,
4
- type Session,
5
- type SupabaseClient,
6
- } from "@supabase/supabase-js";
7
- import type { SupabaseAuthenticationConfig } from "../../../config/config.js";
8
- import { ZudokuError } from "../../util/invariant.js";
9
- import { joinUrl } from "../../util/joinUrl.js";
10
- import type {
11
- AuthActionContext,
12
- AuthActionOptions,
13
- AuthenticationPlugin,
14
- AuthenticationProviderInitializer,
15
- } from "../authentication.js";
16
- import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
17
- import { SignOut } from "../components/SignOut.js";
18
- import { AuthorizationError } from "../errors.js";
19
- import { type UserProfile, useAuthState } from "../state.js";
20
- import { EmailVerificationUi } from "../ui/EmailVerificationUi.js";
21
- import {
22
- ZudokuPasswordResetUi,
23
- ZudokuPasswordUpdateUi,
24
- ZudokuSignInUi,
25
- ZudokuSignUpUi,
26
- } from "../ui/ZudokuAuthUi.js";
27
-
28
- export type SupabaseProviderData = {
29
- type: "supabase";
30
- session: Session;
31
- };
32
-
33
- declare module "../state.js" {
34
- interface ProviderDataRegistry {
35
- supabase: SupabaseProviderData;
36
- }
37
- }
38
-
39
- class SupabaseAuthenticationProvider
40
- extends CoreAuthenticationPlugin
41
- implements AuthenticationPlugin
42
- {
43
- private readonly client: SupabaseClient;
44
- private readonly config: SupabaseAuthenticationConfig;
45
- private readonly providers: string[];
46
- private readonly enableUsernamePassword: boolean;
47
-
48
- constructor(config: SupabaseAuthenticationConfig) {
49
- const { supabaseUrl, supabaseKey } = config;
50
- super();
51
-
52
- this.client = createClient(supabaseUrl, supabaseKey, {
53
- auth: {
54
- autoRefreshToken: true,
55
- persistSession: true,
56
- },
57
- });
58
- this.config = config;
59
-
60
- // Support both 'provider' (deprecated) and 'providers' config
61
- const configuredProviders = config.provider
62
- ? [config.provider]
63
- : (config.providers ?? []);
64
- this.providers = configuredProviders;
65
- this.enableUsernamePassword = !config.onlyThirdPartyProviders;
66
-
67
- this.client.auth.onAuthStateChange(async (event, session) => {
68
- if (session && (event === "SIGNED_IN" || event === "TOKEN_REFRESHED")) {
69
- await this.updateUserState(session);
70
- } else if (event === "SIGNED_OUT") {
71
- useAuthState.getState().setLoggedOut();
72
- }
73
- });
74
- }
75
-
76
- private async updateUserState(session: Session) {
77
- const { user } = session;
78
-
79
- const profile: UserProfile = {
80
- sub: user.id,
81
- email: user.email,
82
- name: user.user_metadata.full_name || user.user_metadata.name,
83
- emailVerified: user.email_confirmed_at != null,
84
- pictureUrl: user.user_metadata.avatar_url,
85
- };
86
-
87
- useAuthState.getState().setLoggedIn({
88
- profile,
89
- providerData: { type: "supabase", session },
90
- });
91
- }
92
-
93
- async getAccessToken(): Promise<string> {
94
- const { data, error } = await this.client.auth.getSession();
95
-
96
- if (error || !data.session) {
97
- throw new AuthorizationError("User is not authenticated");
98
- }
99
-
100
- return data.session.access_token;
101
- }
102
-
103
- async signRequest(request: Request): Promise<Request> {
104
- const accessToken = await this.getAccessToken();
105
- request.headers.set("Authorization", `Bearer ${accessToken}`);
106
- return request;
107
- }
108
-
109
- signUp = async (
110
- { navigate }: AuthActionContext,
111
- { redirectTo }: AuthActionOptions,
112
- ) => {
113
- void navigate(
114
- redirectTo
115
- ? `/signup?redirectTo=${encodeURIComponent(redirectTo)}`
116
- : `/signup`,
117
- );
118
- };
119
-
120
- signIn = async (
121
- { navigate }: AuthActionContext,
122
- { redirectTo }: AuthActionOptions,
123
- ) => {
124
- void navigate(
125
- redirectTo
126
- ? `/signin?redirectTo=${encodeURIComponent(redirectTo)}`
127
- : `/signin`,
128
- );
129
- };
130
-
131
- requestEmailVerification = async (
132
- { navigate }: AuthActionContext,
133
- { redirectTo }: AuthActionOptions,
134
- ) => {
135
- const {
136
- data: { user },
137
- } = await this.client.auth.getUser();
138
- if (!user || !user.email) {
139
- throw new ZudokuError("User is not authenticated", {
140
- title: "User not authenticated",
141
- });
142
- }
143
-
144
- const { error } = await this.client.auth.resend({
145
- type: "signup",
146
- email: user.email,
147
- });
148
- if (error) {
149
- throw Error(getSupabaseErrorMessage(error), { cause: error });
150
- }
151
-
152
- void navigate(
153
- redirectTo
154
- ? `/verify-email?redirectTo=${encodeURIComponent(redirectTo)}`
155
- : `/verify-email`,
156
- );
157
- };
158
-
159
- private onUsernamePasswordSignIn = async (
160
- email: string,
161
- password: string,
162
- ) => {
163
- useAuthState.setState({ isPending: true });
164
- const { error } = await this.client.auth.signInWithPassword({
165
- email,
166
- password,
167
- });
168
- useAuthState.setState({ isPending: false });
169
- if (error) {
170
- throw Error(getSupabaseErrorMessage(error), { cause: error });
171
- }
172
- };
173
-
174
- private onUsernamePasswordSignUp = async (
175
- email: string,
176
- password: string,
177
- ) => {
178
- useAuthState.setState({ isPending: true });
179
- const { data, error } = await this.client.auth.signUp({
180
- email,
181
- password,
182
- options: {
183
- emailRedirectTo: joinUrl(
184
- window.location.origin,
185
- this.config.basePath,
186
- "/verify-email",
187
- ),
188
- },
189
- });
190
- useAuthState.setState({ isPending: false });
191
- if (error) {
192
- throw new Error(getSupabaseErrorMessage(error), { cause: error });
193
- }
194
-
195
- // If user exists and is confirmed, update state
196
- if (data.user && data.session) {
197
- const profile: UserProfile = {
198
- sub: data.user.id,
199
- email: data.user.email,
200
- name: data.user.user_metadata.full_name || data.user.user_metadata.name,
201
- emailVerified: data.user.email_confirmed_at != null,
202
- pictureUrl: data.user.user_metadata.avatar_url,
203
- };
204
-
205
- useAuthState.getState().setLoggedIn({
206
- profile,
207
- providerData: { type: "supabase", session: data.session },
208
- });
209
- }
210
- };
211
-
212
- private onOAuthSignIn = async (providerId: string) => {
213
- useAuthState.setState({ isPending: true });
214
- const { error } = await this.client.auth.signInWithOAuth({
215
- provider: providerId as Provider,
216
- options: {
217
- redirectTo:
218
- this.config.redirectToAfterSignIn ??
219
- joinUrl(window.location.origin, this.config.basePath),
220
- },
221
- });
222
- if (error) {
223
- useAuthState.setState({ isPending: false });
224
- throw new AuthorizationError(error.message);
225
- }
226
- // Note: OAuth sign-in redirects the page, so isPending stays true
227
- };
228
-
229
- private onPasswordReset = async (email: string) => {
230
- const { error } = await this.client.auth.resetPasswordForEmail(email, {
231
- redirectTo: joinUrl(
232
- window.location.origin,
233
- this.config.basePath,
234
- "/update-password",
235
- ),
236
- });
237
- if (error) {
238
- throw Error(getSupabaseErrorMessage(error), { cause: error });
239
- }
240
- };
241
-
242
- private onPasswordUpdate = async (password: string) => {
243
- const { error } = await this.client.auth.updateUser({ password });
244
- if (error) {
245
- throw Error(getSupabaseErrorMessage(error), { cause: error });
246
- }
247
- };
248
-
249
- private onResendVerification = async () => {
250
- const {
251
- data: { user },
252
- } = await this.client.auth.getUser();
253
- if (!user || !user.email) {
254
- throw new ZudokuError("User is not authenticated", {
255
- title: "User not authenticated",
256
- });
257
- }
258
- const { error } = await this.client.auth.resend({
259
- type: "signup",
260
- email: user.email,
261
- });
262
- if (error) {
263
- throw Error(getSupabaseErrorMessage(error), { cause: error });
264
- }
265
- };
266
-
267
- private onCheckVerification = async (): Promise<boolean> => {
268
- const { data, error } = await this.client.auth.getUser();
269
- if (error || !data.user) {
270
- return false;
271
- }
272
-
273
- const isVerified = data.user.email_confirmed_at != null;
274
-
275
- if (isVerified) {
276
- // Refresh the session to get updated token with verified email
277
- await this.client.auth.refreshSession();
278
- const { data: sessionData } = await this.client.auth.getSession();
279
- if (sessionData.session) {
280
- await this.updateUserState(sessionData.session);
281
- }
282
- }
283
-
284
- return isVerified;
285
- };
286
-
287
- getRoutes = () => {
288
- return [
289
- {
290
- path: "/verify-email",
291
- element: (
292
- <EmailVerificationUi
293
- onResendVerification={this.onResendVerification}
294
- onCheckVerification={this.onCheckVerification}
295
- />
296
- ),
297
- },
298
- {
299
- path: "/reset-password",
300
- element: (
301
- <ZudokuPasswordResetUi onPasswordReset={this.onPasswordReset} />
302
- ),
303
- },
304
- {
305
- path: "/update-password",
306
- element: (
307
- <ZudokuPasswordUpdateUi onPasswordUpdate={this.onPasswordUpdate} />
308
- ),
309
- },
310
- {
311
- path: "/signin",
312
- element: (
313
- <ZudokuSignInUi
314
- providers={this.providers}
315
- enableUsernamePassword={this.enableUsernamePassword}
316
- onOAuthSignIn={this.onOAuthSignIn}
317
- onUsernamePasswordSignIn={this.onUsernamePasswordSignIn}
318
- />
319
- ),
320
- },
321
- {
322
- path: "/signup",
323
- element: (
324
- <ZudokuSignUpUi
325
- providers={this.providers}
326
- enableUsernamePassword={this.enableUsernamePassword}
327
- onOAuthSignUp={this.onOAuthSignIn}
328
- onUsernamePasswordSignUp={this.onUsernamePasswordSignUp}
329
- />
330
- ),
331
- },
332
- {
333
- path: "/signout",
334
- element: <SignOut />,
335
- },
336
- ];
337
- };
338
-
339
- signOut = async () => {
340
- const { error } = await this.client.auth.signOut({ scope: "local" });
341
- if (error) {
342
- // biome-ignore lint/suspicious: Logging is better than not doing anything
343
- console.error("Error signing out", error);
344
- }
345
- useAuthState.getState().setLoggedOut();
346
- };
347
-
348
- onPageLoad = async () => {
349
- const { data, error } = await this.client.auth.getSession();
350
-
351
- if (!error && data.session) {
352
- await this.updateUserState(data.session);
353
- }
354
- };
355
- }
356
-
357
- const supabaseAuth: AuthenticationProviderInitializer<
358
- SupabaseAuthenticationConfig
359
- > = (options) => new SupabaseAuthenticationProvider(options);
360
-
361
- export default supabaseAuth;
362
-
363
- const getSupabaseErrorMessage = (error: unknown): string => {
364
- if (!(error instanceof Error)) {
365
- return "An unexpected error occurred. Please try again.";
366
- }
367
-
368
- const errorMessage = error.message;
369
-
370
- // Map common Supabase error messages to user-friendly messages
371
- if (errorMessage.includes("Invalid login credentials")) {
372
- return "The email and password you entered don't match.";
373
- }
374
- if (errorMessage.includes("Email not confirmed")) {
375
- return "Please verify your email address before signing in.";
376
- }
377
- if (errorMessage.includes("User already registered")) {
378
- return "The email address is already used by another account.";
379
- }
380
- if (
381
- errorMessage.includes("Password should be at least") ||
382
- errorMessage.includes("Password must be at least")
383
- ) {
384
- return "The password must be at least 6 characters long.";
385
- }
386
- if (errorMessage.includes("Invalid email")) {
387
- return "That email address isn't correct.";
388
- }
389
- if (errorMessage.includes("Email rate limit exceeded")) {
390
- return "Too many requests. Please wait a moment and try again.";
391
- }
392
- if (errorMessage.includes("For security purposes")) {
393
- return "For security purposes, please wait a moment before trying again.";
394
- }
395
- if (errorMessage.includes("Unable to validate email address")) {
396
- return "Unable to validate email address. Please check and try again.";
397
- }
398
- if (errorMessage.includes("Signups not allowed")) {
399
- return "Sign ups are not allowed at this time.";
400
- }
401
- if (errorMessage.includes("User not found")) {
402
- return "That email address doesn't match an existing account.";
403
- }
404
- if (errorMessage.includes("New password should be different")) {
405
- return "Your new password must be different from your current password.";
406
- }
407
-
408
- // Return the original message if no mapping found
409
- return (
410
- errorMessage || "An error occurred during authentication. Please try again."
411
- );
412
- };
1
+ import {
2
+ createClient,
3
+ type Provider,
4
+ type Session,
5
+ type SupabaseClient,
6
+ } from "@supabase/supabase-js";
7
+ import type { SupabaseAuthenticationConfig } from "../../../config/config.js";
8
+ import { ZudokuError } from "../../util/invariant.js";
9
+ import { joinUrl } from "../../util/joinUrl.js";
10
+ import type {
11
+ AuthActionContext,
12
+ AuthActionOptions,
13
+ AuthenticationPlugin,
14
+ AuthenticationProviderInitializer,
15
+ } from "../authentication.js";
16
+ import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
17
+ import { SignOut } from "../components/SignOut.js";
18
+ import { AuthorizationError } from "../errors.js";
19
+ import { type UserProfile, useAuthState } from "../state.js";
20
+ import { EmailVerificationUi } from "../ui/EmailVerificationUi.js";
21
+ import {
22
+ ZudokuPasswordResetUi,
23
+ ZudokuPasswordUpdateUi,
24
+ ZudokuSignInUi,
25
+ ZudokuSignUpUi,
26
+ } from "../ui/ZudokuAuthUi.js";
27
+
28
+ export type SupabaseProviderData = {
29
+ type: "supabase";
30
+ session: Session;
31
+ };
32
+
33
+ declare module "../state.js" {
34
+ interface ProviderDataRegistry {
35
+ supabase: SupabaseProviderData;
36
+ }
37
+ }
38
+
39
+ class SupabaseAuthenticationProvider
40
+ extends CoreAuthenticationPlugin
41
+ implements AuthenticationPlugin
42
+ {
43
+ private readonly client: SupabaseClient;
44
+ private readonly config: SupabaseAuthenticationConfig;
45
+ private readonly providers: string[];
46
+ private readonly enableUsernamePassword: boolean;
47
+
48
+ constructor(config: SupabaseAuthenticationConfig) {
49
+ const { supabaseUrl, supabaseKey } = config;
50
+ super();
51
+
52
+ this.client = createClient(supabaseUrl, supabaseKey, {
53
+ auth: {
54
+ autoRefreshToken: true,
55
+ persistSession: true,
56
+ },
57
+ });
58
+ this.config = config;
59
+
60
+ // Support both 'provider' (deprecated) and 'providers' config
61
+ const configuredProviders = config.provider
62
+ ? [config.provider]
63
+ : (config.providers ?? []);
64
+ this.providers = configuredProviders;
65
+ this.enableUsernamePassword = !config.onlyThirdPartyProviders;
66
+
67
+ this.client.auth.onAuthStateChange(async (event, session) => {
68
+ if (session && (event === "SIGNED_IN" || event === "TOKEN_REFRESHED")) {
69
+ await this.updateUserState(session);
70
+ } else if (event === "SIGNED_OUT") {
71
+ useAuthState.getState().setLoggedOut();
72
+ }
73
+ });
74
+ }
75
+
76
+ private async updateUserState(session: Session) {
77
+ const { user } = session;
78
+
79
+ const profile: UserProfile = {
80
+ sub: user.id,
81
+ email: user.email,
82
+ name: user.user_metadata.full_name || user.user_metadata.name,
83
+ emailVerified: user.email_confirmed_at != null,
84
+ pictureUrl: user.user_metadata.avatar_url,
85
+ };
86
+
87
+ useAuthState.getState().setLoggedIn({
88
+ profile,
89
+ providerData: { type: "supabase", session },
90
+ });
91
+ }
92
+
93
+ async getAccessToken(): Promise<string> {
94
+ const { data, error } = await this.client.auth.getSession();
95
+
96
+ if (error || !data.session) {
97
+ throw new AuthorizationError("User is not authenticated");
98
+ }
99
+
100
+ return data.session.access_token;
101
+ }
102
+
103
+ async signRequest(request: Request): Promise<Request> {
104
+ const accessToken = await this.getAccessToken();
105
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
106
+ return request;
107
+ }
108
+
109
+ signUp = async (
110
+ { navigate }: AuthActionContext,
111
+ { redirectTo }: AuthActionOptions,
112
+ ) => {
113
+ void navigate(
114
+ redirectTo
115
+ ? `/signup?redirectTo=${encodeURIComponent(redirectTo)}`
116
+ : `/signup`,
117
+ );
118
+ };
119
+
120
+ signIn = async (
121
+ { navigate }: AuthActionContext,
122
+ { redirectTo }: AuthActionOptions = {},
123
+ ) => {
124
+ void navigate(
125
+ redirectTo
126
+ ? `/signin?redirectTo=${encodeURIComponent(redirectTo)}`
127
+ : `/signin`,
128
+ );
129
+ };
130
+
131
+ requestEmailVerification = async (
132
+ { navigate }: AuthActionContext,
133
+ { redirectTo }: AuthActionOptions,
134
+ ) => {
135
+ const {
136
+ data: { user },
137
+ } = await this.client.auth.getUser();
138
+ if (!user || !user.email) {
139
+ throw new ZudokuError("User is not authenticated", {
140
+ title: "User not authenticated",
141
+ });
142
+ }
143
+
144
+ const { error } = await this.client.auth.resend({
145
+ type: "signup",
146
+ email: user.email,
147
+ });
148
+ if (error) {
149
+ throw Error(getSupabaseErrorMessage(error), { cause: error });
150
+ }
151
+
152
+ void navigate(
153
+ redirectTo
154
+ ? `/verify-email?redirectTo=${encodeURIComponent(redirectTo)}`
155
+ : `/verify-email`,
156
+ );
157
+ };
158
+
159
+ private onUsernamePasswordSignIn = async (
160
+ email: string,
161
+ password: string,
162
+ ) => {
163
+ useAuthState.setState({ isPending: true });
164
+ const { error } = await this.client.auth.signInWithPassword({
165
+ email,
166
+ password,
167
+ });
168
+ useAuthState.setState({ isPending: false });
169
+ if (error) {
170
+ throw Error(getSupabaseErrorMessage(error), { cause: error });
171
+ }
172
+ };
173
+
174
+ private onUsernamePasswordSignUp = async (
175
+ email: string,
176
+ password: string,
177
+ ) => {
178
+ useAuthState.setState({ isPending: true });
179
+ const { data, error } = await this.client.auth.signUp({
180
+ email,
181
+ password,
182
+ options: {
183
+ emailRedirectTo: joinUrl(
184
+ window.location.origin,
185
+ this.config.basePath,
186
+ "/verify-email",
187
+ ),
188
+ },
189
+ });
190
+ useAuthState.setState({ isPending: false });
191
+ if (error) {
192
+ throw new Error(getSupabaseErrorMessage(error), { cause: error });
193
+ }
194
+
195
+ // If user exists and is confirmed, update state
196
+ if (data.user && data.session) {
197
+ const profile: UserProfile = {
198
+ sub: data.user.id,
199
+ email: data.user.email,
200
+ name: data.user.user_metadata.full_name || data.user.user_metadata.name,
201
+ emailVerified: data.user.email_confirmed_at != null,
202
+ pictureUrl: data.user.user_metadata.avatar_url,
203
+ };
204
+
205
+ useAuthState.getState().setLoggedIn({
206
+ profile,
207
+ providerData: { type: "supabase", session: data.session },
208
+ });
209
+ }
210
+ };
211
+
212
+ private onOAuthSignIn = async (providerId: string) => {
213
+ useAuthState.setState({ isPending: true });
214
+ const { error } = await this.client.auth.signInWithOAuth({
215
+ provider: providerId as Provider,
216
+ options: {
217
+ redirectTo:
218
+ this.config.redirectToAfterSignIn ??
219
+ joinUrl(window.location.origin, this.config.basePath),
220
+ },
221
+ });
222
+ if (error) {
223
+ useAuthState.setState({ isPending: false });
224
+ throw new AuthorizationError(error.message);
225
+ }
226
+ // Note: OAuth sign-in redirects the page, so isPending stays true
227
+ };
228
+
229
+ private onPasswordReset = async (email: string) => {
230
+ const { error } = await this.client.auth.resetPasswordForEmail(email, {
231
+ redirectTo: joinUrl(
232
+ window.location.origin,
233
+ this.config.basePath,
234
+ "/update-password",
235
+ ),
236
+ });
237
+ if (error) {
238
+ throw Error(getSupabaseErrorMessage(error), { cause: error });
239
+ }
240
+ };
241
+
242
+ private onPasswordUpdate = async (password: string) => {
243
+ const { error } = await this.client.auth.updateUser({ password });
244
+ if (error) {
245
+ throw Error(getSupabaseErrorMessage(error), { cause: error });
246
+ }
247
+ };
248
+
249
+ private onResendVerification = async () => {
250
+ const {
251
+ data: { user },
252
+ } = await this.client.auth.getUser();
253
+ if (!user || !user.email) {
254
+ throw new ZudokuError("User is not authenticated", {
255
+ title: "User not authenticated",
256
+ });
257
+ }
258
+ const { error } = await this.client.auth.resend({
259
+ type: "signup",
260
+ email: user.email,
261
+ });
262
+ if (error) {
263
+ throw Error(getSupabaseErrorMessage(error), { cause: error });
264
+ }
265
+ };
266
+
267
+ private onCheckVerification = async (): Promise<boolean> => {
268
+ const { data, error } = await this.client.auth.getUser();
269
+ if (error || !data.user) {
270
+ return false;
271
+ }
272
+
273
+ const isVerified = data.user.email_confirmed_at != null;
274
+
275
+ if (isVerified) {
276
+ // Refresh the session to get updated token with verified email
277
+ await this.client.auth.refreshSession();
278
+ const { data: sessionData } = await this.client.auth.getSession();
279
+ if (sessionData.session) {
280
+ await this.updateUserState(sessionData.session);
281
+ }
282
+ }
283
+
284
+ return isVerified;
285
+ };
286
+
287
+ getRoutes = () => {
288
+ return [
289
+ {
290
+ path: "/verify-email",
291
+ element: (
292
+ <EmailVerificationUi
293
+ onResendVerification={this.onResendVerification}
294
+ onCheckVerification={this.onCheckVerification}
295
+ />
296
+ ),
297
+ },
298
+ {
299
+ path: "/reset-password",
300
+ element: (
301
+ <ZudokuPasswordResetUi onPasswordReset={this.onPasswordReset} />
302
+ ),
303
+ },
304
+ {
305
+ path: "/update-password",
306
+ element: (
307
+ <ZudokuPasswordUpdateUi onPasswordUpdate={this.onPasswordUpdate} />
308
+ ),
309
+ },
310
+ {
311
+ path: "/signin",
312
+ element: (
313
+ <ZudokuSignInUi
314
+ providers={this.providers}
315
+ enableUsernamePassword={this.enableUsernamePassword}
316
+ onOAuthSignIn={this.onOAuthSignIn}
317
+ onUsernamePasswordSignIn={this.onUsernamePasswordSignIn}
318
+ />
319
+ ),
320
+ },
321
+ {
322
+ path: "/signup",
323
+ element: (
324
+ <ZudokuSignUpUi
325
+ providers={this.providers}
326
+ enableUsernamePassword={this.enableUsernamePassword}
327
+ onOAuthSignUp={this.onOAuthSignIn}
328
+ onUsernamePasswordSignUp={this.onUsernamePasswordSignUp}
329
+ />
330
+ ),
331
+ },
332
+ {
333
+ path: "/signout",
334
+ element: <SignOut />,
335
+ },
336
+ ];
337
+ };
338
+
339
+ signOut = async () => {
340
+ const { error } = await this.client.auth.signOut({ scope: "local" });
341
+ if (error) {
342
+ // biome-ignore lint/suspicious: Logging is better than not doing anything
343
+ console.error("Error signing out", error);
344
+ }
345
+ useAuthState.getState().setLoggedOut();
346
+ };
347
+
348
+ onPageLoad = async () => {
349
+ const { data, error } = await this.client.auth.getSession();
350
+
351
+ if (!error && data.session) {
352
+ await this.updateUserState(data.session);
353
+ }
354
+ };
355
+ }
356
+
357
+ const supabaseAuth: AuthenticationProviderInitializer<
358
+ SupabaseAuthenticationConfig
359
+ > = (options) => new SupabaseAuthenticationProvider(options);
360
+
361
+ export default supabaseAuth;
362
+
363
+ const getSupabaseErrorMessage = (error: unknown): string => {
364
+ if (!(error instanceof Error)) {
365
+ return "An unexpected error occurred. Please try again.";
366
+ }
367
+
368
+ const errorMessage = error.message;
369
+
370
+ // Map common Supabase error messages to user-friendly messages
371
+ if (errorMessage.includes("Invalid login credentials")) {
372
+ return "The email and password you entered don't match.";
373
+ }
374
+ if (errorMessage.includes("Email not confirmed")) {
375
+ return "Please verify your email address before signing in.";
376
+ }
377
+ if (errorMessage.includes("User already registered")) {
378
+ return "The email address is already used by another account.";
379
+ }
380
+ if (
381
+ errorMessage.includes("Password should be at least") ||
382
+ errorMessage.includes("Password must be at least")
383
+ ) {
384
+ return "The password must be at least 6 characters long.";
385
+ }
386
+ if (errorMessage.includes("Invalid email")) {
387
+ return "That email address isn't correct.";
388
+ }
389
+ if (errorMessage.includes("Email rate limit exceeded")) {
390
+ return "Too many requests. Please wait a moment and try again.";
391
+ }
392
+ if (errorMessage.includes("For security purposes")) {
393
+ return "For security purposes, please wait a moment before trying again.";
394
+ }
395
+ if (errorMessage.includes("Unable to validate email address")) {
396
+ return "Unable to validate email address. Please check and try again.";
397
+ }
398
+ if (errorMessage.includes("Signups not allowed")) {
399
+ return "Sign ups are not allowed at this time.";
400
+ }
401
+ if (errorMessage.includes("User not found")) {
402
+ return "That email address doesn't match an existing account.";
403
+ }
404
+ if (errorMessage.includes("New password should be different")) {
405
+ return "Your new password must be different from your current password.";
406
+ }
407
+
408
+ // Return the original message if no mapping found
409
+ return (
410
+ errorMessage || "An error occurred during authentication. Please try again."
411
+ );
412
+ };