@nocobase/app-template-default 1.0.0-beta.74 → 1.0.0-beta.76
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/.agents/rules/build-and-deploy.md +4 -0
- package/.agents/rules/components.md +3 -1
- package/AGENTS.md +7 -1
- package/README.MD +3 -3
- package/cli/plugins.ts +2 -1
- package/client/components/data-table/data-table.tsx +2 -0
- package/client/components/data-table/pagination.tsx +6 -6
- package/client/components/flow-diagram.tsx +645 -83
- package/client/components/flow-layout.ts +117 -7
- package/client/components/lifecycle-diagram.tsx +175 -37
- package/client/components/record-link.tsx +20 -0
- package/client/components/required-role-setup.tsx +93 -0
- package/client/components/role-list.tsx +71 -31
- package/client/components/setup-readiness-state.ts +135 -0
- package/client/components/setup-readiness.tsx +91 -0
- package/client/components/ui/combobox.tsx +300 -0
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
- package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
- package/client/hooks/use-table-url-state.ts +93 -0
- package/client/lib/navigation-transform.ts +2 -1
- package/client/lib/required-role-setup.ts +98 -0
- package/client/lib/return-to.ts +19 -0
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +38 -1
- package/client/locales/system/settings-zh-CN.ts +34 -2
- package/client/locales/system/zh-CN.ts +10 -0
- package/client/pages/settings/default-access-form.tsx +4 -1
- package/client/pages/settings/default-access.tsx +40 -8
- package/client/pages/settings/role-form.tsx +1 -1
- package/client/pages/settings/roles.tsx +47 -6
- package/client/pages/settings/shared.tsx +15 -13
- package/client/pages/settings/user-detail.tsx +15 -259
- package/client/pages/settings/user-form.tsx +299 -147
- package/client/pages/settings/users.tsx +211 -123
- package/client/plugins.ts +2 -0
- package/config.example.yml +6 -5
- package/package.json +16 -14
- package/server/plugins.ts +2 -0
- package/tests/components/default-access.test.tsx +141 -2
- package/tests/components/role-list.test.tsx +73 -8
- package/tests/components/roles.test.tsx +14 -7
- package/tests/components/settings-continuity.test.tsx +426 -0
- package/tests/components/settings-feedback.test.tsx +304 -0
- package/tests/components/settings.test.tsx +1 -1
- package/tests/components/setup-readiness.test.tsx +370 -0
- package/tests/components/table-url-state.test.tsx +95 -0
- package/tests/components/user-drawer.test.tsx +526 -0
- package/tests/logic/authorization-assignment-safety.test.ts +82 -0
- package/tests/logic/foundation-integration.test.ts +62 -0
- package/tests/logic/return-to.test.ts +19 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { expect, it } from 'vitest';
|
|
4
|
+
import { createAppTest } from '@nocobase/app-testing/server';
|
|
5
|
+
import { createStandaloneServer } from '../../server/standalone.js';
|
|
6
|
+
|
|
7
|
+
type Assignment = {
|
|
8
|
+
subjectType: unknown;
|
|
9
|
+
subjectId: unknown;
|
|
10
|
+
permissionSetKey: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function unexpectedGlobalAssignments(
|
|
14
|
+
assignments: readonly Assignment[],
|
|
15
|
+
): Assignment[] {
|
|
16
|
+
return assignments.filter(
|
|
17
|
+
(assignment) =>
|
|
18
|
+
assignment.subjectId === '*' &&
|
|
19
|
+
!(
|
|
20
|
+
assignment.subjectType === 'authenticated' &&
|
|
21
|
+
assignment.permissionSetKey === 'member'
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const test = createAppTest({
|
|
27
|
+
createServer: createStandaloneServer,
|
|
28
|
+
connections: ['main'],
|
|
29
|
+
config: {
|
|
30
|
+
app: { publicOrigin: 'http://localhost' },
|
|
31
|
+
auth: { secret: 'test-auth-secret-at-least-32-characters' },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('initialization does not silently grant business roles to every signed-in user', async ({
|
|
36
|
+
connection,
|
|
37
|
+
}) => {
|
|
38
|
+
const assignments = await connection.query
|
|
39
|
+
.selectFrom('authorizationPermissionSetAssignments')
|
|
40
|
+
.select(['subjectType', 'subjectId', 'permissionSetKey'])
|
|
41
|
+
.execute();
|
|
42
|
+
expect(
|
|
43
|
+
unexpectedGlobalAssignments(assignments),
|
|
44
|
+
'Global role assignments must be explicitly requested and editable in administration. Use per-user role assignments by default; do not remove this guard to make a feature accessible.',
|
|
45
|
+
).toEqual([]);
|
|
46
|
+
const member = await connection.query
|
|
47
|
+
.selectFrom('authorizationPermissionSets')
|
|
48
|
+
.select(['grants'])
|
|
49
|
+
.where('key', '=', 'member')
|
|
50
|
+
.executeTakeFirstOrThrow();
|
|
51
|
+
let grants: unknown = member.grants;
|
|
52
|
+
// Database JSON codecs may preserve the serialized seed value as a string.
|
|
53
|
+
while (typeof grants === 'string') grants = JSON.parse(grants) as unknown;
|
|
54
|
+
expect(
|
|
55
|
+
grants,
|
|
56
|
+
'The inherited platform member role must not conceal application business grants. Assign a separate business role to explicit users.',
|
|
57
|
+
).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('detects wildcard recipients regardless of the role name, while preserving explicit user assignments', () => {
|
|
61
|
+
const global = {
|
|
62
|
+
subjectType: 'authenticated',
|
|
63
|
+
subjectId: '*',
|
|
64
|
+
permissionSetKey: 'task-member',
|
|
65
|
+
};
|
|
66
|
+
expect(
|
|
67
|
+
unexpectedGlobalAssignments([
|
|
68
|
+
{
|
|
69
|
+
subjectType: 'authenticated',
|
|
70
|
+
subjectId: '*',
|
|
71
|
+
permissionSetKey: 'member',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
subjectType: 'user',
|
|
75
|
+
subjectId: 'user-1',
|
|
76
|
+
permissionSetKey: 'task-member',
|
|
77
|
+
},
|
|
78
|
+
global,
|
|
79
|
+
{ ...global, subjectType: 'everyone' },
|
|
80
|
+
]),
|
|
81
|
+
).toEqual([global, { ...global, subjectType: 'everyone' }]);
|
|
82
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveAppRuntime } from '@nocobase/app-client/runtime';
|
|
3
|
+
import { defineAppRuntime } from '@nocobase/app-client/runtime';
|
|
4
|
+
import appRuntime from '../../client/runtime.js';
|
|
5
|
+
import cliPlugins from '../../cli/plugins.js';
|
|
6
|
+
import serverPlugins from '../../server/plugins.js';
|
|
7
|
+
import settingsRoutes from '../../client/pages/settings/routes.js';
|
|
8
|
+
import { buildRouteNavigation } from '../../client/routing/route-navigation.js';
|
|
9
|
+
|
|
10
|
+
describe('default Foundation integration', () => {
|
|
11
|
+
it('registers the Foundation deployment evidence build hook', () => {
|
|
12
|
+
expect(
|
|
13
|
+
cliPlugins.plugins.filter(
|
|
14
|
+
(plugin) => plugin.packageName === '@nocobase/app-plugin-foundation',
|
|
15
|
+
),
|
|
16
|
+
).toHaveLength(1);
|
|
17
|
+
});
|
|
18
|
+
it('exposes Foundation in settings for an unrestricted administrator and removes the empty group when denied', async () => {
|
|
19
|
+
const runtime = await resolveAppRuntime(appRuntime);
|
|
20
|
+
const menu = buildRouteNavigation(runtime.routes, new Set());
|
|
21
|
+
const group = menu.find(({ route }) => route.name === 'foundationSettings');
|
|
22
|
+
const page = group?.children.find(
|
|
23
|
+
({ route }) => route.name === 'foundation',
|
|
24
|
+
)?.route;
|
|
25
|
+
expect(page).toMatchObject({
|
|
26
|
+
path: '/foundation',
|
|
27
|
+
auth: 'required',
|
|
28
|
+
authz: 'unrestricted',
|
|
29
|
+
});
|
|
30
|
+
if (!page) throw new Error('Foundation route missing');
|
|
31
|
+
expect(
|
|
32
|
+
buildRouteNavigation(
|
|
33
|
+
runtime.routes,
|
|
34
|
+
new Set([`${page.packageName}:${page.path}:${page.id}`]),
|
|
35
|
+
).some(({ route }) => route.name === 'foundationSettings'),
|
|
36
|
+
).toBe(false);
|
|
37
|
+
expect(
|
|
38
|
+
serverPlugins.plugins.filter(
|
|
39
|
+
(plugin) => plugin.packageName === '@nocobase/app-plugin-foundation',
|
|
40
|
+
),
|
|
41
|
+
).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('merges Foundation into business settings without changing its protected URL', async () => {
|
|
45
|
+
const runtime = await resolveAppRuntime(
|
|
46
|
+
defineAppRuntime({
|
|
47
|
+
...appRuntime,
|
|
48
|
+
routes: [...appRuntime.routes, settingsRoutes],
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const menu = buildRouteNavigation(runtime.routes, new Set());
|
|
52
|
+
expect(menu.some(({ route }) => route.name === 'foundationSettings')).toBe(
|
|
53
|
+
false,
|
|
54
|
+
);
|
|
55
|
+
const settings = menu.find(({ route }) => route.name === 'settings');
|
|
56
|
+
expect(settings?.children.at(-1)?.route).toMatchObject({
|
|
57
|
+
name: 'foundation',
|
|
58
|
+
path: '/foundation',
|
|
59
|
+
authz: 'unrestricted',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { safeReturnTo } from '../../client/lib/return-to.js';
|
|
3
|
+
it('accepts application-relative state and rejects external or ambiguous destinations', () => {
|
|
4
|
+
expect(safeReturnTo('/settings/processes/loan?q=review&page=2#node')).toBe(
|
|
5
|
+
'/settings/processes/loan?q=review&page=2#node',
|
|
6
|
+
);
|
|
7
|
+
for (const value of [
|
|
8
|
+
null,
|
|
9
|
+
'',
|
|
10
|
+
'https://outside.test',
|
|
11
|
+
'//outside.test',
|
|
12
|
+
'/a/..//outside.test',
|
|
13
|
+
'/\\outside.test',
|
|
14
|
+
'/\n/outside.test',
|
|
15
|
+
'javascript:alert(1)',
|
|
16
|
+
'settings/users',
|
|
17
|
+
])
|
|
18
|
+
expect(safeReturnTo(value)).toBeUndefined();
|
|
19
|
+
});
|