@ttoss/react-auth-strapi 0.7.1 → 0.8.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.
package/README.md CHANGED
@@ -51,6 +51,7 @@ function AuthenticatedApp() {
51
51
 
52
52
  - **Zero configuration**: Pre-configured Strapi authentication handlers
53
53
  - **Complete auth flows**: Sign in, sign up, forgot password, email confirmation
54
+ - **Social sign-in**: Google/Facebook via Strapi's Users & Permissions providers
54
55
  - **Token management**: Automatic refresh token handling with secure storage
55
56
  - **Error handling**: Built-in notifications for authentication errors
56
57
  - **Email verification**: Automatic resend confirmation emails for unverified accounts
@@ -88,6 +89,8 @@ flowchart LR
88
89
  - `POST /auth/send-email-confirmation` - Resend email confirmation
89
90
  - `POST /auth/local/refresh` - Refresh access token
90
91
  - `GET /users/me` - Get current user profile
92
+ - `GET /connect/:provider` - Kicks off a Users & Permissions social provider (e.g. Google)
93
+ - `GET /auth/:provider/callback` - Exchanges the provider's callback query string for a Strapi JWT
91
94
 
92
95
  ## API Reference
93
96
 
@@ -135,6 +138,7 @@ The component automatically handles:
135
138
  sideContent: <BrandingContent />,
136
139
  sideContentPosition: 'left',
137
140
  }}
141
+ socialProviders={['Google']} // Optional: renders social sign-in buttons
138
142
  />
139
143
  ```
140
144
 
@@ -289,6 +293,99 @@ sequenceDiagram
289
293
  A-->>U: Display Sign In screen with success notification
290
294
  ```
291
295
 
296
+ ## Social Sign-In (Google, Facebook)
297
+
298
+ ### Strapi Provider Setup
299
+
300
+ In Strapi Admin → **Settings → Users & Permissions → Providers**, enable the provider (e.g. Google), set its Client ID/Secret, and set **"The redirect URL to your front-end app"** to the route this package's callback handler is mounted at, e.g.:
301
+
302
+ ```
303
+ https://your-app.com/connect/google/redirect
304
+ ```
305
+
306
+ ### Enabling the Button
307
+
308
+ Pass `socialProviders` to `Auth` to render the "Continue with Google" button on the sign-in/sign-up screens. Clicking it redirects the browser to `${apiUrl}/connect/google`, which Strapi uses to start the OAuth flow:
309
+
310
+ ```tsx
311
+ import { Auth } from '@ttoss/react-auth-strapi';
312
+
313
+ function LoginPage() {
314
+ return <Auth socialProviders={['Google']} />;
315
+ }
316
+ ```
317
+
318
+ ### Handling the Redirect
319
+
320
+ Strapi completes the OAuth exchange itself and redirects the browser back to the URL configured in the admin panel, appending a callback query string (e.g. `?access_token=...`). Mount `AuthSocialSignInCallback` at that route to forward the query string to `${apiUrl}/auth/google/callback` and authenticate the user:
321
+
322
+ ```tsx
323
+ import { useNavigate } from 'react-router-dom';
324
+ import { AuthSocialSignInCallback } from '@ttoss/react-auth-strapi';
325
+
326
+ function GoogleRedirectPage() {
327
+ const navigate = useNavigate();
328
+
329
+ return (
330
+ <AuthSocialSignInCallback
331
+ provider="google"
332
+ onSuccess={() => {
333
+ return navigate('/');
334
+ }}
335
+ onError={() => {
336
+ return navigate('/auth');
337
+ }}
338
+ >
339
+ <p>Signing you in…</p>
340
+ </AuthSocialSignInCallback>
341
+ );
342
+ }
343
+ ```
344
+
345
+ ```tsx
346
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
347
+ import { AuthProvider } from '@ttoss/react-auth-strapi';
348
+
349
+ function App() {
350
+ return (
351
+ <AuthProvider apiUrl="https://your-strapi-api.com/api">
352
+ <BrowserRouter>
353
+ <Routes>
354
+ <Route path="/auth" element={<AuthPage />} />
355
+ <Route
356
+ path="/connect/google/redirect"
357
+ element={<GoogleRedirectPage />}
358
+ />
359
+ <Route path="/" element={<HomePage />} />
360
+ </Routes>
361
+ </BrowserRouter>
362
+ </AuthProvider>
363
+ );
364
+ }
365
+ ```
366
+
367
+ ### Flow
368
+
369
+ ```mermaid
370
+ sequenceDiagram
371
+ participant U as User
372
+ participant A as Auth Component
373
+ participant S as Strapi API
374
+ participant G as Google
375
+ participant C as AuthSocialSignInCallback
376
+
377
+ U->>A: Click "Continue with Google"
378
+ A->>S: Redirect to /connect/google
379
+ S->>G: Redirect to Google OAuth consent
380
+ G-->>S: Redirect back with provider code
381
+ S-->>U: Redirect to /connect/google/redirect?access_token=...
382
+ U->>C: Browser loads redirect route
383
+ C->>S: GET /auth/google/callback?access_token=...
384
+ S-->>C: { jwt, refreshToken, user }
385
+ C->>C: setAuthData({ user, tokens, isAuthenticated: true })
386
+ C-->>U: onSuccess() navigates into the app
387
+ ```
388
+
292
389
  ## Error Handling
293
390
 
294
391
  The package integrates with `@ttoss/react-notifications` to display authentication errors:
package/dist/index.cjs CHANGED
@@ -149,6 +149,148 @@ var useAuth = () => {
149
149
 
150
150
  //#endregion
151
151
  //#region src/Auth.tsx
152
+ var GENERIC_ERROR_MESSAGE$1 = "Unable to connect to the server. Please check your connection.";
153
+ var signInWithEmail = async ({
154
+ apiUrl,
155
+ email,
156
+ password,
157
+ notifyError,
158
+ onUnconfirmedEmail
159
+ }) => {
160
+ const response = await fetch(`${apiUrl}/auth/local`, {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json"
164
+ },
165
+ body: JSON.stringify({
166
+ identifier: email,
167
+ password
168
+ })
169
+ });
170
+ const data = await response.json();
171
+ if (!response.ok) {
172
+ if (data.error?.message === "Your account email is not confirmed") {
173
+ await onUnconfirmedEmail();
174
+ return;
175
+ }
176
+ notifyError({
177
+ title: "Sign in failed",
178
+ message: data.error?.message || "An error occurred during sign in."
179
+ });
180
+ return;
181
+ }
182
+ storage.setRefreshToken(data.refreshToken);
183
+ return {
184
+ user: {
185
+ id: data.user.id,
186
+ email: data.user.email,
187
+ emailVerified: data.user.confirmed
188
+ },
189
+ tokens: {
190
+ accessToken: data.jwt,
191
+ refreshToken: data.refreshToken
192
+ },
193
+ isAuthenticated: true
194
+ };
195
+ };
196
+ var resendEmailConfirmation = async ({
197
+ apiUrl,
198
+ email,
199
+ notifyError
200
+ }) => {
201
+ const response = await fetch(`${apiUrl}/auth/send-email-confirmation`, {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json"
205
+ },
206
+ body: JSON.stringify({
207
+ email
208
+ })
209
+ });
210
+ if (!response.ok) {
211
+ notifyError({
212
+ title: "Resend confirmation email failed",
213
+ message: (await response.json()).error?.message || "An error occurred while resending the confirmation email."
214
+ });
215
+ return false;
216
+ }
217
+ return true;
218
+ };
219
+ var registerWithEmail = async ({
220
+ apiUrl,
221
+ email,
222
+ password,
223
+ notifyError
224
+ }) => {
225
+ const response = await fetch(`${apiUrl}/auth/local/register`, {
226
+ method: "POST",
227
+ headers: {
228
+ "Content-Type": "application/json"
229
+ },
230
+ body: JSON.stringify({
231
+ username: email,
232
+ email,
233
+ password
234
+ })
235
+ });
236
+ if (!response.ok) {
237
+ notifyError({
238
+ title: "Sign up failed",
239
+ message: (await response.json()).error?.message || "An error occurred during sign up."
240
+ });
241
+ return false;
242
+ }
243
+ return true;
244
+ };
245
+ var requestPasswordReset = async ({
246
+ apiUrl,
247
+ email,
248
+ notifyError
249
+ }) => {
250
+ const response = await fetch(`${apiUrl}/auth/forgot-password`, {
251
+ method: "POST",
252
+ headers: {
253
+ "Content-Type": "application/json"
254
+ },
255
+ body: JSON.stringify({
256
+ email
257
+ })
258
+ });
259
+ if (!response.ok) {
260
+ notifyError({
261
+ title: "Forgot password failed",
262
+ message: (await response.json()).error?.message || "An error occurred during forgot password."
263
+ });
264
+ return false;
265
+ }
266
+ return true;
267
+ };
268
+ var resetPasswordWithCode = async ({
269
+ apiUrl,
270
+ code,
271
+ newPassword,
272
+ notifyError
273
+ }) => {
274
+ const response = await fetch(`${apiUrl}/auth/reset-password`, {
275
+ method: "POST",
276
+ headers: {
277
+ "Content-Type": "application/json"
278
+ },
279
+ body: JSON.stringify({
280
+ code,
281
+ password: newPassword,
282
+ passwordConfirmation: newPassword
283
+ })
284
+ });
285
+ if (!response.ok) {
286
+ notifyError({
287
+ title: "Reset password failed",
288
+ message: (await response.json()).error?.message || "An error occurred during password reset."
289
+ });
290
+ return false;
291
+ }
292
+ return true;
293
+ };
152
294
  var Auth = props => {
153
295
  const {
154
296
  setAuthData,
@@ -161,193 +303,124 @@ var Auth = props => {
161
303
  const {
162
304
  addNotification
163
305
  } = (0, _ttoss_react_notifications.useNotifications)();
306
+ const notifyError = react.useCallback(({
307
+ title,
308
+ message
309
+ }) => {
310
+ addNotification({
311
+ title,
312
+ message: message || GENERIC_ERROR_MESSAGE$1,
313
+ type: "error"
314
+ });
315
+ }, [addNotification]);
164
316
  const onSignIn = react.useCallback(async ({
165
317
  email,
166
318
  password
167
319
  }) => {
168
320
  try {
169
- const response = await fetch(`${apiUrl}/auth/local`, {
170
- method: "POST",
171
- headers: {
172
- "Content-Type": "application/json"
173
- },
174
- body: JSON.stringify({
175
- identifier: email,
176
- password
177
- })
178
- });
179
- const data = await response.json();
180
- if (!response.ok) {
181
- if (data.error?.message === "Your account email is not confirmed") {
182
- const resendResponse = await fetch(`${apiUrl}/auth/send-email-confirmation`, {
183
- method: "POST",
184
- headers: {
185
- "Content-Type": "application/json"
186
- },
187
- body: JSON.stringify({
188
- email
189
- })
190
- });
191
- if (!resendResponse.ok) {
192
- addNotification({
193
- title: "Resend confirmation email failed",
194
- message: (await resendResponse.json()).error?.message || "An error occurred while resending the confirmation email.",
195
- type: "error"
196
- });
197
- return;
198
- }
199
- setScreen({
321
+ const authData = await signInWithEmail({
322
+ apiUrl,
323
+ email,
324
+ password,
325
+ notifyError,
326
+ onUnconfirmedEmail: async () => {
327
+ if (await resendEmailConfirmation({
328
+ apiUrl,
329
+ email,
330
+ notifyError
331
+ })) setScreen({
200
332
  value: "confirmSignUpCheckEmail"
201
333
  });
202
- return;
203
334
  }
204
- addNotification({
205
- title: "Sign in failed",
206
- message: data.error?.message || "An error occurred during sign in.",
207
- type: "error"
208
- });
209
- return;
210
- }
211
- storage.setRefreshToken(data.refreshToken);
212
- setAuthData({
213
- user: {
214
- id: data.user.id,
215
- email: data.user.email,
216
- emailVerified: data.user.confirmed
217
- },
218
- tokens: {
219
- accessToken: data.jwt,
220
- refreshToken: data.refreshToken
221
- },
222
- isAuthenticated: true
223
335
  });
336
+ if (authData) setAuthData(authData);
224
337
  } catch {
225
- addNotification({
226
- title: "Network Error",
227
- message: "Unable to connect to the server. Please check your connection.",
228
- type: "error"
338
+ notifyError({
339
+ title: "Network Error"
229
340
  });
230
341
  }
231
- }, [setAuthData, setScreen, addNotification, apiUrl]);
342
+ }, [setAuthData, setScreen, notifyError, apiUrl]);
232
343
  const onSignUp = react.useCallback(async ({
233
344
  email,
234
345
  password
235
346
  }) => {
236
347
  try {
237
- const response = await fetch(`${apiUrl}/auth/local/register`, {
238
- method: "POST",
239
- headers: {
240
- "Content-Type": "application/json"
241
- },
242
- body: JSON.stringify({
243
- username: email,
244
- email,
245
- password
246
- })
247
- });
248
- const data = await response.json();
249
- if (!response.ok) {
250
- addNotification({
251
- title: "Sign up failed",
252
- message: data.error?.message || "An error occurred during sign up.",
253
- type: "error"
254
- });
255
- return;
256
- }
257
- setScreen({
348
+ if (await registerWithEmail({
349
+ apiUrl,
350
+ email,
351
+ password,
352
+ notifyError
353
+ })) setScreen({
258
354
  value: "confirmSignUpCheckEmail"
259
355
  });
260
356
  } catch {
261
- addNotification({
262
- title: "Network Error",
263
- message: "Unable to connect to the server. Please check your connection.",
264
- type: "error"
357
+ notifyError({
358
+ title: "Network Error"
265
359
  });
266
360
  }
267
- }, [addNotification, setScreen, apiUrl]);
361
+ }, [setScreen, notifyError, apiUrl]);
268
362
  const onForgotPassword = react.useCallback(async ({
269
363
  email
270
364
  }) => {
271
365
  try {
272
- const response = await fetch(`${apiUrl}/auth/forgot-password`, {
273
- method: "POST",
274
- headers: {
275
- "Content-Type": "application/json"
276
- },
277
- body: JSON.stringify({
278
- email
279
- })
280
- });
281
- const data = await response.json();
282
- if (!response.ok) {
283
- addNotification({
284
- title: "Forgot password failed",
285
- message: data.error?.message || "An error occurred during forgot password.",
286
- type: "error"
287
- });
288
- return;
289
- }
290
- setScreen({
366
+ if (await requestPasswordReset({
367
+ apiUrl,
368
+ email,
369
+ notifyError
370
+ })) setScreen({
291
371
  value: "confirmResetPassword",
292
372
  context: {
293
373
  email
294
374
  }
295
375
  });
296
376
  } catch {
297
- addNotification({
298
- title: "Network Error",
299
- message: "Unable to connect to the server. Please check your connection.",
300
- type: "error"
377
+ notifyError({
378
+ title: "Network Error"
301
379
  });
302
380
  }
303
- }, [addNotification, setScreen, apiUrl]);
381
+ }, [setScreen, notifyError, apiUrl]);
304
382
  const onForgotPasswordResetPassword = react.useCallback(async ({
305
- email: _email,
306
383
  code,
307
384
  newPassword
308
385
  }) => {
309
386
  try {
310
- const response = await fetch(`${apiUrl}/auth/reset-password`, {
311
- method: "POST",
312
- headers: {
313
- "Content-Type": "application/json"
314
- },
315
- body: JSON.stringify({
316
- code,
317
- password: newPassword,
318
- passwordConfirmation: newPassword
319
- })
320
- });
321
- const data = await response.json();
322
- if (!response.ok) {
387
+ if (await resetPasswordWithCode({
388
+ apiUrl,
389
+ code,
390
+ newPassword,
391
+ notifyError
392
+ })) {
323
393
  addNotification({
324
- title: "Reset password failed",
325
- message: data.error?.message || "An error occurred during password reset.",
326
- type: "error"
394
+ title: "Password reset successful",
395
+ message: "You can now sign in with your new password.",
396
+ type: "success"
397
+ });
398
+ setScreen({
399
+ value: "signIn"
327
400
  });
328
- return;
329
401
  }
330
- addNotification({
331
- title: "Password reset successful",
332
- message: "You can now sign in with your new password.",
333
- type: "success"
334
- });
335
- setScreen({
336
- value: "signIn"
337
- });
338
402
  } catch {
339
- addNotification({
340
- title: "Network Error",
341
- message: "Unable to connect to the server. Please check your connection.",
342
- type: "error"
403
+ notifyError({
404
+ title: "Network Error"
343
405
  });
344
406
  }
345
- }, [addNotification, setScreen, apiUrl]);
407
+ }, [setScreen, notifyError, addNotification, apiUrl]);
346
408
  const onConfirmSignUpCheckEmail = react.useCallback(async () => {
347
409
  setScreen({
348
410
  value: "signIn"
349
411
  });
350
412
  }, [setScreen]);
413
+ const onSocialSignIn = react.useCallback(({
414
+ provider
415
+ }) => {
416
+ /**
417
+ * Kicks off Strapi's Users & Permissions provider flow. Strapi handles
418
+ * the OAuth exchange and redirects back to the frontend's configured
419
+ * redirect URL (e.g. `/connect/google/redirect`), which
420
+ * `AuthSocialSignInCallback` consumes.
421
+ */
422
+ window.location.href = `${apiUrl}/connect/${provider.toLowerCase()}`;
423
+ }, [apiUrl]);
351
424
  return /* @__PURE__ */(0, react_jsx_runtime.jsx)(_ttoss_react_auth_core.Auth, {
352
425
  logo: props.logo,
353
426
  layout: props.layout,
@@ -357,11 +430,87 @@ var Auth = props => {
357
430
  onSignUp,
358
431
  onForgotPassword,
359
432
  onForgotPasswordResetPassword,
360
- onConfirmSignUpCheckEmail
433
+ onConfirmSignUpCheckEmail,
434
+ socialProviders: props.socialProviders,
435
+ onSocialSignIn: props.socialProviders ? onSocialSignIn : void 0
436
+ });
437
+ };
438
+
439
+ //#endregion
440
+ //#region src/AuthSocialSignInCallback.tsx
441
+ var GENERIC_ERROR_MESSAGE = "An error occurred during social sign in.";
442
+ var exchangeSocialSignInCallback = async ({
443
+ apiUrl,
444
+ provider
445
+ }) => {
446
+ const response = await fetch(`${apiUrl}/auth/${provider}/callback${window.location.search}`);
447
+ const data = await response.json();
448
+ if (!response.ok) throw new Error(data.error?.message || GENERIC_ERROR_MESSAGE);
449
+ if (data.refreshToken) storage.setRefreshToken(data.refreshToken);
450
+ return {
451
+ user: {
452
+ id: data.user.id,
453
+ email: data.user.email,
454
+ emailVerified: data.user.confirmed
455
+ },
456
+ tokens: {
457
+ accessToken: data.jwt,
458
+ refreshToken: data.refreshToken
459
+ },
460
+ isAuthenticated: true
461
+ };
462
+ };
463
+ /**
464
+ * Mount this at the redirect route configured in Strapi's provider settings
465
+ * (e.g. `/connect/google/redirect`). It reads the callback query string
466
+ * Strapi appended to the redirect, forwards it to
467
+ * `${apiUrl}/auth/:provider/callback`, and authenticates the user with the
468
+ * resulting JWT — the same way `Auth`'s `onSignIn` does for email/password.
469
+ */
470
+ var AuthSocialSignInCallback = props => {
471
+ const {
472
+ provider,
473
+ onSuccess,
474
+ onError,
475
+ children
476
+ } = props;
477
+ const {
478
+ apiUrl,
479
+ setAuthData
480
+ } = useAuth();
481
+ const {
482
+ addNotification
483
+ } = (0, _ttoss_react_notifications.useNotifications)();
484
+ react.useEffect(() => {
485
+ let cancelled = false;
486
+ exchangeSocialSignInCallback({
487
+ apiUrl,
488
+ provider
489
+ }).then(authData => {
490
+ if (cancelled) return;
491
+ setAuthData(authData);
492
+ onSuccess?.();
493
+ }).catch(error => {
494
+ if (cancelled) return;
495
+ const message = error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE;
496
+ addNotification({
497
+ title: "Sign in failed",
498
+ message,
499
+ type: "error"
500
+ });
501
+ onError?.(message);
502
+ });
503
+ return () => {
504
+ cancelled = true;
505
+ };
506
+ }, [apiUrl, provider, setAuthData, addNotification, onSuccess, onError]);
507
+ return /* @__PURE__ */(0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {
508
+ children
361
509
  });
362
510
  };
363
511
 
364
512
  //#endregion
365
513
  exports.Auth = Auth;
366
514
  exports.AuthProvider = AuthProvider;
515
+ exports.AuthSocialSignInCallback = AuthSocialSignInCallback;
367
516
  exports.useAuth = useAuth;
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ import { AuthProps, AuthScreen } from "@ttoss/react-auth-core";
3
3
  import * as React from "react";
4
4
 
5
5
  //#region src/Auth.d.ts
6
- declare const Auth: (props: Pick<AuthProps, "logo" | "layout"> & {
6
+ declare const Auth: (props: Pick<AuthProps, "logo" | "layout" | "socialProviders"> & {
7
7
  initialScreen?: AuthScreen;
8
8
  }) => import("react/jsx-runtime").JSX.Element;
9
9
  //#endregion
@@ -20,4 +20,20 @@ declare const useAuth: () => {
20
20
  apiUrl: string;
21
21
  };
22
22
  //#endregion
23
- export { Auth, AuthProvider, type AuthScreen, useAuth };
23
+ //#region src/AuthSocialSignInCallback.d.ts
24
+ type AuthSocialSignInCallbackProps = {
25
+ /** Strapi provider slug, e.g. `"google"`, matching `/connect/:provider`. */provider: string; /** Called after the callback exchange sets the user as authenticated. */
26
+ onSuccess?: () => void; /** Called if the callback exchange fails, with a user-facing message. */
27
+ onError?: (message: string) => void; /** Rendered while the exchange is in flight (e.g. a spinner). */
28
+ children?: React.ReactNode;
29
+ };
30
+ /**
31
+ * Mount this at the redirect route configured in Strapi's provider settings
32
+ * (e.g. `/connect/google/redirect`). It reads the callback query string
33
+ * Strapi appended to the redirect, forwards it to
34
+ * `${apiUrl}/auth/:provider/callback`, and authenticates the user with the
35
+ * resulting JWT — the same way `Auth`'s `onSignIn` does for email/password.
36
+ */
37
+ declare const AuthSocialSignInCallback: (props: AuthSocialSignInCallbackProps) => import("react/jsx-runtime").JSX.Element;
38
+ //#endregion
39
+ export { Auth, AuthProvider, type AuthScreen, AuthSocialSignInCallback, AuthSocialSignInCallbackProps, useAuth };
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@ import { AuthProps, AuthScreen } from "@ttoss/react-auth-core";
3
3
  import * as React from "react";
4
4
 
5
5
  //#region src/Auth.d.ts
6
- declare const Auth: (props: Pick<AuthProps, "logo" | "layout"> & {
6
+ declare const Auth: (props: Pick<AuthProps, "logo" | "layout" | "socialProviders"> & {
7
7
  initialScreen?: AuthScreen;
8
8
  }) => import("react/jsx-runtime").JSX.Element;
9
9
  //#endregion
@@ -20,4 +20,20 @@ declare const useAuth: () => {
20
20
  apiUrl: string;
21
21
  };
22
22
  //#endregion
23
- export { Auth, AuthProvider, type AuthScreen, useAuth };
23
+ //#region src/AuthSocialSignInCallback.d.ts
24
+ type AuthSocialSignInCallbackProps = {
25
+ /** Strapi provider slug, e.g. `"google"`, matching `/connect/:provider`. */provider: string; /** Called after the callback exchange sets the user as authenticated. */
26
+ onSuccess?: () => void; /** Called if the callback exchange fails, with a user-facing message. */
27
+ onError?: (message: string) => void; /** Rendered while the exchange is in flight (e.g. a spinner). */
28
+ children?: React.ReactNode;
29
+ };
30
+ /**
31
+ * Mount this at the redirect route configured in Strapi's provider settings
32
+ * (e.g. `/connect/google/redirect`). It reads the callback query string
33
+ * Strapi appended to the redirect, forwards it to
34
+ * `${apiUrl}/auth/:provider/callback`, and authenticates the user with the
35
+ * resulting JWT — the same way `Auth`'s `onSignIn` does for email/password.
36
+ */
37
+ declare const AuthSocialSignInCallback: (props: AuthSocialSignInCallbackProps) => import("react/jsx-runtime").JSX.Element;
38
+ //#endregion
39
+ export { Auth, AuthProvider, type AuthScreen, AuthSocialSignInCallback, AuthSocialSignInCallbackProps, useAuth };
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Auth as Auth$1, AuthProvider as AuthProvider$1, useAuth as useAuth$1, useAuthScreen } from "@ttoss/react-auth-core";
3
3
  import { useNotifications } from "@ttoss/react-notifications";
4
4
  import * as React from "react";
5
- import { jsx } from "react/jsx-runtime";
5
+ import { Fragment, jsx } from "react/jsx-runtime";
6
6
 
7
7
  //#region src/storage.ts
8
8
  var AUTH_STORAGE_REFRESH_TOKEN_KEY = "ttoss-strapi-auth-refresh-token";
@@ -118,6 +118,148 @@ var useAuth = () => {
118
118
 
119
119
  //#endregion
120
120
  //#region src/Auth.tsx
121
+ var GENERIC_ERROR_MESSAGE$1 = "Unable to connect to the server. Please check your connection.";
122
+ var signInWithEmail = async ({
123
+ apiUrl,
124
+ email,
125
+ password,
126
+ notifyError,
127
+ onUnconfirmedEmail
128
+ }) => {
129
+ const response = await fetch(`${apiUrl}/auth/local`, {
130
+ method: "POST",
131
+ headers: {
132
+ "Content-Type": "application/json"
133
+ },
134
+ body: JSON.stringify({
135
+ identifier: email,
136
+ password
137
+ })
138
+ });
139
+ const data = await response.json();
140
+ if (!response.ok) {
141
+ if (data.error?.message === "Your account email is not confirmed") {
142
+ await onUnconfirmedEmail();
143
+ return;
144
+ }
145
+ notifyError({
146
+ title: "Sign in failed",
147
+ message: data.error?.message || "An error occurred during sign in."
148
+ });
149
+ return;
150
+ }
151
+ storage.setRefreshToken(data.refreshToken);
152
+ return {
153
+ user: {
154
+ id: data.user.id,
155
+ email: data.user.email,
156
+ emailVerified: data.user.confirmed
157
+ },
158
+ tokens: {
159
+ accessToken: data.jwt,
160
+ refreshToken: data.refreshToken
161
+ },
162
+ isAuthenticated: true
163
+ };
164
+ };
165
+ var resendEmailConfirmation = async ({
166
+ apiUrl,
167
+ email,
168
+ notifyError
169
+ }) => {
170
+ const response = await fetch(`${apiUrl}/auth/send-email-confirmation`, {
171
+ method: "POST",
172
+ headers: {
173
+ "Content-Type": "application/json"
174
+ },
175
+ body: JSON.stringify({
176
+ email
177
+ })
178
+ });
179
+ if (!response.ok) {
180
+ notifyError({
181
+ title: "Resend confirmation email failed",
182
+ message: (await response.json()).error?.message || "An error occurred while resending the confirmation email."
183
+ });
184
+ return false;
185
+ }
186
+ return true;
187
+ };
188
+ var registerWithEmail = async ({
189
+ apiUrl,
190
+ email,
191
+ password,
192
+ notifyError
193
+ }) => {
194
+ const response = await fetch(`${apiUrl}/auth/local/register`, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json"
198
+ },
199
+ body: JSON.stringify({
200
+ username: email,
201
+ email,
202
+ password
203
+ })
204
+ });
205
+ if (!response.ok) {
206
+ notifyError({
207
+ title: "Sign up failed",
208
+ message: (await response.json()).error?.message || "An error occurred during sign up."
209
+ });
210
+ return false;
211
+ }
212
+ return true;
213
+ };
214
+ var requestPasswordReset = async ({
215
+ apiUrl,
216
+ email,
217
+ notifyError
218
+ }) => {
219
+ const response = await fetch(`${apiUrl}/auth/forgot-password`, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json"
223
+ },
224
+ body: JSON.stringify({
225
+ email
226
+ })
227
+ });
228
+ if (!response.ok) {
229
+ notifyError({
230
+ title: "Forgot password failed",
231
+ message: (await response.json()).error?.message || "An error occurred during forgot password."
232
+ });
233
+ return false;
234
+ }
235
+ return true;
236
+ };
237
+ var resetPasswordWithCode = async ({
238
+ apiUrl,
239
+ code,
240
+ newPassword,
241
+ notifyError
242
+ }) => {
243
+ const response = await fetch(`${apiUrl}/auth/reset-password`, {
244
+ method: "POST",
245
+ headers: {
246
+ "Content-Type": "application/json"
247
+ },
248
+ body: JSON.stringify({
249
+ code,
250
+ password: newPassword,
251
+ passwordConfirmation: newPassword
252
+ })
253
+ });
254
+ if (!response.ok) {
255
+ notifyError({
256
+ title: "Reset password failed",
257
+ message: (await response.json()).error?.message || "An error occurred during password reset."
258
+ });
259
+ return false;
260
+ }
261
+ return true;
262
+ };
121
263
  var Auth = props => {
122
264
  const {
123
265
  setAuthData,
@@ -130,193 +272,124 @@ var Auth = props => {
130
272
  const {
131
273
  addNotification
132
274
  } = useNotifications();
275
+ const notifyError = React.useCallback(({
276
+ title,
277
+ message
278
+ }) => {
279
+ addNotification({
280
+ title,
281
+ message: message || GENERIC_ERROR_MESSAGE$1,
282
+ type: "error"
283
+ });
284
+ }, [addNotification]);
133
285
  const onSignIn = React.useCallback(async ({
134
286
  email,
135
287
  password
136
288
  }) => {
137
289
  try {
138
- const response = await fetch(`${apiUrl}/auth/local`, {
139
- method: "POST",
140
- headers: {
141
- "Content-Type": "application/json"
142
- },
143
- body: JSON.stringify({
144
- identifier: email,
145
- password
146
- })
147
- });
148
- const data = await response.json();
149
- if (!response.ok) {
150
- if (data.error?.message === "Your account email is not confirmed") {
151
- const resendResponse = await fetch(`${apiUrl}/auth/send-email-confirmation`, {
152
- method: "POST",
153
- headers: {
154
- "Content-Type": "application/json"
155
- },
156
- body: JSON.stringify({
157
- email
158
- })
159
- });
160
- if (!resendResponse.ok) {
161
- addNotification({
162
- title: "Resend confirmation email failed",
163
- message: (await resendResponse.json()).error?.message || "An error occurred while resending the confirmation email.",
164
- type: "error"
165
- });
166
- return;
167
- }
168
- setScreen({
290
+ const authData = await signInWithEmail({
291
+ apiUrl,
292
+ email,
293
+ password,
294
+ notifyError,
295
+ onUnconfirmedEmail: async () => {
296
+ if (await resendEmailConfirmation({
297
+ apiUrl,
298
+ email,
299
+ notifyError
300
+ })) setScreen({
169
301
  value: "confirmSignUpCheckEmail"
170
302
  });
171
- return;
172
303
  }
173
- addNotification({
174
- title: "Sign in failed",
175
- message: data.error?.message || "An error occurred during sign in.",
176
- type: "error"
177
- });
178
- return;
179
- }
180
- storage.setRefreshToken(data.refreshToken);
181
- setAuthData({
182
- user: {
183
- id: data.user.id,
184
- email: data.user.email,
185
- emailVerified: data.user.confirmed
186
- },
187
- tokens: {
188
- accessToken: data.jwt,
189
- refreshToken: data.refreshToken
190
- },
191
- isAuthenticated: true
192
304
  });
305
+ if (authData) setAuthData(authData);
193
306
  } catch {
194
- addNotification({
195
- title: "Network Error",
196
- message: "Unable to connect to the server. Please check your connection.",
197
- type: "error"
307
+ notifyError({
308
+ title: "Network Error"
198
309
  });
199
310
  }
200
- }, [setAuthData, setScreen, addNotification, apiUrl]);
311
+ }, [setAuthData, setScreen, notifyError, apiUrl]);
201
312
  const onSignUp = React.useCallback(async ({
202
313
  email,
203
314
  password
204
315
  }) => {
205
316
  try {
206
- const response = await fetch(`${apiUrl}/auth/local/register`, {
207
- method: "POST",
208
- headers: {
209
- "Content-Type": "application/json"
210
- },
211
- body: JSON.stringify({
212
- username: email,
213
- email,
214
- password
215
- })
216
- });
217
- const data = await response.json();
218
- if (!response.ok) {
219
- addNotification({
220
- title: "Sign up failed",
221
- message: data.error?.message || "An error occurred during sign up.",
222
- type: "error"
223
- });
224
- return;
225
- }
226
- setScreen({
317
+ if (await registerWithEmail({
318
+ apiUrl,
319
+ email,
320
+ password,
321
+ notifyError
322
+ })) setScreen({
227
323
  value: "confirmSignUpCheckEmail"
228
324
  });
229
325
  } catch {
230
- addNotification({
231
- title: "Network Error",
232
- message: "Unable to connect to the server. Please check your connection.",
233
- type: "error"
326
+ notifyError({
327
+ title: "Network Error"
234
328
  });
235
329
  }
236
- }, [addNotification, setScreen, apiUrl]);
330
+ }, [setScreen, notifyError, apiUrl]);
237
331
  const onForgotPassword = React.useCallback(async ({
238
332
  email
239
333
  }) => {
240
334
  try {
241
- const response = await fetch(`${apiUrl}/auth/forgot-password`, {
242
- method: "POST",
243
- headers: {
244
- "Content-Type": "application/json"
245
- },
246
- body: JSON.stringify({
247
- email
248
- })
249
- });
250
- const data = await response.json();
251
- if (!response.ok) {
252
- addNotification({
253
- title: "Forgot password failed",
254
- message: data.error?.message || "An error occurred during forgot password.",
255
- type: "error"
256
- });
257
- return;
258
- }
259
- setScreen({
335
+ if (await requestPasswordReset({
336
+ apiUrl,
337
+ email,
338
+ notifyError
339
+ })) setScreen({
260
340
  value: "confirmResetPassword",
261
341
  context: {
262
342
  email
263
343
  }
264
344
  });
265
345
  } catch {
266
- addNotification({
267
- title: "Network Error",
268
- message: "Unable to connect to the server. Please check your connection.",
269
- type: "error"
346
+ notifyError({
347
+ title: "Network Error"
270
348
  });
271
349
  }
272
- }, [addNotification, setScreen, apiUrl]);
350
+ }, [setScreen, notifyError, apiUrl]);
273
351
  const onForgotPasswordResetPassword = React.useCallback(async ({
274
- email: _email,
275
352
  code,
276
353
  newPassword
277
354
  }) => {
278
355
  try {
279
- const response = await fetch(`${apiUrl}/auth/reset-password`, {
280
- method: "POST",
281
- headers: {
282
- "Content-Type": "application/json"
283
- },
284
- body: JSON.stringify({
285
- code,
286
- password: newPassword,
287
- passwordConfirmation: newPassword
288
- })
289
- });
290
- const data = await response.json();
291
- if (!response.ok) {
356
+ if (await resetPasswordWithCode({
357
+ apiUrl,
358
+ code,
359
+ newPassword,
360
+ notifyError
361
+ })) {
292
362
  addNotification({
293
- title: "Reset password failed",
294
- message: data.error?.message || "An error occurred during password reset.",
295
- type: "error"
363
+ title: "Password reset successful",
364
+ message: "You can now sign in with your new password.",
365
+ type: "success"
366
+ });
367
+ setScreen({
368
+ value: "signIn"
296
369
  });
297
- return;
298
370
  }
299
- addNotification({
300
- title: "Password reset successful",
301
- message: "You can now sign in with your new password.",
302
- type: "success"
303
- });
304
- setScreen({
305
- value: "signIn"
306
- });
307
371
  } catch {
308
- addNotification({
309
- title: "Network Error",
310
- message: "Unable to connect to the server. Please check your connection.",
311
- type: "error"
372
+ notifyError({
373
+ title: "Network Error"
312
374
  });
313
375
  }
314
- }, [addNotification, setScreen, apiUrl]);
376
+ }, [setScreen, notifyError, addNotification, apiUrl]);
315
377
  const onConfirmSignUpCheckEmail = React.useCallback(async () => {
316
378
  setScreen({
317
379
  value: "signIn"
318
380
  });
319
381
  }, [setScreen]);
382
+ const onSocialSignIn = React.useCallback(({
383
+ provider
384
+ }) => {
385
+ /**
386
+ * Kicks off Strapi's Users & Permissions provider flow. Strapi handles
387
+ * the OAuth exchange and redirects back to the frontend's configured
388
+ * redirect URL (e.g. `/connect/google/redirect`), which
389
+ * `AuthSocialSignInCallback` consumes.
390
+ */
391
+ window.location.href = `${apiUrl}/connect/${provider.toLowerCase()}`;
392
+ }, [apiUrl]);
320
393
  return /* @__PURE__ */jsx(Auth$1, {
321
394
  logo: props.logo,
322
395
  layout: props.layout,
@@ -326,9 +399,84 @@ var Auth = props => {
326
399
  onSignUp,
327
400
  onForgotPassword,
328
401
  onForgotPasswordResetPassword,
329
- onConfirmSignUpCheckEmail
402
+ onConfirmSignUpCheckEmail,
403
+ socialProviders: props.socialProviders,
404
+ onSocialSignIn: props.socialProviders ? onSocialSignIn : void 0
405
+ });
406
+ };
407
+
408
+ //#endregion
409
+ //#region src/AuthSocialSignInCallback.tsx
410
+ var GENERIC_ERROR_MESSAGE = "An error occurred during social sign in.";
411
+ var exchangeSocialSignInCallback = async ({
412
+ apiUrl,
413
+ provider
414
+ }) => {
415
+ const response = await fetch(`${apiUrl}/auth/${provider}/callback${window.location.search}`);
416
+ const data = await response.json();
417
+ if (!response.ok) throw new Error(data.error?.message || GENERIC_ERROR_MESSAGE);
418
+ if (data.refreshToken) storage.setRefreshToken(data.refreshToken);
419
+ return {
420
+ user: {
421
+ id: data.user.id,
422
+ email: data.user.email,
423
+ emailVerified: data.user.confirmed
424
+ },
425
+ tokens: {
426
+ accessToken: data.jwt,
427
+ refreshToken: data.refreshToken
428
+ },
429
+ isAuthenticated: true
430
+ };
431
+ };
432
+ /**
433
+ * Mount this at the redirect route configured in Strapi's provider settings
434
+ * (e.g. `/connect/google/redirect`). It reads the callback query string
435
+ * Strapi appended to the redirect, forwards it to
436
+ * `${apiUrl}/auth/:provider/callback`, and authenticates the user with the
437
+ * resulting JWT — the same way `Auth`'s `onSignIn` does for email/password.
438
+ */
439
+ var AuthSocialSignInCallback = props => {
440
+ const {
441
+ provider,
442
+ onSuccess,
443
+ onError,
444
+ children
445
+ } = props;
446
+ const {
447
+ apiUrl,
448
+ setAuthData
449
+ } = useAuth();
450
+ const {
451
+ addNotification
452
+ } = useNotifications();
453
+ React.useEffect(() => {
454
+ let cancelled = false;
455
+ exchangeSocialSignInCallback({
456
+ apiUrl,
457
+ provider
458
+ }).then(authData => {
459
+ if (cancelled) return;
460
+ setAuthData(authData);
461
+ onSuccess?.();
462
+ }).catch(error => {
463
+ if (cancelled) return;
464
+ const message = error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE;
465
+ addNotification({
466
+ title: "Sign in failed",
467
+ message,
468
+ type: "error"
469
+ });
470
+ onError?.(message);
471
+ });
472
+ return () => {
473
+ cancelled = true;
474
+ };
475
+ }, [apiUrl, provider, setAuthData, addNotification, onSuccess, onError]);
476
+ return /* @__PURE__ */jsx(Fragment, {
477
+ children
330
478
  });
331
479
  };
332
480
 
333
481
  //#endregion
334
- export { Auth, AuthProvider, useAuth };
482
+ export { Auth, AuthProvider, AuthSocialSignInCallback, useAuth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/react-auth-strapi",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Authentication components and abstractions for React apps using Strapi.",
5
5
  "keywords": [
6
6
  "React",
@@ -39,19 +39,19 @@
39
39
  "jest": "^30.4.2",
40
40
  "react": "^19.2.6",
41
41
  "tsdown": "^0.22.2",
42
+ "@ttoss/config": "^1.38.0",
42
43
  "@ttoss/i18n-cli": "^0.9.0",
43
- "@ttoss/react-i18n": "^2.3.1",
44
+ "@ttoss/react-auth-core": "^0.7.1",
44
45
  "@ttoss/react-notifications": "^2.8.6",
45
46
  "@ttoss/test-utils": "^4.2.19",
46
47
  "@ttoss/ui": "^6.9.30",
47
- "@ttoss/react-auth-core": "^0.7.1",
48
- "@ttoss/config": "^1.38.0"
48
+ "@ttoss/react-i18n": "^2.3.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": ">=16.8.0",
52
52
  "@ttoss/react-auth-core": "^0.7.1",
53
- "@ttoss/react-i18n": "^2.3.1",
54
- "@ttoss/react-notifications": "^2.8.6"
53
+ "@ttoss/react-notifications": "^2.8.6",
54
+ "@ttoss/react-i18n": "^2.3.1"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",