@checkstack/auth-frontend 0.0.4 → 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 +172 -0
- package/package.json +1 -1
- package/src/api.ts +2 -2
- package/src/components/ApplicationsTab.tsx +10 -3
- package/src/components/AuthSettingsPage.tsx +66 -64
- package/src/components/LoginPage.tsx +8 -7
- package/src/components/RegisterPage.tsx +5 -8
- package/src/components/RoleDialog.tsx +43 -37
- package/src/components/RolesTab.tsx +10 -10
- package/src/components/StrategiesTab.tsx +3 -3
- package/src/components/TeamAccessEditor.tsx +557 -0
- package/src/components/TeamsTab.tsx +569 -0
- 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 +33 -51
|
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
Dialog,
|
|
4
4
|
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
5
6
|
DialogHeader,
|
|
6
7
|
DialogTitle,
|
|
7
8
|
DialogFooter,
|
|
@@ -18,20 +19,20 @@ import {
|
|
|
18
19
|
AlertDescription,
|
|
19
20
|
} from "@checkstack/ui";
|
|
20
21
|
import { Check } from "lucide-react";
|
|
21
|
-
import type { Role,
|
|
22
|
+
import type { Role, AccessRuleEntry } from "../api";
|
|
22
23
|
|
|
23
24
|
interface RoleDialogProps {
|
|
24
25
|
open: boolean;
|
|
25
26
|
onOpenChange: (open: boolean) => void;
|
|
26
27
|
role?: Role;
|
|
27
|
-
|
|
28
|
-
/** Whether current user has this role (prevents
|
|
28
|
+
accessRulesList: AccessRuleEntry[];
|
|
29
|
+
/** Whether current user has this role (prevents access elevation) */
|
|
29
30
|
isUserRole?: boolean;
|
|
30
31
|
onSave: (params: {
|
|
31
32
|
id?: string;
|
|
32
33
|
name: string;
|
|
33
34
|
description?: string;
|
|
34
|
-
|
|
35
|
+
accessRules: string[];
|
|
35
36
|
}) => Promise<void>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -39,14 +40,14 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
39
40
|
open,
|
|
40
41
|
onOpenChange,
|
|
41
42
|
role,
|
|
42
|
-
|
|
43
|
+
accessRulesList,
|
|
43
44
|
isUserRole = false,
|
|
44
45
|
onSave,
|
|
45
46
|
}) => {
|
|
46
47
|
const [name, setName] = useState(role?.name || "");
|
|
47
48
|
const [description, setDescription] = useState(role?.description || "");
|
|
48
|
-
const [
|
|
49
|
-
new Set(role?.
|
|
49
|
+
const [selectedAccessRules, setSelectedAccessRules] = useState<Set<string>>(
|
|
50
|
+
new Set(role?.accessRules || [])
|
|
50
51
|
);
|
|
51
52
|
const [saving, setSaving] = useState(false);
|
|
52
53
|
|
|
@@ -54,32 +55,32 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
54
55
|
React.useEffect(() => {
|
|
55
56
|
setName(role?.name || "");
|
|
56
57
|
setDescription(role?.description || "");
|
|
57
|
-
|
|
58
|
+
setSelectedAccessRules(new Set(role?.accessRules || []));
|
|
58
59
|
}, [role]);
|
|
59
60
|
|
|
60
61
|
const isEditing = !!role;
|
|
61
62
|
const isAdminRole = role?.id === "admin";
|
|
62
|
-
// Disable
|
|
63
|
-
const
|
|
63
|
+
// Disable access rules for admin (wildcard) or user's own roles (prevent elevation)
|
|
64
|
+
const accessRulesDisabled = isAdminRole || isUserRole;
|
|
64
65
|
|
|
65
|
-
// Group
|
|
66
|
-
const
|
|
67
|
-
for (const perm of
|
|
66
|
+
// Group access rules by plugin
|
|
67
|
+
const accessRulesByPlugin: Record<string, AccessRuleEntry[]> = {};
|
|
68
|
+
for (const perm of accessRulesList) {
|
|
68
69
|
const [plugin] = perm.id.split(".");
|
|
69
|
-
if (!
|
|
70
|
-
|
|
70
|
+
if (!accessRulesByPlugin[plugin]) {
|
|
71
|
+
accessRulesByPlugin[plugin] = [];
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
accessRulesByPlugin[plugin].push(perm);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const
|
|
76
|
-
const newSelected = new Set(
|
|
77
|
-
if (newSelected.has(
|
|
78
|
-
newSelected.delete(
|
|
76
|
+
const handleToggleAccessRule = (accessRuleId: string) => {
|
|
77
|
+
const newSelected = new Set(selectedAccessRules);
|
|
78
|
+
if (newSelected.has(accessRuleId)) {
|
|
79
|
+
newSelected.delete(accessRuleId);
|
|
79
80
|
} else {
|
|
80
|
-
newSelected.add(
|
|
81
|
+
newSelected.add(accessRuleId);
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
+
setSelectedAccessRules(newSelected);
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
const handleSave = async () => {
|
|
@@ -89,7 +90,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
89
90
|
...(isEditing && { id: role.id }),
|
|
90
91
|
name,
|
|
91
92
|
description: description || undefined,
|
|
92
|
-
|
|
93
|
+
accessRules: [...selectedAccessRules],
|
|
93
94
|
});
|
|
94
95
|
onOpenChange(false);
|
|
95
96
|
} catch (error) {
|
|
@@ -111,6 +112,11 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
111
112
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
112
113
|
<DialogHeader>
|
|
113
114
|
<DialogTitle>{isEditing ? "Edit Role" : "Create Role"}</DialogTitle>
|
|
115
|
+
<DialogDescription className="sr-only">
|
|
116
|
+
{isEditing
|
|
117
|
+
? "Modify the settings and access rules for this role"
|
|
118
|
+
: "Create a new role with specific access rules"}
|
|
119
|
+
</DialogDescription>
|
|
114
120
|
</DialogHeader>
|
|
115
121
|
|
|
116
122
|
<div className="space-y-4">
|
|
@@ -135,15 +141,15 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
135
141
|
</div>
|
|
136
142
|
|
|
137
143
|
<div>
|
|
138
|
-
<Label className="text-base">
|
|
144
|
+
<Label className="text-base">Access Rules</Label>
|
|
139
145
|
<p className="text-sm text-muted-foreground mt-1 mb-3">
|
|
140
|
-
Select
|
|
146
|
+
Select access rules to grant to this role. Access rules are
|
|
141
147
|
organized by plugin.
|
|
142
148
|
</p>
|
|
143
149
|
{isAdminRole && (
|
|
144
150
|
<Alert variant="info" className="mb-3">
|
|
145
151
|
<AlertDescription>
|
|
146
|
-
The administrator role has wildcard access to all
|
|
152
|
+
The administrator role has wildcard access to all access rules.
|
|
147
153
|
These cannot be modified.
|
|
148
154
|
</AlertDescription>
|
|
149
155
|
</Alert>
|
|
@@ -151,7 +157,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
151
157
|
{!isAdminRole && isUserRole && (
|
|
152
158
|
<Alert variant="info" className="mb-3">
|
|
153
159
|
<AlertDescription>
|
|
154
|
-
You cannot modify
|
|
160
|
+
You cannot modify access rules for a role you currently have.
|
|
155
161
|
This prevents accidental self-lockout from the system.
|
|
156
162
|
</AlertDescription>
|
|
157
163
|
</Alert>
|
|
@@ -159,10 +165,10 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
159
165
|
<div className="border rounded-lg">
|
|
160
166
|
<Accordion
|
|
161
167
|
type="multiple"
|
|
162
|
-
defaultValue={Object.keys(
|
|
168
|
+
defaultValue={Object.keys(accessRulesByPlugin)}
|
|
163
169
|
className="w-full"
|
|
164
170
|
>
|
|
165
|
-
{Object.entries(
|
|
171
|
+
{Object.entries(accessRulesByPlugin).map(([plugin, perms]) => (
|
|
166
172
|
<AccordionItem key={plugin} value={plugin}>
|
|
167
173
|
<AccordionTrigger className="px-4 hover:no-underline">
|
|
168
174
|
<div className="flex items-center justify-between flex-1 pr-2">
|
|
@@ -171,7 +177,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
171
177
|
</span>
|
|
172
178
|
<span className="text-xs text-muted-foreground">
|
|
173
179
|
{
|
|
174
|
-
perms.filter((p) =>
|
|
180
|
+
perms.filter((p) => selectedAccessRules.has(p.id))
|
|
175
181
|
.length
|
|
176
182
|
}{" "}
|
|
177
183
|
/ {perms.length} selected
|
|
@@ -181,15 +187,15 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
181
187
|
<AccordionContent className="px-4">
|
|
182
188
|
<div
|
|
183
189
|
className={`space-y-${
|
|
184
|
-
|
|
190
|
+
accessRulesDisabled ? "2" : "3"
|
|
185
191
|
} pt-2`}
|
|
186
192
|
>
|
|
187
193
|
{perms.map((perm) => {
|
|
188
194
|
const isAssigned =
|
|
189
|
-
isAdminRole ||
|
|
195
|
+
isAdminRole || selectedAccessRules.has(perm.id);
|
|
190
196
|
|
|
191
|
-
// Use view-style design when
|
|
192
|
-
if (
|
|
197
|
+
// Use view-style design when access rules are disabled
|
|
198
|
+
if (accessRulesDisabled) {
|
|
193
199
|
return (
|
|
194
200
|
<div
|
|
195
201
|
key={perm.id}
|
|
@@ -233,7 +239,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
233
239
|
);
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
// Use editable checkbox design when
|
|
242
|
+
// Use editable checkbox design when access rules are editable
|
|
237
243
|
return (
|
|
238
244
|
<div
|
|
239
245
|
key={perm.id}
|
|
@@ -241,9 +247,9 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
|
|
|
241
247
|
>
|
|
242
248
|
<Checkbox
|
|
243
249
|
id={`perm-${perm.id}`}
|
|
244
|
-
checked={
|
|
250
|
+
checked={selectedAccessRules.has(perm.id)}
|
|
245
251
|
onCheckedChange={() =>
|
|
246
|
-
|
|
252
|
+
handleToggleAccessRule(perm.id)
|
|
247
253
|
}
|
|
248
254
|
className="mt-0.5"
|
|
249
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>
|