@tidecloak/ui-framework 0.0.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/README.md +377 -0
- package/dist/index.d.mts +2739 -0
- package/dist/index.d.ts +2739 -0
- package/dist/index.js +12869 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12703 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/components/common/ActionButton.tsx +234 -0
- package/src/components/common/EmptyState.tsx +140 -0
- package/src/components/common/LoadingSkeleton.tsx +121 -0
- package/src/components/common/RefreshButton.tsx +127 -0
- package/src/components/common/StatusBadge.tsx +177 -0
- package/src/components/common/index.ts +31 -0
- package/src/components/data-table/DataTable.tsx +201 -0
- package/src/components/data-table/PaginatedTable.tsx +247 -0
- package/src/components/data-table/index.ts +2 -0
- package/src/components/dialogs/CollapsibleSection.tsx +184 -0
- package/src/components/dialogs/ConfirmDialog.tsx +264 -0
- package/src/components/dialogs/DetailDialog.tsx +228 -0
- package/src/components/dialogs/index.ts +3 -0
- package/src/components/index.ts +5 -0
- package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
- package/src/components/pages/base/LogsPageBase.tsx +581 -0
- package/src/components/pages/base/RolesPageBase.tsx +1470 -0
- package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
- package/src/components/pages/base/UsersPageBase.tsx +843 -0
- package/src/components/pages/base/index.ts +58 -0
- package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
- package/src/components/pages/connected/LogsPage.tsx +267 -0
- package/src/components/pages/connected/RolesPage.tsx +525 -0
- package/src/components/pages/connected/TemplatesPage.tsx +181 -0
- package/src/components/pages/connected/UsersPage.tsx +237 -0
- package/src/components/pages/connected/index.ts +36 -0
- package/src/components/pages/index.ts +5 -0
- package/src/components/tabs/TabsView.tsx +300 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/ui/index.tsx +1001 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAutoRefresh.ts +119 -0
- package/src/hooks/usePagination.ts +152 -0
- package/src/hooks/useSelection.ts +81 -0
- package/src/index.ts +256 -0
- package/src/theme.ts +185 -0
- package/src/tide/index.ts +19 -0
- package/src/tide/tidePolicy.ts +270 -0
- package/src/types/index.ts +484 -0
- package/src/utils/index.ts +121 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolesPage - Create and manage roles with policy support
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <RolesPage
|
|
7
|
+
* adminAPI={AdminAPI}
|
|
8
|
+
* policyAPI={policyAPI}
|
|
9
|
+
* tideContext={tideContext}
|
|
10
|
+
* currentUsername={username}
|
|
11
|
+
* />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { AdminAPI as DefaultAdminAPI } from "@tidecloak/js";
|
|
17
|
+
import { RolesPageBase, type RolesPageBaseProps, type RoleItem, type RoleTypeConfig } from "../base";
|
|
18
|
+
import { type TemplateAPI } from "./TemplatesPage";
|
|
19
|
+
import { type PolicyApprovalData } from "./ApprovalsPage";
|
|
20
|
+
import { type PolicyLogsAPI } from "./LogsPage";
|
|
21
|
+
import {
|
|
22
|
+
loadTideLibs,
|
|
23
|
+
areTideLibsAvailable,
|
|
24
|
+
createTidePolicyHandler,
|
|
25
|
+
type TideContextMethods,
|
|
26
|
+
} from "../../../tide";
|
|
27
|
+
|
|
28
|
+
// Type for the AdminAPI instance
|
|
29
|
+
type AdminAPIInstance = {
|
|
30
|
+
setRealm?: (realm: string) => void;
|
|
31
|
+
// Realm roles
|
|
32
|
+
getRoles: () => Promise<any>;
|
|
33
|
+
createRole: (role: any) => Promise<any>;
|
|
34
|
+
updateRole: (roleName: string, role: any) => Promise<any>;
|
|
35
|
+
deleteRole: (roleName: string) => Promise<any>;
|
|
36
|
+
// Client roles
|
|
37
|
+
getClientRoles?: (clientId?: string) => Promise<any>;
|
|
38
|
+
createClientRole?: (role: any, clientId?: string) => Promise<any>;
|
|
39
|
+
updateClientRole?: (roleName: string, role: any, clientId?: string) => Promise<any>;
|
|
40
|
+
deleteClientRole?: (roleName: string, clientId?: string) => Promise<any>;
|
|
41
|
+
// Other
|
|
42
|
+
getTemplates?: () => Promise<any>;
|
|
43
|
+
getUsers?: () => Promise<any>;
|
|
44
|
+
getPendingChangeSets?: () => Promise<any>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface RolePolicy {
|
|
48
|
+
roleName: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
contractType: string;
|
|
51
|
+
approvalType: "implicit" | "explicit";
|
|
52
|
+
executionType: "public" | "private";
|
|
53
|
+
threshold: number;
|
|
54
|
+
templateId?: string;
|
|
55
|
+
templateParams?: Record<string, any>;
|
|
56
|
+
createdAt?: string;
|
|
57
|
+
updatedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Implement this to store role policies (or use createLocalStoragePolicyAPI for dev) */
|
|
61
|
+
export interface PolicyAPI {
|
|
62
|
+
getPolicy: (roleName: string) => Promise<RolePolicy | null>;
|
|
63
|
+
upsertPolicy: (policy: RolePolicy) => Promise<RolePolicy | void>;
|
|
64
|
+
deletePolicy?: (roleName: string) => Promise<void>;
|
|
65
|
+
_isLocalStorage?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const POLICY_STORAGE_KEY = "tidecloak_role_policies";
|
|
69
|
+
|
|
70
|
+
/** localStorage adapter for development */
|
|
71
|
+
export function createLocalStoragePolicyAPI(storageKey = POLICY_STORAGE_KEY): PolicyAPI {
|
|
72
|
+
const getAll = (): Record<string, RolePolicy> => {
|
|
73
|
+
try {
|
|
74
|
+
const data = localStorage.getItem(storageKey);
|
|
75
|
+
return data ? JSON.parse(data) : {};
|
|
76
|
+
} catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const saveAll = (policies: Record<string, RolePolicy>) => {
|
|
82
|
+
localStorage.setItem(storageKey, JSON.stringify(policies));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
_isLocalStorage: true as const,
|
|
87
|
+
|
|
88
|
+
getPolicy: async (roleName: string) => {
|
|
89
|
+
const policies = getAll();
|
|
90
|
+
return policies[roleName] || null;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
upsertPolicy: async (policy: RolePolicy) => {
|
|
94
|
+
const policies = getAll();
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const existing = policies[policy.roleName];
|
|
97
|
+
|
|
98
|
+
policies[policy.roleName] = {
|
|
99
|
+
...policy,
|
|
100
|
+
createdAt: existing?.createdAt || now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
saveAll(policies);
|
|
105
|
+
return policies[policy.roleName];
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
deletePolicy: async (roleName: string) => {
|
|
109
|
+
const policies = getAll();
|
|
110
|
+
delete policies[roleName];
|
|
111
|
+
saveAll(policies);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface RolesPageProps<T extends RoleItem = RoleItem>
|
|
117
|
+
extends Omit<
|
|
118
|
+
RolesPageBaseProps<T>,
|
|
119
|
+
| "fetchRoles"
|
|
120
|
+
| "fetchTemplates"
|
|
121
|
+
| "fetchUsers"
|
|
122
|
+
| "fetchPendingApprovals"
|
|
123
|
+
| "fetchRolePolicy"
|
|
124
|
+
| "onCreate"
|
|
125
|
+
| "onUpdate"
|
|
126
|
+
| "onDelete"
|
|
127
|
+
| "onCreatePolicy"
|
|
128
|
+
| "toast"
|
|
129
|
+
| "invalidateQueries"
|
|
130
|
+
| "components"
|
|
131
|
+
| "roleType"
|
|
132
|
+
> {
|
|
133
|
+
/** AdminAPI instance */
|
|
134
|
+
adminAPI?: AdminAPIInstance;
|
|
135
|
+
/** Template adapter for policy templates */
|
|
136
|
+
templateAPI?: TemplateAPI;
|
|
137
|
+
/** Policy storage adapter */
|
|
138
|
+
policyAPI?: PolicyAPI;
|
|
139
|
+
/** Policy logs adapter for activity tracking */
|
|
140
|
+
policyLogsAPI?: PolicyLogsAPI;
|
|
141
|
+
/** Tide context for enclave workflow (from useTideCloak hook) */
|
|
142
|
+
tideContext?: TideContextMethods;
|
|
143
|
+
/** Callback when Tide policy request is created */
|
|
144
|
+
onTideRequestCreated?: (request: any, roleName: string) => Promise<void>;
|
|
145
|
+
/** Realm name */
|
|
146
|
+
realm?: string;
|
|
147
|
+
/** Role type: 'realm', 'client', or 'all' (default: 'realm') */
|
|
148
|
+
roleType?: RoleTypeConfig;
|
|
149
|
+
/** Override fetch roles */
|
|
150
|
+
fetchRoles?: RolesPageBaseProps<T>["fetchRoles"];
|
|
151
|
+
/** Override create */
|
|
152
|
+
onCreate?: RolesPageBaseProps<T>["onCreate"];
|
|
153
|
+
/** Override update */
|
|
154
|
+
onUpdate?: RolesPageBaseProps<T>["onUpdate"];
|
|
155
|
+
/** Override delete */
|
|
156
|
+
onDelete?: RolesPageBaseProps<T>["onDelete"];
|
|
157
|
+
/** Override policy creation */
|
|
158
|
+
onCreatePolicy?: RolesPageBaseProps<T>["onCreatePolicy"];
|
|
159
|
+
/** Toast handler */
|
|
160
|
+
toast?: RolesPageBaseProps<T>["toast"];
|
|
161
|
+
/** Query invalidation */
|
|
162
|
+
invalidateQueries?: RolesPageBaseProps<T>["invalidateQueries"];
|
|
163
|
+
/** Custom components */
|
|
164
|
+
components?: RolesPageBaseProps<T>["components"];
|
|
165
|
+
/** Help text below description */
|
|
166
|
+
helpText?: RolesPageBaseProps<T>["helpText"];
|
|
167
|
+
/** Current username for logging */
|
|
168
|
+
currentUsername?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Warning banner for localStorage
|
|
172
|
+
function LocalStorageWarning({ type }: { type: "policy" | "template" | "both" }) {
|
|
173
|
+
const typeText = type === "both" ? "Policies and templates" : type === "policy" ? "Policies" : "Templates";
|
|
174
|
+
const apiText = type === "both" ? "PolicyAPI and TemplateAPI" : type === "policy" ? "PolicyAPI" : "TemplateAPI";
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
backgroundColor: "#fef3c7",
|
|
180
|
+
border: "1px solid #f59e0b",
|
|
181
|
+
borderRadius: "0.375rem",
|
|
182
|
+
padding: "0.75rem 1rem",
|
|
183
|
+
marginBottom: "1rem",
|
|
184
|
+
display: "flex",
|
|
185
|
+
alignItems: "flex-start",
|
|
186
|
+
gap: "0.5rem",
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<span style={{ fontSize: "1rem" }}>⚠️</span>
|
|
190
|
+
<div style={{ fontSize: "0.875rem", color: "#92400e" }}>
|
|
191
|
+
<strong>Development Mode:</strong> {typeText} are stored in browser localStorage and will not persist across
|
|
192
|
+
devices or browsers. For production, implement your own{" "}
|
|
193
|
+
<code style={{ backgroundColor: "#fde68a", padding: "0 0.25rem", borderRadius: "0.25rem" }}>{apiText}</code>{" "}
|
|
194
|
+
with a backend database.
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function RolesPage<T extends RoleItem = RoleItem>({
|
|
201
|
+
adminAPI: adminAPIProp,
|
|
202
|
+
templateAPI,
|
|
203
|
+
policyAPI,
|
|
204
|
+
policyLogsAPI,
|
|
205
|
+
tideContext,
|
|
206
|
+
onTideRequestCreated,
|
|
207
|
+
realm,
|
|
208
|
+
roleType = "realm",
|
|
209
|
+
fetchRoles: fetchRolesProp,
|
|
210
|
+
onCreate: onCreateProp,
|
|
211
|
+
onUpdate: onUpdateProp,
|
|
212
|
+
onDelete: onDeleteProp,
|
|
213
|
+
onCreatePolicy: onCreatePolicyProp,
|
|
214
|
+
helpText: helpTextProp,
|
|
215
|
+
currentUsername,
|
|
216
|
+
...props
|
|
217
|
+
}: RolesPageProps<T>) {
|
|
218
|
+
// Track if Tide libraries are available
|
|
219
|
+
const [tideLibsReady, setTideLibsReady] = useState(false);
|
|
220
|
+
|
|
221
|
+
// Load Tide libraries on mount
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
loadTideLibs().then((available) => {
|
|
224
|
+
setTideLibsReady(available);
|
|
225
|
+
});
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
// Check if using localStorage APIs
|
|
229
|
+
const isPolicyLocalStorage = policyAPI?._isLocalStorage === true;
|
|
230
|
+
const isTemplateLocalStorage = templateAPI?._isLocalStorage === true;
|
|
231
|
+
// Use provided AdminAPI instance or fall back to default singleton
|
|
232
|
+
const api = (adminAPIProp || DefaultAdminAPI) as AdminAPIInstance;
|
|
233
|
+
|
|
234
|
+
// Set realm if provided
|
|
235
|
+
if (realm && api.setRealm) {
|
|
236
|
+
api.setRealm(realm);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const fetchRoles =
|
|
240
|
+
fetchRolesProp ||
|
|
241
|
+
(async () => {
|
|
242
|
+
let roles: T[] = [];
|
|
243
|
+
|
|
244
|
+
if (roleType === "realm" || roleType === "all") {
|
|
245
|
+
const realmRoles = (await api.getRoles()) as T[];
|
|
246
|
+
// Mark as realm roles
|
|
247
|
+
roles = realmRoles.map((r) => ({ ...r, roleType: "realm" as const, clientRole: false }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if ((roleType === "client" || roleType === "all") && api.getClientRoles) {
|
|
251
|
+
const clientRoles = (await api.getClientRoles()) as T[];
|
|
252
|
+
// Mark as client roles
|
|
253
|
+
const markedClientRoles = clientRoles.map((r) => ({ ...r, roleType: "client" as const, clientRole: true }));
|
|
254
|
+
roles = [...roles, ...markedClientRoles];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { roles };
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const fetchTemplates = async () => {
|
|
261
|
+
// Use templateAPI if provided, otherwise fall back to AdminAPI.getTemplates
|
|
262
|
+
if (templateAPI) {
|
|
263
|
+
const templates = await templateAPI.getTemplates();
|
|
264
|
+
return { templates: templates || [] };
|
|
265
|
+
}
|
|
266
|
+
const templates = api.getTemplates ? await api.getTemplates() : [];
|
|
267
|
+
return { templates: templates || [] };
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const fetchUsers = async () => {
|
|
271
|
+
const users = api.getUsers ? await api.getUsers() : [];
|
|
272
|
+
return users || [];
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const fetchPendingApprovals = async () => {
|
|
276
|
+
const approvals = api.getPendingChangeSets ? await api.getPendingChangeSets() : [];
|
|
277
|
+
return approvals || [];
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Fetch policy using PolicyAPI if provided
|
|
281
|
+
const fetchRolePolicy = policyAPI
|
|
282
|
+
? async (roleName: string) => {
|
|
283
|
+
const policy = await policyAPI.getPolicy(roleName);
|
|
284
|
+
return { policy };
|
|
285
|
+
}
|
|
286
|
+
: async () => ({ policy: null });
|
|
287
|
+
|
|
288
|
+
const onCreate =
|
|
289
|
+
onCreateProp ||
|
|
290
|
+
(async (data: { name: string; description?: string; roleType?: "realm" | "client" }) => {
|
|
291
|
+
const targetType = data.roleType || (roleType === "client" ? "client" : "realm");
|
|
292
|
+
|
|
293
|
+
if (targetType === "client" && api.createClientRole) {
|
|
294
|
+
await api.createClientRole({ name: data.name, description: data.description });
|
|
295
|
+
} else {
|
|
296
|
+
await api.createRole({ name: data.name, description: data.description });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const onUpdate =
|
|
301
|
+
onUpdateProp ||
|
|
302
|
+
(async (data: { name: string; description?: string; clientRole?: boolean }) => {
|
|
303
|
+
// Check if it's a client role
|
|
304
|
+
if (data.clientRole && api.updateClientRole) {
|
|
305
|
+
await api.updateClientRole(data.name, { name: data.name, description: data.description });
|
|
306
|
+
} else {
|
|
307
|
+
await api.updateRole(data.name, { name: data.name, description: data.description });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const onDelete =
|
|
312
|
+
onDeleteProp ||
|
|
313
|
+
(async (roleName: string) => {
|
|
314
|
+
// For delete, we need to determine if it's a client role
|
|
315
|
+
// If roleType is 'client', use client API; if 'realm', use realm API
|
|
316
|
+
// If 'all', we try client first if available, then realm
|
|
317
|
+
if (roleType === "client" && api.deleteClientRole) {
|
|
318
|
+
await api.deleteClientRole(roleName);
|
|
319
|
+
} else if (roleType === "realm") {
|
|
320
|
+
await api.deleteRole(roleName);
|
|
321
|
+
} else if (roleType === "all") {
|
|
322
|
+
// Try to delete from all - one will succeed
|
|
323
|
+
// This is a limitation - ideally we'd know which type
|
|
324
|
+
try {
|
|
325
|
+
await api.deleteRole(roleName);
|
|
326
|
+
} catch {
|
|
327
|
+
if (api.deleteClientRole) {
|
|
328
|
+
await api.deleteClientRole(roleName);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
await api.deleteRole(roleName);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Also delete the policy if policyAPI is provided
|
|
336
|
+
if (policyAPI?.deletePolicy) {
|
|
337
|
+
try {
|
|
338
|
+
await policyAPI.deletePolicy(roleName);
|
|
339
|
+
} catch {
|
|
340
|
+
// Ignore policy deletion errors - role is already deleted
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { approvalCreated: false };
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Determine which policy creation handler to use
|
|
348
|
+
// Priority: 1. User-provided onCreatePolicy, 2. Tide workflow (if available), 3. PolicyAPI localStorage
|
|
349
|
+
const useTideWorkflow = tideContext && tideLibsReady && areTideLibsAvailable() && !onCreatePolicyProp;
|
|
350
|
+
|
|
351
|
+
// Helper to add a pending approval to PolicyApprovalsAPI
|
|
352
|
+
const addPendingApproval = async (
|
|
353
|
+
params: {
|
|
354
|
+
roleName: string;
|
|
355
|
+
policyConfig: any;
|
|
356
|
+
templateId?: string;
|
|
357
|
+
templateParams?: Record<string, any>;
|
|
358
|
+
threshold: number;
|
|
359
|
+
},
|
|
360
|
+
policyRequestData: string,
|
|
361
|
+
contractCode?: string,
|
|
362
|
+
requestedByEmail?: string
|
|
363
|
+
) => {
|
|
364
|
+
const policyId = `policy-${params.roleName}-${Date.now()}`;
|
|
365
|
+
const approval: PolicyApprovalData = {
|
|
366
|
+
id: policyId,
|
|
367
|
+
roleId: params.roleName,
|
|
368
|
+
requestedBy: currentUsername || "unknown",
|
|
369
|
+
requestedByEmail,
|
|
370
|
+
threshold: params.threshold,
|
|
371
|
+
approvalCount: 0,
|
|
372
|
+
rejectionCount: 0,
|
|
373
|
+
commitReady: false,
|
|
374
|
+
approvedBy: [],
|
|
375
|
+
deniedBy: [],
|
|
376
|
+
status: "pending",
|
|
377
|
+
timestamp: Date.now(),
|
|
378
|
+
policyRequestData,
|
|
379
|
+
contractCode,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Add to localStorage approvals (or custom API)
|
|
383
|
+
const storageKey = "tidecloak_policy_approvals";
|
|
384
|
+
try {
|
|
385
|
+
const existing = localStorage.getItem(storageKey);
|
|
386
|
+
const approvals: PolicyApprovalData[] = existing ? JSON.parse(existing) : [];
|
|
387
|
+
approvals.push(approval);
|
|
388
|
+
localStorage.setItem(storageKey, JSON.stringify(approvals));
|
|
389
|
+
} catch {
|
|
390
|
+
// Ignore localStorage errors
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Log the policy creation
|
|
394
|
+
if (policyLogsAPI) {
|
|
395
|
+
await policyLogsAPI.addLog({
|
|
396
|
+
policyId,
|
|
397
|
+
roleId: params.roleName,
|
|
398
|
+
action: "created",
|
|
399
|
+
performedBy: currentUsername || "unknown",
|
|
400
|
+
performedByEmail: requestedByEmail,
|
|
401
|
+
policyStatus: "pending",
|
|
402
|
+
policyThreshold: params.threshold,
|
|
403
|
+
approvalCount: 0,
|
|
404
|
+
rejectionCount: 0,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Policy creation handler
|
|
410
|
+
const onCreatePolicy = (() => {
|
|
411
|
+
// 1. User provided custom handler - use it
|
|
412
|
+
if (onCreatePolicyProp) {
|
|
413
|
+
return onCreatePolicyProp;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 2. Tide workflow available - use it as default
|
|
417
|
+
if (useTideWorkflow && tideContext) {
|
|
418
|
+
const tideHandler = createTidePolicyHandler(tideContext, onTideRequestCreated);
|
|
419
|
+
|
|
420
|
+
return async (params: {
|
|
421
|
+
roleName: string;
|
|
422
|
+
policyConfig: any;
|
|
423
|
+
templateId?: string;
|
|
424
|
+
templateParams?: Record<string, any>;
|
|
425
|
+
threshold: number;
|
|
426
|
+
}) => {
|
|
427
|
+
// Create Tide request (this creates and signs the PolicySignRequest)
|
|
428
|
+
// Returns the signed PolicySignRequest object
|
|
429
|
+
const signedRequest = await tideHandler(params);
|
|
430
|
+
|
|
431
|
+
// Encode the PolicySignRequest to bytes and convert to base64
|
|
432
|
+
// This is what the approval enclave needs to process the request
|
|
433
|
+
const requestBytes = signedRequest.encode();
|
|
434
|
+
const policyRequestData = btoa(String.fromCharCode(...requestBytes));
|
|
435
|
+
|
|
436
|
+
await addPendingApproval(params, policyRequestData);
|
|
437
|
+
|
|
438
|
+
// Also save to PolicyAPI for local tracking if provided
|
|
439
|
+
if (policyAPI) {
|
|
440
|
+
await policyAPI.upsertPolicy({
|
|
441
|
+
roleName: params.roleName,
|
|
442
|
+
enabled: params.policyConfig?.enabled ?? true,
|
|
443
|
+
contractType: params.policyConfig?.contractType || "tide",
|
|
444
|
+
approvalType: params.policyConfig?.approvalType || "explicit",
|
|
445
|
+
executionType: params.policyConfig?.executionType || "private",
|
|
446
|
+
threshold: params.threshold,
|
|
447
|
+
templateId: params.templateId,
|
|
448
|
+
templateParams: params.templateParams,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 3. PolicyAPI only (localStorage or custom backend without Tide)
|
|
455
|
+
// Still add pending approval so it shows up in approvals page
|
|
456
|
+
return async (params: {
|
|
457
|
+
roleName: string;
|
|
458
|
+
policyConfig: any;
|
|
459
|
+
templateId?: string;
|
|
460
|
+
templateParams?: Record<string, any>;
|
|
461
|
+
threshold: number;
|
|
462
|
+
}) => {
|
|
463
|
+
// Add pending approval
|
|
464
|
+
const policyRequestData = JSON.stringify({
|
|
465
|
+
roleName: params.roleName,
|
|
466
|
+
templateId: params.templateId,
|
|
467
|
+
templateParams: params.templateParams,
|
|
468
|
+
threshold: params.threshold,
|
|
469
|
+
approvalType: params.policyConfig?.approvalType || "explicit",
|
|
470
|
+
executionType: params.policyConfig?.executionType || "private",
|
|
471
|
+
});
|
|
472
|
+
await addPendingApproval(params, policyRequestData);
|
|
473
|
+
|
|
474
|
+
// Save to PolicyAPI if provided
|
|
475
|
+
if (policyAPI) {
|
|
476
|
+
await policyAPI.upsertPolicy({
|
|
477
|
+
roleName: params.roleName,
|
|
478
|
+
enabled: params.policyConfig?.enabled ?? true,
|
|
479
|
+
contractType: params.policyConfig?.contractType || "default",
|
|
480
|
+
approvalType: params.policyConfig?.approvalType || "explicit",
|
|
481
|
+
executionType: params.policyConfig?.executionType || "private",
|
|
482
|
+
threshold: params.threshold,
|
|
483
|
+
templateId: params.templateId,
|
|
484
|
+
templateParams: params.templateParams,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
})();
|
|
489
|
+
|
|
490
|
+
// Determine localStorage warning type
|
|
491
|
+
const localStorageWarningType = isPolicyLocalStorage && isTemplateLocalStorage
|
|
492
|
+
? "both"
|
|
493
|
+
: isPolicyLocalStorage
|
|
494
|
+
? "policy"
|
|
495
|
+
: isTemplateLocalStorage
|
|
496
|
+
? "template"
|
|
497
|
+
: null;
|
|
498
|
+
|
|
499
|
+
// Show localStorage warning if using localStorage APIs
|
|
500
|
+
const helpText = localStorageWarningType ? (
|
|
501
|
+
<>
|
|
502
|
+
<LocalStorageWarning type={localStorageWarningType} />
|
|
503
|
+
{helpTextProp}
|
|
504
|
+
</>
|
|
505
|
+
) : (
|
|
506
|
+
helpTextProp
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<RolesPageBase<T>
|
|
511
|
+
fetchRoles={fetchRoles}
|
|
512
|
+
fetchTemplates={fetchTemplates}
|
|
513
|
+
fetchUsers={fetchUsers}
|
|
514
|
+
fetchPendingApprovals={fetchPendingApprovals}
|
|
515
|
+
fetchRolePolicy={fetchRolePolicy}
|
|
516
|
+
onCreate={onCreate}
|
|
517
|
+
onUpdate={onUpdate}
|
|
518
|
+
onDelete={onDelete}
|
|
519
|
+
onCreatePolicy={onCreatePolicy}
|
|
520
|
+
roleType={roleType}
|
|
521
|
+
helpText={helpText}
|
|
522
|
+
{...props}
|
|
523
|
+
/>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemplatesPage - Manage policy templates
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <TemplatesPage api={createLocalStorageTemplateAPI()} />
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TemplatesPageBase, type TemplatesPageBaseProps, type PolicyTemplateItem, type TemplateFormData } from "../base";
|
|
11
|
+
|
|
12
|
+
/** Implement this to store templates (or use createLocalStorageTemplateAPI for dev) */
|
|
13
|
+
export interface TemplateAPI {
|
|
14
|
+
getTemplates: () => Promise<PolicyTemplateItem[]>;
|
|
15
|
+
getTemplate?: (id: string) => Promise<PolicyTemplateItem>;
|
|
16
|
+
createTemplate: (data: TemplateFormData) => Promise<PolicyTemplateItem | void>;
|
|
17
|
+
updateTemplate: (id: string, data: Partial<TemplateFormData>) => Promise<PolicyTemplateItem | void>;
|
|
18
|
+
deleteTemplate: (id: string) => Promise<void>;
|
|
19
|
+
_isLocalStorage?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const STORAGE_KEY = "tidecloak_policy_templates";
|
|
23
|
+
|
|
24
|
+
/** localStorage adapter for development */
|
|
25
|
+
export function createLocalStorageTemplateAPI(storageKey = STORAGE_KEY): TemplateAPI {
|
|
26
|
+
const getAll = (): PolicyTemplateItem[] => {
|
|
27
|
+
try {
|
|
28
|
+
const data = localStorage.getItem(storageKey);
|
|
29
|
+
return data ? JSON.parse(data) : [];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const saveAll = (templates: PolicyTemplateItem[]) => {
|
|
36
|
+
localStorage.setItem(storageKey, JSON.stringify(templates));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
_isLocalStorage: true,
|
|
41
|
+
|
|
42
|
+
getTemplates: async () => {
|
|
43
|
+
return getAll();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getTemplate: async (id: string) => {
|
|
47
|
+
const templates = getAll();
|
|
48
|
+
const template = templates.find((t) => t.id === id);
|
|
49
|
+
if (!template) throw new Error(`Template not found: ${id}`);
|
|
50
|
+
return template;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
createTemplate: async (data: TemplateFormData) => {
|
|
54
|
+
const templates = getAll();
|
|
55
|
+
const newTemplate: PolicyTemplateItem = {
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
...data,
|
|
58
|
+
createdBy: "user",
|
|
59
|
+
};
|
|
60
|
+
templates.push(newTemplate);
|
|
61
|
+
saveAll(templates);
|
|
62
|
+
return newTemplate;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
updateTemplate: async (id: string, data: Partial<TemplateFormData>) => {
|
|
66
|
+
const templates = getAll();
|
|
67
|
+
const index = templates.findIndex((t) => t.id === id);
|
|
68
|
+
if (index === -1) throw new Error(`Template not found: ${id}`);
|
|
69
|
+
templates[index] = { ...templates[index], ...data };
|
|
70
|
+
saveAll(templates);
|
|
71
|
+
return templates[index];
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
deleteTemplate: async (id: string) => {
|
|
75
|
+
const templates = getAll();
|
|
76
|
+
const filtered = templates.filter((t) => t.id !== id);
|
|
77
|
+
if (filtered.length === templates.length) {
|
|
78
|
+
throw new Error(`Template not found: ${id}`);
|
|
79
|
+
}
|
|
80
|
+
saveAll(filtered);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TemplatesPageProps<T extends PolicyTemplateItem = PolicyTemplateItem>
|
|
86
|
+
extends Omit<TemplatesPageBaseProps<T>, "fetchData" | "onCreate" | "onUpdate" | "onDelete"> {
|
|
87
|
+
/** Template storage adapter */
|
|
88
|
+
api: TemplateAPI;
|
|
89
|
+
/** Override fetch templates */
|
|
90
|
+
fetchData?: TemplatesPageBaseProps<T>["fetchData"];
|
|
91
|
+
/** Override create */
|
|
92
|
+
onCreate?: TemplatesPageBaseProps<T>["onCreate"];
|
|
93
|
+
/** Override update */
|
|
94
|
+
onUpdate?: TemplatesPageBaseProps<T>["onUpdate"];
|
|
95
|
+
/** Override delete */
|
|
96
|
+
onDelete?: TemplatesPageBaseProps<T>["onDelete"];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Warning banner for localStorage
|
|
100
|
+
function LocalStorageWarning() {
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
style={{
|
|
104
|
+
backgroundColor: "#fef3c7",
|
|
105
|
+
border: "1px solid #f59e0b",
|
|
106
|
+
borderRadius: "0.375rem",
|
|
107
|
+
padding: "0.75rem 1rem",
|
|
108
|
+
marginBottom: "1rem",
|
|
109
|
+
display: "flex",
|
|
110
|
+
alignItems: "flex-start",
|
|
111
|
+
gap: "0.5rem",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<span style={{ fontSize: "1rem" }}>⚠️</span>
|
|
115
|
+
<div style={{ fontSize: "0.875rem", color: "#92400e" }}>
|
|
116
|
+
<strong>Development Mode:</strong> Templates are stored in browser localStorage and will not persist across
|
|
117
|
+
devices or browsers. For production, implement your own{" "}
|
|
118
|
+
<code style={{ backgroundColor: "#fde68a", padding: "0 0.25rem", borderRadius: "0.25rem" }}>TemplateAPI</code>{" "}
|
|
119
|
+
with a backend database.
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function TemplatesPage<T extends PolicyTemplateItem = PolicyTemplateItem>({
|
|
126
|
+
api,
|
|
127
|
+
fetchData: fetchDataProp,
|
|
128
|
+
onCreate: onCreateProp,
|
|
129
|
+
onUpdate: onUpdateProp,
|
|
130
|
+
onDelete: onDeleteProp,
|
|
131
|
+
helpText: helpTextProp,
|
|
132
|
+
...props
|
|
133
|
+
}: TemplatesPageProps<T>) {
|
|
134
|
+
const isLocalStorage = api._isLocalStorage === true;
|
|
135
|
+
|
|
136
|
+
const fetchData =
|
|
137
|
+
fetchDataProp ||
|
|
138
|
+
(async () => {
|
|
139
|
+
const templates = (await api.getTemplates()) as T[];
|
|
140
|
+
return { templates: templates || [] };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const onCreate =
|
|
144
|
+
onCreateProp ||
|
|
145
|
+
(async (data) => {
|
|
146
|
+
await api.createTemplate(data);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const onUpdate =
|
|
150
|
+
onUpdateProp ||
|
|
151
|
+
(async (id, data) => {
|
|
152
|
+
await api.updateTemplate(id, data);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const onDelete =
|
|
156
|
+
onDeleteProp ||
|
|
157
|
+
(async (id) => {
|
|
158
|
+
await api.deleteTemplate(id);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Show localStorage warning if using localStorage API
|
|
162
|
+
const helpText = isLocalStorage ? (
|
|
163
|
+
<>
|
|
164
|
+
<LocalStorageWarning />
|
|
165
|
+
{helpTextProp}
|
|
166
|
+
</>
|
|
167
|
+
) : (
|
|
168
|
+
helpTextProp
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<TemplatesPageBase<T>
|
|
173
|
+
fetchData={fetchData}
|
|
174
|
+
onCreate={onCreate}
|
|
175
|
+
onUpdate={onUpdate}
|
|
176
|
+
onDelete={onDelete}
|
|
177
|
+
helpText={helpText}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|