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