@classic-homes/auth 0.1.43 → 0.1.44
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/{auth.svelte-LJJ7MGDE.js → auth.svelte-DTSHZMJ4.js} +2 -2
- package/dist/{chunk-7M4DUK45.js → chunk-DSNTNK6T.js} +68 -4
- package/dist/{chunk-BDIQSTES.js → chunk-ES4UOD62.js} +53 -12
- package/dist/{chunk-EVKXT3NR.js → chunk-XSQYERC6.js} +109 -4
- package/dist/chunk-YTMFXVJR.js +216 -0
- package/dist/core/index.d.ts +13 -227
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -4
- package/dist/svelte/index.d.ts +436 -3
- package/dist/svelte/index.js +5 -4
- package/dist/testing/index.d.ts +1 -2
- package/dist/{types-DGN45Uih.d.ts → types-Ct5g1Nbj.d.ts} +101 -1
- package/dist/user-utils-BtLu_jhF.d.ts +414 -0
- package/package.json +1 -1
- package/dist/chunk-IAPPE4US.js +0 -66
- package/dist/config-C-iBNu07.d.ts +0 -86
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-DSNTNK6T.js';
|
|
2
|
+
import './chunk-ES4UOD62.js';
|
|
3
3
|
import './chunk-DCGC6CNV.js';
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { authApi } from './chunk-
|
|
1
|
+
import { authApi } from './chunk-ES4UOD62.js';
|
|
2
2
|
import { decodeJWT, isInitialized, getConfig, getStorage, getDefaultStorage } from './chunk-DCGC6CNV.js';
|
|
3
3
|
|
|
4
4
|
// src/svelte/stores/auth.svelte.ts
|
|
5
5
|
function getStorageKey() {
|
|
6
6
|
return isInitialized() ? getConfig().storageKey ?? "classic_auth" : "classic_auth";
|
|
7
7
|
}
|
|
8
|
+
function getSessionTokenKey() {
|
|
9
|
+
return `${getStorageKey()}_session`;
|
|
10
|
+
}
|
|
8
11
|
function getStorageAdapter() {
|
|
9
12
|
return isInitialized() ? getStorage() : getDefaultStorage();
|
|
10
13
|
}
|
|
@@ -101,7 +104,7 @@ var AuthStore = class {
|
|
|
101
104
|
};
|
|
102
105
|
saveAuthToStorage(this.state);
|
|
103
106
|
if (sessionToken && typeof window !== "undefined") {
|
|
104
|
-
getStorageAdapter().setItem(
|
|
107
|
+
getStorageAdapter().setItem(getSessionTokenKey(), sessionToken);
|
|
105
108
|
}
|
|
106
109
|
if (typeof window !== "undefined") {
|
|
107
110
|
window.dispatchEvent(
|
|
@@ -170,7 +173,7 @@ var AuthStore = class {
|
|
|
170
173
|
};
|
|
171
174
|
const storage = getStorageAdapter();
|
|
172
175
|
storage.removeItem(getStorageKey());
|
|
173
|
-
storage.removeItem(
|
|
176
|
+
storage.removeItem(getSessionTokenKey());
|
|
174
177
|
if (typeof window !== "undefined") {
|
|
175
178
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
|
176
179
|
}
|
|
@@ -196,6 +199,35 @@ var AuthStore = class {
|
|
|
196
199
|
}
|
|
197
200
|
return {};
|
|
198
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Handle session expiration (e.g., token refresh failure).
|
|
204
|
+
* Clears auth state and calls the onSessionExpired callback if configured.
|
|
205
|
+
*
|
|
206
|
+
* Use this instead of logout() when the session expires unexpectedly,
|
|
207
|
+
* to trigger different handling (e.g., redirect with error message).
|
|
208
|
+
*/
|
|
209
|
+
handleSessionExpired() {
|
|
210
|
+
const currentPath = typeof window !== "undefined" ? window.location.pathname : "/";
|
|
211
|
+
this.state = {
|
|
212
|
+
accessToken: null,
|
|
213
|
+
refreshToken: null,
|
|
214
|
+
user: null,
|
|
215
|
+
isAuthenticated: false
|
|
216
|
+
};
|
|
217
|
+
const storage = getStorageAdapter();
|
|
218
|
+
storage.removeItem(getStorageKey());
|
|
219
|
+
storage.removeItem(getSessionTokenKey());
|
|
220
|
+
if (typeof window !== "undefined") {
|
|
221
|
+
window.dispatchEvent(new CustomEvent("auth:session-expired"));
|
|
222
|
+
}
|
|
223
|
+
if (isInitialized()) {
|
|
224
|
+
const config = getConfig();
|
|
225
|
+
if (config.onSessionExpired) {
|
|
226
|
+
config.onSessionExpired(currentPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.notify();
|
|
230
|
+
}
|
|
199
231
|
/**
|
|
200
232
|
* Check if user has a specific permission.
|
|
201
233
|
*/
|
|
@@ -239,6 +271,36 @@ var AuthStore = class {
|
|
|
239
271
|
this.state = loadAuthFromStorage();
|
|
240
272
|
this.notify();
|
|
241
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Reset the store to initial state and clear all storage.
|
|
276
|
+
* Primarily intended for testing scenarios where auth needs to be fully reset.
|
|
277
|
+
*
|
|
278
|
+
* Unlike logout(), this also clears all subscribers and doesn't
|
|
279
|
+
* dispatch events or call config callbacks.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* // In test setup/teardown
|
|
284
|
+
* beforeEach(() => {
|
|
285
|
+
* authStore.reset();
|
|
286
|
+
* });
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
reset() {
|
|
290
|
+
this.state = {
|
|
291
|
+
accessToken: null,
|
|
292
|
+
refreshToken: null,
|
|
293
|
+
user: null,
|
|
294
|
+
isAuthenticated: false
|
|
295
|
+
};
|
|
296
|
+
try {
|
|
297
|
+
const storage = getStorageAdapter();
|
|
298
|
+
storage.removeItem(getStorageKey());
|
|
299
|
+
storage.removeItem(getSessionTokenKey());
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
this.subscribers.clear();
|
|
303
|
+
}
|
|
242
304
|
};
|
|
243
305
|
var authStore = new AuthStore();
|
|
244
306
|
var authActions = {
|
|
@@ -247,13 +309,15 @@ var authActions = {
|
|
|
247
309
|
updateUser: (user) => authStore.updateUser(user),
|
|
248
310
|
logout: () => authStore.logout(),
|
|
249
311
|
logoutWithSSO: () => authStore.logoutWithSSO(),
|
|
312
|
+
handleSessionExpired: () => authStore.handleSessionExpired(),
|
|
250
313
|
hasPermission: (permission) => authStore.hasPermission(permission),
|
|
251
314
|
hasRole: (role) => authStore.hasRole(role),
|
|
252
315
|
hasAnyRole: (roles) => authStore.hasAnyRole(roles),
|
|
253
316
|
hasAllRoles: (roles) => authStore.hasAllRoles(roles),
|
|
254
317
|
hasAnyPermission: (permissions) => authStore.hasAnyPermission(permissions),
|
|
255
318
|
hasAllPermissions: (permissions) => authStore.hasAllPermissions(permissions),
|
|
256
|
-
rehydrate: () => authStore.rehydrate()
|
|
319
|
+
rehydrate: () => authStore.rehydrate(),
|
|
320
|
+
reset: () => authStore.reset()
|
|
257
321
|
};
|
|
258
322
|
var isAuthenticated = {
|
|
259
323
|
subscribe: (subscriber) => {
|
|
@@ -34,9 +34,14 @@ function getRefreshToken() {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function getSessionTokenKey() {
|
|
38
|
+
const config = getConfig();
|
|
39
|
+
const storageKey = config.storageKey ?? "classic_auth";
|
|
40
|
+
return `${storageKey}_session`;
|
|
41
|
+
}
|
|
37
42
|
function getSessionToken() {
|
|
38
43
|
const storage = getStorage();
|
|
39
|
-
return storage.getItem(
|
|
44
|
+
return storage.getItem(getSessionTokenKey());
|
|
40
45
|
}
|
|
41
46
|
function updateStoredTokens(accessToken, refreshToken) {
|
|
42
47
|
const config = getConfig();
|
|
@@ -53,7 +58,7 @@ function updateStoredTokens(accessToken, refreshToken) {
|
|
|
53
58
|
parsed.accessToken = accessToken;
|
|
54
59
|
parsed.refreshToken = refreshToken;
|
|
55
60
|
storage.setItem(storageKey, JSON.stringify(parsed));
|
|
56
|
-
import('./auth.svelte-
|
|
61
|
+
import('./auth.svelte-DTSHZMJ4.js').then(({ authStore }) => {
|
|
57
62
|
authStore.updateTokens(accessToken, refreshToken);
|
|
58
63
|
}).catch(() => {
|
|
59
64
|
});
|
|
@@ -63,7 +68,7 @@ function clearStoredAuth() {
|
|
|
63
68
|
const config = getConfig();
|
|
64
69
|
const storage = getStorage();
|
|
65
70
|
storage.removeItem(config.storageKey ?? "classic_auth");
|
|
66
|
-
storage.removeItem(
|
|
71
|
+
storage.removeItem(getSessionTokenKey());
|
|
67
72
|
}
|
|
68
73
|
async function refreshAccessToken() {
|
|
69
74
|
const refreshToken = getRefreshToken();
|
|
@@ -160,9 +165,21 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
160
165
|
throw new Error("Authentication failed. Please sign in again.");
|
|
161
166
|
}
|
|
162
167
|
} else {
|
|
168
|
+
const REFRESH_TIMEOUT_MS = 3e4;
|
|
163
169
|
return new Promise((resolve, reject) => {
|
|
170
|
+
let settled = false;
|
|
171
|
+
const timeoutId = setTimeout(() => {
|
|
172
|
+
if (!settled) {
|
|
173
|
+
settled = true;
|
|
174
|
+
reject(new Error("Token refresh timed out. Please sign in again."));
|
|
175
|
+
}
|
|
176
|
+
}, REFRESH_TIMEOUT_MS);
|
|
164
177
|
subscribeTokenRefresh(() => {
|
|
165
|
-
|
|
178
|
+
if (!settled) {
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
apiRequest(endpoint, options).then(resolve).catch(reject);
|
|
182
|
+
}
|
|
166
183
|
});
|
|
167
184
|
});
|
|
168
185
|
}
|
|
@@ -259,12 +276,17 @@ var authApi = {
|
|
|
259
276
|
/**
|
|
260
277
|
* Logout the current user.
|
|
261
278
|
* Returns SSO logout URL if applicable for SSO users.
|
|
279
|
+
*
|
|
280
|
+
* Note: API errors are logged via onAuthError callback but still return success
|
|
281
|
+
* so the client can clear local state even if the server call fails.
|
|
262
282
|
*/
|
|
263
283
|
async logout() {
|
|
264
284
|
try {
|
|
265
285
|
const response = await api.post("/auth/logout", {}, true);
|
|
266
286
|
return extractData(response);
|
|
267
|
-
} catch {
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const config = getConfig();
|
|
289
|
+
config.onAuthError?.(error instanceof Error ? error : new Error("Logout API call failed"));
|
|
268
290
|
return { success: true };
|
|
269
291
|
}
|
|
270
292
|
},
|
|
@@ -530,6 +552,8 @@ var authApi = {
|
|
|
530
552
|
// ============================================================================
|
|
531
553
|
/**
|
|
532
554
|
* Get user preferences.
|
|
555
|
+
*
|
|
556
|
+
* @throws Error if the API returns a malformed response
|
|
533
557
|
*/
|
|
534
558
|
async getPreferences(customFetch) {
|
|
535
559
|
const response = await api.get(
|
|
@@ -540,7 +564,10 @@ var authApi = {
|
|
|
540
564
|
if (response && typeof response === "object" && "preferences" in response) {
|
|
541
565
|
return response.preferences;
|
|
542
566
|
}
|
|
543
|
-
|
|
567
|
+
if (response && typeof response === "object") {
|
|
568
|
+
return response;
|
|
569
|
+
}
|
|
570
|
+
throw new Error("Invalid response from preferences API: expected object with preferences");
|
|
544
571
|
},
|
|
545
572
|
/**
|
|
546
573
|
* Update user preferences.
|
|
@@ -572,7 +599,10 @@ var authApi = {
|
|
|
572
599
|
await api.delete("/auth/sso/unlink", true, void 0, { provider, password });
|
|
573
600
|
},
|
|
574
601
|
/**
|
|
575
|
-
* Link an SSO account (redirects to SSO provider).
|
|
602
|
+
* Link an SSO account (redirects to SSO provider via form POST).
|
|
603
|
+
*
|
|
604
|
+
* Uses form submission to avoid exposing the access token in URL parameters,
|
|
605
|
+
* which could leak via browser history, referrer headers, or server logs.
|
|
576
606
|
*/
|
|
577
607
|
async linkSSOAccount(provider = "authentik") {
|
|
578
608
|
if (typeof window === "undefined") return;
|
|
@@ -581,11 +611,22 @@ var authApi = {
|
|
|
581
611
|
throw new Error("Not authenticated");
|
|
582
612
|
}
|
|
583
613
|
const config = getConfig();
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
614
|
+
const form = document.createElement("form");
|
|
615
|
+
form.method = "POST";
|
|
616
|
+
form.action = `${config.baseUrl}/auth/sso/link`;
|
|
617
|
+
form.style.display = "none";
|
|
618
|
+
const tokenInput = document.createElement("input");
|
|
619
|
+
tokenInput.type = "hidden";
|
|
620
|
+
tokenInput.name = "token";
|
|
621
|
+
tokenInput.value = accessToken;
|
|
622
|
+
form.appendChild(tokenInput);
|
|
623
|
+
const providerInput = document.createElement("input");
|
|
624
|
+
providerInput.type = "hidden";
|
|
625
|
+
providerInput.name = "provider";
|
|
626
|
+
providerInput.value = provider;
|
|
627
|
+
form.appendChild(providerInput);
|
|
628
|
+
document.body.appendChild(form);
|
|
629
|
+
form.submit();
|
|
589
630
|
},
|
|
590
631
|
// ============================================================================
|
|
591
632
|
// Security Events
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { authApi } from './chunk-
|
|
1
|
+
import { authApi } from './chunk-ES4UOD62.js';
|
|
2
2
|
|
|
3
3
|
// src/core/guards.ts
|
|
4
4
|
function isMfaChallengeResponse(response) {
|
|
@@ -14,6 +14,26 @@ function getAvailableMethods(response) {
|
|
|
14
14
|
return response.availableMethods ?? ["totp"];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// src/core/errors.ts
|
|
18
|
+
var RoleDeniedError = class _RoleDeniedError extends Error {
|
|
19
|
+
constructor(user, requiredRoles, redirectTo) {
|
|
20
|
+
const userRoles = user.roles?.join(", ") || user.role || "none";
|
|
21
|
+
super(
|
|
22
|
+
`User "${user.username}" does not have required role(s). Has: [${userRoles}], Required: [${requiredRoles.join(", ")}]`
|
|
23
|
+
);
|
|
24
|
+
this.name = "RoleDeniedError";
|
|
25
|
+
this.user = user;
|
|
26
|
+
this.requiredRoles = requiredRoles;
|
|
27
|
+
this.redirectTo = redirectTo;
|
|
28
|
+
if (Error.captureStackTrace) {
|
|
29
|
+
Error.captureStackTrace(this, _RoleDeniedError);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function isRoleDeniedError(error) {
|
|
34
|
+
return error instanceof RoleDeniedError || error?.name === "RoleDeniedError";
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
// src/core/service.ts
|
|
18
38
|
var AuthService = class {
|
|
19
39
|
// ============================================================================
|
|
@@ -27,9 +47,20 @@ var AuthService = class {
|
|
|
27
47
|
*/
|
|
28
48
|
async login(credentials, options) {
|
|
29
49
|
const response = await authApi.login(credentials);
|
|
50
|
+
if (options?.allowedRoles?.length && !isMfaChallengeResponse(response)) {
|
|
51
|
+
const userRoles = response.user.roles || (response.user.role ? [response.user.role] : []);
|
|
52
|
+
const hasAllowedRole = options.allowedRoles.some((r) => userRoles.includes(r));
|
|
53
|
+
if (!hasAllowedRole) {
|
|
54
|
+
throw new RoleDeniedError(
|
|
55
|
+
response.user,
|
|
56
|
+
options.allowedRoles,
|
|
57
|
+
options.onRoleDeniedRedirect
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
30
61
|
if (options?.autoSetAuth !== false && !isMfaChallengeResponse(response)) {
|
|
31
62
|
try {
|
|
32
|
-
const { authStore } = await import('./auth.svelte-
|
|
63
|
+
const { authStore } = await import('./auth.svelte-DTSHZMJ4.js');
|
|
33
64
|
authStore.setAuth(
|
|
34
65
|
response.accessToken,
|
|
35
66
|
response.refreshToken,
|
|
@@ -202,9 +233,20 @@ var AuthService = class {
|
|
|
202
233
|
*/
|
|
203
234
|
async verifyMFAChallenge(data, options) {
|
|
204
235
|
const response = await authApi.verifyMFAChallenge(data);
|
|
236
|
+
if (options?.allowedRoles?.length) {
|
|
237
|
+
const userRoles = response.user.roles || (response.user.role ? [response.user.role] : []);
|
|
238
|
+
const hasAllowedRole = options.allowedRoles.some((r) => userRoles.includes(r));
|
|
239
|
+
if (!hasAllowedRole) {
|
|
240
|
+
throw new RoleDeniedError(
|
|
241
|
+
response.user,
|
|
242
|
+
options.allowedRoles,
|
|
243
|
+
options.onRoleDeniedRedirect
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
205
247
|
if (options?.autoSetAuth !== false) {
|
|
206
248
|
try {
|
|
207
|
-
const { authStore } = await import('./auth.svelte-
|
|
249
|
+
const { authStore } = await import('./auth.svelte-DTSHZMJ4.js');
|
|
208
250
|
authStore.setAuth(
|
|
209
251
|
response.accessToken,
|
|
210
252
|
response.refreshToken,
|
|
@@ -303,4 +345,67 @@ var AuthService = class {
|
|
|
303
345
|
};
|
|
304
346
|
var authService = new AuthService();
|
|
305
347
|
|
|
306
|
-
|
|
348
|
+
// src/core/user-utils.ts
|
|
349
|
+
function getDisplayName(user) {
|
|
350
|
+
if (!user) return "Unknown";
|
|
351
|
+
if (user.firstName && user.lastName) {
|
|
352
|
+
return `${user.firstName} ${user.lastName}`;
|
|
353
|
+
}
|
|
354
|
+
return user.firstName || user.username || user.email || "Unknown";
|
|
355
|
+
}
|
|
356
|
+
function getUserInitials(user, maxLength = 2) {
|
|
357
|
+
const name = getDisplayName(user);
|
|
358
|
+
if (name === "Unknown") {
|
|
359
|
+
return "?";
|
|
360
|
+
}
|
|
361
|
+
return name.split(" ").filter((part) => part.length > 0).map((part) => part[0]).join("").toUpperCase().slice(0, maxLength);
|
|
362
|
+
}
|
|
363
|
+
function getAvatarFallback(user) {
|
|
364
|
+
return getUserInitials(user);
|
|
365
|
+
}
|
|
366
|
+
function getUserEmail(user, masked = false) {
|
|
367
|
+
if (!user?.email) return "";
|
|
368
|
+
if (!masked) {
|
|
369
|
+
return user.email;
|
|
370
|
+
}
|
|
371
|
+
const [local, domain] = user.email.split("@");
|
|
372
|
+
if (!domain) return user.email;
|
|
373
|
+
const maskedLocal = local.length > 1 ? local[0] + "***" : local;
|
|
374
|
+
return `${maskedLocal}@${domain}`;
|
|
375
|
+
}
|
|
376
|
+
function getGreeting(user, includeTime = true) {
|
|
377
|
+
const name = user?.firstName || getDisplayName(user);
|
|
378
|
+
if (!includeTime) {
|
|
379
|
+
return `Hello, ${name}`;
|
|
380
|
+
}
|
|
381
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
382
|
+
let timeGreeting;
|
|
383
|
+
if (hour < 12) {
|
|
384
|
+
timeGreeting = "Good morning";
|
|
385
|
+
} else if (hour < 17) {
|
|
386
|
+
timeGreeting = "Good afternoon";
|
|
387
|
+
} else {
|
|
388
|
+
timeGreeting = "Good evening";
|
|
389
|
+
}
|
|
390
|
+
return `${timeGreeting}, ${name}`;
|
|
391
|
+
}
|
|
392
|
+
function formatUserRoles(user, options = {}) {
|
|
393
|
+
if (!user) return "";
|
|
394
|
+
const { max, separator = ", ", lowercase = false, capitalize = true } = options;
|
|
395
|
+
const roles = user.roles || (user.role ? [user.role] : []);
|
|
396
|
+
if (roles.length === 0) return "";
|
|
397
|
+
const formatRole = (role) => {
|
|
398
|
+
if (lowercase) return role.toLowerCase();
|
|
399
|
+
if (capitalize) return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();
|
|
400
|
+
return role;
|
|
401
|
+
};
|
|
402
|
+
const formatted = roles.map(formatRole);
|
|
403
|
+
if (max && formatted.length > max) {
|
|
404
|
+
const shown = formatted.slice(0, max);
|
|
405
|
+
const remaining = formatted.length - max;
|
|
406
|
+
return `${shown.join(separator)} +${remaining} more`;
|
|
407
|
+
}
|
|
408
|
+
return formatted.join(separator);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { authService } from './chunk-XSQYERC6.js';
|
|
2
|
+
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-DSNTNK6T.js';
|
|
3
|
+
import { isInitialized, initAuth } from './chunk-DCGC6CNV.js';
|
|
4
|
+
|
|
5
|
+
// src/svelte/guards/auth-guard.ts
|
|
6
|
+
function checkAuth(options = {}) {
|
|
7
|
+
const { roles, permissions, requireAllRoles, requireAllPermissions } = options;
|
|
8
|
+
if (!authStore.isAuthenticated) {
|
|
9
|
+
return {
|
|
10
|
+
allowed: false,
|
|
11
|
+
reason: "not_authenticated",
|
|
12
|
+
redirectTo: "/login"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (roles && roles.length > 0) {
|
|
16
|
+
const hasRoles = requireAllRoles ? authStore.hasAllRoles(roles) : authStore.hasAnyRole(roles);
|
|
17
|
+
if (!hasRoles) {
|
|
18
|
+
return {
|
|
19
|
+
allowed: false,
|
|
20
|
+
reason: "missing_role",
|
|
21
|
+
redirectTo: "/unauthorized"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (permissions && permissions.length > 0) {
|
|
26
|
+
const hasPermissions = requireAllPermissions ? authStore.hasAllPermissions(permissions) : authStore.hasAnyPermission(permissions);
|
|
27
|
+
if (!hasPermissions) {
|
|
28
|
+
return {
|
|
29
|
+
allowed: false,
|
|
30
|
+
reason: "missing_permission",
|
|
31
|
+
redirectTo: "/unauthorized"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { allowed: true };
|
|
36
|
+
}
|
|
37
|
+
function createAuthGuard(options = {}) {
|
|
38
|
+
return (onDenied) => {
|
|
39
|
+
const result = checkAuth(options);
|
|
40
|
+
if (!result.allowed) {
|
|
41
|
+
onDenied(result.redirectTo ?? "/login", result.reason ?? "not_authenticated");
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function requireAuth() {
|
|
48
|
+
return checkAuth();
|
|
49
|
+
}
|
|
50
|
+
function requireRole(roles, requireAll = false) {
|
|
51
|
+
const roleArray = Array.isArray(roles) ? roles : [roles];
|
|
52
|
+
return checkAuth({ roles: roleArray, requireAllRoles: requireAll });
|
|
53
|
+
}
|
|
54
|
+
function requirePermission(permissions, requireAll = false) {
|
|
55
|
+
const permArray = Array.isArray(permissions) ? permissions : [permissions];
|
|
56
|
+
return checkAuth({ permissions: permArray, requireAllPermissions: requireAll });
|
|
57
|
+
}
|
|
58
|
+
function protectedLoad(options, loadFn) {
|
|
59
|
+
return async (event) => {
|
|
60
|
+
const result = checkAuth(options);
|
|
61
|
+
if (!result.allowed) {
|
|
62
|
+
return { redirect: result.redirectTo ?? "/login" };
|
|
63
|
+
}
|
|
64
|
+
return loadFn(event);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/svelte/client.ts
|
|
69
|
+
function createAuthClient(options) {
|
|
70
|
+
const { autoRehydrate = true, ...config } = options;
|
|
71
|
+
if (!isInitialized()) {
|
|
72
|
+
initAuth(config);
|
|
73
|
+
}
|
|
74
|
+
if (autoRehydrate && typeof window !== "undefined") {
|
|
75
|
+
authStore.rehydrate();
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
authStore,
|
|
79
|
+
authActions,
|
|
80
|
+
authService,
|
|
81
|
+
isAuthenticated,
|
|
82
|
+
currentUser
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/svelte/utils/nav-filter.ts
|
|
87
|
+
function filterByAccess(items, user, options = {}) {
|
|
88
|
+
const {
|
|
89
|
+
requireAllRoles = false,
|
|
90
|
+
requireAllPermissions = false,
|
|
91
|
+
hideUnrestrictedForGuests = false
|
|
92
|
+
} = options;
|
|
93
|
+
const userRoles = user?.roles || (user?.role ? [user.role] : []);
|
|
94
|
+
const userPerms = user?.permissions || [];
|
|
95
|
+
return items.filter((item) => {
|
|
96
|
+
const hasRoleRestriction = item.roles && item.roles.length > 0;
|
|
97
|
+
const hasPermRestriction = item.permissions && item.permissions.length > 0;
|
|
98
|
+
if (!hasRoleRestriction && !hasPermRestriction) {
|
|
99
|
+
if (hideUnrestrictedForGuests && !user) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (!user) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (hasRoleRestriction) {
|
|
108
|
+
const hasRoles = requireAllRoles ? item.roles.every((r) => userRoles.includes(r)) : item.roles.some((r) => userRoles.includes(r));
|
|
109
|
+
if (!hasRoles) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (hasPermRestriction) {
|
|
114
|
+
const hasPerms = requireAllPermissions ? item.permissions.every((p) => userPerms.includes(p)) : item.permissions.some((p) => userPerms.includes(p));
|
|
115
|
+
if (!hasPerms) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function filterNavSections(sections, user, options = {}) {
|
|
123
|
+
return sections.map((section) => ({
|
|
124
|
+
...section,
|
|
125
|
+
items: filterByAccess(section.items, user, options)
|
|
126
|
+
})).filter((section) => section.items.length > 0);
|
|
127
|
+
}
|
|
128
|
+
function canAccess(item, user, options = {}) {
|
|
129
|
+
return filterByAccess([item], user, options).length > 0;
|
|
130
|
+
}
|
|
131
|
+
function createNavFilter(defaultOptions = {}) {
|
|
132
|
+
return (items, user, options) => {
|
|
133
|
+
return filterByAccess(items, user, { ...defaultOptions, ...options });
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/svelte/hooks/auth-hook.ts
|
|
138
|
+
function createAuthHook(options = {}) {
|
|
139
|
+
const {
|
|
140
|
+
loginPath = "/login",
|
|
141
|
+
publicRoutes = [/^\/auth\//, /^\/api\/auth\//],
|
|
142
|
+
protectedRoutes,
|
|
143
|
+
cookieName = "classic_auth",
|
|
144
|
+
headerName,
|
|
145
|
+
redirectParam = "redirect",
|
|
146
|
+
isAuthenticated: customIsAuthenticated,
|
|
147
|
+
onUnauthenticated,
|
|
148
|
+
onAuthCheck
|
|
149
|
+
} = options;
|
|
150
|
+
return async ({
|
|
151
|
+
event,
|
|
152
|
+
resolve
|
|
153
|
+
}) => {
|
|
154
|
+
const { pathname } = event.url;
|
|
155
|
+
const isPublic = publicRoutes.some((r) => r.test(pathname));
|
|
156
|
+
if (isPublic) {
|
|
157
|
+
return resolve(event);
|
|
158
|
+
}
|
|
159
|
+
if (protectedRoutes && !protectedRoutes.some((r) => r.test(pathname))) {
|
|
160
|
+
return resolve(event);
|
|
161
|
+
}
|
|
162
|
+
let authenticated;
|
|
163
|
+
if (customIsAuthenticated) {
|
|
164
|
+
authenticated = await customIsAuthenticated(event);
|
|
165
|
+
} else {
|
|
166
|
+
const hasCookie = !!event.cookies.get(cookieName);
|
|
167
|
+
const hasHeader = headerName ? !!event.request.headers.get(headerName) : false;
|
|
168
|
+
authenticated = hasCookie || hasHeader;
|
|
169
|
+
}
|
|
170
|
+
onAuthCheck?.(event, authenticated);
|
|
171
|
+
if (!authenticated) {
|
|
172
|
+
if (onUnauthenticated) {
|
|
173
|
+
return onUnauthenticated(event);
|
|
174
|
+
}
|
|
175
|
+
const redirectUrl = encodeURIComponent(pathname + event.url.search);
|
|
176
|
+
const loginUrl = new URL(loginPath, event.url.origin);
|
|
177
|
+
loginUrl.searchParams.set(redirectParam, redirectUrl);
|
|
178
|
+
return new Response(null, {
|
|
179
|
+
status: 302,
|
|
180
|
+
headers: { Location: loginUrl.pathname + loginUrl.search }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return resolve(event);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function matchesRoute(pathname, patterns) {
|
|
187
|
+
return patterns.some((pattern) => pattern.test(pathname));
|
|
188
|
+
}
|
|
189
|
+
var routePatterns = {
|
|
190
|
+
/** Match all /auth/* routes */
|
|
191
|
+
auth: /^\/auth\//,
|
|
192
|
+
/** Match all /api/* routes */
|
|
193
|
+
api: /^\/api\//,
|
|
194
|
+
/** Match all /api/auth/* routes */
|
|
195
|
+
apiAuth: /^\/api\/auth\//,
|
|
196
|
+
/** Match all /public/* routes */
|
|
197
|
+
public: /^\/public\//,
|
|
198
|
+
/** Match all /admin/* routes */
|
|
199
|
+
admin: /^\/admin\//,
|
|
200
|
+
/** Match exact root path */
|
|
201
|
+
root: /^\/$/,
|
|
202
|
+
/** Match health check endpoints */
|
|
203
|
+
health: /^\/(api\/)?(health|healthz|ready|live)\/?$/,
|
|
204
|
+
/**
|
|
205
|
+
* Create a pattern for a specific path prefix.
|
|
206
|
+
* @param prefix - Path prefix (e.g., '/dashboard')
|
|
207
|
+
*/
|
|
208
|
+
prefix: (prefix) => new RegExp(`^${prefix.replace(/\//g, "\\/")}`),
|
|
209
|
+
/**
|
|
210
|
+
* Create a pattern for exact path match.
|
|
211
|
+
* @param path - Exact path (e.g., '/about')
|
|
212
|
+
*/
|
|
213
|
+
exact: (path) => new RegExp(`^${path.replace(/\//g, "\\/")}$`)
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns };
|