@rqdhw3n/react-auth-flow 1.0.5 → 1.0.6
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 +371 -173
- package/dist/index.cjs.js +779 -210
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +126 -65
- package/dist/index.es.js +744 -175
- package/dist/index.es.js.map +1 -1
- package/dist/style.css +209 -47
- package/package.json +24 -20
package/dist/index.cjs.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
-
const
|
|
4
|
+
const react = require("react");
|
|
5
5
|
const reactRouterDom = require("react-router-dom");
|
|
6
|
-
const
|
|
7
|
-
const STYLE_ELEMENT_ID = "rq-auth-flow-styles";
|
|
8
|
-
if (typeof document !== "undefined" && !document.getElementById(STYLE_ELEMENT_ID)) {
|
|
9
|
-
const styleElement = document.createElement("style");
|
|
10
|
-
styleElement.id = STYLE_ELEMENT_ID;
|
|
11
|
-
styleElement.textContent = authStyles;
|
|
12
|
-
document.head.appendChild(styleElement);
|
|
13
|
-
}
|
|
14
|
-
const AuthContext = React.createContext(
|
|
6
|
+
const AuthContext = react.createContext(
|
|
15
7
|
void 0
|
|
16
8
|
);
|
|
17
9
|
AuthContext.displayName = "AuthContext";
|
|
@@ -72,150 +64,304 @@ const DEFAULT_ENDPOINTS = {
|
|
|
72
64
|
refresh: "/auth/refresh",
|
|
73
65
|
forgotPassword: "/auth/forgot-password",
|
|
74
66
|
resetPassword: "/auth/reset-password",
|
|
75
|
-
verifyEmail: "/auth/verify-email"
|
|
76
|
-
|
|
77
|
-
const defaultAdapter = async (url, options) => {
|
|
78
|
-
const response = await fetch(url, options);
|
|
79
|
-
return response;
|
|
67
|
+
verifyEmail: "/auth/verify-email",
|
|
68
|
+
twoFactorVerify: "/auth/2fa/verify"
|
|
80
69
|
};
|
|
70
|
+
async function parseResponse(response) {
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
let errorData = null;
|
|
73
|
+
try {
|
|
74
|
+
errorData = await response.json();
|
|
75
|
+
} catch {
|
|
76
|
+
errorData = {
|
|
77
|
+
error: {
|
|
78
|
+
message: response.statusText || "Request failed",
|
|
79
|
+
statusCode: response.status
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw errorData;
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 204) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
89
|
+
if (contentType.includes("application/json")) {
|
|
90
|
+
return response.json();
|
|
91
|
+
}
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
return text ? { message: text } : {};
|
|
94
|
+
}
|
|
95
|
+
function isResponseLike(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && "ok" in value && "status" in value && "headers" in value;
|
|
97
|
+
}
|
|
98
|
+
function createDefaultRequestAdapter(config) {
|
|
99
|
+
const { baseURL, credentials, headers } = config;
|
|
100
|
+
return async ({ endpoint, method, data, headers: requestHeaders }) => {
|
|
101
|
+
const response = await fetch(`${baseURL}${endpoint}`, {
|
|
102
|
+
method,
|
|
103
|
+
credentials,
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
...headers,
|
|
107
|
+
...requestHeaders
|
|
108
|
+
},
|
|
109
|
+
body: data === void 0 ? void 0 : JSON.stringify(data)
|
|
110
|
+
});
|
|
111
|
+
return parseResponse(response);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createLegacyAdapter(config) {
|
|
115
|
+
const { adapter, baseURL, credentials, headers } = config;
|
|
116
|
+
return async ({ endpoint, method, data, headers: requestHeaders }) => {
|
|
117
|
+
const response = await adapter(`${baseURL}${endpoint}`, {
|
|
118
|
+
method,
|
|
119
|
+
credentials,
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
...headers,
|
|
123
|
+
...requestHeaders
|
|
124
|
+
},
|
|
125
|
+
body: data === void 0 ? void 0 : JSON.stringify(data)
|
|
126
|
+
});
|
|
127
|
+
return parseResponse(response);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
81
130
|
function createAuthClient(config = {}) {
|
|
82
131
|
const {
|
|
83
|
-
baseURL
|
|
132
|
+
baseURL,
|
|
133
|
+
baseUrl,
|
|
84
134
|
endpoints = {},
|
|
85
|
-
headers = {},
|
|
135
|
+
headers: initialHeaders = {},
|
|
86
136
|
credentials = "include",
|
|
87
|
-
|
|
137
|
+
requestAdapter,
|
|
138
|
+
adapter
|
|
88
139
|
} = config;
|
|
89
|
-
const
|
|
140
|
+
const resolvedBaseURL = baseURL ?? baseUrl ?? "";
|
|
141
|
+
const finalEndpoints = {
|
|
142
|
+
...DEFAULT_ENDPOINTS,
|
|
143
|
+
...endpoints
|
|
144
|
+
};
|
|
145
|
+
const headers = { ...initialHeaders };
|
|
146
|
+
const resolvedAdapter = requestAdapter ?? (adapter ? createLegacyAdapter({
|
|
147
|
+
adapter,
|
|
148
|
+
baseURL: resolvedBaseURL,
|
|
149
|
+
credentials,
|
|
150
|
+
headers
|
|
151
|
+
}) : createDefaultRequestAdapter({
|
|
152
|
+
baseURL: resolvedBaseURL,
|
|
153
|
+
credentials,
|
|
154
|
+
headers
|
|
155
|
+
}));
|
|
90
156
|
async function request(method, endpoint, data) {
|
|
91
|
-
const url = `${baseURL}${endpoint}`;
|
|
92
|
-
const options = {
|
|
93
|
-
method,
|
|
94
|
-
credentials,
|
|
95
|
-
headers: {
|
|
96
|
-
"Content-Type": "application/json",
|
|
97
|
-
...headers
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
if (data) {
|
|
101
|
-
options.body = JSON.stringify(data);
|
|
102
|
-
}
|
|
103
157
|
try {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
message: response.statusText,
|
|
113
|
-
statusCode: response.status
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
throw errorData;
|
|
118
|
-
}
|
|
119
|
-
const contentType = response.headers.get("content-type");
|
|
120
|
-
if (contentType && contentType.includes("application/json")) {
|
|
121
|
-
return await response.json();
|
|
158
|
+
const result = await resolvedAdapter({
|
|
159
|
+
endpoint,
|
|
160
|
+
method,
|
|
161
|
+
data,
|
|
162
|
+
headers
|
|
163
|
+
});
|
|
164
|
+
if (isResponseLike(result)) {
|
|
165
|
+
return await parseResponse(result);
|
|
122
166
|
}
|
|
123
|
-
return {};
|
|
167
|
+
return result ?? {};
|
|
124
168
|
} catch (error) {
|
|
125
169
|
throw normalizeError(error);
|
|
126
170
|
}
|
|
127
171
|
}
|
|
128
172
|
return {
|
|
129
|
-
/**
|
|
130
|
-
* Login with email and password
|
|
131
|
-
*/
|
|
132
173
|
async login(email, password, rememberMe) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
138
|
-
return response;
|
|
174
|
+
return request("POST", finalEndpoints.login, {
|
|
175
|
+
email,
|
|
176
|
+
password,
|
|
177
|
+
rememberMe
|
|
178
|
+
});
|
|
139
179
|
},
|
|
140
|
-
/**
|
|
141
|
-
* Register a new account
|
|
142
|
-
*/
|
|
143
180
|
async register(payload) {
|
|
144
|
-
|
|
145
|
-
"POST",
|
|
146
|
-
finalEndpoints.register,
|
|
147
|
-
payload
|
|
148
|
-
);
|
|
149
|
-
return response;
|
|
181
|
+
return request("POST", finalEndpoints.register, payload);
|
|
150
182
|
},
|
|
151
|
-
/**
|
|
152
|
-
* Logout the user
|
|
153
|
-
*/
|
|
154
183
|
async logout() {
|
|
155
184
|
await request("POST", finalEndpoints.logout);
|
|
156
185
|
},
|
|
157
|
-
/**
|
|
158
|
-
* Get current user
|
|
159
|
-
*/
|
|
160
186
|
async me() {
|
|
161
|
-
|
|
162
|
-
"GET",
|
|
163
|
-
finalEndpoints.me
|
|
164
|
-
);
|
|
165
|
-
return response;
|
|
187
|
+
return request("GET", finalEndpoints.me);
|
|
166
188
|
},
|
|
167
|
-
/**
|
|
168
|
-
* Refresh the authentication session
|
|
169
|
-
*/
|
|
170
189
|
async refresh() {
|
|
171
|
-
|
|
172
|
-
"POST",
|
|
173
|
-
finalEndpoints.refresh
|
|
174
|
-
);
|
|
175
|
-
return response;
|
|
190
|
+
return request("POST", finalEndpoints.refresh);
|
|
176
191
|
},
|
|
177
|
-
/**
|
|
178
|
-
* Request a password reset
|
|
179
|
-
*/
|
|
180
192
|
async forgotPassword(email) {
|
|
181
|
-
|
|
193
|
+
return request("POST", finalEndpoints.forgotPassword, {
|
|
194
|
+
email
|
|
195
|
+
});
|
|
182
196
|
},
|
|
183
|
-
/**
|
|
184
|
-
* Reset password with token
|
|
185
|
-
*/
|
|
186
197
|
async resetPassword(token, password, confirmPassword) {
|
|
187
|
-
|
|
198
|
+
return request("POST", finalEndpoints.resetPassword, {
|
|
188
199
|
token,
|
|
189
200
|
password,
|
|
190
201
|
confirmPassword
|
|
191
202
|
});
|
|
192
203
|
},
|
|
193
|
-
/**
|
|
194
|
-
* Verify email with token
|
|
195
|
-
*/
|
|
196
204
|
async verifyEmail(token, email) {
|
|
197
|
-
|
|
205
|
+
return request("POST", finalEndpoints.verifyEmail, {
|
|
206
|
+
token,
|
|
207
|
+
email
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
async verifyTwoFactor(code) {
|
|
211
|
+
return request("POST", finalEndpoints.twoFactorVerify, {
|
|
212
|
+
code
|
|
213
|
+
});
|
|
198
214
|
},
|
|
199
|
-
/**
|
|
200
|
-
* Set custom headers for subsequent requests
|
|
201
|
-
*/
|
|
202
215
|
setHeaders(newHeaders) {
|
|
203
216
|
Object.assign(headers, newHeaders);
|
|
204
217
|
},
|
|
205
|
-
/**
|
|
206
|
-
* Get current endpoints configuration
|
|
207
|
-
*/
|
|
208
218
|
getEndpoints() {
|
|
209
219
|
return finalEndpoints;
|
|
210
220
|
}
|
|
211
221
|
};
|
|
212
222
|
}
|
|
223
|
+
const DEFAULT_STORAGE_KEY = "rq-auth-flow-user";
|
|
224
|
+
const DEFAULT_OTP_LENGTH = 6;
|
|
225
|
+
let memoryUser = null;
|
|
226
|
+
function getStorage() {
|
|
227
|
+
if (typeof window === "undefined") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return window.localStorage;
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function readStoredUser(storageKey) {
|
|
237
|
+
const storage = getStorage();
|
|
238
|
+
if (!storage) {
|
|
239
|
+
return memoryUser;
|
|
240
|
+
}
|
|
241
|
+
const rawValue = storage.getItem(storageKey);
|
|
242
|
+
if (!rawValue) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(rawValue);
|
|
247
|
+
} catch {
|
|
248
|
+
storage.removeItem(storageKey);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function writeStoredUser(storageKey, user) {
|
|
253
|
+
const storage = getStorage();
|
|
254
|
+
memoryUser = user;
|
|
255
|
+
if (!storage) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (user === null) {
|
|
259
|
+
storage.removeItem(storageKey);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
storage.setItem(storageKey, JSON.stringify(user));
|
|
263
|
+
}
|
|
264
|
+
function createAdminUser(email, mockUser) {
|
|
265
|
+
return {
|
|
266
|
+
id: 1,
|
|
267
|
+
name: "Admin Test",
|
|
268
|
+
email,
|
|
269
|
+
roles: ["admin"],
|
|
270
|
+
permissions: ["users.manage", "billing.edit"],
|
|
271
|
+
...mockUser
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function createRegisteredUser(name, email, mockUser) {
|
|
275
|
+
return {
|
|
276
|
+
id: 2,
|
|
277
|
+
name: name || "New User",
|
|
278
|
+
email,
|
|
279
|
+
roles: ["user"],
|
|
280
|
+
permissions: ["users.create"],
|
|
281
|
+
...mockUser
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function toSuccessResponse(message, user) {
|
|
285
|
+
return user ? { success: true, message, user } : { success: true, message };
|
|
286
|
+
}
|
|
287
|
+
function createMockAuthAdapter(options = {}) {
|
|
288
|
+
const {
|
|
289
|
+
endpoints = {},
|
|
290
|
+
mockStorageKey = DEFAULT_STORAGE_KEY,
|
|
291
|
+
mockUser
|
|
292
|
+
} = options;
|
|
293
|
+
const resolvedEndpoints = {
|
|
294
|
+
login: "/auth/login",
|
|
295
|
+
register: "/auth/register",
|
|
296
|
+
logout: "/auth/logout",
|
|
297
|
+
me: "/auth/me",
|
|
298
|
+
refresh: "/auth/refresh",
|
|
299
|
+
forgotPassword: "/auth/forgot-password",
|
|
300
|
+
resetPassword: "/auth/reset-password",
|
|
301
|
+
verifyEmail: "/auth/verify-email",
|
|
302
|
+
twoFactorVerify: "/auth/2fa/verify",
|
|
303
|
+
...endpoints
|
|
304
|
+
};
|
|
305
|
+
return async ({ endpoint, data }) => {
|
|
306
|
+
if (endpoint === resolvedEndpoints.login) {
|
|
307
|
+
const payload = data ?? {};
|
|
308
|
+
const user = createAdminUser(payload.email ?? "admin@example.com", mockUser);
|
|
309
|
+
writeStoredUser(mockStorageKey, user);
|
|
310
|
+
return toSuccessResponse("Mock login successful.", user);
|
|
311
|
+
}
|
|
312
|
+
if (endpoint === resolvedEndpoints.register) {
|
|
313
|
+
const payload = data ?? {};
|
|
314
|
+
const user = createRegisteredUser(
|
|
315
|
+
payload.name,
|
|
316
|
+
payload.email ?? "user@example.com",
|
|
317
|
+
mockUser
|
|
318
|
+
);
|
|
319
|
+
writeStoredUser(mockStorageKey, user);
|
|
320
|
+
return toSuccessResponse("Mock registration successful.", user);
|
|
321
|
+
}
|
|
322
|
+
if (endpoint === resolvedEndpoints.logout) {
|
|
323
|
+
writeStoredUser(mockStorageKey, null);
|
|
324
|
+
return toSuccessResponse("Mock logout successful.");
|
|
325
|
+
}
|
|
326
|
+
if (endpoint === resolvedEndpoints.me || endpoint === resolvedEndpoints.refresh) {
|
|
327
|
+
const user = readStoredUser(mockStorageKey);
|
|
328
|
+
return user ? { success: true, user } : { success: true, user: null };
|
|
329
|
+
}
|
|
330
|
+
if (endpoint === resolvedEndpoints.forgotPassword) {
|
|
331
|
+
return toSuccessResponse("Mock password reset request accepted.");
|
|
332
|
+
}
|
|
333
|
+
if (endpoint === resolvedEndpoints.resetPassword) {
|
|
334
|
+
return toSuccessResponse("Mock password reset successful.");
|
|
335
|
+
}
|
|
336
|
+
if (endpoint === resolvedEndpoints.verifyEmail) {
|
|
337
|
+
return toSuccessResponse("Mock email verification successful.");
|
|
338
|
+
}
|
|
339
|
+
if (endpoint === resolvedEndpoints.twoFactorVerify) {
|
|
340
|
+
const payload = data ?? {};
|
|
341
|
+
const code = payload.code?.trim() ?? "";
|
|
342
|
+
if (code.length !== DEFAULT_OTP_LENGTH) {
|
|
343
|
+
throw {
|
|
344
|
+
code: "INVALID_2FA_CODE",
|
|
345
|
+
message: `Mock 2FA code must be exactly ${DEFAULT_OTP_LENGTH} characters.`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return toSuccessResponse("Mock 2FA verification successful.");
|
|
349
|
+
}
|
|
350
|
+
return toSuccessResponse("Mock request successful.");
|
|
351
|
+
};
|
|
352
|
+
}
|
|
213
353
|
const initialState = {
|
|
214
354
|
user: null,
|
|
215
355
|
isAuthenticated: false,
|
|
216
356
|
isLoading: false,
|
|
217
357
|
error: null
|
|
218
358
|
};
|
|
359
|
+
const defaultTheme = {
|
|
360
|
+
primaryColor: "#2563eb",
|
|
361
|
+
primaryHoverColor: "#1d4ed8",
|
|
362
|
+
radius: "14px",
|
|
363
|
+
fontFamily: "inherit"
|
|
364
|
+
};
|
|
219
365
|
function authReducer(state, action) {
|
|
220
366
|
switch (action.type) {
|
|
221
367
|
case "SET_LOADING":
|
|
@@ -231,65 +377,129 @@ function authReducer(state, action) {
|
|
|
231
377
|
isLoading: false
|
|
232
378
|
};
|
|
233
379
|
case "LOGOUT":
|
|
234
|
-
return initialState;
|
|
380
|
+
return { ...initialState };
|
|
235
381
|
default:
|
|
236
382
|
return state;
|
|
237
383
|
}
|
|
238
384
|
}
|
|
385
|
+
function toAuthError(error) {
|
|
386
|
+
if (!error) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return typeof error === "string" ? {
|
|
390
|
+
code: "AUTH_ERROR",
|
|
391
|
+
message: error
|
|
392
|
+
} : error;
|
|
393
|
+
}
|
|
239
394
|
const AuthProvider = ({
|
|
240
395
|
children,
|
|
241
|
-
baseURL
|
|
396
|
+
baseURL,
|
|
397
|
+
baseUrl,
|
|
242
398
|
endpoints = {},
|
|
399
|
+
headers = {},
|
|
400
|
+
credentials = "include",
|
|
243
401
|
onAuthError,
|
|
244
402
|
autoRefresh = true,
|
|
245
|
-
|
|
246
|
-
|
|
403
|
+
autoRestore = true,
|
|
404
|
+
refreshInterval = 5 * 60 * 1e3,
|
|
405
|
+
requestAdapter,
|
|
406
|
+
theme,
|
|
407
|
+
mock = false,
|
|
408
|
+
mockStorageKey = "rq-auth-flow-user",
|
|
409
|
+
mockUser
|
|
247
410
|
}) => {
|
|
248
|
-
const [state, dispatch] =
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
411
|
+
const [state, dispatch] = react.useReducer(authReducer, initialState);
|
|
412
|
+
const resolvedTheme = { ...defaultTheme, ...theme };
|
|
413
|
+
const resolvedBaseURL = baseURL ?? baseUrl ?? "";
|
|
414
|
+
const client = react.useMemo(() => {
|
|
415
|
+
const resolvedRequestAdapter = mock ? createMockAuthAdapter({
|
|
252
416
|
endpoints,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
417
|
+
mockStorageKey,
|
|
418
|
+
mockUser
|
|
419
|
+
}) : requestAdapter;
|
|
420
|
+
return createAuthClient({
|
|
421
|
+
baseURL: resolvedBaseURL,
|
|
422
|
+
endpoints,
|
|
423
|
+
headers,
|
|
424
|
+
credentials,
|
|
425
|
+
requestAdapter: resolvedRequestAdapter
|
|
426
|
+
});
|
|
427
|
+
}, [
|
|
428
|
+
credentials,
|
|
429
|
+
endpoints,
|
|
430
|
+
headers,
|
|
431
|
+
mock,
|
|
432
|
+
mockStorageKey,
|
|
433
|
+
mockUser,
|
|
434
|
+
requestAdapter,
|
|
435
|
+
resolvedBaseURL
|
|
436
|
+
]);
|
|
437
|
+
const handleError = react.useCallback(
|
|
258
438
|
(error) => {
|
|
259
439
|
dispatch({ type: "SET_ERROR", payload: error });
|
|
260
440
|
onAuthError?.(error);
|
|
261
441
|
},
|
|
262
442
|
[onAuthError]
|
|
263
443
|
);
|
|
264
|
-
const
|
|
444
|
+
const clearError = react.useCallback(() => {
|
|
445
|
+
dispatch({ type: "SET_ERROR", payload: null });
|
|
446
|
+
}, []);
|
|
447
|
+
const setError = react.useCallback(
|
|
448
|
+
(error) => {
|
|
449
|
+
const nextError = toAuthError(error);
|
|
450
|
+
dispatch({ type: "SET_ERROR", payload: nextError });
|
|
451
|
+
if (nextError) {
|
|
452
|
+
onAuthError?.(nextError);
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
[onAuthError]
|
|
456
|
+
);
|
|
457
|
+
const setUser = react.useCallback((user) => {
|
|
458
|
+
dispatch({ type: "SET_USER", payload: user });
|
|
459
|
+
}, []);
|
|
460
|
+
const resolveAction = react.useCallback((response) => {
|
|
461
|
+
if (response?.user) {
|
|
462
|
+
dispatch({ type: "SET_USER", payload: response.user });
|
|
463
|
+
return response.user;
|
|
464
|
+
}
|
|
465
|
+
dispatch({ type: "SET_ERROR", payload: null });
|
|
466
|
+
return null;
|
|
467
|
+
}, []);
|
|
468
|
+
const restoreSession = react.useCallback(async () => {
|
|
265
469
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
266
470
|
try {
|
|
267
471
|
const response = await client.me();
|
|
268
472
|
if (response.user) {
|
|
269
473
|
dispatch({ type: "SET_USER", payload: response.user });
|
|
270
|
-
|
|
271
|
-
dispatch({ type: "LOGOUT" });
|
|
474
|
+
return response.user;
|
|
272
475
|
}
|
|
273
|
-
|
|
274
|
-
|
|
476
|
+
dispatch({ type: "LOGOUT" });
|
|
477
|
+
return null;
|
|
478
|
+
} catch {
|
|
479
|
+
dispatch({ type: "LOGOUT" });
|
|
480
|
+
return null;
|
|
275
481
|
}
|
|
276
482
|
}, [client]);
|
|
277
|
-
const refreshSession =
|
|
483
|
+
const refreshSession = react.useCallback(async () => {
|
|
484
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
278
485
|
try {
|
|
279
486
|
const response = await client.refresh();
|
|
280
|
-
|
|
281
|
-
dispatch({ type: "SET_USER", payload: response.user });
|
|
282
|
-
}
|
|
487
|
+
return resolveAction(response);
|
|
283
488
|
} catch (error) {
|
|
284
489
|
const authError = normalizeError(error);
|
|
285
490
|
handleError(authError);
|
|
491
|
+
return null;
|
|
286
492
|
}
|
|
287
|
-
}, [client, handleError]);
|
|
288
|
-
const login =
|
|
493
|
+
}, [client, handleError, resolveAction]);
|
|
494
|
+
const login = react.useCallback(
|
|
289
495
|
async (payload) => {
|
|
290
496
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
291
497
|
try {
|
|
292
|
-
const response = await client.login(
|
|
498
|
+
const response = await client.login(
|
|
499
|
+
payload.email,
|
|
500
|
+
payload.password,
|
|
501
|
+
payload.rememberMe
|
|
502
|
+
);
|
|
293
503
|
if (!response.user) {
|
|
294
504
|
throw new Error("No user data in response");
|
|
295
505
|
}
|
|
@@ -303,7 +513,7 @@ const AuthProvider = ({
|
|
|
303
513
|
},
|
|
304
514
|
[client, handleError]
|
|
305
515
|
);
|
|
306
|
-
const register =
|
|
516
|
+
const register = react.useCallback(
|
|
307
517
|
async (payload) => {
|
|
308
518
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
309
519
|
try {
|
|
@@ -321,93 +531,174 @@ const AuthProvider = ({
|
|
|
321
531
|
},
|
|
322
532
|
[client, handleError]
|
|
323
533
|
);
|
|
324
|
-
const logout =
|
|
534
|
+
const logout = react.useCallback(async () => {
|
|
325
535
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
326
536
|
try {
|
|
327
537
|
await client.logout();
|
|
328
|
-
} catch
|
|
329
|
-
console.error("Logout error:", error);
|
|
538
|
+
} catch {
|
|
330
539
|
} finally {
|
|
331
540
|
dispatch({ type: "LOGOUT" });
|
|
332
541
|
}
|
|
333
542
|
}, [client]);
|
|
334
|
-
const forgotPassword =
|
|
543
|
+
const forgotPassword = react.useCallback(
|
|
335
544
|
async (payload) => {
|
|
336
545
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
337
546
|
try {
|
|
338
|
-
await client.forgotPassword(payload.email);
|
|
339
|
-
|
|
547
|
+
const response = await client.forgotPassword(payload.email);
|
|
548
|
+
resolveAction(response);
|
|
549
|
+
return response;
|
|
340
550
|
} catch (error) {
|
|
341
551
|
const authError = normalizeError(error);
|
|
342
552
|
handleError(authError);
|
|
343
553
|
throw authError;
|
|
344
554
|
}
|
|
345
555
|
},
|
|
346
|
-
[client, handleError]
|
|
556
|
+
[client, handleError, resolveAction]
|
|
347
557
|
);
|
|
348
|
-
const resetPassword =
|
|
558
|
+
const resetPassword = react.useCallback(
|
|
349
559
|
async (payload) => {
|
|
350
560
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
351
561
|
try {
|
|
352
|
-
await client.resetPassword(
|
|
562
|
+
const response = await client.resetPassword(
|
|
353
563
|
payload.token,
|
|
354
564
|
payload.password,
|
|
355
565
|
payload.confirmPassword
|
|
356
566
|
);
|
|
357
|
-
|
|
567
|
+
resolveAction(response);
|
|
568
|
+
return response;
|
|
358
569
|
} catch (error) {
|
|
359
570
|
const authError = normalizeError(error);
|
|
360
571
|
handleError(authError);
|
|
361
572
|
throw authError;
|
|
362
573
|
}
|
|
363
574
|
},
|
|
364
|
-
[client, handleError]
|
|
575
|
+
[client, handleError, resolveAction]
|
|
365
576
|
);
|
|
366
|
-
const verifyEmail =
|
|
577
|
+
const verifyEmail = react.useCallback(
|
|
367
578
|
async (payload) => {
|
|
368
579
|
dispatch({ type: "SET_LOADING", payload: true });
|
|
369
580
|
try {
|
|
370
|
-
await client.verifyEmail(payload.token, payload.email);
|
|
371
|
-
|
|
581
|
+
const response = await client.verifyEmail(payload.token, payload.email);
|
|
582
|
+
resolveAction(response);
|
|
583
|
+
return response;
|
|
372
584
|
} catch (error) {
|
|
373
585
|
const authError = normalizeError(error);
|
|
374
586
|
handleError(authError);
|
|
375
587
|
throw authError;
|
|
376
588
|
}
|
|
377
589
|
},
|
|
378
|
-
[client, handleError]
|
|
590
|
+
[client, handleError, resolveAction]
|
|
379
591
|
);
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
592
|
+
const verifyTwoFactor = react.useCallback(
|
|
593
|
+
async (payload) => {
|
|
594
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
595
|
+
try {
|
|
596
|
+
const response = await client.verifyTwoFactor(payload.code);
|
|
597
|
+
resolveAction(response);
|
|
598
|
+
return response;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const authError = normalizeError(error);
|
|
601
|
+
handleError(authError);
|
|
602
|
+
throw authError;
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
[client, handleError, resolveAction]
|
|
606
|
+
);
|
|
607
|
+
const hasRole = react.useCallback(
|
|
608
|
+
(role) => state.user?.roles?.includes(role) ?? false,
|
|
609
|
+
[state.user]
|
|
610
|
+
);
|
|
611
|
+
const hasAnyRole = react.useCallback(
|
|
612
|
+
(roles) => roles.some((role) => state.user?.roles?.includes(role)),
|
|
613
|
+
[state.user]
|
|
614
|
+
);
|
|
615
|
+
const hasPermission = react.useCallback(
|
|
616
|
+
(permission) => state.user?.permissions?.includes(permission) ?? false,
|
|
617
|
+
[state.user]
|
|
618
|
+
);
|
|
619
|
+
const hasAnyPermission = react.useCallback(
|
|
620
|
+
(permissions) => permissions.some(
|
|
621
|
+
(permission) => state.user?.permissions?.includes(permission)
|
|
622
|
+
),
|
|
623
|
+
[state.user]
|
|
624
|
+
);
|
|
625
|
+
const hasAllPermissions = react.useCallback(
|
|
626
|
+
(permissions) => permissions.every(
|
|
627
|
+
(permission) => state.user?.permissions?.includes(permission)
|
|
628
|
+
),
|
|
629
|
+
[state.user]
|
|
630
|
+
);
|
|
631
|
+
react.useEffect(() => {
|
|
632
|
+
if (!autoRestore) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
void restoreSession();
|
|
636
|
+
}, [autoRestore, restoreSession]);
|
|
637
|
+
react.useEffect(() => {
|
|
387
638
|
if (!autoRefresh || !state.isAuthenticated) {
|
|
388
639
|
return;
|
|
389
640
|
}
|
|
390
|
-
const
|
|
391
|
-
refreshSession();
|
|
641
|
+
const intervalId = window.setInterval(() => {
|
|
642
|
+
void refreshSession();
|
|
392
643
|
}, refreshInterval);
|
|
393
|
-
return () => clearInterval(
|
|
394
|
-
}, [autoRefresh,
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
644
|
+
return () => window.clearInterval(intervalId);
|
|
645
|
+
}, [autoRefresh, refreshInterval, refreshSession, state.isAuthenticated]);
|
|
646
|
+
const themeStyle = react.useMemo(
|
|
647
|
+
() => ({
|
|
648
|
+
"--rq-auth-primary": resolvedTheme.primaryColor,
|
|
649
|
+
"--rq-auth-primary-hover": resolvedTheme.primaryHoverColor,
|
|
650
|
+
"--rq-auth-radius": resolvedTheme.radius,
|
|
651
|
+
"--rq-auth-font-family": resolvedTheme.fontFamily,
|
|
652
|
+
fontFamily: resolvedTheme.fontFamily
|
|
653
|
+
}),
|
|
654
|
+
[resolvedTheme.fontFamily, resolvedTheme.primaryColor, resolvedTheme.primaryHoverColor, resolvedTheme.radius]
|
|
655
|
+
);
|
|
656
|
+
const value = react.useMemo(
|
|
657
|
+
() => ({
|
|
658
|
+
...state,
|
|
659
|
+
login,
|
|
660
|
+
register,
|
|
661
|
+
logout,
|
|
662
|
+
forgotPassword,
|
|
663
|
+
resetPassword,
|
|
664
|
+
verifyEmail,
|
|
665
|
+
verifyTwoFactor,
|
|
666
|
+
refreshSession,
|
|
667
|
+
restoreSession,
|
|
668
|
+
clearError,
|
|
669
|
+
setError,
|
|
670
|
+
setUser,
|
|
671
|
+
hasRole,
|
|
672
|
+
hasAnyRole,
|
|
673
|
+
hasPermission,
|
|
674
|
+
hasAnyPermission,
|
|
675
|
+
hasAllPermissions
|
|
676
|
+
}),
|
|
677
|
+
[
|
|
678
|
+
clearError,
|
|
679
|
+
forgotPassword,
|
|
680
|
+
hasAllPermissions,
|
|
681
|
+
hasAnyPermission,
|
|
682
|
+
hasAnyRole,
|
|
683
|
+
hasPermission,
|
|
684
|
+
hasRole,
|
|
685
|
+
login,
|
|
686
|
+
logout,
|
|
687
|
+
refreshSession,
|
|
688
|
+
register,
|
|
689
|
+
resetPassword,
|
|
690
|
+
restoreSession,
|
|
691
|
+
setError,
|
|
692
|
+
setUser,
|
|
693
|
+
state,
|
|
694
|
+
verifyEmail,
|
|
695
|
+
verifyTwoFactor
|
|
696
|
+
]
|
|
697
|
+
);
|
|
698
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rq-auth-theme", style: themeStyle, children }) });
|
|
408
699
|
};
|
|
409
700
|
function useAuth() {
|
|
410
|
-
const context =
|
|
701
|
+
const context = react.useContext(AuthContext);
|
|
411
702
|
if (!context) {
|
|
412
703
|
throw new Error("useAuth must be used within an AuthProvider");
|
|
413
704
|
}
|
|
@@ -418,6 +709,10 @@ function cn(...classes) {
|
|
|
418
709
|
}
|
|
419
710
|
const AUTH_FORM_CLASSES = {
|
|
420
711
|
container: "rq-auth-container",
|
|
712
|
+
card: "rq-auth-card",
|
|
713
|
+
shell: "rq-auth-shell",
|
|
714
|
+
brand: "rq-auth-brand",
|
|
715
|
+
footer: "rq-auth-footer",
|
|
421
716
|
header: "rq-auth-header",
|
|
422
717
|
form: "rq-auth-form",
|
|
423
718
|
field: "rq-auth-field",
|
|
@@ -432,8 +727,33 @@ const AUTH_FORM_CLASSES = {
|
|
|
432
727
|
subtitle: "rq-auth-subtitle",
|
|
433
728
|
checkbox: "rq-auth-checkbox",
|
|
434
729
|
checkboxInput: "rq-auth-checkbox-input",
|
|
435
|
-
checkboxLabel: "rq-auth-checkbox-label"
|
|
730
|
+
checkboxLabel: "rq-auth-checkbox-label",
|
|
731
|
+
otpGroup: "rq-auth-otp-group",
|
|
732
|
+
otpInput: "rq-auth-otp-input",
|
|
733
|
+
socialButton: "rq-auth-social-button",
|
|
734
|
+
socialIcon: "rq-auth-social-icon",
|
|
735
|
+
socialLabel: "rq-auth-social-label"
|
|
436
736
|
};
|
|
737
|
+
const AuthLayout = ({
|
|
738
|
+
children,
|
|
739
|
+
title,
|
|
740
|
+
subtitle,
|
|
741
|
+
className,
|
|
742
|
+
cardClassName,
|
|
743
|
+
brand,
|
|
744
|
+
footer
|
|
745
|
+
}) => {
|
|
746
|
+
return /* @__PURE__ */ jsxRuntime.jsx("section", { className: cn(AUTH_FORM_CLASSES.shell, className), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(AUTH_FORM_CLASSES.card, cardClassName), children: [
|
|
747
|
+
brand ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: AUTH_FORM_CLASSES.brand, children: brand }) : null,
|
|
748
|
+
(title || subtitle) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: AUTH_FORM_CLASSES.header, children: [
|
|
749
|
+
title ? /* @__PURE__ */ jsxRuntime.jsx("h1", { className: AUTH_FORM_CLASSES.title, children: title }) : null,
|
|
750
|
+
subtitle ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: AUTH_FORM_CLASSES.subtitle, children: subtitle }) : null
|
|
751
|
+
] }),
|
|
752
|
+
children,
|
|
753
|
+
footer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: AUTH_FORM_CLASSES.footer, children: footer }) : null
|
|
754
|
+
] }) });
|
|
755
|
+
};
|
|
756
|
+
AuthLayout.displayName = "AuthLayout";
|
|
437
757
|
const LoginForm = ({
|
|
438
758
|
className,
|
|
439
759
|
formClassName,
|
|
@@ -454,15 +774,21 @@ const LoginForm = ({
|
|
|
454
774
|
onSuccess,
|
|
455
775
|
onError
|
|
456
776
|
}) => {
|
|
457
|
-
const {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
777
|
+
const {
|
|
778
|
+
login,
|
|
779
|
+
clearError,
|
|
780
|
+
isLoading: authLoading,
|
|
781
|
+
error: authError
|
|
782
|
+
} = useAuth();
|
|
783
|
+
const [formError, setFormError] = react.useState("");
|
|
784
|
+
const [email, setEmail] = react.useState("");
|
|
785
|
+
const [password, setPassword] = react.useState("");
|
|
786
|
+
const [rememberMe, setRememberMe] = react.useState(false);
|
|
462
787
|
const isLoading = authLoading;
|
|
463
788
|
const error = formError || authError?.message;
|
|
464
789
|
const handleSubmit = async (e) => {
|
|
465
790
|
e.preventDefault();
|
|
791
|
+
clearError();
|
|
466
792
|
setFormError("");
|
|
467
793
|
if (!email || !password) {
|
|
468
794
|
setFormError("Email and password are required");
|
|
@@ -587,16 +913,22 @@ const RegisterForm = ({
|
|
|
587
913
|
onSuccess,
|
|
588
914
|
onError
|
|
589
915
|
}) => {
|
|
590
|
-
const {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
916
|
+
const {
|
|
917
|
+
register,
|
|
918
|
+
clearError,
|
|
919
|
+
isLoading: authLoading,
|
|
920
|
+
error: authError
|
|
921
|
+
} = useAuth();
|
|
922
|
+
const [formError, setFormError] = react.useState("");
|
|
923
|
+
const [name, setName] = react.useState("");
|
|
924
|
+
const [email, setEmail] = react.useState("");
|
|
925
|
+
const [password, setPassword] = react.useState("");
|
|
926
|
+
const [confirmPassword, setConfirmPassword] = react.useState("");
|
|
596
927
|
const isLoading = authLoading;
|
|
597
928
|
const error = formError || authError?.message;
|
|
598
929
|
const handleSubmit = async (e) => {
|
|
599
930
|
e.preventDefault();
|
|
931
|
+
clearError();
|
|
600
932
|
setFormError("");
|
|
601
933
|
if (!name || !email || !password || !confirmPassword) {
|
|
602
934
|
setFormError("All fields are required");
|
|
@@ -764,16 +1096,18 @@ const ForgotPasswordForm = ({
|
|
|
764
1096
|
}) => {
|
|
765
1097
|
const {
|
|
766
1098
|
forgotPassword,
|
|
1099
|
+
clearError,
|
|
767
1100
|
isLoading: authLoading,
|
|
768
1101
|
error: authError
|
|
769
1102
|
} = useAuth();
|
|
770
|
-
const [formError, setFormError] =
|
|
771
|
-
const [email, setEmail] =
|
|
772
|
-
const [successMessage, setSuccessMessage] =
|
|
1103
|
+
const [formError, setFormError] = react.useState("");
|
|
1104
|
+
const [email, setEmail] = react.useState("");
|
|
1105
|
+
const [successMessage, setSuccessMessage] = react.useState("");
|
|
773
1106
|
const isLoading = authLoading;
|
|
774
1107
|
const error = formError || authError?.message;
|
|
775
1108
|
const handleSubmit = async (e) => {
|
|
776
1109
|
e.preventDefault();
|
|
1110
|
+
clearError();
|
|
777
1111
|
setFormError("");
|
|
778
1112
|
setSuccessMessage("");
|
|
779
1113
|
if (!email) {
|
|
@@ -876,17 +1210,19 @@ const ResetPasswordForm = ({
|
|
|
876
1210
|
}) => {
|
|
877
1211
|
const {
|
|
878
1212
|
resetPassword,
|
|
1213
|
+
clearError,
|
|
879
1214
|
isLoading: authLoading,
|
|
880
1215
|
error: authError
|
|
881
1216
|
} = useAuth();
|
|
882
|
-
const [formError, setFormError] =
|
|
883
|
-
const [password, setPassword] =
|
|
884
|
-
const [confirmPassword, setConfirmPassword] =
|
|
885
|
-
const [successMessage, setSuccessMessage] =
|
|
1217
|
+
const [formError, setFormError] = react.useState("");
|
|
1218
|
+
const [password, setPassword] = react.useState("");
|
|
1219
|
+
const [confirmPassword, setConfirmPassword] = react.useState("");
|
|
1220
|
+
const [successMessage, setSuccessMessage] = react.useState("");
|
|
886
1221
|
const isLoading = authLoading;
|
|
887
1222
|
const error = formError || authError?.message;
|
|
888
1223
|
const handleSubmit = async (e) => {
|
|
889
1224
|
e.preventDefault();
|
|
1225
|
+
clearError();
|
|
890
1226
|
setFormError("");
|
|
891
1227
|
setSuccessMessage("");
|
|
892
1228
|
if (!password || !confirmPassword) {
|
|
@@ -1030,17 +1366,19 @@ const VerifyEmailForm = ({
|
|
|
1030
1366
|
}) => {
|
|
1031
1367
|
const {
|
|
1032
1368
|
verifyEmail,
|
|
1369
|
+
clearError,
|
|
1033
1370
|
isLoading: authLoading,
|
|
1034
1371
|
error: authError
|
|
1035
1372
|
} = useAuth();
|
|
1036
|
-
const [formError, setFormError] =
|
|
1037
|
-
const [email, setEmail] =
|
|
1038
|
-
const [token, setToken] =
|
|
1039
|
-
const [successMessage, setSuccessMessage] =
|
|
1373
|
+
const [formError, setFormError] = react.useState("");
|
|
1374
|
+
const [email, setEmail] = react.useState(initialEmail || "");
|
|
1375
|
+
const [token, setToken] = react.useState(initialToken || "");
|
|
1376
|
+
const [successMessage, setSuccessMessage] = react.useState("");
|
|
1040
1377
|
const isLoading = authLoading;
|
|
1041
1378
|
const error = formError || authError?.message;
|
|
1042
1379
|
const handleSubmit = async (e) => {
|
|
1043
1380
|
e.preventDefault();
|
|
1381
|
+
clearError();
|
|
1044
1382
|
setFormError("");
|
|
1045
1383
|
setSuccessMessage("");
|
|
1046
1384
|
if (!token) {
|
|
@@ -1064,7 +1402,7 @@ const VerifyEmailForm = ({
|
|
|
1064
1402
|
});
|
|
1065
1403
|
}
|
|
1066
1404
|
};
|
|
1067
|
-
|
|
1405
|
+
react.useEffect(() => {
|
|
1068
1406
|
if (token && email && !successMessage && !error) {
|
|
1069
1407
|
void handleSubmit({ preventDefault: () => {
|
|
1070
1408
|
} });
|
|
@@ -1145,44 +1483,275 @@ const VerifyEmailForm = ({
|
|
|
1145
1483
|
] });
|
|
1146
1484
|
};
|
|
1147
1485
|
VerifyEmailForm.displayName = "VerifyEmailForm";
|
|
1486
|
+
function toDigits(value, length) {
|
|
1487
|
+
return Array.from({ length }, (_, index) => value[index] ?? "");
|
|
1488
|
+
}
|
|
1489
|
+
const OtpInput = ({
|
|
1490
|
+
length = 6,
|
|
1491
|
+
value,
|
|
1492
|
+
onChange,
|
|
1493
|
+
onComplete,
|
|
1494
|
+
disabled = false,
|
|
1495
|
+
className,
|
|
1496
|
+
inputClassName
|
|
1497
|
+
}) => {
|
|
1498
|
+
const [internalValue, setInternalValue] = react.useState("");
|
|
1499
|
+
const inputRefs = react.useRef([]);
|
|
1500
|
+
const baseId = react.useId();
|
|
1501
|
+
const currentValue = value ?? internalValue;
|
|
1502
|
+
const digits = react.useMemo(
|
|
1503
|
+
() => toDigits(currentValue.slice(0, length), length),
|
|
1504
|
+
[currentValue, length]
|
|
1505
|
+
);
|
|
1506
|
+
const commitValue = (nextDigits) => {
|
|
1507
|
+
const nextValue = nextDigits.join("").slice(0, length);
|
|
1508
|
+
if (value === void 0) {
|
|
1509
|
+
setInternalValue(nextValue);
|
|
1510
|
+
}
|
|
1511
|
+
onChange?.(nextValue);
|
|
1512
|
+
if (nextDigits.every(Boolean) && nextValue.length === length) {
|
|
1513
|
+
onComplete?.(nextValue);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
const focusInput = (index) => {
|
|
1517
|
+
inputRefs.current[index]?.focus();
|
|
1518
|
+
inputRefs.current[index]?.select();
|
|
1519
|
+
};
|
|
1520
|
+
const handleChange = (index, nextChunk) => {
|
|
1521
|
+
const sanitized = nextChunk.replace(/\s+/g, "");
|
|
1522
|
+
const nextDigits = [...digits];
|
|
1523
|
+
if (!sanitized) {
|
|
1524
|
+
nextDigits[index] = "";
|
|
1525
|
+
commitValue(nextDigits);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (sanitized.length > 1) {
|
|
1529
|
+
sanitized.slice(0, length - index).split("").forEach((character, offset) => {
|
|
1530
|
+
nextDigits[index + offset] = character;
|
|
1531
|
+
});
|
|
1532
|
+
commitValue(nextDigits);
|
|
1533
|
+
focusInput(Math.min(index + sanitized.length, length - 1));
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
nextDigits[index] = sanitized;
|
|
1537
|
+
commitValue(nextDigits);
|
|
1538
|
+
if (index < length - 1) {
|
|
1539
|
+
focusInput(index + 1);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
const handleKeyDown = (index, event) => {
|
|
1543
|
+
if (event.key === "Backspace" && !digits[index] && index > 0) {
|
|
1544
|
+
focusInput(index - 1);
|
|
1545
|
+
}
|
|
1546
|
+
if (event.key === "ArrowLeft" && index > 0) {
|
|
1547
|
+
event.preventDefault();
|
|
1548
|
+
focusInput(index - 1);
|
|
1549
|
+
}
|
|
1550
|
+
if (event.key === "ArrowRight" && index < length - 1) {
|
|
1551
|
+
event.preventDefault();
|
|
1552
|
+
focusInput(index + 1);
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
const handlePaste = (index, event) => {
|
|
1556
|
+
event.preventDefault();
|
|
1557
|
+
handleChange(index, event.clipboardData.getData("text"));
|
|
1558
|
+
};
|
|
1559
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1560
|
+
"div",
|
|
1561
|
+
{
|
|
1562
|
+
className: cn(AUTH_FORM_CLASSES.otpGroup, className),
|
|
1563
|
+
style: {
|
|
1564
|
+
"--rq-auth-otp-columns": String(length)
|
|
1565
|
+
},
|
|
1566
|
+
children: digits.map((digit, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1567
|
+
"input",
|
|
1568
|
+
{
|
|
1569
|
+
ref: (element) => {
|
|
1570
|
+
inputRefs.current[index] = element;
|
|
1571
|
+
},
|
|
1572
|
+
id: `${baseId}-${index}`,
|
|
1573
|
+
type: "text",
|
|
1574
|
+
inputMode: "text",
|
|
1575
|
+
autoComplete: index === 0 ? "one-time-code" : "off",
|
|
1576
|
+
maxLength: 1,
|
|
1577
|
+
value: digit,
|
|
1578
|
+
disabled,
|
|
1579
|
+
className: cn(AUTH_FORM_CLASSES.otpInput, inputClassName),
|
|
1580
|
+
onChange: (event) => handleChange(index, event.target.value),
|
|
1581
|
+
onKeyDown: (event) => handleKeyDown(index, event),
|
|
1582
|
+
onPaste: (event) => handlePaste(index, event),
|
|
1583
|
+
"aria-label": `OTP digit ${index + 1}`
|
|
1584
|
+
},
|
|
1585
|
+
`otp-${index}`
|
|
1586
|
+
))
|
|
1587
|
+
}
|
|
1588
|
+
);
|
|
1589
|
+
};
|
|
1590
|
+
OtpInput.displayName = "OtpInput";
|
|
1591
|
+
const TwoFactorForm = ({
|
|
1592
|
+
onSuccess,
|
|
1593
|
+
onError,
|
|
1594
|
+
className,
|
|
1595
|
+
submitButtonText = "Verify Code",
|
|
1596
|
+
loadingText = "Verifying...",
|
|
1597
|
+
title = "Two-factor authentication",
|
|
1598
|
+
subtitle = "Enter the verification code to continue.",
|
|
1599
|
+
length = 6
|
|
1600
|
+
}) => {
|
|
1601
|
+
const {
|
|
1602
|
+
verifyTwoFactor,
|
|
1603
|
+
clearError,
|
|
1604
|
+
isLoading,
|
|
1605
|
+
error: authError
|
|
1606
|
+
} = useAuth();
|
|
1607
|
+
const [code, setCode] = react.useState("");
|
|
1608
|
+
const [formError, setFormError] = react.useState("");
|
|
1609
|
+
const error = formError || authError?.message;
|
|
1610
|
+
const handleSubmit = async (event) => {
|
|
1611
|
+
event.preventDefault();
|
|
1612
|
+
clearError();
|
|
1613
|
+
setFormError("");
|
|
1614
|
+
if (code.length !== length) {
|
|
1615
|
+
setFormError(`Enter the full ${length}-digit code.`);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
const response = await verifyTwoFactor({ code });
|
|
1620
|
+
onSuccess?.(response);
|
|
1621
|
+
} catch (caughtError) {
|
|
1622
|
+
const message = caughtError instanceof Error ? caughtError.message : "Two-factor verification failed";
|
|
1623
|
+
setFormError(message);
|
|
1624
|
+
onError?.({
|
|
1625
|
+
code: "TWO_FACTOR_ERROR",
|
|
1626
|
+
message
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(AUTH_FORM_CLASSES.container, className), children: [
|
|
1631
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: AUTH_FORM_CLASSES.header, children: [
|
|
1632
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: AUTH_FORM_CLASSES.title, children: title }),
|
|
1633
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: AUTH_FORM_CLASSES.subtitle, children: subtitle })
|
|
1634
|
+
] }),
|
|
1635
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: AUTH_FORM_CLASSES.form, children: [
|
|
1636
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: AUTH_FORM_CLASSES.field, children: [
|
|
1637
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: AUTH_FORM_CLASSES.label, children: "Verification code" }),
|
|
1638
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1639
|
+
OtpInput,
|
|
1640
|
+
{
|
|
1641
|
+
length,
|
|
1642
|
+
value: code,
|
|
1643
|
+
onChange: setCode,
|
|
1644
|
+
disabled: isLoading
|
|
1645
|
+
}
|
|
1646
|
+
)
|
|
1647
|
+
] }),
|
|
1648
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: AUTH_FORM_CLASSES.error, role: "alert", children: error }) : null,
|
|
1649
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1650
|
+
"button",
|
|
1651
|
+
{
|
|
1652
|
+
type: "submit",
|
|
1653
|
+
disabled: isLoading,
|
|
1654
|
+
className: AUTH_FORM_CLASSES.button,
|
|
1655
|
+
children: isLoading ? loadingText : submitButtonText
|
|
1656
|
+
}
|
|
1657
|
+
)
|
|
1658
|
+
] })
|
|
1659
|
+
] });
|
|
1660
|
+
};
|
|
1661
|
+
TwoFactorForm.displayName = "TwoFactorForm";
|
|
1662
|
+
const defaultLabels = {
|
|
1663
|
+
google: "Continue with Google",
|
|
1664
|
+
github: "Continue with GitHub",
|
|
1665
|
+
facebook: "Continue with Facebook",
|
|
1666
|
+
custom: "Continue"
|
|
1667
|
+
};
|
|
1668
|
+
const defaultIcons = {
|
|
1669
|
+
google: "G",
|
|
1670
|
+
github: "GH",
|
|
1671
|
+
facebook: "f",
|
|
1672
|
+
custom: "+"
|
|
1673
|
+
};
|
|
1674
|
+
const SocialLoginButton = ({
|
|
1675
|
+
provider,
|
|
1676
|
+
label,
|
|
1677
|
+
icon,
|
|
1678
|
+
onClick,
|
|
1679
|
+
className,
|
|
1680
|
+
disabled = false
|
|
1681
|
+
}) => {
|
|
1682
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1683
|
+
"button",
|
|
1684
|
+
{
|
|
1685
|
+
type: "button",
|
|
1686
|
+
className: cn(AUTH_FORM_CLASSES.socialButton, className),
|
|
1687
|
+
"data-provider": provider,
|
|
1688
|
+
onClick,
|
|
1689
|
+
disabled,
|
|
1690
|
+
children: [
|
|
1691
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: AUTH_FORM_CLASSES.socialIcon, "aria-hidden": "true", children: icon ?? defaultIcons[provider] }),
|
|
1692
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: AUTH_FORM_CLASSES.socialLabel, children: label ?? defaultLabels[provider] })
|
|
1693
|
+
]
|
|
1694
|
+
}
|
|
1695
|
+
);
|
|
1696
|
+
};
|
|
1697
|
+
SocialLoginButton.displayName = "SocialLoginButton";
|
|
1148
1698
|
const ProtectedRoute = ({
|
|
1149
1699
|
children,
|
|
1150
1700
|
redirectTo = "/login",
|
|
1151
1701
|
fallback,
|
|
1152
1702
|
roles,
|
|
1153
|
-
permissions
|
|
1703
|
+
permissions,
|
|
1704
|
+
unauthorizedTo,
|
|
1705
|
+
requireAllPermissions = true
|
|
1154
1706
|
}) => {
|
|
1155
|
-
const {
|
|
1707
|
+
const {
|
|
1708
|
+
isAuthenticated,
|
|
1709
|
+
isLoading,
|
|
1710
|
+
hasAllPermissions,
|
|
1711
|
+
hasAnyPermission,
|
|
1712
|
+
hasAnyRole
|
|
1713
|
+
} = useAuth();
|
|
1156
1714
|
if (isLoading) {
|
|
1157
1715
|
return fallback || /* @__PURE__ */ jsxRuntime.jsx("div", { className: "auth-loading", children: "Loading..." });
|
|
1158
1716
|
}
|
|
1159
1717
|
if (!isAuthenticated) {
|
|
1160
1718
|
return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: redirectTo, replace: true });
|
|
1161
1719
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (permissions && permissions.length > 0) {
|
|
1169
|
-
const hasPermission = user?.permissions?.some(
|
|
1170
|
-
(permission) => permissions.includes(permission)
|
|
1171
|
-
);
|
|
1172
|
-
if (!hasPermission) {
|
|
1173
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "auth-forbidden", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "You do not have permission to access this page." }) });
|
|
1174
|
-
}
|
|
1720
|
+
const hasRequiredRole = !roles?.length || hasAnyRole(roles);
|
|
1721
|
+
const hasRequiredPermission = !permissions?.length || (requireAllPermissions ? hasAllPermissions(permissions) : hasAnyPermission(permissions));
|
|
1722
|
+
if (!hasRequiredRole || !hasRequiredPermission) {
|
|
1723
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: unauthorizedTo ?? redirectTo, replace: true });
|
|
1175
1724
|
}
|
|
1176
1725
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
1177
1726
|
};
|
|
1178
1727
|
ProtectedRoute.displayName = "ProtectedRoute";
|
|
1728
|
+
const GuestRoute = ({
|
|
1729
|
+
children,
|
|
1730
|
+
redirectTo = "/",
|
|
1731
|
+
fallback
|
|
1732
|
+
}) => {
|
|
1733
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
1734
|
+
if (isLoading) {
|
|
1735
|
+
return fallback || /* @__PURE__ */ jsxRuntime.jsx("div", { className: "auth-loading", children: "Loading..." });
|
|
1736
|
+
}
|
|
1737
|
+
if (isAuthenticated) {
|
|
1738
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: redirectTo, replace: true });
|
|
1739
|
+
}
|
|
1740
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
1741
|
+
};
|
|
1742
|
+
GuestRoute.displayName = "GuestRoute";
|
|
1179
1743
|
exports.AuthContext = AuthContext;
|
|
1744
|
+
exports.AuthLayout = AuthLayout;
|
|
1180
1745
|
exports.AuthProvider = AuthProvider;
|
|
1181
1746
|
exports.ForgotPasswordForm = ForgotPasswordForm;
|
|
1747
|
+
exports.GuestRoute = GuestRoute;
|
|
1182
1748
|
exports.LoginForm = LoginForm;
|
|
1749
|
+
exports.OtpInput = OtpInput;
|
|
1183
1750
|
exports.ProtectedRoute = ProtectedRoute;
|
|
1184
1751
|
exports.RegisterForm = RegisterForm;
|
|
1185
1752
|
exports.ResetPasswordForm = ResetPasswordForm;
|
|
1753
|
+
exports.SocialLoginButton = SocialLoginButton;
|
|
1754
|
+
exports.TwoFactorForm = TwoFactorForm;
|
|
1186
1755
|
exports.VerifyEmailForm = VerifyEmailForm;
|
|
1187
1756
|
exports.cn = cn;
|
|
1188
1757
|
exports.createAuthClient = createAuthClient;
|