@checkstack/auth-frontend 0.1.0 → 0.2.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/CHANGELOG.md +93 -0
- package/package.json +1 -1
- package/src/api.ts +2 -2
- package/src/components/ApplicationsTab.tsx +1 -1
- package/src/components/AuthSettingsPage.tsx +34 -57
- package/src/components/LoginPage.tsx +8 -7
- package/src/components/RegisterPage.tsx +5 -8
- package/src/components/RoleDialog.tsx +39 -39
- package/src/components/RolesTab.tsx +10 -10
- package/src/components/StrategiesTab.tsx +3 -3
- package/src/components/TeamAccessEditor.tsx +9 -12
- package/src/components/TeamsTab.tsx +1 -1
- package/src/components/UsersTab.tsx +2 -2
- package/src/hooks/{usePermissions.ts → useAccessRules.ts} +10 -10
- package/src/index.test.tsx +83 -37
- package/src/index.tsx +29 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# @checkstack/auth-frontend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
8
|
+
|
|
9
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
10
|
+
|
|
11
|
+
## Changes
|
|
12
|
+
|
|
13
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
14
|
+
|
|
15
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
16
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
17
|
+
- Added `access()` builder for individual access rules
|
|
18
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
19
|
+
|
|
20
|
+
### API Changes
|
|
21
|
+
|
|
22
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
23
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
24
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
25
|
+
- Route `permission:` field → `accessRule:` field
|
|
26
|
+
|
|
27
|
+
### UI Changes
|
|
28
|
+
|
|
29
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
30
|
+
- "You don't have permission..." → "You don't have access..."
|
|
31
|
+
- All permission-related UI text updated
|
|
32
|
+
|
|
33
|
+
### Documentation & Templates
|
|
34
|
+
|
|
35
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
36
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
37
|
+
- All code examples use new AccessRule API
|
|
38
|
+
|
|
39
|
+
## Migration Guide
|
|
40
|
+
|
|
41
|
+
### Backend Plugins
|
|
42
|
+
|
|
43
|
+
```diff
|
|
44
|
+
- import { permissionList } from "./permissions";
|
|
45
|
+
- env.registerPermissions(permissionList);
|
|
46
|
+
+ import { accessRules } from "./access";
|
|
47
|
+
+ env.registerAccessRules(accessRules);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### RPC Contracts
|
|
51
|
+
|
|
52
|
+
```diff
|
|
53
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
54
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Frontend Hooks
|
|
58
|
+
|
|
59
|
+
```diff
|
|
60
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
61
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Routes
|
|
65
|
+
|
|
66
|
+
```diff
|
|
67
|
+
- permission: permissions.entityRead.id,
|
|
68
|
+
+ accessRule: access.read,
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- 95eeec7: # Auto-login after credential registration
|
|
74
|
+
|
|
75
|
+
Users are now automatically logged in after successful registration when using the credential (email & password) authentication strategy.
|
|
76
|
+
|
|
77
|
+
## Changes
|
|
78
|
+
|
|
79
|
+
### Backend (`@checkstack/auth-backend`)
|
|
80
|
+
|
|
81
|
+
- Added `autoSignIn: true` to the `emailAndPassword` configuration in better-auth
|
|
82
|
+
- Users no longer need to manually log in after registration; a session is created immediately upon successful sign-up
|
|
83
|
+
|
|
84
|
+
### Frontend (`@checkstack/auth-frontend`)
|
|
85
|
+
|
|
86
|
+
- Updated `RegisterPage` to use full page navigation after registration to ensure the session state refreshes correctly
|
|
87
|
+
- Updated `LoginPage` to use full page navigation after login to ensure fresh permissions state when switching between users
|
|
88
|
+
|
|
89
|
+
- Updated dependencies [9faec1f]
|
|
90
|
+
- Updated dependencies [f533141]
|
|
91
|
+
- @checkstack/auth-common@0.2.0
|
|
92
|
+
- @checkstack/common@0.2.0
|
|
93
|
+
- @checkstack/frontend-api@0.1.0
|
|
94
|
+
- @checkstack/ui@0.2.0
|
|
95
|
+
|
|
3
96
|
## 0.1.0
|
|
4
97
|
|
|
5
98
|
### Minor Changes
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -25,10 +25,10 @@ export interface Role {
|
|
|
25
25
|
description?: string | null;
|
|
26
26
|
isSystem?: boolean;
|
|
27
27
|
isAssignable?: boolean; // False for anonymous role - not assignable to users
|
|
28
|
-
|
|
28
|
+
accessRules?: string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface
|
|
31
|
+
export interface AccessRuleEntry {
|
|
32
32
|
id: string;
|
|
33
33
|
description?: string;
|
|
34
34
|
}
|
|
@@ -303,7 +303,7 @@ export const ApplicationsTab: React.FC<ApplicationsTabProps> = ({
|
|
|
303
303
|
|
|
304
304
|
{!canManageApplications && (
|
|
305
305
|
<p className="text-xs text-muted-foreground mt-4">
|
|
306
|
-
You don't have
|
|
306
|
+
You don't have access to manage applications.
|
|
307
307
|
</p>
|
|
308
308
|
)}
|
|
309
309
|
</CardContent>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import React, { useEffect, useState, useMemo } from "react";
|
|
2
2
|
import { useSearchParams } from "react-router-dom";
|
|
3
|
-
import { useApi,
|
|
3
|
+
import { useApi, accessApiRef, rpcApiRef } from "@checkstack/frontend-api";
|
|
4
4
|
import { PageLayout, useToast, Tabs, TabPanel } from "@checkstack/ui";
|
|
5
|
-
import { authApiRef, AuthUser, Role, AuthStrategy, Permission } from "../api";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
authApiRef,
|
|
7
|
+
AuthUser,
|
|
8
|
+
Role,
|
|
9
|
+
AuthStrategy,
|
|
10
|
+
AccessRuleEntry,
|
|
11
|
+
} from "../api";
|
|
12
|
+
import { authAccess, AuthApi } from "@checkstack/auth-common";
|
|
10
13
|
import { Shield, Settings2, Users, Key, Users2 } from "lucide-react";
|
|
11
14
|
import { UsersTab } from "./UsersTab";
|
|
12
15
|
import { RolesTab } from "./RolesTab";
|
|
@@ -18,7 +21,7 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
18
21
|
const authApi = useApi(authApiRef);
|
|
19
22
|
const rpcApi = useApi(rpcApiRef);
|
|
20
23
|
const authClient = rpcApi.forPlugin(AuthApi);
|
|
21
|
-
const
|
|
24
|
+
const accessApi = useApi(accessApiRef);
|
|
22
25
|
const toast = useToast();
|
|
23
26
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
24
27
|
|
|
@@ -29,13 +32,11 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
29
32
|
>("users");
|
|
30
33
|
const [users, setUsers] = useState<(AuthUser & { roles: string[] })[]>([]);
|
|
31
34
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
32
|
-
const [
|
|
35
|
+
const [accessRuleEntries, setAccessRuleEntries] = useState<AccessRuleEntry[]>([]);
|
|
33
36
|
const [strategies, setStrategies] = useState<AuthStrategy[]>([]);
|
|
34
37
|
const [loading, setLoading] = useState(true);
|
|
35
38
|
|
|
36
|
-
const canReadUsers =
|
|
37
|
-
authPermissions.usersRead.id
|
|
38
|
-
);
|
|
39
|
+
const canReadUsers = accessApi.useAccess(authAccess.users.read);
|
|
39
40
|
|
|
40
41
|
// Handle ?tab= URL parameters (from command palette)
|
|
41
42
|
useEffect(() => {
|
|
@@ -58,44 +59,20 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
58
59
|
}
|
|
59
60
|
}, [searchParams, setSearchParams]);
|
|
60
61
|
|
|
61
|
-
const canManageUsers =
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
const canDeleteRoles = permissionApi.usePermission(
|
|
77
|
-
authPermissions.rolesDelete.id
|
|
78
|
-
);
|
|
79
|
-
const canManageRoles = permissionApi.usePermission(
|
|
80
|
-
authPermissions.rolesManage.id
|
|
81
|
-
);
|
|
82
|
-
const canManageStrategies = permissionApi.usePermission(
|
|
83
|
-
authPermissions.strategiesManage.id
|
|
84
|
-
);
|
|
85
|
-
const canManageRegistration = permissionApi.usePermission(
|
|
86
|
-
authPermissions.registrationManage.id
|
|
87
|
-
);
|
|
88
|
-
const canManageApplications = permissionApi.usePermission(
|
|
89
|
-
authPermissions.applicationsManage.id
|
|
90
|
-
);
|
|
91
|
-
const canReadTeams = permissionApi.usePermission(
|
|
92
|
-
authPermissions.teamsRead.id
|
|
93
|
-
);
|
|
94
|
-
const canManageTeams = permissionApi.usePermission(
|
|
95
|
-
authPermissions.teamsManage.id
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const permissionsLoading =
|
|
62
|
+
const canManageUsers = accessApi.useAccess(authAccess.users.manage);
|
|
63
|
+
const canCreateUsers = accessApi.useAccess(authAccess.users.create);
|
|
64
|
+
const canReadRoles = accessApi.useAccess(authAccess.roles.read);
|
|
65
|
+
const canCreateRoles = accessApi.useAccess(authAccess.roles.create);
|
|
66
|
+
const canUpdateRoles = accessApi.useAccess(authAccess.roles.update);
|
|
67
|
+
const canDeleteRoles = accessApi.useAccess(authAccess.roles.delete);
|
|
68
|
+
const canManageRoles = accessApi.useAccess(authAccess.roles.manage);
|
|
69
|
+
const canManageStrategies = accessApi.useAccess(authAccess.strategies);
|
|
70
|
+
const canManageRegistration = accessApi.useAccess(authAccess.registration);
|
|
71
|
+
const canManageApplications = accessApi.useAccess(authAccess.applications);
|
|
72
|
+
const canReadTeams = accessApi.useAccess(authAccess.teams.read);
|
|
73
|
+
const canManageTeams = accessApi.useAccess(authAccess.teams.manage);
|
|
74
|
+
|
|
75
|
+
const accessRulesLoading =
|
|
99
76
|
loading ||
|
|
100
77
|
canReadUsers.loading ||
|
|
101
78
|
canReadRoles.loading ||
|
|
@@ -103,17 +80,17 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
103
80
|
canManageApplications.loading ||
|
|
104
81
|
canReadTeams.loading;
|
|
105
82
|
|
|
106
|
-
const
|
|
83
|
+
const hasAnyAccess =
|
|
107
84
|
canReadUsers.allowed ||
|
|
108
85
|
canReadRoles.allowed ||
|
|
109
86
|
canManageStrategies.allowed ||
|
|
110
87
|
canManageApplications.allowed ||
|
|
111
88
|
canReadTeams.allowed;
|
|
112
89
|
|
|
113
|
-
// Special case: if user is not logged in, show
|
|
114
|
-
const isAllowed = session.data?.user ?
|
|
90
|
+
// Special case: if user is not logged in, show access denied
|
|
91
|
+
const isAllowed = session.data?.user ? hasAnyAccess : false;
|
|
115
92
|
|
|
116
|
-
// Compute visible tabs based on
|
|
93
|
+
// Compute visible tabs based on access rules
|
|
117
94
|
const visibleTabs = useMemo(() => {
|
|
118
95
|
const tabs: Array<{
|
|
119
96
|
id: "users" | "roles" | "teams" | "strategies" | "applications";
|
|
@@ -129,7 +106,7 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
129
106
|
if (canReadRoles.allowed)
|
|
130
107
|
tabs.push({
|
|
131
108
|
id: "roles",
|
|
132
|
-
label: "Roles &
|
|
109
|
+
label: "Roles & Access Rules",
|
|
133
110
|
icon: <Shield size={18} />,
|
|
134
111
|
});
|
|
135
112
|
if (canReadTeams.allowed)
|
|
@@ -176,11 +153,11 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
176
153
|
roles: string[];
|
|
177
154
|
})[];
|
|
178
155
|
const rolesData = await authClient.getRoles();
|
|
179
|
-
const
|
|
156
|
+
const accessRulesData = await authClient.getAccessRules();
|
|
180
157
|
const strategiesData = await authClient.getStrategies();
|
|
181
158
|
setUsers(usersData);
|
|
182
159
|
setRoles(rolesData);
|
|
183
|
-
|
|
160
|
+
setAccessRuleEntries(accessRulesData);
|
|
184
161
|
setStrategies(strategiesData);
|
|
185
162
|
} catch (error: unknown) {
|
|
186
163
|
const message =
|
|
@@ -204,7 +181,7 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
204
181
|
return (
|
|
205
182
|
<PageLayout
|
|
206
183
|
title="Authentication Settings"
|
|
207
|
-
loading={
|
|
184
|
+
loading={accessRulesLoading}
|
|
208
185
|
allowed={isAllowed}
|
|
209
186
|
>
|
|
210
187
|
<Tabs
|
|
@@ -235,7 +212,7 @@ export const AuthSettingsPage: React.FC = () => {
|
|
|
235
212
|
<TabPanel id="roles" activeTab={activeTab}>
|
|
236
213
|
<RolesTab
|
|
237
214
|
roles={roles}
|
|
238
|
-
|
|
215
|
+
accessRulesList={accessRuleEntries}
|
|
239
216
|
userRoleIds={currentUserRoleIds}
|
|
240
217
|
canReadRoles={canReadRoles.allowed}
|
|
241
218
|
canCreateRoles={canCreateRoles.allowed}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Link
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
3
|
import { LogIn, LogOut, AlertCircle } from "lucide-react";
|
|
4
4
|
import {
|
|
5
5
|
useApi,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
} from "@checkstack/ui";
|
|
39
39
|
import { authApiRef } from "../api";
|
|
40
40
|
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
41
|
-
import {
|
|
41
|
+
import { useAccessRules } from "../hooks/useAccessRules";
|
|
42
42
|
import { useAuthClient } from "../lib/auth-client";
|
|
43
43
|
import { SocialProviderButton } from "./SocialProviderButton";
|
|
44
44
|
import { useEffect } from "react";
|
|
@@ -47,7 +47,7 @@ export const LoginPage = () => {
|
|
|
47
47
|
const [email, setEmail] = useState("");
|
|
48
48
|
const [password, setPassword] = useState("");
|
|
49
49
|
const [loading, setLoading] = useState(false);
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
const authApi = useApi(authApiRef);
|
|
52
52
|
const rpcApi = useApi(rpcApiRef);
|
|
53
53
|
const authRpcClient = rpcApi.forPlugin(AuthApi);
|
|
@@ -74,7 +74,8 @@ export const LoginPage = () => {
|
|
|
74
74
|
if (error) {
|
|
75
75
|
console.error("Login failed:", error);
|
|
76
76
|
} else {
|
|
77
|
-
|
|
77
|
+
// Use full page navigation to ensure session/permissions state refreshes
|
|
78
|
+
globalThis.location.href = "/";
|
|
78
79
|
}
|
|
79
80
|
} finally {
|
|
80
81
|
setLoading(false);
|
|
@@ -273,7 +274,7 @@ export const LogoutMenuItem = (_props: UserMenuItemsContext) => {
|
|
|
273
274
|
export const LoginNavbarAction = () => {
|
|
274
275
|
const authApi = useApi(authApiRef);
|
|
275
276
|
const { data: session, isPending } = authApi.useSession();
|
|
276
|
-
const {
|
|
277
|
+
const { accessRules, loading: accessRulesLoading } = useAccessRules();
|
|
277
278
|
const authClient = useAuthClient();
|
|
278
279
|
const [hasCredentialAccount, setHasCredentialAccount] =
|
|
279
280
|
useState<boolean>(false);
|
|
@@ -295,7 +296,7 @@ export const LoginNavbarAction = () => {
|
|
|
295
296
|
});
|
|
296
297
|
}, [session?.user, authClient]);
|
|
297
298
|
|
|
298
|
-
if (isPending ||
|
|
299
|
+
if (isPending || accessRulesLoading || credentialLoading) {
|
|
299
300
|
return <div className="w-20 h-9 bg-muted animate-pulse rounded-full" />;
|
|
300
301
|
}
|
|
301
302
|
|
|
@@ -306,7 +307,7 @@ export const LoginNavbarAction = () => {
|
|
|
306
307
|
);
|
|
307
308
|
const hasBottomItems = bottomExtensions.length > 0;
|
|
308
309
|
const menuContext: UserMenuItemsContext = {
|
|
309
|
-
|
|
310
|
+
accessRules,
|
|
310
311
|
hasCredentialAccount,
|
|
311
312
|
};
|
|
312
313
|
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Link
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
3
|
import { AlertCircle } from "lucide-react";
|
|
4
4
|
import { useApi, rpcApiRef } from "@checkstack/frontend-api";
|
|
5
5
|
import { authApiRef } from "../api";
|
|
6
|
-
import {
|
|
7
|
-
AuthApi,
|
|
8
|
-
authRoutes,
|
|
9
|
-
passwordSchema,
|
|
10
|
-
} from "@checkstack/auth-common";
|
|
6
|
+
import { AuthApi, authRoutes, passwordSchema } from "@checkstack/auth-common";
|
|
11
7
|
import { resolveRoute } from "@checkstack/common";
|
|
12
8
|
import {
|
|
13
9
|
Button,
|
|
@@ -35,7 +31,7 @@ export const RegisterPage = () => {
|
|
|
35
31
|
const [password, setPassword] = useState("");
|
|
36
32
|
const [loading, setLoading] = useState(false);
|
|
37
33
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
38
|
-
|
|
34
|
+
|
|
39
35
|
const authApi = useApi(authApiRef);
|
|
40
36
|
const rpcApi = useApi(rpcApiRef);
|
|
41
37
|
const authRpcClient = rpcApi.forPlugin(AuthApi);
|
|
@@ -87,7 +83,8 @@ export const RegisterPage = () => {
|
|
|
87
83
|
if (res.error) {
|
|
88
84
|
console.error("Registration failed:", res.error);
|
|
89
85
|
} else {
|
|
90
|
-
|
|
86
|
+
// Use full page navigation to ensure session state refreshes in navbar
|
|
87
|
+
globalThis.location.href = "/";
|
|
91
88
|
}
|
|
92
89
|
} catch (error) {
|
|
93
90
|
console.error("Registration failed:", error);
|
|
@@ -19,20 +19,20 @@ import {
|
|
|
19
19
|
AlertDescription,
|
|
20
20
|
} from "@checkstack/ui";
|
|
21
21
|
import { Check } from "lucide-react";
|
|
22
|
-
import type { Role,
|
|
22
|
+
import type { Role, AccessRuleEntry } from "../api";
|
|
23
23
|
|
|
24
24
|
interface RoleDialogProps {
|
|
25
25
|
open: boolean;
|
|
26
26
|
onOpenChange: (open: boolean) => void;
|
|
27
27
|
role?: Role;
|
|
28
|
-
|
|
29
|
-
/** Whether current user has this role (prevents
|
|
28
|
+
accessRulesList: AccessRuleEntry[];
|
|
29
|
+
/** Whether current user has this role (prevents access elevation) */
|
|
30
30
|
isUserRole?: boolean;
|
|
31
31
|
onSave: (params: {
|
|
32
32
|
id?: string;
|
|
33
33
|
name: string;
|
|
34
34
|
description?: string;
|
|
35
|
-
|
|
35
|
+
accessRules: string[];
|
|
36
36
|
}) => Promise<void>;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -40,14 +40,14 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
40
40
|
open,
|
|
41
41
|
onOpenChange,
|
|
42
42
|
role,
|
|
43
|
-
|
|
43
|
+
accessRulesList,
|
|
44
44
|
isUserRole = false,
|
|
45
45
|
onSave,
|
|
46
46
|
}) => {
|
|
47
47
|
const [name, setName] = useState(role?.name || "");
|
|
48
48
|
const [description, setDescription] = useState(role?.description || "");
|
|
49
|
-
const [
|
|
50
|
-
new Set(role?.
|
|
49
|
+
const [selectedAccessRules, setSelectedAccessRules] = useState<Set<string>>(
|
|
50
|
+
new Set(role?.accessRules || [])
|
|
51
51
|
);
|
|
52
52
|
const [saving, setSaving] = useState(false);
|
|
53
53
|
|
|
@@ -55,32 +55,32 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
55
55
|
React.useEffect(() => {
|
|
56
56
|
setName(role?.name || "");
|
|
57
57
|
setDescription(role?.description || "");
|
|
58
|
-
|
|
58
|
+
setSelectedAccessRules(new Set(role?.accessRules || []));
|
|
59
59
|
}, [role]);
|
|
60
60
|
|
|
61
61
|
const isEditing = !!role;
|
|
62
62
|
const isAdminRole = role?.id === "admin";
|
|
63
|
-
// Disable
|
|
64
|
-
const
|
|
63
|
+
// Disable access rules for admin (wildcard) or user's own roles (prevent elevation)
|
|
64
|
+
const accessRulesDisabled = isAdminRole || isUserRole;
|
|
65
65
|
|
|
66
|
-
// Group
|
|
67
|
-
const
|
|
68
|
-
for (const perm of
|
|
66
|
+
// Group access rules by plugin
|
|
67
|
+
const accessRulesByPlugin: Record<string, AccessRuleEntry[]> = {};
|
|
68
|
+
for (const perm of accessRulesList) {
|
|
69
69
|
const [plugin] = perm.id.split(".");
|
|
70
|
-
if (!
|
|
71
|
-
|
|
70
|
+
if (!accessRulesByPlugin[plugin]) {
|
|
71
|
+
accessRulesByPlugin[plugin] = [];
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
accessRulesByPlugin[plugin].push(perm);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
const newSelected = new Set(
|
|
78
|
-
if (newSelected.has(
|
|
79
|
-
newSelected.delete(
|
|
76
|
+
const handleToggleAccessRule = (accessRuleId: string) => {
|
|
77
|
+
const newSelected = new Set(selectedAccessRules);
|
|
78
|
+
if (newSelected.has(accessRuleId)) {
|
|
79
|
+
newSelected.delete(accessRuleId);
|
|
80
80
|
} else {
|
|
81
|
-
newSelected.add(
|
|
81
|
+
newSelected.add(accessRuleId);
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
setSelectedAccessRules(newSelected);
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
const handleSave = async () => {
|
|
@@ -90,7 +90,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
90
90
|
...(isEditing && { id: role.id }),
|
|
91
91
|
name,
|
|
92
92
|
description: description || undefined,
|
|
93
|
-
|
|
93
|
+
accessRules: [...selectedAccessRules],
|
|
94
94
|
});
|
|
95
95
|
onOpenChange(false);
|
|
96
96
|
} catch (error) {
|
|
@@ -114,8 +114,8 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
114
114
|
<DialogTitle>{isEditing ? "Edit Role" : "Create Role"}</DialogTitle>
|
|
115
115
|
<DialogDescription className="sr-only">
|
|
116
116
|
{isEditing
|
|
117
|
-
? "Modify the settings and
|
|
118
|
-
: "Create a new role with specific
|
|
117
|
+
? "Modify the settings and access rules for this role"
|
|
118
|
+
: "Create a new role with specific access rules"}
|
|
119
119
|
</DialogDescription>
|
|
120
120
|
</DialogHeader>
|
|
121
121
|
|
|
@@ -141,15 +141,15 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
143
|
<div>
|
|
144
|
-
<Label className="text-base">
|
|
144
|
+
<Label className="text-base">Access Rules</Label>
|
|
145
145
|
<p className="text-sm text-muted-foreground mt-1 mb-3">
|
|
146
|
-
Select
|
|
146
|
+
Select access rules to grant to this role. Access rules are
|
|
147
147
|
organized by plugin.
|
|
148
148
|
</p>
|
|
149
149
|
{isAdminRole && (
|
|
150
150
|
<Alert variant="info" className="mb-3">
|
|
151
151
|
<AlertDescription>
|
|
152
|
-
The administrator role has wildcard access to all
|
|
152
|
+
The administrator role has wildcard access to all access rules.
|
|
153
153
|
These cannot be modified.
|
|
154
154
|
</AlertDescription>
|
|
155
155
|
</Alert>
|
|
@@ -157,7 +157,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
157
157
|
{!isAdminRole && isUserRole && (
|
|
158
158
|
<Alert variant="info" className="mb-3">
|
|
159
159
|
<AlertDescription>
|
|
160
|
-
You cannot modify
|
|
160
|
+
You cannot modify access rules for a role you currently have.
|
|
161
161
|
This prevents accidental self-lockout from the system.
|
|
162
162
|
</AlertDescription>
|
|
163
163
|
</Alert>
|
|
@@ -165,10 +165,10 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
165
165
|
<div className="border rounded-lg">
|
|
166
166
|
<Accordion
|
|
167
167
|
type="multiple"
|
|
168
|
-
defaultValue={Object.keys(
|
|
168
|
+
defaultValue={Object.keys(accessRulesByPlugin)}
|
|
169
169
|
className="w-full"
|
|
170
170
|
>
|
|
171
|
-
{Object.entries(
|
|
171
|
+
{Object.entries(accessRulesByPlugin).map(([plugin, perms]) => (
|
|
172
172
|
<AccordionItem key={plugin} value={plugin}>
|
|
173
173
|
<AccordionTrigger className="px-4 hover:no-underline">
|
|
174
174
|
<div className="flex items-center justify-between flex-1 pr-2">
|
|
@@ -177,7 +177,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
177
177
|
</span>
|
|
178
178
|
<span className="text-xs text-muted-foreground">
|
|
179
179
|
{
|
|
180
|
-
perms.filter((p) =>
|
|
180
|
+
perms.filter((p) => selectedAccessRules.has(p.id))
|
|
181
181
|
.length
|
|
182
182
|
}{" "}
|
|
183
183
|
/ {perms.length} selected
|
|
@@ -187,15 +187,15 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
187
187
|
<AccordionContent className="px-4">
|
|
188
188
|
<div
|
|
189
189
|
className={`space-y-${
|
|
190
|
-
|
|
190
|
+
accessRulesDisabled ? "2" : "3"
|
|
191
191
|
} pt-2`}
|
|
192
192
|
>
|
|
193
193
|
{perms.map((perm) => {
|
|
194
194
|
const isAssigned =
|
|
195
|
-
isAdminRole ||
|
|
195
|
+
isAdminRole || selectedAccessRules.has(perm.id);
|
|
196
196
|
|
|
197
|
-
// Use view-style design when
|
|
198
|
-
if (
|
|
197
|
+
// Use view-style design when access rules are disabled
|
|
198
|
+
if (accessRulesDisabled) {
|
|
199
199
|
return (
|
|
200
200
|
<div
|
|
201
201
|
key={perm.id}
|
|
@@ -239,7 +239,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
239
239
|
);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
// Use editable checkbox design when
|
|
242
|
+
// Use editable checkbox design when access rules are editable
|
|
243
243
|
return (
|
|
244
244
|
<div
|
|
245
245
|
key={perm.id}
|
|
@@ -247,9 +247,9 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
247
247
|
>
|
|
248
248
|
<Checkbox
|
|
249
249
|
id={`perm-${perm.id}`}
|
|
250
|
-
checked={
|
|
250
|
+
checked={selectedAccessRules.has(perm.id)}
|
|
251
251
|
onCheckedChange={() =>
|
|
252
|
-
|
|
252
|
+
handleToggleAccessRule(perm.id)
|
|
253
253
|
}
|
|
254
254
|
className="mt-0.5"
|
|
255
255
|
/>
|
|
@@ -19,12 +19,12 @@ import { Plus, Edit, Trash2 } from "lucide-react";
|
|
|
19
19
|
import { useApi } from "@checkstack/frontend-api";
|
|
20
20
|
import { rpcApiRef } from "@checkstack/frontend-api";
|
|
21
21
|
import { AuthApi } from "@checkstack/auth-common";
|
|
22
|
-
import type { Role,
|
|
22
|
+
import type { Role, AccessRuleEntry } from "../api";
|
|
23
23
|
import { RoleDialog } from "./RoleDialog";
|
|
24
24
|
|
|
25
25
|
export interface RolesTabProps {
|
|
26
26
|
roles: Role[];
|
|
27
|
-
|
|
27
|
+
accessRulesList: AccessRuleEntry[];
|
|
28
28
|
userRoleIds: string[];
|
|
29
29
|
canReadRoles: boolean;
|
|
30
30
|
canCreateRoles: boolean;
|
|
@@ -35,7 +35,7 @@ export interface RolesTabProps {
|
|
|
35
35
|
|
|
36
36
|
export const RolesTab: React.FC<RolesTabProps> = ({
|
|
37
37
|
roles,
|
|
38
|
-
|
|
38
|
+
accessRulesList,
|
|
39
39
|
userRoleIds,
|
|
40
40
|
canReadRoles,
|
|
41
41
|
canCreateRoles,
|
|
@@ -65,7 +65,7 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
65
65
|
id?: string;
|
|
66
66
|
name: string;
|
|
67
67
|
description?: string;
|
|
68
|
-
|
|
68
|
+
accessRules: string[];
|
|
69
69
|
}) => {
|
|
70
70
|
try {
|
|
71
71
|
if (params.id) {
|
|
@@ -73,14 +73,14 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
73
73
|
id: params.id,
|
|
74
74
|
name: params.name,
|
|
75
75
|
description: params.description,
|
|
76
|
-
|
|
76
|
+
accessRules: params.accessRules,
|
|
77
77
|
});
|
|
78
78
|
toast.success("Role updated successfully");
|
|
79
79
|
} else {
|
|
80
80
|
await authClient.createRole({
|
|
81
81
|
name: params.name,
|
|
82
82
|
description: params.description,
|
|
83
|
-
|
|
83
|
+
accessRules: params.accessRules,
|
|
84
84
|
});
|
|
85
85
|
toast.success("Role created successfully");
|
|
86
86
|
}
|
|
@@ -128,7 +128,7 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
128
128
|
<TableHeader>
|
|
129
129
|
<TableRow>
|
|
130
130
|
<TableHead>Role</TableHead>
|
|
131
|
-
<TableHead>
|
|
131
|
+
<TableHead>Access Rules</TableHead>
|
|
132
132
|
<TableHead className="text-right">Actions</TableHead>
|
|
133
133
|
</TableRow>
|
|
134
134
|
</TableHeader>
|
|
@@ -159,7 +159,7 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
159
159
|
</TableCell>
|
|
160
160
|
<TableCell>
|
|
161
161
|
<span className="text-sm text-muted-foreground">
|
|
162
|
-
{role.
|
|
162
|
+
{role.accessRules?.length || 0} access rules
|
|
163
163
|
</span>
|
|
164
164
|
</TableCell>
|
|
165
165
|
<TableCell className="text-right">
|
|
@@ -192,7 +192,7 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
192
192
|
)
|
|
193
193
|
) : (
|
|
194
194
|
<p className="text-muted-foreground">
|
|
195
|
-
You don't have
|
|
195
|
+
You don't have access to view roles.
|
|
196
196
|
</p>
|
|
197
197
|
)}
|
|
198
198
|
</CardContent>
|
|
@@ -202,7 +202,7 @@ export const RolesTab: React.FC<RolesTabProps> = ({
|
|
|
202
202
|
open={roleDialogOpen}
|
|
203
203
|
onOpenChange={setRoleDialogOpen}
|
|
204
204
|
role={editingRole}
|
|
205
|
-
|
|
205
|
+
accessRulesList={accessRulesList}
|
|
206
206
|
isUserRole={editingRole ? userRoleIds.includes(editingRole.id) : false}
|
|
207
207
|
onSave={handleSaveRole}
|
|
208
208
|
/>
|
|
@@ -64,7 +64,7 @@ export const StrategiesTab: React.FC<StrategiesTabProps> = ({
|
|
|
64
64
|
setStrategyConfigs(configs);
|
|
65
65
|
}, [strategies]);
|
|
66
66
|
|
|
67
|
-
// Fetch registration data when we have
|
|
67
|
+
// Fetch registration data when we have access
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
if (!canManageRegistration) {
|
|
70
70
|
setLoadingRegistration(false);
|
|
@@ -206,7 +206,7 @@ export const StrategiesTab: React.FC<StrategiesTabProps> = ({
|
|
|
206
206
|
)
|
|
207
207
|
) : (
|
|
208
208
|
<p className="text-sm text-muted-foreground">
|
|
209
|
-
You don't have
|
|
209
|
+
You don't have access to manage registration settings.
|
|
210
210
|
</p>
|
|
211
211
|
)}
|
|
212
212
|
</CardContent>
|
|
@@ -268,7 +268,7 @@ export const StrategiesTab: React.FC<StrategiesTabProps> = ({
|
|
|
268
268
|
|
|
269
269
|
{!canManageStrategies && (
|
|
270
270
|
<p className="text-xs text-muted-foreground mt-4">
|
|
271
|
-
You don't have
|
|
271
|
+
You don't have access to manage strategies.
|
|
272
272
|
</p>
|
|
273
273
|
)}
|
|
274
274
|
</div>
|
|
@@ -26,11 +26,8 @@ import {
|
|
|
26
26
|
Settings,
|
|
27
27
|
Lock,
|
|
28
28
|
} from "lucide-react";
|
|
29
|
-
import { useApi, rpcApiRef,
|
|
30
|
-
import {
|
|
31
|
-
AuthApi,
|
|
32
|
-
permissions as authPermissions,
|
|
33
|
-
} from "@checkstack/auth-common";
|
|
29
|
+
import { useApi, rpcApiRef, accessApiRef } from "@checkstack/frontend-api";
|
|
30
|
+
import { AuthApi, authAccess } from "@checkstack/auth-common";
|
|
34
31
|
|
|
35
32
|
interface TeamAccess {
|
|
36
33
|
teamId: string;
|
|
@@ -72,12 +69,12 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
72
69
|
onChange,
|
|
73
70
|
}) => {
|
|
74
71
|
const rpcApi = useApi(rpcApiRef);
|
|
75
|
-
const
|
|
72
|
+
const accessApi = useApi(accessApiRef);
|
|
76
73
|
const authClient = rpcApi.forPlugin(AuthApi);
|
|
77
74
|
const toast = useToast();
|
|
78
75
|
|
|
79
|
-
const { allowed: canManageTeams } =
|
|
80
|
-
|
|
76
|
+
const { allowed: canManageTeams } = accessApi.useAccess(
|
|
77
|
+
authAccess.teams.manage
|
|
81
78
|
);
|
|
82
79
|
|
|
83
80
|
const [expanded, setExpanded] = useState(initialExpanded);
|
|
@@ -270,7 +267,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
270
267
|
Team Only
|
|
271
268
|
</Label>
|
|
272
269
|
<span className="text-xs text-muted-foreground">
|
|
273
|
-
(Bypass global
|
|
270
|
+
(Bypass global accesss)
|
|
274
271
|
</span>
|
|
275
272
|
</div>
|
|
276
273
|
<Toggle
|
|
@@ -320,7 +317,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
320
317
|
<div className="space-y-2">
|
|
321
318
|
{accessList.length === 0 ? (
|
|
322
319
|
<p className="text-sm text-muted-foreground text-center py-2">
|
|
323
|
-
No team restrictions. All users with
|
|
320
|
+
No team restrictions. All users with access can access.
|
|
324
321
|
</p>
|
|
325
322
|
) : (
|
|
326
323
|
accessList.map((access) => (
|
|
@@ -472,7 +469,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
472
469
|
</Label>
|
|
473
470
|
<p className="text-xs text-muted-foreground">
|
|
474
471
|
When enabled, only team members can access (global
|
|
475
|
-
|
|
472
|
+
access bypassed)
|
|
476
473
|
</p>
|
|
477
474
|
</div>
|
|
478
475
|
</div>
|
|
@@ -488,7 +485,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
488
485
|
{accessList.length === 0 ? (
|
|
489
486
|
<p className="text-sm text-muted-foreground text-center py-4 bg-muted/30 rounded-lg">
|
|
490
487
|
No team restrictions configured. All users with appropriate
|
|
491
|
-
|
|
488
|
+
access can view this resource.
|
|
492
489
|
</p>
|
|
493
490
|
) : (
|
|
494
491
|
<div className="border rounded-lg divide-y">
|
|
@@ -130,7 +130,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
130
130
|
<AlertDescription>
|
|
131
131
|
You cannot modify roles for your own account. This security
|
|
132
132
|
measure prevents accidental self-lockout from the system and
|
|
133
|
-
|
|
133
|
+
access elevation.
|
|
134
134
|
</AlertDescription>
|
|
135
135
|
</Alert>
|
|
136
136
|
{canReadUsers ? (
|
|
@@ -210,7 +210,7 @@ export const UsersTab: React.FC<UsersTabProps> = ({
|
|
|
210
210
|
)
|
|
211
211
|
) : (
|
|
212
212
|
<p className="text-muted-foreground">
|
|
213
|
-
You don't have
|
|
213
|
+
You don't have access to list users.
|
|
214
214
|
</p>
|
|
215
215
|
)}
|
|
216
216
|
</CardContent>
|
|
@@ -3,10 +3,10 @@ import { useAuthClient } from "../lib/auth-client";
|
|
|
3
3
|
import { rpcApiRef, useApi } from "@checkstack/frontend-api";
|
|
4
4
|
import { AuthApi } from "@checkstack/auth-common";
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const useAccessRules = () => {
|
|
7
7
|
const authClient = useAuthClient();
|
|
8
8
|
const { data: session, isPending: sessionPending } = authClient.useSession();
|
|
9
|
-
const [
|
|
9
|
+
const [accessRules, setAccessRules] = useState<string[]>([]);
|
|
10
10
|
const [loading, setLoading] = useState(true);
|
|
11
11
|
const rpcApi = useApi(rpcApiRef);
|
|
12
12
|
|
|
@@ -18,26 +18,26 @@ export const usePermissions = () => {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (!session?.user) {
|
|
21
|
-
|
|
21
|
+
setAccessRules([]);
|
|
22
22
|
setLoading(false);
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const fetchAccessRules = async () => {
|
|
27
27
|
try {
|
|
28
28
|
const authRpc = rpcApi.forPlugin(AuthApi);
|
|
29
|
-
const data = await authRpc.
|
|
30
|
-
if (Array.isArray(data.
|
|
31
|
-
|
|
29
|
+
const data = await authRpc.accessRules();
|
|
30
|
+
if (Array.isArray(data.accessRules)) {
|
|
31
|
+
setAccessRules(data.accessRules);
|
|
32
32
|
}
|
|
33
33
|
} catch (error) {
|
|
34
|
-
console.error("Failed to fetch
|
|
34
|
+
console.error("Failed to fetch access rules", error);
|
|
35
35
|
} finally {
|
|
36
36
|
setLoading(false);
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
|
|
39
|
+
fetchAccessRules();
|
|
40
40
|
}, [session?.user?.id, sessionPending, rpcApi]);
|
|
41
41
|
|
|
42
|
-
return {
|
|
42
|
+
return { accessRules, loading };
|
|
43
43
|
};
|
package/src/index.test.tsx
CHANGED
|
@@ -1,95 +1,141 @@
|
|
|
1
1
|
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
2
|
import { authPlugin } from "./index";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { accessApiRef } from "@checkstack/frontend-api";
|
|
4
|
+
import type { AccessRule } from "@checkstack/common";
|
|
5
|
+
import { useAccessRules } from "./hooks/useAccessRules";
|
|
5
6
|
|
|
6
|
-
// Mock the
|
|
7
|
-
mock.module("./hooks/
|
|
8
|
-
|
|
7
|
+
// Mock the useAccessRules hook
|
|
8
|
+
mock.module("./hooks/useAccessRules", () => ({
|
|
9
|
+
useAccessRules: mock(),
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Test access rule objects
|
|
13
|
+
const testReadAccess: AccessRule = {
|
|
14
|
+
id: "test.read",
|
|
15
|
+
resource: "test",
|
|
16
|
+
level: "read",
|
|
17
|
+
description: "Test read access",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const testManageAccess: AccessRule = {
|
|
21
|
+
id: "test.manage",
|
|
22
|
+
resource: "test",
|
|
23
|
+
level: "manage",
|
|
24
|
+
description: "Test manage access",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const otherAccess: AccessRule = {
|
|
28
|
+
id: "other.read",
|
|
29
|
+
resource: "other",
|
|
30
|
+
level: "read",
|
|
31
|
+
description: "Other read access",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("AuthAccessApi", () => {
|
|
35
|
+
let accessApi: {
|
|
36
|
+
useAccess: (p: AccessRule) => { loading: boolean; allowed: boolean };
|
|
14
37
|
};
|
|
15
38
|
|
|
16
39
|
beforeEach(() => {
|
|
17
|
-
const apiDef = authPlugin.apis?.find(
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
if (!apiDef) throw new Error("Permission API not found in plugin");
|
|
21
|
-
permissionApi = apiDef.factory({ get: () => ({}) } as any) as any;
|
|
40
|
+
const apiDef = authPlugin.apis?.find((a) => a.ref.id === accessApiRef.id);
|
|
41
|
+
if (!apiDef) throw new Error("Access API not found in plugin");
|
|
42
|
+
accessApi = apiDef.factory({ get: () => ({}) } as any) as any;
|
|
22
43
|
});
|
|
23
44
|
|
|
24
|
-
it("should return true if user has the
|
|
25
|
-
(
|
|
26
|
-
|
|
45
|
+
it("should return true if user has the access rule", () => {
|
|
46
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
47
|
+
accessRules: ["test.read"],
|
|
27
48
|
loading: false,
|
|
28
49
|
});
|
|
29
50
|
|
|
30
|
-
expect(
|
|
51
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
31
52
|
loading: false,
|
|
32
53
|
allowed: true,
|
|
33
54
|
});
|
|
34
55
|
});
|
|
35
56
|
|
|
36
|
-
it("should return false if user is missing the
|
|
37
|
-
(
|
|
38
|
-
|
|
57
|
+
it("should return false if user is missing the access rule", () => {
|
|
58
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
59
|
+
accessRules: ["other.read"],
|
|
39
60
|
loading: false,
|
|
40
61
|
});
|
|
41
62
|
|
|
42
|
-
expect(
|
|
63
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
43
64
|
loading: false,
|
|
44
65
|
allowed: false,
|
|
45
66
|
});
|
|
46
67
|
});
|
|
47
68
|
|
|
48
|
-
it("should return false if no session data (no
|
|
49
|
-
(
|
|
50
|
-
|
|
69
|
+
it("should return false if no session data (no access rules)", () => {
|
|
70
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
71
|
+
accessRules: [],
|
|
51
72
|
loading: false,
|
|
52
73
|
});
|
|
53
74
|
|
|
54
|
-
expect(
|
|
75
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
55
76
|
loading: false,
|
|
56
77
|
allowed: false,
|
|
57
78
|
});
|
|
58
79
|
});
|
|
59
80
|
|
|
60
|
-
it("should return false if no user
|
|
61
|
-
(
|
|
62
|
-
|
|
81
|
+
it("should return false if no user access rules (empty array)", () => {
|
|
82
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
83
|
+
accessRules: [],
|
|
63
84
|
loading: false,
|
|
64
85
|
});
|
|
65
86
|
|
|
66
|
-
expect(
|
|
87
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
67
88
|
loading: false,
|
|
68
89
|
allowed: false,
|
|
69
90
|
});
|
|
70
91
|
});
|
|
71
92
|
|
|
72
|
-
it("should return true if user has the wildcard
|
|
73
|
-
(
|
|
74
|
-
|
|
93
|
+
it("should return true if user has the wildcard access rule", () => {
|
|
94
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
95
|
+
accessRules: ["*"],
|
|
75
96
|
loading: false,
|
|
76
97
|
});
|
|
77
98
|
|
|
78
|
-
expect(
|
|
99
|
+
expect(accessApi.useAccess(otherAccess)).toEqual({
|
|
79
100
|
loading: false,
|
|
80
101
|
allowed: true,
|
|
81
102
|
});
|
|
82
103
|
});
|
|
83
104
|
|
|
84
|
-
it("should return
|
|
85
|
-
(
|
|
86
|
-
|
|
105
|
+
it("should return true if user has manage access for a manage check", () => {
|
|
106
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
107
|
+
accessRules: ["test.manage"],
|
|
108
|
+
loading: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(accessApi.useAccess(testManageAccess)).toEqual({
|
|
112
|
+
loading: false,
|
|
113
|
+
allowed: true,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return loading state if access rules are loading", () => {
|
|
118
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
119
|
+
accessRules: [],
|
|
87
120
|
loading: true,
|
|
88
121
|
});
|
|
89
122
|
|
|
90
|
-
expect(
|
|
123
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
91
124
|
loading: true,
|
|
92
125
|
allowed: false,
|
|
93
126
|
});
|
|
94
127
|
});
|
|
128
|
+
|
|
129
|
+
it("should return true if user has manage access for a read check", () => {
|
|
130
|
+
(useAccessRules as ReturnType<typeof mock>).mockReturnValue({
|
|
131
|
+
accessRules: ["test.manage"],
|
|
132
|
+
loading: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// User has test.manage, which implies test.read
|
|
136
|
+
expect(accessApi.useAccess(testReadAccess)).toEqual({
|
|
137
|
+
loading: false,
|
|
138
|
+
allowed: true,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
95
141
|
});
|
package/src/index.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
3
|
ApiRef,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
accessApiRef,
|
|
5
|
+
AccessApi,
|
|
6
6
|
createFrontendPlugin,
|
|
7
7
|
createSlotExtension,
|
|
8
8
|
NavbarRightSlot,
|
|
@@ -22,71 +22,52 @@ import { ChangePasswordPage } from "./components/ChangePasswordPage";
|
|
|
22
22
|
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
23
23
|
import { getAuthClientLazy } from "./lib/auth-client";
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { useAccessRules } from "./hooks/useAccessRules";
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import type { AccessRule } from "@checkstack/common";
|
|
28
28
|
import { useNavigate } from "react-router-dom";
|
|
29
29
|
import { Settings2, Key } from "lucide-react";
|
|
30
30
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
31
31
|
import { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
32
32
|
import { AuthSettingsPage } from "./components/AuthSettingsPage";
|
|
33
33
|
import {
|
|
34
|
-
|
|
34
|
+
authAccess,
|
|
35
35
|
authRoutes,
|
|
36
36
|
pluginMetadata,
|
|
37
37
|
} from "@checkstack/auth-common";
|
|
38
38
|
import { resolveRoute } from "@checkstack/common";
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// If no user, or user has no permissions, return false
|
|
49
|
-
if (!permissions || permissions.length === 0) {
|
|
50
|
-
return { loading: false, allowed: false };
|
|
51
|
-
}
|
|
52
|
-
const allowed =
|
|
53
|
-
permissions.includes("*") || permissions.includes(permission);
|
|
54
|
-
return { loading: false, allowed };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
useResourcePermission(
|
|
58
|
-
resource: string,
|
|
59
|
-
action: PermissionAction
|
|
60
|
-
): { loading: boolean; allowed: boolean } {
|
|
61
|
-
const { permissions, loading } = usePermissions();
|
|
40
|
+
/**
|
|
41
|
+
* Unified access API implementation.
|
|
42
|
+
* Uses AccessRule objects for access checks.
|
|
43
|
+
*/
|
|
44
|
+
class AuthAccessApi implements AccessApi {
|
|
45
|
+
useAccess(accessRule: AccessRule): { loading: boolean; allowed: boolean } {
|
|
46
|
+
const { accessRules, loading } = useAccessRules();
|
|
62
47
|
|
|
63
48
|
if (loading) {
|
|
64
49
|
return { loading: true, allowed: false };
|
|
65
50
|
}
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
// If no user, or user has no access rules, return false
|
|
53
|
+
if (!accessRules || accessRules.length === 0) {
|
|
68
54
|
return { loading: false, allowed: false };
|
|
69
55
|
}
|
|
70
56
|
|
|
71
|
-
const
|
|
72
|
-
const hasResourceManage = permissions.includes(`${resource}.manage`);
|
|
73
|
-
const hasSpecificPermission = permissions.includes(`${resource}.${action}`);
|
|
57
|
+
const accessRuleId = accessRule.id;
|
|
74
58
|
|
|
75
|
-
// manage implies read
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
hasResourceManage ||
|
|
79
|
-
(action === "read" && hasResourceManage) ||
|
|
80
|
-
hasSpecificPermission;
|
|
59
|
+
// Check wildcard, exact match, or manage implies read
|
|
60
|
+
const isWildcard = accessRules.includes("*");
|
|
61
|
+
const hasExact = accessRules.includes(accessRuleId);
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
// For read actions, also check if user has manage access for the same resource
|
|
64
|
+
const hasManage =
|
|
65
|
+
accessRule.level === "read"
|
|
66
|
+
? accessRules.includes(`${accessRule.resource}.manage`)
|
|
67
|
+
: false;
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
loading:
|
|
87
|
-
allowed: boolean;
|
|
88
|
-
} {
|
|
89
|
-
return this.useResourcePermission(resource, "manage");
|
|
69
|
+
const allowed = isWildcard || hasExact || hasManage;
|
|
70
|
+
return { loading: false, allowed };
|
|
90
71
|
}
|
|
91
72
|
}
|
|
92
73
|
|
|
@@ -180,8 +161,8 @@ export const authPlugin = createFrontendPlugin({
|
|
|
180
161
|
factory: () => new BetterAuthApi(),
|
|
181
162
|
},
|
|
182
163
|
{
|
|
183
|
-
ref:
|
|
184
|
-
factory: () => new
|
|
164
|
+
ref: accessApiRef as ApiRef<unknown>,
|
|
165
|
+
factory: () => new AuthAccessApi(),
|
|
185
166
|
},
|
|
186
167
|
],
|
|
187
168
|
routes: [
|
|
@@ -222,12 +203,9 @@ export const authPlugin = createFrontendPlugin({
|
|
|
222
203
|
},
|
|
223
204
|
createSlotExtension(UserMenuItemsSlot, {
|
|
224
205
|
id: "auth.user-menu.settings",
|
|
225
|
-
component: ({
|
|
206
|
+
component: ({ accessRules: userPerms }: UserMenuItemsContext) => {
|
|
226
207
|
const navigate = useNavigate();
|
|
227
|
-
const qualifiedId =
|
|
228
|
-
pluginMetadata,
|
|
229
|
-
authPermissions.strategiesManage
|
|
230
|
-
);
|
|
208
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${authAccess.strategies.id}`;
|
|
231
209
|
const canManage =
|
|
232
210
|
userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
233
211
|
|