@checkstack/auth-frontend 0.1.0 → 0.3.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 +155 -0
- package/package.json +1 -1
- package/src/api.ts +2 -2
- package/src/components/ApplicationsTab.tsx +89 -90
- package/src/components/AuthSettingsPage.tsx +87 -100
- package/src/components/LoginPage.tsx +15 -22
- package/src/components/RegisterPage.tsx +17 -28
- package/src/components/RoleDialog.tsx +49 -53
- package/src/components/RolesTab.tsx +60 -47
- package/src/components/StrategiesTab.tsx +90 -96
- package/src/components/TeamAccessEditor.tsx +131 -123
- package/src/components/TeamsTab.tsx +180 -162
- package/src/components/UsersTab.tsx +43 -32
- package/src/hooks/useAccessRules.ts +32 -0
- package/src/hooks/useEnabledStrategies.ts +10 -41
- package/src/index.test.tsx +83 -37
- package/src/index.tsx +29 -51
- package/src/lib/auth-client.ts +17 -22
- package/src/hooks/usePermissions.ts +0 -43
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Card,
|
|
4
4
|
CardHeader,
|
|
@@ -26,11 +26,12 @@ import {
|
|
|
26
26
|
Settings,
|
|
27
27
|
Lock,
|
|
28
28
|
} from "lucide-react";
|
|
29
|
-
import { useApi, rpcApiRef, permissionApiRef } from "@checkstack/frontend-api";
|
|
30
29
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
useApi,
|
|
31
|
+
usePluginClient,
|
|
32
|
+
accessApiRef,
|
|
33
|
+
} from "@checkstack/frontend-api";
|
|
34
|
+
import { AuthApi, authAccess } from "@checkstack/auth-common";
|
|
34
35
|
|
|
35
36
|
interface TeamAccess {
|
|
36
37
|
teamId: string;
|
|
@@ -71,145 +72,152 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
71
72
|
compact = false,
|
|
72
73
|
onChange,
|
|
73
74
|
}) => {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const authClient = rpcApi.forPlugin(AuthApi);
|
|
75
|
+
const accessApi = useApi(accessApiRef);
|
|
76
|
+
const authClient = usePluginClient(AuthApi);
|
|
77
77
|
const toast = useToast();
|
|
78
78
|
|
|
79
|
-
const { allowed: canManageTeams } =
|
|
80
|
-
|
|
79
|
+
const { allowed: canManageTeams } = accessApi.useAccess(
|
|
80
|
+
authAccess.teams.manage
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
const [expanded, setExpanded] = useState(initialExpanded);
|
|
84
|
-
const [loading, setLoading] = useState(false);
|
|
85
|
-
const [accessList, setAccessList] = useState<TeamAccess[]>([]);
|
|
86
|
-
const [teams, setTeams] = useState<Team[]>([]);
|
|
87
84
|
const [selectedTeamId, setSelectedTeamId] = useState("");
|
|
88
|
-
const [adding, setAdding] = useState(false);
|
|
89
|
-
const [teamOnly, setTeamOnly] = useState(false);
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
setTeams(teamsData);
|
|
101
|
-
setTeamOnly(settingsData.teamOnly);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
toast.error(
|
|
104
|
-
error instanceof Error ? error.message : "Failed to load team access"
|
|
105
|
-
);
|
|
106
|
-
} finally {
|
|
107
|
-
setLoading(false);
|
|
108
|
-
}
|
|
109
|
-
}, [authClient, resourceType, resourceId, toast]);
|
|
86
|
+
// Query: Team access for this resource
|
|
87
|
+
const {
|
|
88
|
+
data: accessList = [],
|
|
89
|
+
isLoading: accessLoading,
|
|
90
|
+
refetch: refetchAccess,
|
|
91
|
+
} = authClient.getResourceTeamAccess.useQuery(
|
|
92
|
+
{ resourceType, resourceId },
|
|
93
|
+
{ enabled: expanded && !!resourceId }
|
|
94
|
+
);
|
|
110
95
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}, [expanded, resourceId, loadData]);
|
|
96
|
+
// Query: All teams
|
|
97
|
+
const { data: teams = [], isLoading: teamsLoading } =
|
|
98
|
+
authClient.getTeams.useQuery({}, { enabled: expanded && !!resourceId });
|
|
116
99
|
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
// Query: Resource access settings (teamOnly flag)
|
|
101
|
+
const {
|
|
102
|
+
data: settings,
|
|
103
|
+
isLoading: settingsLoading,
|
|
104
|
+
refetch: refetchSettings,
|
|
105
|
+
} = authClient.getResourceAccessSettings.useQuery(
|
|
106
|
+
{ resourceType, resourceId },
|
|
107
|
+
{ enabled: expanded && !!resourceId }
|
|
108
|
+
);
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
canManage: false,
|
|
128
|
-
});
|
|
129
|
-
toast.success("Team access granted");
|
|
110
|
+
const loading = accessLoading || teamsLoading || settingsLoading;
|
|
111
|
+
const teamOnly = settings?.teamOnly ?? false;
|
|
112
|
+
|
|
113
|
+
// Mutations
|
|
114
|
+
const setAccessMutation = authClient.setResourceTeamAccess.useMutation({
|
|
115
|
+
onSuccess: () => {
|
|
116
|
+
toast.success("Team access updated");
|
|
130
117
|
setSelectedTeamId("");
|
|
131
|
-
|
|
118
|
+
void refetchAccess();
|
|
132
119
|
onChange?.();
|
|
133
|
-
}
|
|
120
|
+
},
|
|
121
|
+
onError: (error) => {
|
|
134
122
|
toast.error(
|
|
135
|
-
error instanceof Error ? error.message : "Failed to
|
|
123
|
+
error instanceof Error ? error.message : "Failed to update access"
|
|
136
124
|
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
141
127
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
await authClient.setResourceTeamAccess({
|
|
148
|
-
resourceType,
|
|
149
|
-
resourceId,
|
|
150
|
-
teamId,
|
|
151
|
-
...updates,
|
|
152
|
-
});
|
|
153
|
-
await loadData();
|
|
128
|
+
const removeAccessMutation = authClient.removeResourceTeamAccess.useMutation({
|
|
129
|
+
onSuccess: () => {
|
|
130
|
+
toast.success("Team access removed");
|
|
131
|
+
void refetchAccess();
|
|
154
132
|
onChange?.();
|
|
155
|
-
}
|
|
133
|
+
},
|
|
134
|
+
onError: (error) => {
|
|
156
135
|
toast.error(
|
|
157
|
-
error instanceof Error ? error.message : "Failed to
|
|
136
|
+
error instanceof Error ? error.message : "Failed to remove access"
|
|
158
137
|
);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
138
|
+
},
|
|
139
|
+
});
|
|
161
140
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
resourceType,
|
|
166
|
-
resourceId,
|
|
167
|
-
teamOnly: newTeamOnly,
|
|
168
|
-
});
|
|
169
|
-
setTeamOnly(newTeamOnly);
|
|
141
|
+
const setSettingsMutation = authClient.setResourceAccessSettings.useMutation({
|
|
142
|
+
onSuccess: () => {
|
|
143
|
+
void refetchSettings();
|
|
170
144
|
onChange?.();
|
|
171
|
-
}
|
|
145
|
+
},
|
|
146
|
+
onError: (error) => {
|
|
172
147
|
toast.error(
|
|
173
148
|
error instanceof Error ? error.message : "Failed to update settings"
|
|
174
149
|
);
|
|
175
|
-
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Reset teamOnly when all teams removed
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
// This is handled reactively when mutations complete
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const handleAddTeam = () => {
|
|
159
|
+
if (!selectedTeamId) return;
|
|
160
|
+
setAccessMutation.mutate({
|
|
161
|
+
resourceType,
|
|
162
|
+
resourceId,
|
|
163
|
+
teamId: selectedTeamId,
|
|
164
|
+
canRead: true,
|
|
165
|
+
canManage: false,
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleUpdateAccess = (
|
|
170
|
+
teamId: string,
|
|
171
|
+
updates: { canRead?: boolean; canManage?: boolean }
|
|
172
|
+
) => {
|
|
173
|
+
const currentAccess = (accessList as TeamAccess[]).find(
|
|
174
|
+
(a) => a.teamId === teamId
|
|
175
|
+
);
|
|
176
|
+
setAccessMutation.mutate({
|
|
177
|
+
resourceType,
|
|
178
|
+
resourceId,
|
|
179
|
+
teamId,
|
|
180
|
+
canRead: updates.canRead ?? currentAccess?.canRead ?? true,
|
|
181
|
+
canManage: updates.canManage ?? currentAccess?.canManage ?? false,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleUpdateSettings = (newTeamOnly: boolean) => {
|
|
186
|
+
setSettingsMutation.mutate({
|
|
187
|
+
resourceType,
|
|
188
|
+
resourceId,
|
|
189
|
+
teamOnly: newTeamOnly,
|
|
190
|
+
});
|
|
176
191
|
};
|
|
177
192
|
|
|
178
|
-
const handleRemoveAccess =
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
const handleRemoveAccess = (teamId: string) => {
|
|
194
|
+
removeAccessMutation.mutate({
|
|
195
|
+
resourceType,
|
|
196
|
+
resourceId,
|
|
197
|
+
teamId,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Check if this was the last team - if so, reset teamOnly
|
|
201
|
+
const remainingTeams = (accessList as TeamAccess[]).filter(
|
|
202
|
+
(a) => a.teamId !== teamId
|
|
203
|
+
);
|
|
204
|
+
if (remainingTeams.length === 0 && teamOnly) {
|
|
205
|
+
setSettingsMutation.mutate({
|
|
181
206
|
resourceType,
|
|
182
207
|
resourceId,
|
|
183
|
-
|
|
208
|
+
teamOnly: false,
|
|
184
209
|
});
|
|
185
|
-
|
|
186
|
-
// Check if this was the last team - if so, clear settings too
|
|
187
|
-
const remainingTeams = accessList.filter((a) => a.teamId !== teamId);
|
|
188
|
-
if (remainingTeams.length === 0 && teamOnly) {
|
|
189
|
-
// Reset teamOnly when no teams have access
|
|
190
|
-
await authClient.setResourceAccessSettings({
|
|
191
|
-
resourceType,
|
|
192
|
-
resourceId,
|
|
193
|
-
teamOnly: false,
|
|
194
|
-
});
|
|
195
|
-
setTeamOnly(false);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
toast.success("Team access removed");
|
|
199
|
-
await loadData();
|
|
200
|
-
onChange?.();
|
|
201
|
-
} catch (error) {
|
|
202
|
-
toast.error(
|
|
203
|
-
error instanceof Error ? error.message : "Failed to remove access"
|
|
204
|
-
);
|
|
205
210
|
}
|
|
206
211
|
};
|
|
207
212
|
|
|
208
213
|
// Get teams that don't already have access
|
|
209
|
-
const
|
|
210
|
-
|
|
214
|
+
const typedAccessList = accessList as TeamAccess[];
|
|
215
|
+
const availableTeams = (teams as Team[]).filter(
|
|
216
|
+
(t) => !typedAccessList.some((a) => a.teamId === t.id)
|
|
211
217
|
);
|
|
212
218
|
|
|
219
|
+
const adding = setAccessMutation.isPending;
|
|
220
|
+
|
|
213
221
|
// Compact summary mode
|
|
214
222
|
if (!expanded) {
|
|
215
223
|
return (
|
|
@@ -223,9 +231,9 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
223
231
|
>
|
|
224
232
|
<Users2 className="h-4 w-4" />
|
|
225
233
|
<span>Team Access</span>
|
|
226
|
-
{
|
|
234
|
+
{typedAccessList.length > 0 && (
|
|
227
235
|
<Badge variant="secondary" className="ml-1">
|
|
228
|
-
{
|
|
236
|
+
{typedAccessList.length}
|
|
229
237
|
</Badge>
|
|
230
238
|
)}
|
|
231
239
|
</Button>
|
|
@@ -259,7 +267,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
259
267
|
) : (
|
|
260
268
|
<>
|
|
261
269
|
{/* Resource-level Team Only setting */}
|
|
262
|
-
{
|
|
270
|
+
{typedAccessList.length > 0 && (
|
|
263
271
|
<div className="flex items-center justify-between p-2 bg-muted/30 rounded-md">
|
|
264
272
|
<div className="flex items-center gap-2">
|
|
265
273
|
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
@@ -270,7 +278,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
270
278
|
Team Only
|
|
271
279
|
</Label>
|
|
272
280
|
<span className="text-xs text-muted-foreground">
|
|
273
|
-
(Bypass global
|
|
281
|
+
(Bypass global accesss)
|
|
274
282
|
</span>
|
|
275
283
|
</div>
|
|
276
284
|
<Toggle
|
|
@@ -318,12 +326,12 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
318
326
|
|
|
319
327
|
{/* Access list */}
|
|
320
328
|
<div className="space-y-2">
|
|
321
|
-
{
|
|
329
|
+
{typedAccessList.length === 0 ? (
|
|
322
330
|
<p className="text-sm text-muted-foreground text-center py-2">
|
|
323
|
-
No team restrictions. All users with
|
|
331
|
+
No team restrictions. All users with access can access.
|
|
324
332
|
</p>
|
|
325
333
|
) : (
|
|
326
|
-
|
|
334
|
+
typedAccessList.map((access) => (
|
|
327
335
|
<div
|
|
328
336
|
key={access.teamId}
|
|
329
337
|
className="flex items-center justify-between p-2 bg-muted/50 rounded-md"
|
|
@@ -462,7 +470,7 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
462
470
|
)}
|
|
463
471
|
|
|
464
472
|
{/* Resource-level Team Only setting */}
|
|
465
|
-
{
|
|
473
|
+
{typedAccessList.length > 0 && (
|
|
466
474
|
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
|
|
467
475
|
<div className="flex items-center gap-2">
|
|
468
476
|
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
@@ -471,8 +479,8 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
471
479
|
Team Only Mode
|
|
472
480
|
</Label>
|
|
473
481
|
<p className="text-xs text-muted-foreground">
|
|
474
|
-
When enabled, only team members can access (global
|
|
475
|
-
|
|
482
|
+
When enabled, only team members can access (global access
|
|
483
|
+
bypassed)
|
|
476
484
|
</p>
|
|
477
485
|
</div>
|
|
478
486
|
</div>
|
|
@@ -485,14 +493,14 @@ export const TeamAccessEditor: React.FC<TeamAccessEditorProps> = ({
|
|
|
485
493
|
)}
|
|
486
494
|
|
|
487
495
|
{/* Access list */}
|
|
488
|
-
{
|
|
496
|
+
{typedAccessList.length === 0 ? (
|
|
489
497
|
<p className="text-sm text-muted-foreground text-center py-4 bg-muted/30 rounded-lg">
|
|
490
498
|
No team restrictions configured. All users with appropriate
|
|
491
|
-
|
|
499
|
+
access can view this resource.
|
|
492
500
|
</p>
|
|
493
501
|
) : (
|
|
494
502
|
<div className="border rounded-lg divide-y">
|
|
495
|
-
{
|
|
503
|
+
{typedAccessList.map((access) => (
|
|
496
504
|
<div
|
|
497
505
|
key={access.teamId}
|
|
498
506
|
className="p-3 flex items-center justify-between"
|