@nocobase/app-template-default 1.0.0-beta.76 → 1.0.0-beta.78
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/client/components/default-access-editor.tsx +79 -52
- package/client/components/long-text.tsx +200 -0
- package/client/components/permission-editor.tsx +534 -341
- package/client/locales/system/settings-en-US.ts +14 -3
- package/client/locales/system/settings-zh-CN.ts +13 -3
- package/client/pages/home/use-copy.ts +6 -3
- package/client/pages/settings/default-access-form.tsx +10 -1
- package/client/pages/settings/default-access-model.ts +6 -2
- package/client/pages/settings/page-permissions.ts +166 -1
- package/client/pages/settings/permission-filters.tsx +9 -2
- package/client/pages/settings/permission-workspace.ts +330 -0
- package/client/pages/settings/permissions-model.ts +62 -53
- package/client/pages/settings/role-form.tsx +30 -94
- package/package.json +16 -16
- package/server/config/app.ts +1 -1
- package/server/config/session.ts +7 -5
- package/tests/components/default-access-editor.test.tsx +130 -0
- package/tests/components/permission-editor.test.tsx +613 -572
- package/tests/components/permission-filter-binding.test.tsx +20 -0
- package/tests/components/settings.test.tsx +33 -8
- package/tests/components/user-drawer.test.tsx +14 -3
- package/tests/components/user-permissions.test.tsx +47 -13
- package/tests/logic/config.test.ts +3 -1
- package/tests/logic/page-permission-navigation.test.ts +394 -0
- package/tests/logic/permission-workspace.test.ts +570 -0
- package/tests/logic/session-isolation.test.ts +250 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
2
|
+
import type {
|
|
3
|
+
AuthorizationOptionsResponse,
|
|
4
|
+
LocalizedText,
|
|
5
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
6
|
+
import type { CatalogResource } from './permissions-model.js';
|
|
7
|
+
|
|
8
|
+
export interface PermissionWorkspaceSelection {
|
|
9
|
+
readonly business: readonly {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly actions: readonly string[];
|
|
13
|
+
readonly actionDescriptions?: Readonly<Record<string, LocalizedText>>;
|
|
14
|
+
readonly scopeTitles?: Readonly<Record<string, LocalizedText>>;
|
|
15
|
+
readonly actionScopeTitles?: Readonly<
|
|
16
|
+
Record<string, Readonly<Record<string, LocalizedText>>>
|
|
17
|
+
>;
|
|
18
|
+
/** UI-only omission; registered and stored scope values remain unchanged. */
|
|
19
|
+
readonly hiddenActionScopes?: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
readonly configurationActions?: readonly {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly routes: readonly string[];
|
|
23
|
+
}[];
|
|
24
|
+
readonly configurationTitle?: LocalizedText;
|
|
25
|
+
}[];
|
|
26
|
+
readonly settings: readonly {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly actions: readonly string[];
|
|
29
|
+
}[];
|
|
30
|
+
readonly filterFieldTitles?: Readonly<
|
|
31
|
+
Record<string, Readonly<Record<string, LocalizedText>>>
|
|
32
|
+
>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Add only business operations and configuration controls this application exposes. */
|
|
36
|
+
export const permissionWorkspaceSelection: PermissionWorkspaceSelection = {
|
|
37
|
+
business: [],
|
|
38
|
+
settings: [
|
|
39
|
+
{
|
|
40
|
+
id: 'authorization.permission-sets',
|
|
41
|
+
actions: ['read', 'create', 'update'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'authorization.default-access',
|
|
45
|
+
actions: ['read', 'create', 'update', 'delete'],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Share application-owned data captions across role and default-scope editors. */
|
|
51
|
+
export function permissionScopeTitle(
|
|
52
|
+
resource: { readonly type: string; readonly id: string },
|
|
53
|
+
scope: { readonly collection: string; readonly title: LocalizedText },
|
|
54
|
+
selection: PermissionWorkspaceSelection = permissionWorkspaceSelection,
|
|
55
|
+
action?: string,
|
|
56
|
+
): LocalizedText {
|
|
57
|
+
const selected = selection.business.find((entry) =>
|
|
58
|
+
resource.type === 'database.collection'
|
|
59
|
+
? entry.scopeTitles?.[scope.collection] !== undefined
|
|
60
|
+
: entry.type === resource.type && entry.id === resource.id,
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
(action
|
|
64
|
+
? selected?.actionScopeTitles?.[action]?.[scope.collection]
|
|
65
|
+
: undefined) ??
|
|
66
|
+
selected?.scopeTitles?.[scope.collection] ??
|
|
67
|
+
scope.title
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function permissionScopeVisible(
|
|
72
|
+
resource: { readonly type: string; readonly id: string },
|
|
73
|
+
action: string,
|
|
74
|
+
scopeKey: string,
|
|
75
|
+
selection: PermissionWorkspaceSelection = permissionWorkspaceSelection,
|
|
76
|
+
): boolean {
|
|
77
|
+
return !selection.business
|
|
78
|
+
.find((entry) => entry.type === resource.type && entry.id === resource.id)
|
|
79
|
+
?.hiddenActionScopes?.[action]?.includes(scopeKey);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function permissionFilterFieldLabel(
|
|
83
|
+
collection: string,
|
|
84
|
+
field: string,
|
|
85
|
+
text: (key: string, fallback?: string) => string,
|
|
86
|
+
selection: PermissionWorkspaceSelection = permissionWorkspaceSelection,
|
|
87
|
+
): string {
|
|
88
|
+
const title = selection.filterFieldTitles?.[collection]?.[field];
|
|
89
|
+
return title
|
|
90
|
+
? typeof title === 'string'
|
|
91
|
+
? title
|
|
92
|
+
: text(title.key, title.defaultValue ?? field)
|
|
93
|
+
: field;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasConfigurationPage(
|
|
97
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
98
|
+
resourceId: string,
|
|
99
|
+
): boolean {
|
|
100
|
+
return routes.some(
|
|
101
|
+
(route) =>
|
|
102
|
+
(route.componentLoader &&
|
|
103
|
+
typeof route.authz === 'object' &&
|
|
104
|
+
route.authz.resource.type === 'settings' &&
|
|
105
|
+
route.authz.resource.id === resourceId) ||
|
|
106
|
+
hasConfigurationPage(route.children ?? [], resourceId),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasUserManagement(
|
|
111
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
112
|
+
): boolean {
|
|
113
|
+
return routes.some(
|
|
114
|
+
(route) =>
|
|
115
|
+
(route.componentLoader &&
|
|
116
|
+
typeof route.authz === 'object' &&
|
|
117
|
+
route.authz.resource.type === 'user') ||
|
|
118
|
+
hasUserManagement(route.children ?? []),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A configuration action needs an actual application-owned page, not just an installed capability. */
|
|
123
|
+
function hasApplicationPage(
|
|
124
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
125
|
+
names: readonly string[],
|
|
126
|
+
): boolean {
|
|
127
|
+
return routes.some(
|
|
128
|
+
(route) =>
|
|
129
|
+
(route.source === 'application' &&
|
|
130
|
+
route.componentLoader &&
|
|
131
|
+
names.includes(route.name)) ||
|
|
132
|
+
hasApplicationPage(route.children ?? [], names),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Application-owned presentation, not a copy of the complete capability catalog.
|
|
138
|
+
* Select actual business actions in permissionWorkspaceSelection as features are built.
|
|
139
|
+
* Settings additionally require a real configuration page and registered action.
|
|
140
|
+
* Installing a plugin never opts all its management capabilities into this form.
|
|
141
|
+
*/
|
|
142
|
+
export function permissionWorkspaceCatalog(
|
|
143
|
+
source: AuthorizationOptionsResponse,
|
|
144
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
145
|
+
pages: readonly CatalogResource[],
|
|
146
|
+
selection: PermissionWorkspaceSelection = permissionWorkspaceSelection,
|
|
147
|
+
): AuthorizationOptionsResponse {
|
|
148
|
+
const configurationBusiness: CatalogResource[] = [];
|
|
149
|
+
const visibleSections: readonly string[] = ['pages', 'business'];
|
|
150
|
+
const settings = source.sections
|
|
151
|
+
.flatMap((section) => section.subsections.flatMap((sub) => sub.resources))
|
|
152
|
+
.flatMap((resource) => {
|
|
153
|
+
const selected = selection.settings.find(
|
|
154
|
+
(entry) => entry.id === resource.id,
|
|
155
|
+
);
|
|
156
|
+
if (
|
|
157
|
+
resource.type !== 'settings' ||
|
|
158
|
+
!selected ||
|
|
159
|
+
!hasConfigurationPage(routes, resource.id)
|
|
160
|
+
)
|
|
161
|
+
return [];
|
|
162
|
+
const actions = resource.actions.filter((action) =>
|
|
163
|
+
selected.actions.includes(action.name),
|
|
164
|
+
);
|
|
165
|
+
return actions.length ? [{ ...resource, actions }] : [];
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
...source,
|
|
169
|
+
sections: [
|
|
170
|
+
...(hasUserManagement(routes)
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
name: 'users',
|
|
174
|
+
title: { key: 'settings.userPermissions' },
|
|
175
|
+
order: 0,
|
|
176
|
+
subsections: [
|
|
177
|
+
...[
|
|
178
|
+
{
|
|
179
|
+
name: 'users.information',
|
|
180
|
+
title: 'userInformation',
|
|
181
|
+
actions: ['read', 'create', 'update'],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'users.security',
|
|
185
|
+
title: 'accountSecurity',
|
|
186
|
+
actions: ['disable', 'enable', 'revoke-sessions'],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'users.roles',
|
|
190
|
+
title: 'roleAssignment',
|
|
191
|
+
actions: ['assign-role'],
|
|
192
|
+
},
|
|
193
|
+
].map((group) => ({
|
|
194
|
+
name: group.name,
|
|
195
|
+
title: { key: `settings.${group.title}` },
|
|
196
|
+
resources: [
|
|
197
|
+
{
|
|
198
|
+
type: 'user',
|
|
199
|
+
id: '*',
|
|
200
|
+
title: { key: `settings.${group.title}` },
|
|
201
|
+
actions: [
|
|
202
|
+
{ name: 'read', title: { key: 'settings.readUsers' } },
|
|
203
|
+
{
|
|
204
|
+
name: 'create',
|
|
205
|
+
title: { key: 'settings.createUsers' },
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'update',
|
|
209
|
+
title: { key: 'settings.updateUsers' },
|
|
210
|
+
},
|
|
211
|
+
{ name: 'disable', title: { key: 'settings.disable' } },
|
|
212
|
+
{ name: 'enable', title: { key: 'settings.enable' } },
|
|
213
|
+
{
|
|
214
|
+
name: 'assign-role',
|
|
215
|
+
title: { key: 'settings.assignRole' },
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'revoke-sessions',
|
|
219
|
+
title: { key: 'settings.revokeSessions' },
|
|
220
|
+
},
|
|
221
|
+
].filter((action) => group.actions.includes(action.name)),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
})),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
: []),
|
|
229
|
+
...source.sections
|
|
230
|
+
.filter((section) => visibleSections.includes(section.name))
|
|
231
|
+
.map((section) => ({
|
|
232
|
+
...section,
|
|
233
|
+
subsections: section.subsections.map((sub) => ({
|
|
234
|
+
...sub,
|
|
235
|
+
resources:
|
|
236
|
+
sub.recordType?.type === 'page'
|
|
237
|
+
? [...sub.resources, ...pages]
|
|
238
|
+
: sub.resources.flatMap((resource) => {
|
|
239
|
+
const selected = selection.business.find(
|
|
240
|
+
(entry) =>
|
|
241
|
+
entry.type === resource.type &&
|
|
242
|
+
entry.id === resource.id,
|
|
243
|
+
);
|
|
244
|
+
if (!selected) return [];
|
|
245
|
+
const actions = resource.actions.filter((action) =>
|
|
246
|
+
selected.actions.includes(action.name),
|
|
247
|
+
);
|
|
248
|
+
if (!actions.length) return [];
|
|
249
|
+
const projected = {
|
|
250
|
+
...resource,
|
|
251
|
+
actions: actions.map((action) => ({
|
|
252
|
+
...action,
|
|
253
|
+
...(selected.actionDescriptions?.[action.name]
|
|
254
|
+
? {
|
|
255
|
+
description:
|
|
256
|
+
selected.actionDescriptions[action.name],
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
})),
|
|
260
|
+
...(resource.dataScopes
|
|
261
|
+
? {
|
|
262
|
+
dataScopes: Object.fromEntries(
|
|
263
|
+
Object.entries(resource.dataScopes).map(
|
|
264
|
+
([action, scopes]) => [
|
|
265
|
+
action,
|
|
266
|
+
scopes.map((scope) => ({
|
|
267
|
+
...scope,
|
|
268
|
+
title: permissionScopeTitle(
|
|
269
|
+
resource,
|
|
270
|
+
scope,
|
|
271
|
+
selection,
|
|
272
|
+
action,
|
|
273
|
+
),
|
|
274
|
+
})),
|
|
275
|
+
],
|
|
276
|
+
),
|
|
277
|
+
),
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
const isConfiguration = (name: string) =>
|
|
282
|
+
selected.configurationActions?.some(
|
|
283
|
+
(entry) => entry.name === name,
|
|
284
|
+
);
|
|
285
|
+
const configurationActions = projected.actions.filter(
|
|
286
|
+
(action) => {
|
|
287
|
+
const entry = selected.configurationActions?.find(
|
|
288
|
+
(entry) => entry.name === action.name,
|
|
289
|
+
);
|
|
290
|
+
return (
|
|
291
|
+
entry && hasApplicationPage(routes, entry.routes)
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
if (configurationActions.length)
|
|
296
|
+
configurationBusiness.push({
|
|
297
|
+
...projected,
|
|
298
|
+
title: selected.configurationTitle ?? projected.title,
|
|
299
|
+
actions: configurationActions,
|
|
300
|
+
});
|
|
301
|
+
const businessActions = projected.actions.filter(
|
|
302
|
+
(action) => !isConfiguration(action.name),
|
|
303
|
+
);
|
|
304
|
+
return businessActions.length
|
|
305
|
+
? [{ ...projected, actions: businessActions }]
|
|
306
|
+
: [];
|
|
307
|
+
}),
|
|
308
|
+
})),
|
|
309
|
+
})),
|
|
310
|
+
...(settings.length || configurationBusiness.length
|
|
311
|
+
? [
|
|
312
|
+
{
|
|
313
|
+
name: 'configuration',
|
|
314
|
+
title: { key: 'settings.configurationPermissions' },
|
|
315
|
+
order: 200,
|
|
316
|
+
subsections: [
|
|
317
|
+
{
|
|
318
|
+
name: 'configuration',
|
|
319
|
+
title: { key: 'settings.configurationPermissions' },
|
|
320
|
+
resources: [...settings, ...configurationBusiness],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
]
|
|
325
|
+
: []),
|
|
326
|
+
].filter((section) =>
|
|
327
|
+
section.subsections.some((sub) => sub.resources.length > 0),
|
|
328
|
+
),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -64,13 +64,18 @@ const sectionDescriptions: Readonly<Record<string, string>> = {
|
|
|
64
64
|
pages: 'pageAccessHint',
|
|
65
65
|
business: 'businessOperationsHint',
|
|
66
66
|
users: 'userAccountsHint',
|
|
67
|
-
|
|
67
|
+
configuration: 'configurationPermissionsHint',
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
export function permissionSections(
|
|
71
71
|
catalog: AuthorizationOptionsResponse,
|
|
72
72
|
grants: readonly PermissionGrant[],
|
|
73
73
|
text: (value: LocalizedText) => string,
|
|
74
|
+
showScope: (
|
|
75
|
+
resource: CatalogResource,
|
|
76
|
+
action: string,
|
|
77
|
+
scopeKey: string,
|
|
78
|
+
) => boolean = () => true,
|
|
74
79
|
): readonly PermissionEditorSection[] {
|
|
75
80
|
const sharedActionHint = (resource: CatalogResource): string | undefined => {
|
|
76
81
|
const first = resource.actions[0]?.description;
|
|
@@ -125,59 +130,63 @@ export function permissionSections(
|
|
|
125
130
|
return {
|
|
126
131
|
...row,
|
|
127
132
|
scopes: granted
|
|
128
|
-
? (resource.dataScopes?.[action.name] ?? [])
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
id: JSON.stringify([
|
|
139
|
-
resource.type,
|
|
140
|
-
resource.id,
|
|
141
|
-
action.name,
|
|
142
|
-
scope.key,
|
|
143
|
-
]),
|
|
144
|
-
label: text(
|
|
145
|
-
typeof scope.title === 'string' &&
|
|
146
|
-
scope.title === scope.collection
|
|
147
|
-
? { key: 'settings.recordScope' }
|
|
148
|
-
: scope.title,
|
|
149
|
-
),
|
|
150
|
-
levels: [
|
|
151
|
-
...(preserved
|
|
152
|
-
? [
|
|
153
|
-
{
|
|
154
|
-
value,
|
|
155
|
-
label: text({
|
|
156
|
-
key: 'settings.preservedRecordScope',
|
|
157
|
-
}),
|
|
158
|
-
},
|
|
159
|
-
]
|
|
160
|
-
: []),
|
|
161
|
-
{
|
|
162
|
-
value: '',
|
|
163
|
-
label: text({ key: 'settings.noRecordScope' }),
|
|
164
|
-
},
|
|
165
|
-
...scope.recordAccess.map((key) => ({
|
|
166
|
-
value: key,
|
|
167
|
-
label: text(
|
|
168
|
-
catalog.recordAccess.find(
|
|
169
|
-
(entry) => entry.key === key,
|
|
170
|
-
)?.title ?? key,
|
|
171
|
-
),
|
|
172
|
-
})),
|
|
173
|
-
],
|
|
174
|
-
level: value,
|
|
175
|
-
hint:
|
|
176
|
-
value === ''
|
|
177
|
-
? text({ key: 'settings.noAdditionalScopeHint' })
|
|
133
|
+
? (resource.dataScopes?.[action.name] ?? [])
|
|
134
|
+
.filter((scope) =>
|
|
135
|
+
showScope(resource, action.name, scope.key),
|
|
136
|
+
)
|
|
137
|
+
.map((scope) => {
|
|
138
|
+
const value = scopeSelectionKey(
|
|
139
|
+
granted.policy?.scopes &&
|
|
140
|
+
typeof granted.policy.scopes === 'object'
|
|
141
|
+
? Reflect.get(granted.policy.scopes, scope.key)
|
|
178
142
|
: undefined,
|
|
179
|
-
|
|
180
|
-
|
|
143
|
+
);
|
|
144
|
+
const preserved =
|
|
145
|
+
value !== '' && !scope.recordAccess.includes(value);
|
|
146
|
+
return {
|
|
147
|
+
id: JSON.stringify([
|
|
148
|
+
resource.type,
|
|
149
|
+
resource.id,
|
|
150
|
+
action.name,
|
|
151
|
+
scope.key,
|
|
152
|
+
]),
|
|
153
|
+
label: text(
|
|
154
|
+
typeof scope.title === 'string' &&
|
|
155
|
+
scope.title === scope.collection
|
|
156
|
+
? { key: 'settings.recordScope' }
|
|
157
|
+
: scope.title,
|
|
158
|
+
),
|
|
159
|
+
levels: [
|
|
160
|
+
...(preserved
|
|
161
|
+
? [
|
|
162
|
+
{
|
|
163
|
+
value,
|
|
164
|
+
label: text({
|
|
165
|
+
key: 'settings.preservedRecordScope',
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
{
|
|
171
|
+
value: '',
|
|
172
|
+
label: text({ key: 'settings.noRecordScope' }),
|
|
173
|
+
},
|
|
174
|
+
...scope.recordAccess.map((key) => ({
|
|
175
|
+
value: key,
|
|
176
|
+
label: text(
|
|
177
|
+
catalog.recordAccess.find(
|
|
178
|
+
(entry) => entry.key === key,
|
|
179
|
+
)?.title ?? key,
|
|
180
|
+
),
|
|
181
|
+
})),
|
|
182
|
+
],
|
|
183
|
+
level: value,
|
|
184
|
+
hint:
|
|
185
|
+
value === ''
|
|
186
|
+
? text({ key: 'settings.noAdditionalScopeHint' })
|
|
187
|
+
: undefined,
|
|
188
|
+
};
|
|
189
|
+
})
|
|
181
190
|
: [],
|
|
182
191
|
};
|
|
183
192
|
}),
|
|
@@ -11,17 +11,22 @@ import {
|
|
|
11
11
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
12
12
|
import {
|
|
13
13
|
useAuthorizationClient,
|
|
14
|
-
useCan,
|
|
15
14
|
type AuthorizationOptionsResponse,
|
|
16
15
|
type LocalizedText,
|
|
17
16
|
type PermissionGrant,
|
|
18
17
|
type PermissionSet,
|
|
19
18
|
} from '@nocobase/app-plugin-authorization/client';
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
permissionNavigationSections,
|
|
21
|
+
pagePermissionResources,
|
|
22
|
+
} from './page-permissions.js';
|
|
21
23
|
import { pagePermissionTitles } from './page-permission-titles.js';
|
|
24
|
+
import {
|
|
25
|
+
permissionScopeVisible,
|
|
26
|
+
permissionWorkspaceCatalog,
|
|
27
|
+
} from './permission-workspace.js';
|
|
22
28
|
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
|
|
23
29
|
import {
|
|
24
|
-
Link,
|
|
25
30
|
useLocation,
|
|
26
31
|
useNavigate,
|
|
27
32
|
useOutletContext,
|
|
@@ -74,10 +79,6 @@ function RoleEditor(): ReactElement {
|
|
|
74
79
|
const options = useSettingsData<{ data: AuthorizationOptionsResponse }>(
|
|
75
80
|
'authorization/permissionSets/options',
|
|
76
81
|
);
|
|
77
|
-
const canReadDefaults = useCan({
|
|
78
|
-
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
79
|
-
action: 'read',
|
|
80
|
-
}).can;
|
|
81
82
|
const canCreate = useRolePermission('create');
|
|
82
83
|
const canUpdate = useRolePermission('update');
|
|
83
84
|
const [nameDraft, setNameDraft] = useState<string>();
|
|
@@ -105,87 +106,16 @@ function RoleEditor(): ReactElement {
|
|
|
105
106
|
const grants = grantsDraft ?? initial?.grants ?? [];
|
|
106
107
|
const catalog = useMemo(() => {
|
|
107
108
|
if (!options.data) return undefined;
|
|
108
|
-
const source = options.data.data;
|
|
109
109
|
const pages = pagePermissionResources(
|
|
110
110
|
app.runtime.routes,
|
|
111
111
|
pagePermissionTitles,
|
|
112
112
|
(path) => t('settings.unnamedPage', { path }),
|
|
113
113
|
);
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
title: { key: 'settings.userPermissions' },
|
|
120
|
-
order: 0,
|
|
121
|
-
subsections: [
|
|
122
|
-
...[
|
|
123
|
-
{
|
|
124
|
-
name: 'users.information',
|
|
125
|
-
title: 'userInformation',
|
|
126
|
-
actions: ['read', 'create', 'update', 'invite', 'delete'],
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: 'users.security',
|
|
130
|
-
title: 'accountSecurity',
|
|
131
|
-
actions: [
|
|
132
|
-
'reset-password',
|
|
133
|
-
'disable',
|
|
134
|
-
'enable',
|
|
135
|
-
'revoke-sessions',
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'users.roles',
|
|
140
|
-
title: 'roleAssignment',
|
|
141
|
-
actions: ['assign-role'],
|
|
142
|
-
},
|
|
143
|
-
].map((group) => ({
|
|
144
|
-
name: group.name,
|
|
145
|
-
title: { key: `settings.${group.title}` },
|
|
146
|
-
resources: [
|
|
147
|
-
{
|
|
148
|
-
type: 'user',
|
|
149
|
-
id: '*',
|
|
150
|
-
title: { key: `settings.${group.title}` },
|
|
151
|
-
actions: [
|
|
152
|
-
{ name: 'read', title: { key: 'settings.readUsers' } },
|
|
153
|
-
{ name: 'create', title: { key: 'settings.createUsers' } },
|
|
154
|
-
{ name: 'update', title: { key: 'settings.updateUsers' } },
|
|
155
|
-
{ name: 'invite', title: { key: 'settings.inviteUsers' } },
|
|
156
|
-
{ name: 'delete', title: { key: 'settings.deleteUsers' } },
|
|
157
|
-
{
|
|
158
|
-
name: 'reset-password',
|
|
159
|
-
title: { key: 'settings.resetUserPassword' },
|
|
160
|
-
},
|
|
161
|
-
{ name: 'disable', title: { key: 'settings.disable' } },
|
|
162
|
-
{ name: 'enable', title: { key: 'settings.enable' } },
|
|
163
|
-
{
|
|
164
|
-
name: 'assign-role',
|
|
165
|
-
title: { key: 'settings.assignRole' },
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
name: 'revoke-sessions',
|
|
169
|
-
title: { key: 'settings.revokeSessions' },
|
|
170
|
-
},
|
|
171
|
-
].filter((action) => group.actions.includes(action.name)),
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
})),
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
...source.sections.map((section) => ({
|
|
178
|
-
...section,
|
|
179
|
-
subsections: section.subsections.map((sub) => ({
|
|
180
|
-
...sub,
|
|
181
|
-
resources:
|
|
182
|
-
sub.recordType?.type === 'page'
|
|
183
|
-
? [...sub.resources, ...pages]
|
|
184
|
-
: sub.resources,
|
|
185
|
-
})),
|
|
186
|
-
})),
|
|
187
|
-
],
|
|
188
|
-
};
|
|
114
|
+
return permissionWorkspaceCatalog(
|
|
115
|
+
options.data.data,
|
|
116
|
+
app.runtime.routes,
|
|
117
|
+
pages,
|
|
118
|
+
);
|
|
189
119
|
}, [options.data, app, t]);
|
|
190
120
|
const allowed = creating
|
|
191
121
|
? canCreate
|
|
@@ -247,7 +177,13 @@ function RoleEditor(): ReactElement {
|
|
|
247
177
|
};
|
|
248
178
|
const sections = catalog
|
|
249
179
|
? configurePermissionScopes(
|
|
250
|
-
|
|
180
|
+
permissionNavigationSections(
|
|
181
|
+
permissionSections(catalog, grants, text, permissionScopeVisible),
|
|
182
|
+
app.runtime.routes,
|
|
183
|
+
pagePermissionTitles,
|
|
184
|
+
(path) => t('settings.unnamedPage', { path }),
|
|
185
|
+
text,
|
|
186
|
+
),
|
|
251
187
|
catalog,
|
|
252
188
|
grants,
|
|
253
189
|
(key, fallback) => t(key, { defaultValue: fallback ?? key }),
|
|
@@ -343,16 +279,9 @@ function RoleEditor(): ReactElement {
|
|
|
343
279
|
{t('settings.protectedHint')}
|
|
344
280
|
</p>
|
|
345
281
|
)}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
to='/default-access'
|
|
350
|
-
className='text-sm text-foreground underline-offset-4 hover:underline'
|
|
351
|
-
>
|
|
352
|
-
{t('settings.defaultAccess.manage')}
|
|
353
|
-
</Link>
|
|
354
|
-
</div>
|
|
355
|
-
)}
|
|
282
|
+
<p className='text-xs text-muted-foreground'>
|
|
283
|
+
{t('settings.permissionSaveHint')}
|
|
284
|
+
</p>
|
|
356
285
|
<PermissionEditor
|
|
357
286
|
sections={sections}
|
|
358
287
|
readOnly={!allowed || busy}
|
|
@@ -365,8 +294,15 @@ function RoleEditor(): ReactElement {
|
|
|
365
294
|
enabledOnly: t('settings.enabledOnly'),
|
|
366
295
|
clear: t('settings.clear'),
|
|
367
296
|
empty: t('settings.noPermissions'),
|
|
297
|
+
emptyDescription: t('settings.permissionEmptyHint'),
|
|
298
|
+
readOnly: t('settings.permissionReadOnlyHint'),
|
|
299
|
+
managedPermission: t('settings.permissionManagedHint'),
|
|
368
300
|
noResults: t('settings.noResults'),
|
|
369
301
|
navigation: t('settings.permissionCategories'),
|
|
302
|
+
expandGroup: (name) =>
|
|
303
|
+
t('settings.expandPermissionGroup', { name }),
|
|
304
|
+
collapseGroup: (name) =>
|
|
305
|
+
t('settings.collapsePermissionGroup', { name }),
|
|
370
306
|
}}
|
|
371
307
|
onEnabledChange={(id, enabled) => {
|
|
372
308
|
if (!catalog) return;
|