@startsimpli/auth 0.1.0 → 0.1.2
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/dist/chunk-CDNZRZ7Q.mjs +767 -0
- package/dist/chunk-CDNZRZ7Q.mjs.map +1 -0
- package/dist/chunk-S6J5FYQY.mjs +134 -0
- package/dist/chunk-S6J5FYQY.mjs.map +1 -0
- package/dist/chunk-TA46ASDJ.mjs +37 -0
- package/dist/chunk-TA46ASDJ.mjs.map +1 -0
- package/dist/client/index.d.mts +175 -0
- package/dist/client/index.d.ts +175 -0
- package/dist/client/index.js +858 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +5 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +971 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.d.mts +83 -0
- package/dist/server/index.d.ts +83 -0
- package/dist/server/index.js +242 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +191 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +209 -0
- package/dist/types/index.d.ts +209 -0
- package/dist/types/index.js +43 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +50 -18
- package/src/__tests__/auth-client.test.ts +125 -0
- package/src/__tests__/auth-fetch.test.ts +128 -0
- package/src/__tests__/token-storage.test.ts +61 -0
- package/src/__tests__/validation.test.ts +60 -0
- package/src/client/auth-client.ts +11 -1
- package/src/client/functions.ts +83 -14
- package/src/types/index.ts +100 -0
- package/src/utils/validation.ts +190 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { getTokenExpiresAt, isTokenExpired, shouldRefreshToken, getCsrfToken } from './chunk-S6J5FYQY.mjs';
|
|
2
|
+
import { hasRolePermission } from './chunk-TA46ASDJ.mjs';
|
|
3
|
+
import { createContext, useState, useEffect, useCallback, useContext, useMemo } from 'react';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/client/auth-client.ts
|
|
7
|
+
var AuthClient = class {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.session = null;
|
|
10
|
+
this.refreshTimer = null;
|
|
11
|
+
this.isRefreshing = false;
|
|
12
|
+
this.refreshPromise = null;
|
|
13
|
+
this.config = {
|
|
14
|
+
tokenRefreshInterval: 4 * 60 * 1e3,
|
|
15
|
+
// 4 minutes
|
|
16
|
+
onSessionExpired: () => {
|
|
17
|
+
},
|
|
18
|
+
onUnauthorized: () => {
|
|
19
|
+
},
|
|
20
|
+
...config
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Login with email and password
|
|
25
|
+
*/
|
|
26
|
+
async login(email, password) {
|
|
27
|
+
const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
credentials: "include",
|
|
31
|
+
// Include cookies for refresh token
|
|
32
|
+
body: JSON.stringify({ email, password })
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const error = await response.json().catch(() => ({ detail: "Login failed" }));
|
|
36
|
+
throw new Error(error.detail || "Login failed");
|
|
37
|
+
}
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const expiresAt = getTokenExpiresAt(data.access);
|
|
40
|
+
if (!expiresAt) {
|
|
41
|
+
throw new Error("Invalid token received");
|
|
42
|
+
}
|
|
43
|
+
const tempSession = {
|
|
44
|
+
user: data.user || { id: "", email: "", firstName: "", lastName: "", isEmailVerified: false, createdAt: "", updatedAt: "" },
|
|
45
|
+
accessToken: data.access,
|
|
46
|
+
expiresAt
|
|
47
|
+
};
|
|
48
|
+
this.session = tempSession;
|
|
49
|
+
if (!data.user) {
|
|
50
|
+
try {
|
|
51
|
+
const user = await this.getCurrentUser();
|
|
52
|
+
this.session.user = user;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Failed to fetch user data after login:", error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.startRefreshTimer();
|
|
58
|
+
return this.session;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Logout and clear session
|
|
62
|
+
*/
|
|
63
|
+
async logout() {
|
|
64
|
+
try {
|
|
65
|
+
await fetch(`${this.config.apiBaseUrl}/api/v1/auth/logout/`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: this.getAuthHeaders(),
|
|
68
|
+
credentials: "include"
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("Logout error:", error);
|
|
72
|
+
} finally {
|
|
73
|
+
this.clearSession();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Refresh access token using refresh token cookie
|
|
78
|
+
*/
|
|
79
|
+
async refreshToken() {
|
|
80
|
+
if (this.isRefreshing && this.refreshPromise) {
|
|
81
|
+
return this.refreshPromise;
|
|
82
|
+
}
|
|
83
|
+
this.isRefreshing = true;
|
|
84
|
+
this.refreshPromise = this.performTokenRefresh();
|
|
85
|
+
try {
|
|
86
|
+
const newToken = await this.refreshPromise;
|
|
87
|
+
return newToken;
|
|
88
|
+
} finally {
|
|
89
|
+
this.isRefreshing = false;
|
|
90
|
+
this.refreshPromise = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async performTokenRefresh() {
|
|
94
|
+
const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/refresh/`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
credentials: "include"
|
|
98
|
+
// Send refresh token cookie
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
this.clearSession();
|
|
102
|
+
this.config.onSessionExpired();
|
|
103
|
+
throw new Error("Token refresh failed");
|
|
104
|
+
}
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
const expiresAt = getTokenExpiresAt(data.access);
|
|
107
|
+
if (!expiresAt) {
|
|
108
|
+
throw new Error("Invalid token received");
|
|
109
|
+
}
|
|
110
|
+
if (this.session) {
|
|
111
|
+
this.session.accessToken = data.access;
|
|
112
|
+
this.session.expiresAt = expiresAt;
|
|
113
|
+
}
|
|
114
|
+
this.startRefreshTimer();
|
|
115
|
+
return data.access;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get current user data from backend
|
|
119
|
+
*/
|
|
120
|
+
async getCurrentUser() {
|
|
121
|
+
const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/me/`, {
|
|
122
|
+
headers: this.getAuthHeaders(),
|
|
123
|
+
credentials: "include"
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
if (response.status === 401) {
|
|
127
|
+
this.config.onUnauthorized();
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Failed to fetch user data");
|
|
130
|
+
}
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
const raw = data.user || data;
|
|
133
|
+
const user = {
|
|
134
|
+
id: raw.id,
|
|
135
|
+
email: raw.email,
|
|
136
|
+
firstName: raw.first_name || raw.firstName || "",
|
|
137
|
+
lastName: raw.last_name || raw.lastName || "",
|
|
138
|
+
isEmailVerified: raw.is_email_verified ?? raw.isEmailVerified ?? false,
|
|
139
|
+
createdAt: raw.created_at || raw.createdAt || "",
|
|
140
|
+
updatedAt: raw.updated_at || raw.updatedAt || ""
|
|
141
|
+
};
|
|
142
|
+
if (this.session) {
|
|
143
|
+
this.session.user = user;
|
|
144
|
+
}
|
|
145
|
+
return user;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get current session
|
|
149
|
+
*/
|
|
150
|
+
getSession() {
|
|
151
|
+
if (!this.session) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (isTokenExpired(this.session.accessToken)) {
|
|
155
|
+
this.clearSession();
|
|
156
|
+
this.config.onSessionExpired();
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return this.session;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Set session (for SSR/hydration)
|
|
163
|
+
*/
|
|
164
|
+
setSession(session) {
|
|
165
|
+
this.session = session;
|
|
166
|
+
this.startRefreshTimer();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get auth headers for API requests
|
|
170
|
+
*/
|
|
171
|
+
getAuthHeaders() {
|
|
172
|
+
const session = this.getSession();
|
|
173
|
+
if (!session) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
Authorization: `Bearer ${session.accessToken}`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get valid access token (refreshes if needed)
|
|
182
|
+
*/
|
|
183
|
+
async getAccessToken() {
|
|
184
|
+
const session = this.getSession();
|
|
185
|
+
if (!session) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (shouldRefreshToken(session.accessToken)) {
|
|
189
|
+
try {
|
|
190
|
+
return await this.refreshToken();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Token refresh failed:", error);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return session.accessToken;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Start automatic token refresh timer
|
|
200
|
+
*/
|
|
201
|
+
startRefreshTimer() {
|
|
202
|
+
if (this.refreshTimer) {
|
|
203
|
+
clearInterval(this.refreshTimer);
|
|
204
|
+
}
|
|
205
|
+
this.refreshTimer = setInterval(() => {
|
|
206
|
+
const session = this.getSession();
|
|
207
|
+
if (session && shouldRefreshToken(session.accessToken)) {
|
|
208
|
+
this.refreshToken().catch((error) => {
|
|
209
|
+
console.error("Auto-refresh failed:", error);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}, this.config.tokenRefreshInterval);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Clear session and stop refresh timer
|
|
216
|
+
*/
|
|
217
|
+
clearSession() {
|
|
218
|
+
this.session = null;
|
|
219
|
+
if (this.refreshTimer) {
|
|
220
|
+
clearInterval(this.refreshTimer);
|
|
221
|
+
this.refreshTimer = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Cleanup resources
|
|
226
|
+
*/
|
|
227
|
+
destroy() {
|
|
228
|
+
this.clearSession();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var AuthContext = createContext(void 0);
|
|
232
|
+
function AuthProvider({
|
|
233
|
+
children,
|
|
234
|
+
config,
|
|
235
|
+
initialSession
|
|
236
|
+
}) {
|
|
237
|
+
const [authClient] = useState(() => new AuthClient(config));
|
|
238
|
+
const [state, setState] = useState(() => ({
|
|
239
|
+
session: initialSession || null,
|
|
240
|
+
isLoading: !initialSession,
|
|
241
|
+
isAuthenticated: !!initialSession
|
|
242
|
+
}));
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (initialSession) {
|
|
245
|
+
authClient.setSession(initialSession);
|
|
246
|
+
setState({
|
|
247
|
+
session: initialSession,
|
|
248
|
+
isLoading: false,
|
|
249
|
+
isAuthenticated: true
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
const session = authClient.getSession();
|
|
253
|
+
setState({
|
|
254
|
+
session,
|
|
255
|
+
isLoading: false,
|
|
256
|
+
isAuthenticated: !!session
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}, [authClient, initialSession]);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const originalOnExpired = config.onSessionExpired;
|
|
262
|
+
config.onSessionExpired = () => {
|
|
263
|
+
setState({
|
|
264
|
+
session: null,
|
|
265
|
+
isLoading: false,
|
|
266
|
+
isAuthenticated: false
|
|
267
|
+
});
|
|
268
|
+
originalOnExpired?.();
|
|
269
|
+
};
|
|
270
|
+
return () => {
|
|
271
|
+
authClient.destroy();
|
|
272
|
+
};
|
|
273
|
+
}, [authClient, config]);
|
|
274
|
+
const login = useCallback(
|
|
275
|
+
async (email, password) => {
|
|
276
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
277
|
+
try {
|
|
278
|
+
const session = await authClient.login(email, password);
|
|
279
|
+
setState({
|
|
280
|
+
session,
|
|
281
|
+
isLoading: false,
|
|
282
|
+
isAuthenticated: true
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
setState((prev) => ({ ...prev, isLoading: false }));
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
[authClient]
|
|
290
|
+
);
|
|
291
|
+
const logout = useCallback(async () => {
|
|
292
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
293
|
+
try {
|
|
294
|
+
await authClient.logout();
|
|
295
|
+
} finally {
|
|
296
|
+
setState({
|
|
297
|
+
session: null,
|
|
298
|
+
isLoading: false,
|
|
299
|
+
isAuthenticated: false
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}, [authClient]);
|
|
303
|
+
const refreshUser = useCallback(async () => {
|
|
304
|
+
try {
|
|
305
|
+
const user = await authClient.getCurrentUser();
|
|
306
|
+
setState((prev) => ({
|
|
307
|
+
...prev,
|
|
308
|
+
session: prev.session ? { ...prev.session, user } : null
|
|
309
|
+
}));
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error("Failed to refresh user:", error);
|
|
312
|
+
}
|
|
313
|
+
}, [authClient]);
|
|
314
|
+
const getAccessToken2 = useCallback(async () => {
|
|
315
|
+
return authClient.getAccessToken();
|
|
316
|
+
}, [authClient]);
|
|
317
|
+
const value = {
|
|
318
|
+
...state,
|
|
319
|
+
login,
|
|
320
|
+
logout,
|
|
321
|
+
refreshUser,
|
|
322
|
+
getAccessToken: getAccessToken2
|
|
323
|
+
};
|
|
324
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
|
|
325
|
+
}
|
|
326
|
+
function useAuthContext() {
|
|
327
|
+
const context = useContext(AuthContext);
|
|
328
|
+
if (!context) {
|
|
329
|
+
throw new Error("useAuthContext must be used within AuthProvider");
|
|
330
|
+
}
|
|
331
|
+
return context;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/client/use-auth.ts
|
|
335
|
+
function useAuth() {
|
|
336
|
+
const {
|
|
337
|
+
session,
|
|
338
|
+
isLoading,
|
|
339
|
+
isAuthenticated,
|
|
340
|
+
login,
|
|
341
|
+
logout,
|
|
342
|
+
refreshUser,
|
|
343
|
+
getAccessToken: getAccessToken2
|
|
344
|
+
} = useAuthContext();
|
|
345
|
+
return {
|
|
346
|
+
user: session?.user || null,
|
|
347
|
+
session,
|
|
348
|
+
isLoading,
|
|
349
|
+
isAuthenticated,
|
|
350
|
+
login,
|
|
351
|
+
logout,
|
|
352
|
+
refreshUser,
|
|
353
|
+
getAccessToken: getAccessToken2
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function usePermissions() {
|
|
357
|
+
const { user } = useAuth();
|
|
358
|
+
const currentCompanyId = user?.currentCompanyId || null;
|
|
359
|
+
const currentCompany = useMemo(() => {
|
|
360
|
+
if (!user?.companies || !currentCompanyId) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return user.companies.find((c) => c.id === currentCompanyId);
|
|
364
|
+
}, [user, currentCompanyId]);
|
|
365
|
+
const currentRole = currentCompany?.role || null;
|
|
366
|
+
const hasRole = (requiredRole, companyId) => {
|
|
367
|
+
if (!user?.companies) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const targetCompanyId = companyId || currentCompanyId;
|
|
371
|
+
if (!targetCompanyId) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const company = user.companies.find((c) => c.id === targetCompanyId);
|
|
375
|
+
if (!company) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return hasRolePermission(company.role, requiredRole);
|
|
379
|
+
};
|
|
380
|
+
const isOwner = (companyId) => {
|
|
381
|
+
return hasRole("owner", companyId);
|
|
382
|
+
};
|
|
383
|
+
const isAdmin = (companyId) => {
|
|
384
|
+
return hasRole("admin", companyId);
|
|
385
|
+
};
|
|
386
|
+
const canEdit = (companyId) => {
|
|
387
|
+
return hasRole("member", companyId);
|
|
388
|
+
};
|
|
389
|
+
const canView = (companyId) => {
|
|
390
|
+
return hasRole("viewer", companyId);
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
hasRole,
|
|
394
|
+
isOwner,
|
|
395
|
+
isAdmin,
|
|
396
|
+
canEdit,
|
|
397
|
+
canView,
|
|
398
|
+
currentRole,
|
|
399
|
+
currentCompanyId
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/client/functions.ts
|
|
404
|
+
var API_BASE = "/api/v1";
|
|
405
|
+
var AUTH_PATHS = {
|
|
406
|
+
TOKEN: `${API_BASE}/auth/token/`,
|
|
407
|
+
TOKEN_REFRESH: `${API_BASE}/auth/token/refresh/`,
|
|
408
|
+
REGISTER: `${API_BASE}/auth/register/`,
|
|
409
|
+
LOGOUT: `${API_BASE}/auth/logout/`,
|
|
410
|
+
FORGOT_PASSWORD: `${API_BASE}/auth/forgot-password/`,
|
|
411
|
+
RESET_PASSWORD: `${API_BASE}/auth/reset-password/`,
|
|
412
|
+
VERIFY_EMAIL: `${API_BASE}/auth/verify-email/`,
|
|
413
|
+
RESEND_VERIFICATION: `${API_BASE}/auth/resend-verification/`,
|
|
414
|
+
OAUTH_GOOGLE_INITIATE: `${API_BASE}/auth/oauth/google/initiate/`,
|
|
415
|
+
OAUTH_GOOGLE_CALLBACK: `${API_BASE}/auth/oauth/google/callback/`,
|
|
416
|
+
ME: `${API_BASE}/auth/me/`
|
|
417
|
+
};
|
|
418
|
+
var AUTH_BASE_URL = typeof process !== "undefined" && (process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_BASE_URL) || "";
|
|
419
|
+
var AUTH_BASE_IS_ABSOLUTE = /^https?:\/\//i.test(AUTH_BASE_URL);
|
|
420
|
+
function isAbsoluteUrl(url) {
|
|
421
|
+
return /^https?:\/\//i.test(url);
|
|
422
|
+
}
|
|
423
|
+
function resolveAuthUrl(path) {
|
|
424
|
+
if (isAbsoluteUrl(path)) return path;
|
|
425
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
426
|
+
if (AUTH_BASE_IS_ABSOLUTE) {
|
|
427
|
+
return new URL(normalized, AUTH_BASE_URL).toString();
|
|
428
|
+
}
|
|
429
|
+
if (normalized.startsWith("/api/")) return normalized;
|
|
430
|
+
if (!AUTH_BASE_URL) return normalized;
|
|
431
|
+
if (AUTH_BASE_URL.endsWith("/") && normalized.startsWith("/")) {
|
|
432
|
+
return `${AUTH_BASE_URL.slice(0, -1)}${normalized}`;
|
|
433
|
+
}
|
|
434
|
+
return `${AUTH_BASE_URL}${normalized}`;
|
|
435
|
+
}
|
|
436
|
+
var TOKEN_STORAGE_KEY = "auth_access_token";
|
|
437
|
+
var _memToken = null;
|
|
438
|
+
function _sessionStorageAvailable() {
|
|
439
|
+
try {
|
|
440
|
+
return typeof window !== "undefined" && !!window.sessionStorage;
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function getAccessToken() {
|
|
446
|
+
if (_sessionStorageAvailable()) {
|
|
447
|
+
return sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
|
448
|
+
}
|
|
449
|
+
return _memToken;
|
|
450
|
+
}
|
|
451
|
+
function setAccessToken(token) {
|
|
452
|
+
if (_sessionStorageAvailable()) {
|
|
453
|
+
if (token === null) {
|
|
454
|
+
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
455
|
+
} else {
|
|
456
|
+
sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
_memToken = token;
|
|
461
|
+
}
|
|
462
|
+
var AUTH_TIMEOUT_MS = 15e3;
|
|
463
|
+
function extractApiError(d, fallback) {
|
|
464
|
+
if (typeof d.detail === "string") return d.detail;
|
|
465
|
+
for (const val of Object.values(d)) {
|
|
466
|
+
if (typeof val === "string") return val;
|
|
467
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "string") return val[0];
|
|
468
|
+
}
|
|
469
|
+
return fallback;
|
|
470
|
+
}
|
|
471
|
+
function fetchWithTimeout(url, options) {
|
|
472
|
+
const controller = new AbortController();
|
|
473
|
+
const timer = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS);
|
|
474
|
+
return fetch(url, { ...options, signal: controller.signal }).finally(
|
|
475
|
+
() => clearTimeout(timer)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
function normalizeUser(raw) {
|
|
479
|
+
if (!raw || typeof raw !== "object") return null;
|
|
480
|
+
const obj = raw;
|
|
481
|
+
const payload = obj.user && typeof obj.user === "object" ? obj.user : obj;
|
|
482
|
+
if (!payload?.id || !payload?.email) return null;
|
|
483
|
+
const firstName = payload.first_name ?? payload.firstName ?? null;
|
|
484
|
+
const lastName = payload.last_name ?? payload.lastName ?? null;
|
|
485
|
+
const name = payload.name ?? ([firstName, lastName].filter(Boolean).join(" ") || null);
|
|
486
|
+
return {
|
|
487
|
+
id: payload.id,
|
|
488
|
+
email: payload.email,
|
|
489
|
+
name,
|
|
490
|
+
firstName,
|
|
491
|
+
lastName,
|
|
492
|
+
groups: Array.isArray(payload.groups) ? payload.groups : [],
|
|
493
|
+
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
|
|
494
|
+
isActive: payload.isActive ?? payload.is_active,
|
|
495
|
+
isEmailVerified: payload.isEmailVerified ?? payload.is_email_verified
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function parseAuthResponse(data) {
|
|
499
|
+
if (!data || typeof data !== "object") return {};
|
|
500
|
+
const obj = data;
|
|
501
|
+
const payload = obj.data && typeof obj.data === "object" ? obj.data : obj;
|
|
502
|
+
const access = payload.access || payload.access_token || payload.token;
|
|
503
|
+
const userRaw = payload.user || payload.me || payload.profile || payload;
|
|
504
|
+
const user = normalizeUser(userRaw);
|
|
505
|
+
return { access, user: user ?? void 0 };
|
|
506
|
+
}
|
|
507
|
+
async function signInWithCredentials(email, password) {
|
|
508
|
+
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN), {
|
|
509
|
+
method: "POST",
|
|
510
|
+
headers: { "Content-Type": "application/json" },
|
|
511
|
+
credentials: "include",
|
|
512
|
+
body: JSON.stringify({ email, password })
|
|
513
|
+
});
|
|
514
|
+
const data = await response.json().catch(() => ({}));
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const d = data;
|
|
517
|
+
if (d?.error === "ACCOUNT_LOCKED" && d?.locked_until) {
|
|
518
|
+
throw new Error(`Account locked until ${new Date(d.locked_until).toLocaleTimeString()}`);
|
|
519
|
+
}
|
|
520
|
+
const message = d?.detail || d?.error || "Authentication failed";
|
|
521
|
+
throw new Error(message);
|
|
522
|
+
}
|
|
523
|
+
const parsed = parseAuthResponse(data);
|
|
524
|
+
if (parsed.access) {
|
|
525
|
+
setAccessToken(parsed.access);
|
|
526
|
+
}
|
|
527
|
+
return parsed;
|
|
528
|
+
}
|
|
529
|
+
async function registerAccount(payload) {
|
|
530
|
+
const rawName = payload.name?.trim() || "";
|
|
531
|
+
const [firstFromName, ...rest] = rawName ? rawName.split(/\s+/) : [];
|
|
532
|
+
const lastFromName = rest.length ? rest.join(" ") : void 0;
|
|
533
|
+
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.REGISTER), {
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: { "Content-Type": "application/json" },
|
|
536
|
+
credentials: "include",
|
|
537
|
+
body: JSON.stringify({
|
|
538
|
+
email: payload.email,
|
|
539
|
+
password: payload.password,
|
|
540
|
+
password_confirm: payload.passwordConfirm,
|
|
541
|
+
first_name: payload.firstName ?? firstFromName ?? void 0,
|
|
542
|
+
last_name: payload.lastName ?? lastFromName ?? void 0
|
|
543
|
+
})
|
|
544
|
+
});
|
|
545
|
+
const data = await response.json().catch(() => ({}));
|
|
546
|
+
if (!response.ok) {
|
|
547
|
+
throw new Error(extractApiError(data, "Registration failed"));
|
|
548
|
+
}
|
|
549
|
+
const parsed = parseAuthResponse(data);
|
|
550
|
+
if (parsed.access) {
|
|
551
|
+
setAccessToken(parsed.access);
|
|
552
|
+
}
|
|
553
|
+
return parsed;
|
|
554
|
+
}
|
|
555
|
+
async function requestPasswordReset(email) {
|
|
556
|
+
const response = await fetch(resolveAuthUrl(AUTH_PATHS.FORGOT_PASSWORD), {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "Content-Type": "application/json" },
|
|
559
|
+
body: JSON.stringify({ email })
|
|
560
|
+
});
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
const data = await response.json().catch(() => ({}));
|
|
563
|
+
const d = data;
|
|
564
|
+
const message = d?.detail || d?.error || "Failed to send reset link";
|
|
565
|
+
throw new Error(message);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function resetPassword(payload) {
|
|
569
|
+
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.RESET_PASSWORD), {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { "Content-Type": "application/json" },
|
|
572
|
+
body: JSON.stringify({
|
|
573
|
+
token: payload.token,
|
|
574
|
+
password: payload.password,
|
|
575
|
+
password_confirm: payload.passwordConfirm,
|
|
576
|
+
...payload.email ? { email: payload.email } : {}
|
|
577
|
+
})
|
|
578
|
+
});
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
const data = await response.json().catch(() => ({}));
|
|
581
|
+
const d = data;
|
|
582
|
+
const message = d?.detail || d?.error || "Failed to reset password";
|
|
583
|
+
throw new Error(message);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function verifyEmail(token) {
|
|
587
|
+
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.VERIFY_EMAIL), {
|
|
588
|
+
method: "POST",
|
|
589
|
+
headers: { "Content-Type": "application/json" },
|
|
590
|
+
body: JSON.stringify({ token })
|
|
591
|
+
});
|
|
592
|
+
if (!response.ok) {
|
|
593
|
+
const data = await response.json().catch(() => ({}));
|
|
594
|
+
const d = data;
|
|
595
|
+
const message = d?.detail || d?.error || "Failed to verify email";
|
|
596
|
+
throw new Error(message);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function resendVerification(access) {
|
|
600
|
+
const response = await fetch(resolveAuthUrl(AUTH_PATHS.RESEND_VERIFICATION), {
|
|
601
|
+
method: "POST",
|
|
602
|
+
headers: {
|
|
603
|
+
"Content-Type": "application/json",
|
|
604
|
+
...access ? { Authorization: `Bearer ${access}` } : {}
|
|
605
|
+
},
|
|
606
|
+
credentials: "include"
|
|
607
|
+
});
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
const data = await response.json().catch(() => ({}));
|
|
610
|
+
const d = data;
|
|
611
|
+
const message = d?.detail || d?.error || "Failed to resend verification email";
|
|
612
|
+
throw new Error(message);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function initiateGoogleOAuth(redirectUri) {
|
|
616
|
+
const response = await fetch(resolveAuthUrl(AUTH_PATHS.OAUTH_GOOGLE_INITIATE), {
|
|
617
|
+
method: "POST",
|
|
618
|
+
headers: { "Content-Type": "application/json" },
|
|
619
|
+
credentials: "include",
|
|
620
|
+
body: JSON.stringify({ redirect_uri: redirectUri })
|
|
621
|
+
});
|
|
622
|
+
const data = await response.json().catch(() => ({}));
|
|
623
|
+
if (!response.ok) {
|
|
624
|
+
const d = data;
|
|
625
|
+
const message = d?.detail || d?.error || "Failed to initiate Google OAuth";
|
|
626
|
+
throw new Error(message);
|
|
627
|
+
}
|
|
628
|
+
return data;
|
|
629
|
+
}
|
|
630
|
+
async function completeGoogleOAuth(code, state) {
|
|
631
|
+
const response = await fetchWithTimeout(
|
|
632
|
+
resolveAuthUrl(
|
|
633
|
+
`${AUTH_PATHS.OAUTH_GOOGLE_CALLBACK}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
|
|
634
|
+
),
|
|
635
|
+
{ credentials: "include" }
|
|
636
|
+
);
|
|
637
|
+
const data = await response.json().catch(() => ({}));
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
const d = data;
|
|
640
|
+
const message = d?.detail || d?.error || "OAuth authentication failed";
|
|
641
|
+
throw new Error(message);
|
|
642
|
+
}
|
|
643
|
+
const parsed = parseAuthResponse(data);
|
|
644
|
+
if (parsed.access) {
|
|
645
|
+
setAccessToken(parsed.access);
|
|
646
|
+
}
|
|
647
|
+
return parsed;
|
|
648
|
+
}
|
|
649
|
+
async function fetchCsrfToken() {
|
|
650
|
+
if (getCsrfToken()) return;
|
|
651
|
+
try {
|
|
652
|
+
await fetch(resolveAuthUrl(`${API_BASE}/auth/csrf/`), {
|
|
653
|
+
credentials: "include",
|
|
654
|
+
cache: "no-store"
|
|
655
|
+
});
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.warn("[auth] CSRF token fetch failed \u2014 token refresh will fail:", err);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async function refreshAccessToken() {
|
|
661
|
+
await fetchCsrfToken();
|
|
662
|
+
const csrfToken = getCsrfToken();
|
|
663
|
+
if (!csrfToken) return null;
|
|
664
|
+
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN_REFRESH), {
|
|
665
|
+
method: "POST",
|
|
666
|
+
headers: {
|
|
667
|
+
"Content-Type": "application/json",
|
|
668
|
+
"X-CSRFToken": csrfToken
|
|
669
|
+
},
|
|
670
|
+
credentials: "include"
|
|
671
|
+
});
|
|
672
|
+
const data = await response.json().catch(() => ({}));
|
|
673
|
+
if (!response.ok) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const parsed = parseAuthResponse(data);
|
|
677
|
+
if (parsed.access) {
|
|
678
|
+
setAccessToken(parsed.access);
|
|
679
|
+
return parsed.access;
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
async function getMe() {
|
|
684
|
+
const token = getAccessToken();
|
|
685
|
+
if (!token) return null;
|
|
686
|
+
const response = await fetch(resolveAuthUrl(AUTH_PATHS.ME), {
|
|
687
|
+
headers: {
|
|
688
|
+
Authorization: `Bearer ${token}`
|
|
689
|
+
},
|
|
690
|
+
credentials: "include",
|
|
691
|
+
cache: "no-store"
|
|
692
|
+
});
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const data = await response.json().catch(() => ({}));
|
|
697
|
+
const obj = data;
|
|
698
|
+
const payload = obj.data && typeof obj.data === "object" ? obj.data : obj;
|
|
699
|
+
const userRaw = payload.user || payload;
|
|
700
|
+
return normalizeUser(userRaw);
|
|
701
|
+
}
|
|
702
|
+
async function signOut() {
|
|
703
|
+
const csrfToken = getCsrfToken();
|
|
704
|
+
const token = getAccessToken();
|
|
705
|
+
try {
|
|
706
|
+
await fetch(resolveAuthUrl(AUTH_PATHS.LOGOUT), {
|
|
707
|
+
method: "POST",
|
|
708
|
+
headers: {
|
|
709
|
+
"Content-Type": "application/json",
|
|
710
|
+
...csrfToken ? { "X-CSRFToken": csrfToken } : {},
|
|
711
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
712
|
+
},
|
|
713
|
+
credentials: "include"
|
|
714
|
+
});
|
|
715
|
+
} finally {
|
|
716
|
+
setAccessToken(null);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
var _refreshPromise = null;
|
|
720
|
+
function _refreshOnce() {
|
|
721
|
+
if (!_refreshPromise) {
|
|
722
|
+
_refreshPromise = refreshAccessToken().finally(() => {
|
|
723
|
+
_refreshPromise = null;
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return _refreshPromise;
|
|
727
|
+
}
|
|
728
|
+
async function authFetch(input, init = {}) {
|
|
729
|
+
const token = getAccessToken();
|
|
730
|
+
const headers = new Headers(init.headers || {});
|
|
731
|
+
if (token && !headers.has("Authorization")) {
|
|
732
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
733
|
+
}
|
|
734
|
+
const resolvedInput = typeof input === "string" ? resolveAuthUrl(input) : input;
|
|
735
|
+
const response = await fetch(resolvedInput, {
|
|
736
|
+
...init,
|
|
737
|
+
headers,
|
|
738
|
+
credentials: init.credentials ?? "include"
|
|
739
|
+
});
|
|
740
|
+
if (response.status === 401) {
|
|
741
|
+
const refreshed = await _refreshOnce();
|
|
742
|
+
if (refreshed) {
|
|
743
|
+
const retryHeaders = new Headers(init.headers || {});
|
|
744
|
+
retryHeaders.set("Authorization", `Bearer ${refreshed}`);
|
|
745
|
+
return fetch(resolvedInput, {
|
|
746
|
+
...init,
|
|
747
|
+
headers: retryHeaders,
|
|
748
|
+
credentials: init.credentials ?? "include"
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return response;
|
|
753
|
+
}
|
|
754
|
+
function hasPermission(user, permission) {
|
|
755
|
+
if (!user) return false;
|
|
756
|
+
if (!user.permissions || user.permissions.length === 0) return false;
|
|
757
|
+
return user.permissions.includes(permission);
|
|
758
|
+
}
|
|
759
|
+
function hasGroup(user, group) {
|
|
760
|
+
if (!user) return false;
|
|
761
|
+
if (!user.groups || user.groups.length === 0) return false;
|
|
762
|
+
return user.groups.includes(group);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export { AuthClient, AuthProvider, authFetch, completeGoogleOAuth, getAccessToken, getMe, hasGroup, hasPermission, initiateGoogleOAuth, refreshAccessToken, registerAccount, requestPasswordReset, resendVerification, resetPassword, resolveAuthUrl, setAccessToken, signInWithCredentials, signOut, useAuth, useAuthContext, usePermissions, verifyEmail };
|
|
766
|
+
//# sourceMappingURL=chunk-CDNZRZ7Q.mjs.map
|
|
767
|
+
//# sourceMappingURL=chunk-CDNZRZ7Q.mjs.map
|