@classic-homes/auth 0.1.56 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -1
- package/dist/auth.svelte-J62VV4TA.js +3 -0
- package/dist/{chunk-2QYFVJEM.js → chunk-2N2MDCWL.js} +71 -2
- package/dist/{chunk-UG4GSPGL.js → chunk-ILRNO4WH.js} +30 -7
- package/dist/{chunk-6NTZVDLW.js → chunk-O47YKG2F.js} +57 -1
- package/dist/{chunk-CPFM7JEI.js → chunk-QBTMNWJH.js} +46 -42
- package/dist/{chunk-MPPV5S7S.js → chunk-VVREZ25E.js} +8 -11
- package/dist/core/index.d.ts +143 -4
- package/dist/core/index.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/svelte/index.d.ts +43 -7
- package/dist/svelte/index.js +5 -5
- package/dist/testing/index.d.ts +7 -4
- package/dist/testing/index.js +14 -16
- package/dist/{types-Di6SR1q5.d.ts → types-DXLdCdNC.d.ts} +27 -3
- package/dist/{user-utils-C-JbXqbj.d.ts → user-utils-mQMLo7Il.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/auth.svelte-WPCYPHEE.js +0 -3
package/README.md
CHANGED
|
@@ -202,6 +202,74 @@ export function load({ url }) {
|
|
|
202
202
|
}
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
## Roles
|
|
206
|
+
|
|
207
|
+
A user holds a **set** of roles (`user.roles`). Their permissions are the union
|
|
208
|
+
across all of them, so a user's access is broader than any single role.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { authStore } from '@classic-homes/auth/svelte';
|
|
212
|
+
|
|
213
|
+
// A manager who is also an agent
|
|
214
|
+
authStore.user?.roles; // ['manager', 'agent']
|
|
215
|
+
|
|
216
|
+
authStore.hasRole('manager'); // true
|
|
217
|
+
authStore.hasRole('agent'); // true — secondary roles count
|
|
218
|
+
authStore.hasAnyRole(['admin', 'agent']); // true
|
|
219
|
+
authStore.hasAllRoles(['manager', 'agent']); // true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Role checks are exact membership.** Holding `admin` does not imply `agent`:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
authStore.user?.roles; // ['admin']
|
|
226
|
+
authStore.hasRole('agent'); // false
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This mirrors the API, so the UI can't offer something the server will reject.
|
|
230
|
+
To let admins through as well, list the roles you accept:
|
|
231
|
+
`checkAuth({ roles: ['admin', 'agent'] })`.
|
|
232
|
+
|
|
233
|
+
### Displaying a role
|
|
234
|
+
|
|
235
|
+
`computePrimaryRole` resolves the highest-tier role for display. It's a label,
|
|
236
|
+
not a capability — never gate on it:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { computePrimaryRole, getEffectiveLevel } from '@classic-homes/auth/core';
|
|
240
|
+
|
|
241
|
+
computePrimaryRole(['user', 'admin']); // 'admin'
|
|
242
|
+
computePrimaryRole([]); // 'guest'
|
|
243
|
+
getEffectiveLevel(['user', 'manager']); // 3
|
|
244
|
+
|
|
245
|
+
// Or format the whole set
|
|
246
|
+
import { formatUserRoles } from '@classic-homes/auth/core';
|
|
247
|
+
formatUserRoles(user); // 'Manager, Agent'
|
|
248
|
+
formatUserRoles(user, { max: 1 }); // 'Manager +1 more'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Managing another user's roles
|
|
252
|
+
|
|
253
|
+
Requires `users:read` to list and `users:update` to change:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { authApi } from '@classic-homes/auth/core';
|
|
257
|
+
|
|
258
|
+
const { roles, primaryRole } = await authApi.getUserRoles(userId);
|
|
259
|
+
|
|
260
|
+
await authApi.grantUserRole(userId, 'agent'); // idempotent
|
|
261
|
+
await authApi.revokeUserRole(userId, 'agent');
|
|
262
|
+
await authApi.setUserRoles(userId, ['manager', 'agent']); // replaces the set
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Two server rules to design around:
|
|
266
|
+
|
|
267
|
+
- **A role change doesn't affect that user's live session.** Permissions are
|
|
268
|
+
computed when a token is minted, so a grant takes effect on their next token
|
|
269
|
+
refresh — not their next request.
|
|
270
|
+
- The server refuses to remove a user's **last** role, or to grant a role at or
|
|
271
|
+
above the acting user's own level. Both come back as errors.
|
|
272
|
+
|
|
205
273
|
## API Reference
|
|
206
274
|
|
|
207
275
|
### Core Exports
|
|
@@ -231,8 +299,20 @@ import {
|
|
|
231
299
|
isTokenExpired,
|
|
232
300
|
getTokenRemainingTime,
|
|
233
301
|
|
|
302
|
+
// Roles
|
|
303
|
+
ROLE_NAMES,
|
|
304
|
+
ROLE_HIERARCHY,
|
|
305
|
+
getUserRoles,
|
|
306
|
+
computePrimaryRole,
|
|
307
|
+
getEffectiveLevel,
|
|
308
|
+
hasRole,
|
|
309
|
+
hasAnyRole,
|
|
310
|
+
hasAllRoles,
|
|
311
|
+
|
|
234
312
|
// Types
|
|
235
313
|
type User,
|
|
314
|
+
type UserRolesResponse,
|
|
315
|
+
type RoleName,
|
|
236
316
|
type AuthState,
|
|
237
317
|
type LoginCredentials,
|
|
238
318
|
type LoginResponse,
|
|
@@ -648,7 +728,7 @@ import {
|
|
|
648
728
|
} from '@classic-homes/auth/testing';
|
|
649
729
|
|
|
650
730
|
// Use pre-defined users
|
|
651
|
-
expect(mockUser.
|
|
731
|
+
expect(mockUser.roles).toEqual(['user']);
|
|
652
732
|
expect(mockAdminUser.permissions).toContain('manage:system');
|
|
653
733
|
|
|
654
734
|
// Create custom users
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-
|
|
1
|
+
import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-O47YKG2F.js';
|
|
2
2
|
|
|
3
3
|
// src/core/client.ts
|
|
4
4
|
var isRefreshing = false;
|
|
@@ -71,7 +71,7 @@ function updateStoredTokens(accessToken, refreshToken) {
|
|
|
71
71
|
parsed.accessToken = accessToken;
|
|
72
72
|
parsed.refreshToken = refreshToken;
|
|
73
73
|
storage.setItem(storageKey, JSON.stringify(parsed));
|
|
74
|
-
import('./auth.svelte-
|
|
74
|
+
import('./auth.svelte-J62VV4TA.js').then(({ authStore }) => {
|
|
75
75
|
authStore.updateTokens(accessToken, refreshToken);
|
|
76
76
|
}).catch(() => {
|
|
77
77
|
});
|
|
@@ -728,6 +728,75 @@ var authApi = {
|
|
|
728
728
|
if (params?.type) queryParams.append("type", params.type);
|
|
729
729
|
const response = await api.get(`/auth/security/events?${queryParams.toString()}`, true, customFetch);
|
|
730
730
|
return extractData(response);
|
|
731
|
+
},
|
|
732
|
+
// ============================================================================
|
|
733
|
+
// User Roles
|
|
734
|
+
// ============================================================================
|
|
735
|
+
//
|
|
736
|
+
// Managing another user's roles. Requires `users:read` to list and
|
|
737
|
+
// `users:update` to change.
|
|
738
|
+
//
|
|
739
|
+
// Two server rules worth designing around rather than discovering:
|
|
740
|
+
// - A role change does NOT affect that user's live session. Permissions are
|
|
741
|
+
// computed when a token is minted, so the grant takes effect on their next
|
|
742
|
+
// token refresh — not on their next request.
|
|
743
|
+
// - The server refuses a change that would remove a user's last role, or
|
|
744
|
+
// that would grant a role at or above the acting user's own level. Both
|
|
745
|
+
// come back as errors, so surface them rather than assuming success.
|
|
746
|
+
/**
|
|
747
|
+
* List a user's roles.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* const { roles, primaryRole } = await authApi.getUserRoles(userId);
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
async getUserRoles(userId, customFetch) {
|
|
755
|
+
const response = await api.get(
|
|
756
|
+
`/users/${userId}/roles`,
|
|
757
|
+
true,
|
|
758
|
+
customFetch
|
|
759
|
+
);
|
|
760
|
+
return extractData(response);
|
|
761
|
+
},
|
|
762
|
+
/**
|
|
763
|
+
* Grant a role to a user. Idempotent — granting a role they already hold
|
|
764
|
+
* succeeds and returns the unchanged set.
|
|
765
|
+
*
|
|
766
|
+
* @returns The user's full role set after the grant.
|
|
767
|
+
*/
|
|
768
|
+
async grantUserRole(userId, role) {
|
|
769
|
+
const response = await api.put(
|
|
770
|
+
`/users/${userId}/roles/${role}`,
|
|
771
|
+
{},
|
|
772
|
+
true
|
|
773
|
+
);
|
|
774
|
+
return extractData(response);
|
|
775
|
+
},
|
|
776
|
+
/**
|
|
777
|
+
* Revoke a role from a user.
|
|
778
|
+
*
|
|
779
|
+
* Rejected by the server if it would leave the user with no roles.
|
|
780
|
+
*
|
|
781
|
+
* @returns The user's full role set after the revoke.
|
|
782
|
+
*/
|
|
783
|
+
async revokeUserRole(userId, role) {
|
|
784
|
+
const response = await api.delete(
|
|
785
|
+
`/users/${userId}/roles/${role}`,
|
|
786
|
+
true
|
|
787
|
+
);
|
|
788
|
+
return extractData(response);
|
|
789
|
+
},
|
|
790
|
+
/**
|
|
791
|
+
* Replace a user's entire role set.
|
|
792
|
+
*
|
|
793
|
+
* Roles absent from `roles` are revoked, so read-modify-write against
|
|
794
|
+
* {@link getUserRoles} rather than passing a set built from stale state.
|
|
795
|
+
* Use {@link grantUserRole} / {@link revokeUserRole} for additive changes.
|
|
796
|
+
*/
|
|
797
|
+
async setUserRoles(userId, roles) {
|
|
798
|
+
const response = await api.put(`/users/${userId}`, { roles }, true);
|
|
799
|
+
return extractData(response);
|
|
731
800
|
}
|
|
732
801
|
};
|
|
733
802
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { authService } from './chunk-
|
|
2
|
-
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-
|
|
3
|
-
import { isInitialized, initAuth } from './chunk-
|
|
1
|
+
import { authService } from './chunk-VVREZ25E.js';
|
|
2
|
+
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-QBTMNWJH.js';
|
|
3
|
+
import { isInitialized, initAuth, getUserRoles } from './chunk-O47YKG2F.js';
|
|
4
4
|
|
|
5
5
|
// src/svelte/guards/auth-guard.ts
|
|
6
6
|
function checkAuth(options = {}) {
|
|
@@ -88,11 +88,13 @@ function filterByAccess(items, user, options = {}) {
|
|
|
88
88
|
const {
|
|
89
89
|
requireAllRoles = false,
|
|
90
90
|
requireAllPermissions = false,
|
|
91
|
-
hideUnrestrictedForGuests = false
|
|
91
|
+
hideUnrestrictedForGuests = false,
|
|
92
|
+
childrenKey,
|
|
93
|
+
pruneEmptyParents = true
|
|
92
94
|
} = options;
|
|
93
|
-
const userRoles =
|
|
95
|
+
const userRoles = getUserRoles(user);
|
|
94
96
|
const userPerms = user?.permissions || [];
|
|
95
|
-
|
|
97
|
+
function isVisible(item) {
|
|
96
98
|
const hasRoleRestriction = item.roles && item.roles.length > 0;
|
|
97
99
|
const hasPermRestriction = item.permissions && item.permissions.length > 0;
|
|
98
100
|
if (!hasRoleRestriction && !hasPermRestriction) {
|
|
@@ -117,7 +119,28 @@ function filterByAccess(items, user, options = {}) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
return true;
|
|
120
|
-
}
|
|
122
|
+
}
|
|
123
|
+
function filterLevel(level) {
|
|
124
|
+
const visible = level.filter(isVisible);
|
|
125
|
+
if (!childrenKey) {
|
|
126
|
+
return visible;
|
|
127
|
+
}
|
|
128
|
+
const result = [];
|
|
129
|
+
for (const item of visible) {
|
|
130
|
+
const children = item[childrenKey];
|
|
131
|
+
if (!Array.isArray(children)) {
|
|
132
|
+
result.push(item);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const filteredChildren = filterLevel(children);
|
|
136
|
+
if (pruneEmptyParents && children.length > 0 && filteredChildren.length === 0 && !item.href) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
result.push({ ...item, [childrenKey]: filteredChildren });
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
return filterLevel(items);
|
|
121
144
|
}
|
|
122
145
|
function filterNavSections(sections, user, options = {}) {
|
|
123
146
|
return sections.map((section) => ({
|
|
@@ -151,4 +151,60 @@ function extractClaims(token, claims) {
|
|
|
151
151
|
return result;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
// src/core/roles.ts
|
|
155
|
+
var ROLE_NAMES = [
|
|
156
|
+
"admin",
|
|
157
|
+
"manager",
|
|
158
|
+
"agent",
|
|
159
|
+
"builder",
|
|
160
|
+
"customer_service",
|
|
161
|
+
"demo",
|
|
162
|
+
"user",
|
|
163
|
+
"vendor",
|
|
164
|
+
"customer",
|
|
165
|
+
"subcontractor",
|
|
166
|
+
"guest"
|
|
167
|
+
];
|
|
168
|
+
var ROLE_HIERARCHY = {
|
|
169
|
+
guest: 0,
|
|
170
|
+
user: 1,
|
|
171
|
+
vendor: 1,
|
|
172
|
+
customer: 1,
|
|
173
|
+
subcontractor: 1,
|
|
174
|
+
agent: 2,
|
|
175
|
+
builder: 2,
|
|
176
|
+
customer_service: 2,
|
|
177
|
+
demo: 2,
|
|
178
|
+
manager: 3,
|
|
179
|
+
admin: 4
|
|
180
|
+
};
|
|
181
|
+
function getUserRoles(user) {
|
|
182
|
+
return user?.roles ?? [];
|
|
183
|
+
}
|
|
184
|
+
function computePrimaryRole(roles) {
|
|
185
|
+
const held = new Set(roles);
|
|
186
|
+
for (const role of ROLE_NAMES) {
|
|
187
|
+
if (held.has(role)) return role;
|
|
188
|
+
}
|
|
189
|
+
return "guest";
|
|
190
|
+
}
|
|
191
|
+
function getEffectiveLevel(roles) {
|
|
192
|
+
let max = 0;
|
|
193
|
+
for (const role of roles) {
|
|
194
|
+
max = Math.max(max, ROLE_HIERARCHY[role] ?? 0);
|
|
195
|
+
}
|
|
196
|
+
return max;
|
|
197
|
+
}
|
|
198
|
+
function hasRole(user, role) {
|
|
199
|
+
return getUserRoles(user).includes(role);
|
|
200
|
+
}
|
|
201
|
+
function hasAnyRole(user, roles) {
|
|
202
|
+
const held = getUserRoles(user);
|
|
203
|
+
return roles.some((role) => held.includes(role));
|
|
204
|
+
}
|
|
205
|
+
function hasAllRoles(user, roles) {
|
|
206
|
+
const held = getUserRoles(user);
|
|
207
|
+
return roles.every((role) => held.includes(role));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, resetConfig };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { authApi } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { authApi } from './chunk-2N2MDCWL.js';
|
|
2
|
+
import { isInitialized, getConfig, hasRole, hasAnyRole, hasAllRoles, decodeJWT, getStorage, getDefaultStorage } from './chunk-O47YKG2F.js';
|
|
3
3
|
|
|
4
4
|
// src/svelte/stores/auth.svelte.ts
|
|
5
|
+
var STORAGE_VERSION = 2;
|
|
5
6
|
function getStorageKey() {
|
|
6
7
|
return isInitialized() ? getConfig().storageKey ?? "classic_auth" : "classic_auth";
|
|
7
8
|
}
|
|
@@ -11,32 +12,37 @@ function getSessionTokenKey() {
|
|
|
11
12
|
function getStorageAdapter() {
|
|
12
13
|
return isInitialized() ? getStorage() : getDefaultStorage();
|
|
13
14
|
}
|
|
15
|
+
function emptyAuthState() {
|
|
16
|
+
return {
|
|
17
|
+
accessToken: null,
|
|
18
|
+
refreshToken: null,
|
|
19
|
+
user: null,
|
|
20
|
+
isAuthenticated: false
|
|
21
|
+
};
|
|
22
|
+
}
|
|
14
23
|
function loadAuthFromStorage() {
|
|
15
24
|
try {
|
|
16
25
|
const storage = getStorageAdapter();
|
|
17
26
|
const data = storage.getItem(getStorageKey());
|
|
18
27
|
if (!data) {
|
|
19
|
-
return
|
|
20
|
-
accessToken: null,
|
|
21
|
-
refreshToken: null,
|
|
22
|
-
user: null,
|
|
23
|
-
isAuthenticated: false
|
|
24
|
-
};
|
|
28
|
+
return emptyAuthState();
|
|
25
29
|
}
|
|
26
30
|
const parsed = JSON.parse(data);
|
|
31
|
+
if (parsed.version !== STORAGE_VERSION) {
|
|
32
|
+
storage.removeItem(getStorageKey());
|
|
33
|
+
return emptyAuthState();
|
|
34
|
+
}
|
|
35
|
+
const accessToken = parsed.accessToken ?? null;
|
|
27
36
|
return {
|
|
28
|
-
accessToken
|
|
37
|
+
accessToken,
|
|
29
38
|
refreshToken: parsed.refreshToken ?? null,
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
// Normalize on read as well as write: a payload can be stale, hand-written,
|
|
40
|
+
// or predate a field, and every role predicate downstream reads `roles`.
|
|
41
|
+
user: parsed.user ? normalizeUser(parsed.user, accessToken) : null,
|
|
42
|
+
isAuthenticated: !!accessToken && !!parsed.user
|
|
32
43
|
};
|
|
33
44
|
} catch {
|
|
34
|
-
return
|
|
35
|
-
accessToken: null,
|
|
36
|
-
refreshToken: null,
|
|
37
|
-
user: null,
|
|
38
|
-
isAuthenticated: false
|
|
39
|
-
};
|
|
45
|
+
return emptyAuthState();
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
function saveAuthToStorage(state) {
|
|
@@ -46,11 +52,22 @@ function saveAuthToStorage(state) {
|
|
|
46
52
|
if (!state.accessToken) {
|
|
47
53
|
storage.removeItem(key);
|
|
48
54
|
} else {
|
|
49
|
-
storage.setItem(key, JSON.stringify(state));
|
|
55
|
+
storage.setItem(key, JSON.stringify({ ...state, version: STORAGE_VERSION }));
|
|
50
56
|
}
|
|
51
57
|
} catch {
|
|
52
58
|
}
|
|
53
59
|
}
|
|
60
|
+
function normalizeUser(user, accessToken) {
|
|
61
|
+
const jwtPayload = accessToken ? decodeJWT(accessToken) : null;
|
|
62
|
+
return {
|
|
63
|
+
...user,
|
|
64
|
+
roles: firstNonEmpty(jwtPayload?.roles, user.roles),
|
|
65
|
+
permissions: firstNonEmpty(jwtPayload?.permissions, user.permissions)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function firstNonEmpty(...candidates) {
|
|
69
|
+
return candidates.find((candidate) => candidate && candidate.length > 0) ?? [];
|
|
70
|
+
}
|
|
54
71
|
var AuthStore = class {
|
|
55
72
|
constructor() {
|
|
56
73
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
@@ -90,12 +107,7 @@ var AuthStore = class {
|
|
|
90
107
|
* Set auth data after successful login.
|
|
91
108
|
*/
|
|
92
109
|
setAuth(accessToken, refreshToken, user, sessionToken) {
|
|
93
|
-
const
|
|
94
|
-
const userWithPermissions = {
|
|
95
|
-
...user,
|
|
96
|
-
permissions: user.permissions || jwtPayload?.permissions || [],
|
|
97
|
-
roles: user.roles || jwtPayload?.roles || (user.role ? [user.role] : [])
|
|
98
|
-
};
|
|
110
|
+
const userWithPermissions = normalizeUser(user, accessToken);
|
|
99
111
|
this.state = {
|
|
100
112
|
accessToken,
|
|
101
113
|
refreshToken,
|
|
@@ -119,12 +131,7 @@ var AuthStore = class {
|
|
|
119
131
|
* Update tokens (e.g., after refresh).
|
|
120
132
|
*/
|
|
121
133
|
updateTokens(accessToken, refreshToken) {
|
|
122
|
-
const
|
|
123
|
-
const updatedUser = this.state.user ? {
|
|
124
|
-
...this.state.user,
|
|
125
|
-
permissions: jwtPayload?.permissions || this.state.user.permissions || [],
|
|
126
|
-
roles: jwtPayload?.roles || this.state.user.roles || (this.state.user.role ? [this.state.user.role] : [])
|
|
127
|
-
} : null;
|
|
134
|
+
const updatedUser = this.state.user ? normalizeUser(this.state.user, accessToken) : null;
|
|
128
135
|
this.state = {
|
|
129
136
|
...this.state,
|
|
130
137
|
accessToken,
|
|
@@ -141,12 +148,7 @@ var AuthStore = class {
|
|
|
141
148
|
* Update user data (e.g., after profile update).
|
|
142
149
|
*/
|
|
143
150
|
updateUser(user) {
|
|
144
|
-
const
|
|
145
|
-
const updatedUser = {
|
|
146
|
-
...user,
|
|
147
|
-
permissions: user.permissions || jwtPayload?.permissions || [],
|
|
148
|
-
roles: user.roles || jwtPayload?.roles || (user.role ? [user.role] : [])
|
|
149
|
-
};
|
|
151
|
+
const updatedUser = normalizeUser(user, this.state.accessToken);
|
|
150
152
|
this.state = {
|
|
151
153
|
...this.state,
|
|
152
154
|
user: updatedUser
|
|
@@ -235,22 +237,24 @@ var AuthStore = class {
|
|
|
235
237
|
return this.state.user?.permissions?.includes(permission) ?? false;
|
|
236
238
|
}
|
|
237
239
|
/**
|
|
238
|
-
* Check if user
|
|
240
|
+
* Check if the user holds a specific role.
|
|
241
|
+
*
|
|
242
|
+
* Exact membership — holding `admin` does not imply `agent`.
|
|
239
243
|
*/
|
|
240
244
|
hasRole(role) {
|
|
241
|
-
return this.state.user
|
|
245
|
+
return hasRole(this.state.user, role);
|
|
242
246
|
}
|
|
243
247
|
/**
|
|
244
|
-
* Check if user
|
|
248
|
+
* Check if the user holds any of the specified roles.
|
|
245
249
|
*/
|
|
246
250
|
hasAnyRole(roles) {
|
|
247
|
-
return
|
|
251
|
+
return hasAnyRole(this.state.user, roles);
|
|
248
252
|
}
|
|
249
253
|
/**
|
|
250
|
-
* Check if user
|
|
254
|
+
* Check if the user holds all of the specified roles.
|
|
251
255
|
*/
|
|
252
256
|
hasAllRoles(roles) {
|
|
253
|
-
return
|
|
257
|
+
return hasAllRoles(this.state.user, roles);
|
|
254
258
|
}
|
|
255
259
|
/**
|
|
256
260
|
* Check if user has any of the specified permissions.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { authApi } from './chunk-
|
|
1
|
+
import { authApi } from './chunk-2N2MDCWL.js';
|
|
2
|
+
import { getUserRoles, hasAnyRole } from './chunk-O47YKG2F.js';
|
|
2
3
|
|
|
3
4
|
// src/core/guards.ts
|
|
4
5
|
function isMfaChallengeResponse(response) {
|
|
@@ -17,7 +18,7 @@ function getAvailableMethods(response) {
|
|
|
17
18
|
// src/core/errors.ts
|
|
18
19
|
var RoleDeniedError = class _RoleDeniedError extends Error {
|
|
19
20
|
constructor(user, requiredRoles, redirectTo) {
|
|
20
|
-
const userRoles = user.
|
|
21
|
+
const userRoles = getUserRoles(user).join(", ") || "none";
|
|
21
22
|
super(
|
|
22
23
|
`User "${user.username}" does not have required role(s). Has: [${userRoles}], Required: [${requiredRoles.join(", ")}]`
|
|
23
24
|
);
|
|
@@ -48,9 +49,7 @@ var AuthService = class {
|
|
|
48
49
|
async login(credentials, options) {
|
|
49
50
|
const response = await authApi.login(credentials);
|
|
50
51
|
if (options?.allowedRoles?.length && !isMfaChallengeResponse(response)) {
|
|
51
|
-
|
|
52
|
-
const hasAllowedRole = options.allowedRoles.some((r) => userRoles.includes(r));
|
|
53
|
-
if (!hasAllowedRole) {
|
|
52
|
+
if (!hasAnyRole(response.user, options.allowedRoles)) {
|
|
54
53
|
throw new RoleDeniedError(
|
|
55
54
|
response.user,
|
|
56
55
|
options.allowedRoles,
|
|
@@ -60,7 +59,7 @@ var AuthService = class {
|
|
|
60
59
|
}
|
|
61
60
|
if (options?.autoSetAuth !== false && !isMfaChallengeResponse(response)) {
|
|
62
61
|
try {
|
|
63
|
-
const { authStore } = await import('./auth.svelte-
|
|
62
|
+
const { authStore } = await import('./auth.svelte-J62VV4TA.js');
|
|
64
63
|
authStore.setAuth(
|
|
65
64
|
response.accessToken,
|
|
66
65
|
response.refreshToken,
|
|
@@ -234,9 +233,7 @@ var AuthService = class {
|
|
|
234
233
|
async verifyMFAChallenge(data, options) {
|
|
235
234
|
const response = await authApi.verifyMFAChallenge(data);
|
|
236
235
|
if (options?.allowedRoles?.length) {
|
|
237
|
-
|
|
238
|
-
const hasAllowedRole = options.allowedRoles.some((r) => userRoles.includes(r));
|
|
239
|
-
if (!hasAllowedRole) {
|
|
236
|
+
if (!hasAnyRole(response.user, options.allowedRoles)) {
|
|
240
237
|
throw new RoleDeniedError(
|
|
241
238
|
response.user,
|
|
242
239
|
options.allowedRoles,
|
|
@@ -246,7 +243,7 @@ var AuthService = class {
|
|
|
246
243
|
}
|
|
247
244
|
if (options?.autoSetAuth !== false) {
|
|
248
245
|
try {
|
|
249
|
-
const { authStore } = await import('./auth.svelte-
|
|
246
|
+
const { authStore } = await import('./auth.svelte-J62VV4TA.js');
|
|
250
247
|
authStore.setAuth(
|
|
251
248
|
response.accessToken,
|
|
252
249
|
response.refreshToken,
|
|
@@ -392,7 +389,7 @@ function getGreeting(user, includeTime = true) {
|
|
|
392
389
|
function formatUserRoles(user, options = {}) {
|
|
393
390
|
if (!user) return "";
|
|
394
391
|
const { max, separator = ", ", lowercase = false, capitalize = true } = options;
|
|
395
|
-
const roles =
|
|
392
|
+
const roles = getUserRoles(user);
|
|
396
393
|
if (roles.length === 0) return "";
|
|
397
394
|
const formatRole = (role) => {
|
|
398
395
|
if (lowercase) return role.toLowerCase();
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from '../types-
|
|
2
|
-
export { a as AuthConfig, b as AuthState, l as ResetPasswordData, S as SSOConfig, o as StorageAdapter,
|
|
3
|
-
export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-
|
|
1
|
+
import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination, q as UserRolesResponse } from '../types-DXLdCdNC.js';
|
|
2
|
+
export { a as AuthConfig, b as AuthState, l as ResetPasswordData, S as SSOConfig, o as StorageAdapter, r as createMemoryStorage, s as createSessionStorage, t as getConfig, u as getDefaultStorage, v as getFetch, w as getStorage, x as initAuth, y as isInitialized, z as resetConfig } from '../types-DXLdCdNC.js';
|
|
3
|
+
export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-mQMLo7Il.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* HTTP Client
|
|
@@ -284,6 +284,38 @@ declare const authApi: {
|
|
|
284
284
|
events: SecurityEvent[];
|
|
285
285
|
pagination: Pagination;
|
|
286
286
|
}>;
|
|
287
|
+
/**
|
|
288
|
+
* List a user's roles.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* const { roles, primaryRole } = await authApi.getUserRoles(userId);
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
getUserRoles(userId: string, customFetch?: typeof fetch): Promise<UserRolesResponse>;
|
|
296
|
+
/**
|
|
297
|
+
* Grant a role to a user. Idempotent — granting a role they already hold
|
|
298
|
+
* succeeds and returns the unchanged set.
|
|
299
|
+
*
|
|
300
|
+
* @returns The user's full role set after the grant.
|
|
301
|
+
*/
|
|
302
|
+
grantUserRole(userId: string, role: string): Promise<UserRolesResponse>;
|
|
303
|
+
/**
|
|
304
|
+
* Revoke a role from a user.
|
|
305
|
+
*
|
|
306
|
+
* Rejected by the server if it would leave the user with no roles.
|
|
307
|
+
*
|
|
308
|
+
* @returns The user's full role set after the revoke.
|
|
309
|
+
*/
|
|
310
|
+
revokeUserRole(userId: string, role: string): Promise<UserRolesResponse>;
|
|
311
|
+
/**
|
|
312
|
+
* Replace a user's entire role set.
|
|
313
|
+
*
|
|
314
|
+
* Roles absent from `roles` are revoked, so read-modify-write against
|
|
315
|
+
* {@link getUserRoles} rather than passing a set built from stale state.
|
|
316
|
+
* Use {@link grantUserRole} / {@link revokeUserRole} for additive changes.
|
|
317
|
+
*/
|
|
318
|
+
setUserRoles(userId: string, roles: string[]): Promise<User>;
|
|
287
319
|
};
|
|
288
320
|
|
|
289
321
|
/**
|
|
@@ -413,4 +445,111 @@ declare function getTokenExpiration(token: string): Date | null;
|
|
|
413
445
|
*/
|
|
414
446
|
declare function extractClaims<T extends string>(token: string, claims: T[]): Pick<JWTPayload, T> | null;
|
|
415
447
|
|
|
416
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Role Utilities
|
|
450
|
+
*
|
|
451
|
+
* A user holds a set of roles. Their effective permissions are the UNION of
|
|
452
|
+
* every role's permissions and their effective level is the MAX tier across
|
|
453
|
+
* them — the server computes both and bakes the result into the JWT.
|
|
454
|
+
*
|
|
455
|
+
* This module mirrors the role *taxonomy* from the API
|
|
456
|
+
* (`chapi/src/config/roles.ts`, `chapi/src/lib/users/roles.ts`). It deliberately
|
|
457
|
+
* does NOT mirror the permission definitions: `permissions` arrives pre-computed
|
|
458
|
+
* on the token, so duplicating the role→permission mapping here would only give
|
|
459
|
+
* it a second place to drift.
|
|
460
|
+
*
|
|
461
|
+
* SECURITY: everything here is for UI decisions only. Authorization is enforced
|
|
462
|
+
* server-side; a client-side role check is a hint, not a gate.
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Every role name, ordered from most to least privileged.
|
|
467
|
+
*
|
|
468
|
+
* Order is load-bearing: {@link computePrimaryRole} walks this list and takes
|
|
469
|
+
* the first match, which is what makes the result deterministic when several
|
|
470
|
+
* roles share a tier (e.g. `agent` + `builder`).
|
|
471
|
+
*
|
|
472
|
+
* Mirrors `ROLE_NAMES` in the API. `builder`/`customer_service`/`demo` are
|
|
473
|
+
* staff-tier clones of `agent`; `vendor`/`customer`/`subcontractor` are clones
|
|
474
|
+
* of `user`, row-scoped to their own homes.
|
|
475
|
+
*/
|
|
476
|
+
declare const ROLE_NAMES: readonly ["admin", "manager", "agent", "builder", "customer_service", "demo", "user", "vendor", "customer", "subcontractor", "guest"];
|
|
477
|
+
/**
|
|
478
|
+
* A known role name.
|
|
479
|
+
*
|
|
480
|
+
* Note that role-carrying values stay `string` at API boundaries — the server
|
|
481
|
+
* can add a role before this package ships a matching release, and an unknown
|
|
482
|
+
* role must degrade (contribute no permissions, level 0) rather than break
|
|
483
|
+
* decoding. This type is for authoring ergonomics, not runtime validation.
|
|
484
|
+
*/
|
|
485
|
+
type RoleName = (typeof ROLE_NAMES)[number];
|
|
486
|
+
/**
|
|
487
|
+
* Privilege level per role. Higher wins. Mirrors `ROLE_HIERARCHY` in the API.
|
|
488
|
+
*
|
|
489
|
+
* Used only to derive a display-facing primary role and effective level —
|
|
490
|
+
* never to imply one role from another. See {@link computePrimaryRole}.
|
|
491
|
+
*/
|
|
492
|
+
declare const ROLE_HIERARCHY: Record<string, number>;
|
|
493
|
+
/**
|
|
494
|
+
* Read a user's role set.
|
|
495
|
+
*
|
|
496
|
+
* The single place that normalizes roles off a user object, so callers never
|
|
497
|
+
* have to care whether the field is absent (e.g. a hand-built object, or a
|
|
498
|
+
* response shape that predates multi-role support).
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* const roles = getUserRoles(user); // ['manager', 'agent']
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
declare function getUserRoles(user: Pick<User, 'roles'> | null | undefined): string[];
|
|
506
|
+
/**
|
|
507
|
+
* The highest-tier role a user holds, for display.
|
|
508
|
+
*
|
|
509
|
+
* Resolved by {@link ROLE_NAMES} order rather than by comparing levels, so ties
|
|
510
|
+
* within a tier resolve deterministically and identically to the server.
|
|
511
|
+
* Returns `guest` when the set is empty or holds only unrecognized roles.
|
|
512
|
+
*
|
|
513
|
+
* This is a label, not a capability — a user's actual access is the union of
|
|
514
|
+
* all their roles, which is strictly broader than their primary role alone.
|
|
515
|
+
* Never gate on this; use {@link hasRole} or the store's role predicates.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* computePrimaryRole(['user', 'admin']); // 'admin'
|
|
520
|
+
* computePrimaryRole(['agent', 'builder']); // 'agent' — ROLE_NAMES order breaks the tie
|
|
521
|
+
* computePrimaryRole([]); // 'guest'
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
declare function computePrimaryRole(roles: readonly string[]): RoleName;
|
|
525
|
+
/**
|
|
526
|
+
* Effective privilege level — the MAX across all roles held.
|
|
527
|
+
* Unrecognized roles contribute 0.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```typescript
|
|
531
|
+
* getEffectiveLevel(['user', 'manager']); // 3
|
|
532
|
+
* getEffectiveLevel([]); // 0
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
declare function getEffectiveLevel(roles: readonly string[]): number;
|
|
536
|
+
/**
|
|
537
|
+
* Whether a user holds a specific role.
|
|
538
|
+
*
|
|
539
|
+
* Exact membership — holding `admin` does NOT imply `agent`. This matches the
|
|
540
|
+
* API's `requireRole`, so the UI can't offer something the server will then
|
|
541
|
+
* reject with a 403.
|
|
542
|
+
*/
|
|
543
|
+
declare function hasRole(user: Pick<User, 'roles'> | null | undefined, role: string): boolean;
|
|
544
|
+
/**
|
|
545
|
+
* Whether a user holds at least one of the given roles.
|
|
546
|
+
* An empty `roles` list matches nothing.
|
|
547
|
+
*/
|
|
548
|
+
declare function hasAnyRole(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
|
|
549
|
+
/**
|
|
550
|
+
* Whether a user holds every one of the given roles.
|
|
551
|
+
* An empty `roles` list matches vacuously (nothing was required).
|
|
552
|
+
*/
|
|
553
|
+
declare function hasAllRoles(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
|
|
554
|
+
|
|
555
|
+
export { ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFASetupResponse, MFAStatus, Pagination, ProfileUpdateData, ROLE_HIERARCHY, ROLE_NAMES, RegisterData, RegisterResponse, type RoleName, SecurityEvent, Session, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, updateStoredTokens };
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-
|
|
2
|
-
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-
|
|
3
|
-
export { createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-
|
|
1
|
+
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-VVREZ25E.js';
|
|
2
|
+
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-2N2MDCWL.js';
|
|
3
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-O47YKG2F.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as ApiKey, a as AuthConfig, b as AuthState, C as ChangePasswordData, c as CreateApiKeyRequest, d as CreateApiKeyResponse, D as Device, L as LinkedAccount, e as LoginCredentials, f as LoginResponse, g as LogoutResponse, M as MFAChallengeData, h as MFASetupResponse, i as MFAStatus, P as Pagination, j as ProfileUpdateData, R as RegisterData, k as RegisterResponse, l as ResetPasswordData, S as SSOConfig, m as SecurityEvent, n as Session, o as StorageAdapter, U as User, p as UserPreferences, q as
|
|
2
|
-
export { ApiRequestOptions, ApiResponse, JWTPayload, api, apiRequest, authApi, clearStoredAuth, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, updateStoredTokens } from './core/index.js';
|
|
3
|
-
export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from './user-utils-
|
|
1
|
+
export { A as ApiKey, a as AuthConfig, b as AuthState, C as ChangePasswordData, c as CreateApiKeyRequest, d as CreateApiKeyResponse, D as Device, L as LinkedAccount, e as LoginCredentials, f as LoginResponse, g as LogoutResponse, M as MFAChallengeData, h as MFASetupResponse, i as MFAStatus, P as Pagination, j as ProfileUpdateData, R as RegisterData, k as RegisterResponse, l as ResetPasswordData, S as SSOConfig, m as SecurityEvent, n as Session, o as StorageAdapter, U as User, p as UserPreferences, q as UserRolesResponse, r as createMemoryStorage, s as createSessionStorage, t as getConfig, u as getDefaultStorage, v as getFetch, w as getStorage, x as initAuth, y as isInitialized, z as resetConfig } from './types-DXLdCdNC.js';
|
|
2
|
+
export { ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RoleName, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, updateStoredTokens } from './core/index.js';
|
|
3
|
+
export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from './user-utils-mQMLo7Il.js';
|
|
4
4
|
export { AuthClient, AuthGuardOptions, AuthGuardResult, AuthHookOptions, CreateAuthClientOptions, NavFilterOptions, RoleRestrictedItem, authActions, authStore, canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, currentUser, filterByAccess, filterNavSections, isAuthenticated, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './svelte/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-
|
|
2
|
-
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-
|
|
4
|
-
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-
|
|
5
|
-
export { createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-
|
|
1
|
+
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-ILRNO4WH.js';
|
|
2
|
+
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-VVREZ25E.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-QBTMNWJH.js';
|
|
4
|
+
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-2N2MDCWL.js';
|
|
5
|
+
export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-O47YKG2F.js';
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { b as AuthState, U as User, a as AuthConfig } from '../types-
|
|
2
|
-
export {
|
|
3
|
-
import { a as authService } from '../user-utils-
|
|
4
|
-
export { R as RoleDeniedError, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-
|
|
1
|
+
import { b as AuthState, U as User, a as AuthConfig } from '../types-DXLdCdNC.js';
|
|
2
|
+
export { x as initAuth, y as isInitialized } from '../types-DXLdCdNC.js';
|
|
3
|
+
import { a as authService } from '../user-utils-mQMLo7Il.js';
|
|
4
|
+
export { R as RoleDeniedError, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-mQMLo7Il.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Auth Store
|
|
@@ -69,15 +69,17 @@ declare class AuthStore {
|
|
|
69
69
|
*/
|
|
70
70
|
hasPermission(permission: string): boolean;
|
|
71
71
|
/**
|
|
72
|
-
* Check if user
|
|
72
|
+
* Check if the user holds a specific role.
|
|
73
|
+
*
|
|
74
|
+
* Exact membership — holding `admin` does not imply `agent`.
|
|
73
75
|
*/
|
|
74
76
|
hasRole(role: string): boolean;
|
|
75
77
|
/**
|
|
76
|
-
* Check if user
|
|
78
|
+
* Check if the user holds any of the specified roles.
|
|
77
79
|
*/
|
|
78
80
|
hasAnyRole(roles: string[]): boolean;
|
|
79
81
|
/**
|
|
80
|
-
* Check if user
|
|
82
|
+
* Check if the user holds all of the specified roles.
|
|
81
83
|
*/
|
|
82
84
|
hasAllRoles(roles: string[]): boolean;
|
|
83
85
|
/**
|
|
@@ -379,6 +381,13 @@ declare function createAuthClient(options: CreateAuthClientOptions): AuthClient;
|
|
|
379
381
|
interface RoleRestrictedItem {
|
|
380
382
|
/** Optional item identifier */
|
|
381
383
|
id?: string;
|
|
384
|
+
/**
|
|
385
|
+
* Where this item navigates to, if anywhere.
|
|
386
|
+
*
|
|
387
|
+
* Only consulted when pruning empty parents: an item with an `href` survives
|
|
388
|
+
* losing all its children, since it remains a destination in its own right.
|
|
389
|
+
*/
|
|
390
|
+
href?: string;
|
|
382
391
|
/**
|
|
383
392
|
* Roles that can see this item.
|
|
384
393
|
* By default, user needs ANY of these roles (use requireAllRoles for ALL).
|
|
@@ -411,6 +420,27 @@ interface NavFilterOptions {
|
|
|
411
420
|
* If false (default), items without restrictions are visible to everyone.
|
|
412
421
|
*/
|
|
413
422
|
hideUnrestrictedForGuests?: boolean;
|
|
423
|
+
/**
|
|
424
|
+
* Property holding nested child items (e.g. 'children' for submenus).
|
|
425
|
+
*
|
|
426
|
+
* Set this for tree-shaped navigation. Without it, filtering is flat: a
|
|
427
|
+
* restricted child under an unrestricted parent stays visible, because
|
|
428
|
+
* nothing ever looks inside the parent.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```typescript
|
|
432
|
+
* filterByAccess(navItems, user, { childrenKey: 'children' });
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
childrenKey?: string;
|
|
436
|
+
/**
|
|
437
|
+
* If true, a parent whose children are all filtered away is dropped too.
|
|
438
|
+
*
|
|
439
|
+
* Only applies to items that had children to begin with, and only when they
|
|
440
|
+
* are pure containers — an item with its own `href` stays, since it is still
|
|
441
|
+
* navigable on its own. Requires `childrenKey`. Default: true.
|
|
442
|
+
*/
|
|
443
|
+
pruneEmptyParents?: boolean;
|
|
414
444
|
}
|
|
415
445
|
/**
|
|
416
446
|
* Filter navigation items based on user roles and permissions.
|
|
@@ -431,6 +461,12 @@ interface NavFilterOptions {
|
|
|
431
461
|
* const filtered = filterByAccess(navItems, user);
|
|
432
462
|
* // Returns items the user can see based on their roles/permissions
|
|
433
463
|
* ```
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```typescript
|
|
467
|
+
* // Tree-shaped navigation — without childrenKey, nested items are NOT filtered
|
|
468
|
+
* const filtered = filterByAccess(navItems, user, { childrenKey: 'children' });
|
|
469
|
+
* ```
|
|
434
470
|
*/
|
|
435
471
|
declare function filterByAccess<T extends RoleRestrictedItem>(items: T[], user: User | null, options?: NavFilterOptions): T[];
|
|
436
472
|
/**
|
package/dist/svelte/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-
|
|
2
|
-
export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-
|
|
4
|
-
import '../chunk-
|
|
5
|
-
export { initAuth, isInitialized } from '../chunk-
|
|
1
|
+
export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-ILRNO4WH.js';
|
|
2
|
+
export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-VVREZ25E.js';
|
|
3
|
+
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-QBTMNWJH.js';
|
|
4
|
+
import '../chunk-2N2MDCWL.js';
|
|
5
|
+
export { initAuth, isInitialized } from '../chunk-O47YKG2F.js';
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as User, D as Device, f as LoginResponse, m as SecurityEvent, n as Session, A as ApiKey, L as LinkedAccount, g as LogoutResponse, h as MFASetupResponse, i as MFAStatus, k as RegisterResponse, p as UserPreferences, o as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-
|
|
1
|
+
import { U as User, D as Device, f as LoginResponse, m as SecurityEvent, n as Session, A as ApiKey, L as LinkedAccount, g as LogoutResponse, h as MFASetupResponse, i as MFAStatus, k as RegisterResponse, p as UserPreferences, o as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-DXLdCdNC.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* User Fixtures
|
|
@@ -588,15 +588,18 @@ declare class MockAuthStore {
|
|
|
588
588
|
*/
|
|
589
589
|
hasPermission(permission: string): boolean;
|
|
590
590
|
/**
|
|
591
|
-
* Check if user
|
|
591
|
+
* Check if the user holds a specific role.
|
|
592
|
+
*
|
|
593
|
+
* Delegates to the same helpers as the real store — a mock that decides
|
|
594
|
+
* access differently from production is worse than no mock.
|
|
592
595
|
*/
|
|
593
596
|
hasRole(role: string): boolean;
|
|
594
597
|
/**
|
|
595
|
-
* Check if user
|
|
598
|
+
* Check if the user holds any of the specified roles.
|
|
596
599
|
*/
|
|
597
600
|
hasAnyRole(roles: string[]): boolean;
|
|
598
601
|
/**
|
|
599
|
-
* Check if user
|
|
602
|
+
* Check if the user holds all of the specified roles.
|
|
600
603
|
*/
|
|
601
604
|
hasAllRoles(roles: string[]): boolean;
|
|
602
605
|
/**
|
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { initAuth, resetConfig, isTokenExpired, decodeJWT } from '../chunk-
|
|
1
|
+
import { hasRole, hasAnyRole, hasAllRoles, initAuth, resetConfig, getUserRoles, isTokenExpired, decodeJWT } from '../chunk-O47YKG2F.js';
|
|
2
2
|
|
|
3
3
|
// src/testing/fixtures/users.ts
|
|
4
4
|
var mockUser = {
|
|
@@ -8,7 +8,6 @@ var mockUser = {
|
|
|
8
8
|
firstName: "Test",
|
|
9
9
|
lastName: "User",
|
|
10
10
|
phone: "+1234567890",
|
|
11
|
-
role: "user",
|
|
12
11
|
roles: ["user"],
|
|
13
12
|
permissions: ["read:profile", "write:profile"],
|
|
14
13
|
isActive: true,
|
|
@@ -24,7 +23,6 @@ var mockAdminUser = {
|
|
|
24
23
|
firstName: "Admin",
|
|
25
24
|
lastName: "User",
|
|
26
25
|
phone: "+1234567891",
|
|
27
|
-
role: "admin",
|
|
28
26
|
roles: ["admin", "user"],
|
|
29
27
|
permissions: [
|
|
30
28
|
"read:profile",
|
|
@@ -48,7 +46,6 @@ var mockSSOUser = {
|
|
|
48
46
|
email: "sso@example.com",
|
|
49
47
|
firstName: "SSO",
|
|
50
48
|
lastName: "User",
|
|
51
|
-
role: "user",
|
|
52
49
|
roles: ["user"],
|
|
53
50
|
permissions: ["read:profile", "write:profile"],
|
|
54
51
|
isActive: true,
|
|
@@ -64,7 +61,6 @@ var mockMFAUser = {
|
|
|
64
61
|
email: "mfa@example.com",
|
|
65
62
|
firstName: "MFA",
|
|
66
63
|
lastName: "User",
|
|
67
|
-
role: "user",
|
|
68
64
|
roles: ["user"],
|
|
69
65
|
permissions: ["read:profile", "write:profile"],
|
|
70
66
|
isActive: true,
|
|
@@ -79,7 +75,6 @@ var mockUnverifiedUser = {
|
|
|
79
75
|
email: "unverified@example.com",
|
|
80
76
|
firstName: "Unverified",
|
|
81
77
|
lastName: "User",
|
|
82
|
-
role: "user",
|
|
83
78
|
roles: ["user"],
|
|
84
79
|
permissions: [],
|
|
85
80
|
isActive: true,
|
|
@@ -93,7 +88,6 @@ var mockInactiveUser = {
|
|
|
93
88
|
email: "inactive@example.com",
|
|
94
89
|
firstName: "Inactive",
|
|
95
90
|
lastName: "User",
|
|
96
|
-
role: "user",
|
|
97
91
|
roles: ["user"],
|
|
98
92
|
permissions: [],
|
|
99
93
|
isActive: false,
|
|
@@ -110,7 +104,6 @@ function createMockUser(overrides = {}) {
|
|
|
110
104
|
}
|
|
111
105
|
function createMockUserWithRoles(roles, permissions, overrides = {}) {
|
|
112
106
|
return createMockUser({
|
|
113
|
-
role: roles[0] ?? "user",
|
|
114
107
|
roles,
|
|
115
108
|
permissions,
|
|
116
109
|
...overrides
|
|
@@ -1279,22 +1272,25 @@ var MockAuthStore = class {
|
|
|
1279
1272
|
return this.state.user?.permissions?.includes(permission) ?? false;
|
|
1280
1273
|
}
|
|
1281
1274
|
/**
|
|
1282
|
-
* Check if user
|
|
1275
|
+
* Check if the user holds a specific role.
|
|
1276
|
+
*
|
|
1277
|
+
* Delegates to the same helpers as the real store — a mock that decides
|
|
1278
|
+
* access differently from production is worse than no mock.
|
|
1283
1279
|
*/
|
|
1284
1280
|
hasRole(role) {
|
|
1285
|
-
return this.state.user
|
|
1281
|
+
return hasRole(this.state.user, role);
|
|
1286
1282
|
}
|
|
1287
1283
|
/**
|
|
1288
|
-
* Check if user
|
|
1284
|
+
* Check if the user holds any of the specified roles.
|
|
1289
1285
|
*/
|
|
1290
1286
|
hasAnyRole(roles) {
|
|
1291
|
-
return
|
|
1287
|
+
return hasAnyRole(this.state.user, roles);
|
|
1292
1288
|
}
|
|
1293
1289
|
/**
|
|
1294
|
-
* Check if user
|
|
1290
|
+
* Check if the user holds all of the specified roles.
|
|
1295
1291
|
*/
|
|
1296
1292
|
hasAllRoles(roles) {
|
|
1297
|
-
return
|
|
1293
|
+
return hasAllRoles(this.state.user, roles);
|
|
1298
1294
|
}
|
|
1299
1295
|
/**
|
|
1300
1296
|
* Check if user has any of the specified permissions.
|
|
@@ -1833,13 +1829,15 @@ function assertLacksPermissions(user, permissions, message) {
|
|
|
1833
1829
|
}
|
|
1834
1830
|
}
|
|
1835
1831
|
function assertHasRoles(user, roles, message) {
|
|
1836
|
-
const
|
|
1832
|
+
const held = getUserRoles(user);
|
|
1833
|
+
const missing = roles.filter((r) => !held.includes(r));
|
|
1837
1834
|
if (missing.length > 0) {
|
|
1838
1835
|
throw new Error(message ?? `User missing roles: ${missing.join(", ")}`);
|
|
1839
1836
|
}
|
|
1840
1837
|
}
|
|
1841
1838
|
function assertLacksRoles(user, roles, message) {
|
|
1842
|
-
const
|
|
1839
|
+
const held = getUserRoles(user);
|
|
1840
|
+
const present = roles.filter((r) => held.includes(r));
|
|
1843
1841
|
if (present.length > 0) {
|
|
1844
1842
|
throw new Error(message ?? `User should not have roles: ${present.join(", ")}`);
|
|
1845
1843
|
}
|
|
@@ -149,8 +149,13 @@ interface User {
|
|
|
149
149
|
firstName?: string;
|
|
150
150
|
lastName?: string;
|
|
151
151
|
phone?: string;
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Every role the user holds. `permissions` is the union across all of them,
|
|
154
|
+
* so this is broader than any single role — see `computePrimaryRole` in
|
|
155
|
+
* `./roles.js` if you need one role to display.
|
|
156
|
+
*/
|
|
157
|
+
roles: string[];
|
|
158
|
+
/** Union of the permissions granted by every role in `roles`. */
|
|
154
159
|
permissions: string[];
|
|
155
160
|
isActive: boolean;
|
|
156
161
|
emailVerified: boolean;
|
|
@@ -159,6 +164,21 @@ interface User {
|
|
|
159
164
|
createdAt: string;
|
|
160
165
|
lastLoginAt?: string;
|
|
161
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The result of reading or changing a user's roles.
|
|
169
|
+
*/
|
|
170
|
+
interface UserRolesResponse {
|
|
171
|
+
userId: string;
|
|
172
|
+
/** The user's full role set after the operation. */
|
|
173
|
+
roles: string[];
|
|
174
|
+
/**
|
|
175
|
+
* The highest-tier role in `roles`, as resolved by the server.
|
|
176
|
+
*
|
|
177
|
+
* A display label, not a capability — the user's actual access is the union
|
|
178
|
+
* of every role in `roles`, which is broader than this one alone.
|
|
179
|
+
*/
|
|
180
|
+
primaryRole: string;
|
|
181
|
+
}
|
|
162
182
|
interface AuthState {
|
|
163
183
|
accessToken: string | null;
|
|
164
184
|
refreshToken: string | null;
|
|
@@ -227,6 +247,10 @@ interface ApiKey {
|
|
|
227
247
|
description?: string;
|
|
228
248
|
keyPreview: string;
|
|
229
249
|
permissions: string[];
|
|
250
|
+
/**
|
|
251
|
+
* The single role this key acts as. Unlike a user — who holds a set of roles
|
|
252
|
+
* whose permissions are unioned — an API key is scoped to at most one role.
|
|
253
|
+
*/
|
|
230
254
|
role: string | null;
|
|
231
255
|
isActive: boolean;
|
|
232
256
|
expiresAt: string | null;
|
|
@@ -347,4 +371,4 @@ interface ResetPasswordData {
|
|
|
347
371
|
newPassword: string;
|
|
348
372
|
}
|
|
349
373
|
|
|
350
|
-
export { type ApiKey as A, type ChangePasswordData as C, type Device as D, type LinkedAccount as L, type MFAChallengeData as M, type Pagination as P, type RegisterData as R, type SSOConfig as S, type User as U, type AuthConfig as a, type AuthState as b, type CreateApiKeyRequest as c, type CreateApiKeyResponse as d, type LoginCredentials as e, type LoginResponse as f, type LogoutResponse as g, type MFASetupResponse as h, type MFAStatus as i, type ProfileUpdateData as j, type RegisterResponse as k, type ResetPasswordData as l, type SecurityEvent as m, type Session as n, type StorageAdapter as o, type UserPreferences as p,
|
|
374
|
+
export { type ApiKey as A, type ChangePasswordData as C, type Device as D, type LinkedAccount as L, type MFAChallengeData as M, type Pagination as P, type RegisterData as R, type SSOConfig as S, type User as U, type AuthConfig as a, type AuthState as b, type CreateApiKeyRequest as c, type CreateApiKeyResponse as d, type LoginCredentials as e, type LoginResponse as f, type LogoutResponse as g, type MFASetupResponse as h, type MFAStatus as i, type ProfileUpdateData as j, type RegisterResponse as k, type ResetPasswordData as l, type SecurityEvent as m, type Session as n, type StorageAdapter as o, type UserPreferences as p, type UserRolesResponse as q, createMemoryStorage as r, createSessionStorage as s, getConfig as t, getDefaultStorage as u, getFetch as v, getStorage as w, initAuth as x, isInitialized as y, resetConfig as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from './types-
|
|
1
|
+
import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from './types-DXLdCdNC.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Auth Service
|
package/package.json
CHANGED