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