@ttoss/react-auth-strapi 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Terezinha Tech Operations (ttoss)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # @ttoss/react-auth-strapi
2
+
3
+ Pre-configured authentication components for React applications using Strapi CMS backend with [strapi-plugin-refresh-token](https://github.com/redon2/strapi-plugin-refresh-token).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ttoss/react-auth-strapi @ttoss/react-notifications
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```tsx
14
+ import { AuthProvider, Auth, useAuth } from '@ttoss/react-auth-strapi';
15
+ import { NotificationProvider } from '@ttoss/react-notifications';
16
+
17
+ // 1. Wrap your app with providers
18
+ function App() {
19
+ return (
20
+ <NotificationProvider>
21
+ <AuthProvider apiUrl="https://your-strapi-api.com/api">
22
+ <AuthenticatedApp />
23
+ </AuthProvider>
24
+ </NotificationProvider>
25
+ );
26
+ }
27
+
28
+ // 2. Use the pre-built Auth component
29
+ function LoginPage() {
30
+ return <Auth />;
31
+ }
32
+
33
+ // 3. Access authentication state
34
+ function AuthenticatedApp() {
35
+ const { isAuthenticated, user, signOut } = useAuth();
36
+
37
+ if (!isAuthenticated) {
38
+ return <LoginPage />;
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <p>Welcome, {user?.email}</p>
44
+ <button onClick={signOut}>Sign Out</button>
45
+ </div>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - **Zero configuration**: Pre-configured Strapi authentication handlers
53
+ - **Complete auth flows**: Sign in, sign up, forgot password, email confirmation
54
+ - **Token management**: Automatic refresh token handling with secure storage
55
+ - **Error handling**: Built-in notifications for authentication errors
56
+ - **Email verification**: Automatic resend confirmation emails for unverified accounts
57
+
58
+ ## Strapi Backend Setup
59
+
60
+ This package requires Strapi with specific plugins and configuration:
61
+
62
+ ### Required Strapi Plugins
63
+
64
+ 1. **Users & Permissions plugin** (built-in)
65
+ 2. **[strapi-plugin-refresh-token](https://github.com/redon2/strapi-plugin-refresh-token)** for token refresh functionality
66
+
67
+ ```bash
68
+ npm install strapi-plugin-refresh-token
69
+ ```
70
+
71
+ ### Strapi API Endpoints
72
+
73
+ The package expects these Strapi endpoints to be available:
74
+
75
+ ```mermaid
76
+ flowchart LR
77
+ A[Auth Component] --> B[ /auth/local]
78
+ A --> C[ /auth/local/register]
79
+ A --> D[ /auth/forgot-password]
80
+ A --> E[ /auth/send-email-confirmation]
81
+ A --> F[ /auth/local/refresh]
82
+ A --> G[ /users/me]
83
+ ```
84
+
85
+ - `POST /auth/local` - Sign in with email/password
86
+ - `POST /auth/local/register` - User registration
87
+ - `POST /auth/forgot-password` - Request password reset
88
+ - `POST /auth/send-email-confirmation` - Resend email confirmation
89
+ - `POST /auth/local/refresh` - Refresh access token
90
+ - `GET /users/me` - Get current user profile
91
+
92
+ ## API Reference
93
+
94
+ ### AuthProvider
95
+
96
+ ```tsx
97
+ <AuthProvider
98
+ apiUrl="https://your-strapi-api.com/api" // Your Strapi API base URL
99
+ >
100
+ {children}
101
+ </AuthProvider>
102
+ ```
103
+
104
+ The provider automatically:
105
+
106
+ - Manages refresh tokens in localStorage
107
+ - Handles token refresh on app startup
108
+ - Provides authentication context to child components
109
+
110
+ ### Auth Component
111
+
112
+ Pre-configured authentication flow with all handlers implemented:
113
+
114
+ ```tsx
115
+ <Auth />
116
+ ```
117
+
118
+ The component automatically handles:
119
+
120
+ - **Sign in**: Authenticates with Strapi local strategy
121
+ - **Sign up**: Registers new users and triggers email confirmation
122
+ - **Forgot password**: Initiates password reset flow
123
+ - **Email confirmation**: Manages email verification process
124
+ - **Error notifications**: Shows user-friendly error messages
125
+
126
+ ### useAuth Hook
127
+
128
+ Enhanced version of the core useAuth hook with Strapi-specific context:
129
+
130
+ ```tsx
131
+ const {
132
+ isAuthenticated, // boolean
133
+ user, // { id: string, email: string, emailVerified?: boolean }
134
+ tokens, // { accessToken: string, refreshToken: string }
135
+ signOut, // () => Promise<void>
136
+ setAuthData, // Update auth state manually
137
+ apiUrl, // Strapi API URL from context
138
+ } = useAuth();
139
+ ```
140
+
141
+ ## Authentication Flow
142
+
143
+ ```mermaid
144
+ sequenceDiagram
145
+ participant U as User
146
+ participant A as Auth Component
147
+ participant S as Strapi API
148
+ participant L as LocalStorage
149
+
150
+ U->>A: Enter credentials
151
+ A->>S: POST /auth/local
152
+ S-->>A: { jwt, refreshToken, user }
153
+ A->>L: Store refreshToken
154
+ A->>A: setAuthData({ user, tokens, isAuthenticated: true })
155
+
156
+ Note over A,L: On app restart
157
+ A->>L: Get stored refreshToken
158
+ A->>S: POST /auth/local/refresh
159
+ S-->>A: { jwt, refreshToken }
160
+ A->>S: GET /users/me (with jwt)
161
+ S-->>A: user data
162
+ A->>A: Restore authentication state
163
+ ```
164
+
165
+ ## Error Handling
166
+
167
+ The package integrates with `@ttoss/react-notifications` to display authentication errors:
168
+
169
+ - **Invalid credentials**: "Sign in failed" notification
170
+ - **Unconfirmed email**: Automatically resends confirmation email
171
+ - **Registration errors**: "Sign up failed" with specific error message
172
+ - **Network errors**: Generic error messages for API failures
173
+
174
+ ## Token Security
175
+
176
+ ### Refresh Token Storage
177
+
178
+ - Refresh tokens are stored in `localStorage` with the key `ttoss-strapi-auth-refresh-token`
179
+ - Access tokens are kept in memory only (React state)
180
+ - Tokens are automatically cleared on sign out or authentication errors
181
+
182
+ ### Automatic Token Refresh
183
+
184
+ - On app startup, attempts to refresh tokens if a refresh token exists
185
+ - If refresh fails, user is automatically signed out
186
+ - No manual token management required
187
+
188
+ ## Dependencies
189
+
190
+ - `@ttoss/react-auth-core`: Core authentication abstractions
191
+ - `@ttoss/react-notifications`: Error and success notifications
192
+
193
+ ## Type Definitions
194
+
195
+ ```tsx
196
+ type StrapiUser = {
197
+ id: string;
198
+ email: string;
199
+ emailVerified?: boolean;
200
+ };
201
+
202
+ type StrapiTokens = {
203
+ accessToken: string; // JWT from Strapi
204
+ refreshToken: string; // Refresh token from plugin
205
+ };
206
+
207
+ type StrapiAuthResponse = {
208
+ jwt: string;
209
+ refreshToken: string;
210
+ user: {
211
+ id: string;
212
+ email: string;
213
+ confirmed: boolean;
214
+ };
215
+ };
216
+ ```
@@ -0,0 +1,291 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+
3
+ // src/Auth.tsx
4
+ import { Auth as AuthCore, useAuthScreen } from "@ttoss/react-auth-core";
5
+ import { useNotifications } from "@ttoss/react-notifications";
6
+ import * as React2 from "react";
7
+
8
+ // src/AuthProvider.tsx
9
+ import { AuthProvider as AuthProviderCore, useAuth as useAuthCore } from "@ttoss/react-auth-core";
10
+ import * as React from "react";
11
+
12
+ // src/storage.ts
13
+ var AUTH_STORAGE_REFRESH_TOKEN_KEY = "ttoss-strapi-auth-refresh-token";
14
+ var isBrowser = typeof window !== "undefined";
15
+ var getLocalStorage = () => {
16
+ try {
17
+ return isBrowser ? window.localStorage : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
22
+ var storage = {
23
+ getRefreshToken: () => {
24
+ const ls = getLocalStorage();
25
+ if (!ls) return null;
26
+ try {
27
+ return ls.getItem(AUTH_STORAGE_REFRESH_TOKEN_KEY);
28
+ } catch {
29
+ return null;
30
+ }
31
+ },
32
+ setRefreshToken: refreshToken => {
33
+ const ls = getLocalStorage();
34
+ if (!ls) return;
35
+ try {
36
+ ls.setItem(AUTH_STORAGE_REFRESH_TOKEN_KEY, refreshToken);
37
+ } catch {}
38
+ },
39
+ clearRefreshToken: () => {
40
+ const ls = getLocalStorage();
41
+ if (!ls) return;
42
+ try {
43
+ ls.removeItem(AUTH_STORAGE_REFRESH_TOKEN_KEY);
44
+ } catch {}
45
+ }
46
+ };
47
+
48
+ // src/AuthProvider.tsx
49
+ import { jsx } from "react/jsx-runtime";
50
+ var AuthContext = React.createContext({
51
+ apiUrl: ""
52
+ });
53
+ var AuthProvider = props => {
54
+ const getAuthData = React.useCallback(async () => {
55
+ try {
56
+ const refreshToken = storage.getRefreshToken();
57
+ if (!refreshToken) {
58
+ return null;
59
+ }
60
+ const refreshResponse = await fetch(`${props.apiUrl}/auth/local/refresh`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json"
64
+ },
65
+ body: JSON.stringify({
66
+ refreshToken
67
+ })
68
+ });
69
+ const refreshData = await refreshResponse.json();
70
+ if ("error" in refreshData) {
71
+ storage.clearRefreshToken();
72
+ return null;
73
+ }
74
+ const meResponse = await fetch(`${props.apiUrl}/users/me`, {
75
+ headers: {
76
+ Authorization: `Bearer ${refreshData.jwt}`
77
+ }
78
+ });
79
+ const data = await meResponse.json();
80
+ if (!meResponse.ok) {
81
+ storage.clearRefreshToken();
82
+ return null;
83
+ }
84
+ return {
85
+ user: {
86
+ id: data.id,
87
+ email: data.email,
88
+ emailVerified: data.confirmed ?? data.emailVerified ?? data.user?.confirmed
89
+ },
90
+ tokens: {
91
+ accessToken: refreshData.jwt,
92
+ refreshToken: refreshData.refreshToken
93
+ },
94
+ isAuthenticated: true
95
+ };
96
+ } catch {
97
+ storage.clearRefreshToken();
98
+ return null;
99
+ }
100
+ }, [props.apiUrl]);
101
+ const signOut = React.useCallback(async () => {
102
+ storage.clearRefreshToken();
103
+ }, []);
104
+ return /* @__PURE__ */jsx(AuthContext.Provider, {
105
+ value: {
106
+ apiUrl: props.apiUrl
107
+ },
108
+ children: /* @__PURE__ */jsx(AuthProviderCore, {
109
+ getAuthData,
110
+ signOut,
111
+ children: props.children
112
+ })
113
+ });
114
+ };
115
+ var useAuth = () => {
116
+ const authCore = useAuthCore();
117
+ const {
118
+ apiUrl
119
+ } = React.useContext(AuthContext);
120
+ return {
121
+ apiUrl,
122
+ ...authCore
123
+ };
124
+ };
125
+
126
+ // src/Auth.tsx
127
+ import { jsx as jsx2 } from "react/jsx-runtime";
128
+ var Auth = () => {
129
+ const {
130
+ setAuthData,
131
+ apiUrl
132
+ } = useAuth();
133
+ const {
134
+ screen,
135
+ setScreen
136
+ } = useAuthScreen();
137
+ const {
138
+ addNotification
139
+ } = useNotifications();
140
+ const onSignIn = React2.useCallback(async ({
141
+ email,
142
+ password
143
+ }) => {
144
+ try {
145
+ const response = await fetch(`${apiUrl}/auth/local`, {
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/json"
149
+ },
150
+ body: JSON.stringify({
151
+ identifier: email,
152
+ password
153
+ })
154
+ });
155
+ const data = await response.json();
156
+ if (!response.ok) {
157
+ const errorMessage = data.error?.message;
158
+ if (errorMessage === "Your account email is not confirmed") {
159
+ const resendResponse = await fetch(`${apiUrl}/auth/send-email-confirmation`, {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/json"
163
+ },
164
+ body: JSON.stringify({
165
+ email
166
+ })
167
+ });
168
+ if (!resendResponse.ok) {
169
+ const resendData = await resendResponse.json();
170
+ addNotification({
171
+ title: "Resend confirmation email failed",
172
+ message: resendData.error?.message || "An error occurred while resending the confirmation email.",
173
+ type: "error"
174
+ });
175
+ return;
176
+ }
177
+ setScreen({
178
+ value: "confirmSignUpCheckEmail"
179
+ });
180
+ return;
181
+ }
182
+ addNotification({
183
+ title: "Sign in failed",
184
+ message: data.error?.message || "An error occurred during sign in.",
185
+ type: "error"
186
+ });
187
+ return;
188
+ }
189
+ storage.setRefreshToken(data.refreshToken);
190
+ setAuthData({
191
+ user: {
192
+ id: data.user.id,
193
+ email: data.user.email,
194
+ emailVerified: data.user.confirmed
195
+ },
196
+ tokens: {
197
+ accessToken: data.jwt,
198
+ refreshToken: data.refreshToken
199
+ },
200
+ isAuthenticated: true
201
+ });
202
+ } catch {
203
+ addNotification({
204
+ title: "Network Error",
205
+ message: "Unable to connect to the server. Please check your connection.",
206
+ type: "error"
207
+ });
208
+ }
209
+ }, [setAuthData, setScreen, addNotification, apiUrl]);
210
+ const onSignUp = React2.useCallback(async ({
211
+ email,
212
+ password
213
+ }) => {
214
+ try {
215
+ const response = await fetch(`${apiUrl}/auth/local/register`, {
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/json"
219
+ },
220
+ body: JSON.stringify({
221
+ username: email,
222
+ // Assuming username is the same as email
223
+ email,
224
+ password
225
+ })
226
+ });
227
+ const data = await response.json();
228
+ if (!response.ok) {
229
+ addNotification({
230
+ title: "Sign up failed",
231
+ message: data.error?.message || "An error occurred during sign up.",
232
+ type: "error"
233
+ });
234
+ return;
235
+ }
236
+ setScreen({
237
+ value: "confirmSignUpCheckEmail"
238
+ });
239
+ } catch {
240
+ addNotification({
241
+ title: "Network Error",
242
+ message: "Unable to connect to the server. Please check your connection.",
243
+ type: "error"
244
+ });
245
+ }
246
+ }, [addNotification, setScreen, apiUrl]);
247
+ const onForgotPassword = React2.useCallback(async ({
248
+ email
249
+ }) => {
250
+ try {
251
+ const response = await fetch(`${apiUrl}/auth/forgot-password`, {
252
+ method: "POST",
253
+ headers: {
254
+ "Content-Type": "application/json"
255
+ },
256
+ body: JSON.stringify({
257
+ email
258
+ })
259
+ });
260
+ const data = await response.json();
261
+ if (!response.ok) {
262
+ addNotification({
263
+ title: "Forgot password failed",
264
+ message: data.error?.message || "An error occurred during forgot password.",
265
+ type: "error"
266
+ });
267
+ return;
268
+ }
269
+ } catch {
270
+ addNotification({
271
+ title: "Network Error",
272
+ message: "Unable to connect to the server. Please check your connection.",
273
+ type: "error"
274
+ });
275
+ }
276
+ }, [addNotification, apiUrl]);
277
+ const onConfirmSignUpCheckEmail = React2.useCallback(async () => {
278
+ setScreen({
279
+ value: "signIn"
280
+ });
281
+ }, [setScreen]);
282
+ return /* @__PURE__ */jsx2(AuthCore, {
283
+ screen,
284
+ setScreen,
285
+ onSignIn,
286
+ onSignUp,
287
+ onForgotPassword,
288
+ onConfirmSignUpCheckEmail
289
+ });
290
+ };
291
+ export { Auth, AuthProvider, useAuth };
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as _ttoss_react_auth_core from '@ttoss/react-auth-core';
3
+ import * as React from 'react';
4
+
5
+ declare const Auth: () => react_jsx_runtime.JSX.Element;
6
+
7
+ declare const AuthProvider: (props: React.PropsWithChildren<{
8
+ apiUrl: string;
9
+ }>) => react_jsx_runtime.JSX.Element;
10
+ declare const useAuth: () => {
11
+ signOut?: () => Promise<void>;
12
+ isAuthenticated: boolean;
13
+ user: _ttoss_react_auth_core.AuthUser | null;
14
+ tokens: _ttoss_react_auth_core.AuthTokens | null;
15
+ setAuthData: React.Dispatch<React.SetStateAction<_ttoss_react_auth_core.AuthData>>;
16
+ apiUrl: string;
17
+ };
18
+
19
+ export { Auth, AuthProvider, useAuth };
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as _ttoss_react_auth_core from '@ttoss/react-auth-core';
3
+ import * as React from 'react';
4
+
5
+ declare const Auth: () => react_jsx_runtime.JSX.Element;
6
+
7
+ declare const AuthProvider: (props: React.PropsWithChildren<{
8
+ apiUrl: string;
9
+ }>) => react_jsx_runtime.JSX.Element;
10
+ declare const useAuth: () => {
11
+ signOut?: () => Promise<void>;
12
+ isAuthenticated: boolean;
13
+ user: _ttoss_react_auth_core.AuthUser | null;
14
+ tokens: _ttoss_react_auth_core.AuthTokens | null;
15
+ setAuthData: React.Dispatch<React.SetStateAction<_ttoss_react_auth_core.AuthData>>;
16
+ apiUrl: string;
17
+ };
18
+
19
+ export { Auth, AuthProvider, useAuth };