@percepta/create 3.3.0 → 3.4.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.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/templates/monorepo/README.md +5 -3
  4. package/templates/monorepo/access/dev-grants.yaml.example +3 -8
  5. package/templates/monorepo/package.json.template +1 -1
  6. package/templates/webapp/AGENTS.md +6 -7
  7. package/templates/webapp/README.md +5 -3
  8. package/templates/webapp/agent-skills/access-control.md +15 -22
  9. package/templates/webapp/eslint.config.mjs +35 -7
  10. package/templates/webapp/package.json.template +4 -1
  11. package/templates/webapp/scripts/seed.ts +71 -24
  12. package/templates/webapp/src/access/access.manifest.ts +14 -1
  13. package/templates/webapp/src/access/schema.zed +1 -5
  14. package/templates/webapp/src/app/(admin)/admin/_components/AdminTabs.tsx +33 -0
  15. package/templates/webapp/src/app/(admin)/admin/_components/PrincipalMultiInput.tsx +248 -0
  16. package/templates/webapp/src/app/(admin)/admin/_lib/accessAdmin.ts +62 -0
  17. package/templates/webapp/src/app/(admin)/admin/page.tsx +683 -0
  18. package/templates/webapp/src/app/(admin)/layout.tsx +43 -0
  19. package/templates/webapp/src/app/(app)/layout.tsx +13 -3
  20. package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +1 -1
  21. package/templates/webapp/src/app/global-error.tsx +1 -2
  22. package/templates/webapp/src/components/Header.tsx +23 -2
  23. package/templates/webapp/src/server/api/routers/access.ts +2 -2
  24. package/templates/webapp/src/services/access/AppAccessControl.ts +49 -0
  25. package/templates/webapp/src/services/inngest/events/AppEvents.ts +2 -2
  26. package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +6 -10
  27. package/templates/webapp/src/app/(app)/admin/_lib/PrincipalRoleTable.tsx +0 -113
  28. package/templates/webapp/src/app/(app)/admin/_lib/accessAdmin.ts +0 -85
  29. package/templates/webapp/src/app/(app)/admin/groups/page.tsx +0 -117
  30. package/templates/webapp/src/app/(app)/admin/users/page.tsx +0 -79
package/README.md CHANGED
@@ -8,7 +8,7 @@ Scaffold and manage Mosaic packages.
8
8
  npx @percepta/create
9
9
  ```
10
10
 
11
- That's it. The CLI prompts you for the package type, repo name, and package name as needed. Defaults yield a running app — sign in as `admin@example.com` / `password`.
11
+ That's it. The CLI prompts you for the package type, repo name, and package name as needed. Defaults yield a running app — sign in as `app-admin@example.com` / `password`.
12
12
 
13
13
  ## Options (mostly for automation)
14
14
 
@@ -43,11 +43,11 @@ The bare command above is the canonical UX. The flags below exist for tests and
43
43
  When you scaffold a webapp (the default flow), `create` automatically runs:
44
44
 
45
45
  1. `pnpm install` (at the monorepo root)
46
- 2. `pnpm run setup` — Docker Compose Postgres + Drizzle migrations + seed user
46
+ 2. `pnpm run setup` — Docker Compose Postgres + Drizzle migrations + seed users
47
47
  3. `pnpm dev` — Next.js dev server
48
48
  4. Opens the served URL in your default browser
49
49
 
50
- Sign in as `admin@example.com` / `password` to start building.
50
+ Sign in as `app-admin@example.com` / `password` to start building.
51
51
 
52
52
  To bail out of the auto-run and get manual next-steps instead, pass `--skip-install`. Then you can run install / setup / dev yourself when ready.
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^24.1.0",
39
39
  "@types/validate-npm-package-name": "^4.0.2",
40
40
  "vitest": "^4.0.0",
41
- "@percepta/build": "1.0.0"
41
+ "@percepta/build": "1.1.0"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=18.0.0"
@@ -41,7 +41,7 @@ packages/
41
41
  Application builders define app-local Zed schemas in each package's
42
42
  `src/access/` directory. The root access scripts merge those schemas with the
43
43
  shared `core/*` schema and apply customer-owned grants such
44
- as application owners, application members, and bootstrap customer admins.
44
+ as application admins, application users, and bootstrap customer admins.
45
45
 
46
46
  ```bash
47
47
  pnpm access:merge
@@ -53,8 +53,10 @@ PR CI merges the customer schema and runs static schema/manifest validation.
53
53
  `access:apply` is reserved for trusted deploy jobs and should run once per
54
54
  target environment with that environment's SpiceDB credentials.
55
55
 
56
- For local development, copy the example fixture files in `access/`, fill in
57
- customer-global user/group IDs from `auth/`, then run:
56
+ For local development, `pnpm run setup` seeds the generated app's default dev
57
+ users and access grants. For additional local grants, copy the example fixture
58
+ files in `access/`, fill in customer-global user/group IDs from `auth/`, then
59
+ run:
58
60
 
59
61
  ```bash
60
62
  pnpm access:seed-grants
@@ -5,15 +5,10 @@ customerAdmins:
5
5
 
6
6
  applications:
7
7
  - appNamespace: "people_app"
8
- owners:
8
+ admins:
9
9
  - userId: "00000000-0000-0000-0000-000000000000"
10
- members:
11
- - groupId: "11111111-1111-1111-1111-111111111111"
12
-
13
- appRoles:
14
- - appNamespace: "people_app"
15
- role: "admin"
16
- subjects:
10
+ users:
17
11
  - groupId: "11111111-1111-1111-1111-111111111111"
18
12
 
13
+ appRoles: []
19
14
  resourceRelations: []
@@ -28,7 +28,7 @@
28
28
  "pnpm": ">=9"
29
29
  },
30
30
  "devDependencies": {
31
- "@percepta/access-control": "0.3.2",
31
+ "@percepta/access-control": "0.6.0",
32
32
  "@types/node": "^24.1.0",
33
33
  "eslint": "^9.18.0",
34
34
  "rimraf": "^5.0.5",
@@ -17,7 +17,7 @@ Next.js 15 full-stack application scaffolded from the Mosaic webapp template via
17
17
  - `pnpm db:migrate` — apply migrations
18
18
  - `pnpm db:setup-and-migrate` — create DB + migrate
19
19
  - `pnpm db:studio` — setup/migrate the database, then open Drizzle Studio
20
- - `pnpm db:seed` — seed default shared-auth dev users and local access grants (admin@example.com and user@example.com / password)
20
+ - `pnpm db:seed` — seed shared-auth dev users and local grants for customer admin, app admin, app user, and app non-user personas (all use password)
21
21
 
22
22
  **Package manager**: Always use `pnpm`, never `npm` or `yarn`.
23
23
 
@@ -109,15 +109,14 @@ Also includes composite components: `ButtonWithDropdown`, `Combobox`, `IconButto
109
109
 
110
110
  ### @percepta/build — Shared Build Configs
111
111
 
112
- Provides centralized ESLint, Prettier, TypeScript, and Vitest configuration.
112
+ Provides centralized formatter, TypeScript, bundler, and Vitest configuration.
113
113
 
114
114
  ```js
115
115
  // eslint.config.mjs
116
- import createEslintConfig from "@percepta/build/eslint";
117
- export default [
118
- ...createEslintConfig({ type: "react", dirname: import.meta.dirname }),
119
- // app-specific overrides...
120
- ];
116
+ import eslint from "@eslint/js";
117
+ import tseslint from "typescript-eslint";
118
+
119
+ export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended);
121
120
  ```
122
121
 
123
122
  ```json
@@ -134,11 +134,13 @@ BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
134
134
  BETTER_AUTH_URL=http://localhost:3000
135
135
  ```
136
136
 
137
- To create a dev user:
137
+ To create dev users:
138
138
  ```bash
139
139
  pnpm db:seed
140
- # Creates admin@example.com / password as an app admin
141
- # Creates user@example.com / password as a non-admin app member
140
+ # Creates customer-admin@example.com / password as a customer admin
141
+ # Creates app-admin@example.com / password as an app admin
142
+ # Creates app-user@example.com / password as an app user
143
+ # Creates non-user@example.com / password with no app access
142
144
  ```
143
145
 
144
146
  ## Access Control
@@ -15,10 +15,10 @@ There are three separate authorization layers:
15
15
  | Layer | Owner | Stored as |
16
16
  |-------|-------|-----------|
17
17
  | Customer admins | Customer bootstrap / Applications admin | `core/customer:main#admin@core/user:<users.id>` |
18
- | Application access | Customer Applications admin | `core/application:<app>#owner/member@core/user:<users.id>` or `core/group:<groups.id>#member` |
19
- | App roles/resources | This app's owner/admin UI and app code | `<app>/system:main#<role>@<subject>` and app resource relationships |
18
+ | Default application roles | Customer Applications admin | `core/application:<app>#admin/user@core/user:<users.id>` or `core/group:<groups.id>#member` |
19
+ | App roles/resources | This app's admin UI and app code | `<app>/system:main#<role>@<subject>` and app resource relationships |
20
20
 
21
- Customer admins do not automatically get application access. To enter an app, a user or group must be an application `owner` or `member`.
21
+ Customer admins do not automatically get application access. They can manage default app roles, but to enter the app as a user, a user or group must be an application `admin` or `user`.
22
22
 
23
23
  ## Source Of Truth
24
24
 
@@ -40,11 +40,7 @@ Every app-authored SpiceDB definition must live under this app's namespace:
40
40
 
41
41
  ```zed
42
42
  definition __APP_NAME_SNAKE__/system {
43
- relation application: core/application
44
- relation admin: core/user | core/group#member
45
-
46
- permission access_app = application->access
47
- permission manage_roles = access_app & (application->owner + admin)
43
+ // Add application-specific roles and permissions here.
48
44
  }
49
45
  ```
50
46
 
@@ -61,36 +57,33 @@ Do not use email addresses, IdP external IDs, group slugs, or display names in S
61
57
 
62
58
  Application access means "can open this app at all." It comes from the customer-level `core/application` object.
63
59
 
64
- The starter system permission is:
60
+ The core application permission is:
65
61
 
66
62
  ```zed
67
- permission access_app = application->access
63
+ permission app_access = user + admin
68
64
  ```
69
65
 
70
66
  The generated app gates the protected app layout with `canAccessApplication()`. Keep that gate in place for authenticated app pages. In tRPC, `protectedProcedure` means authenticated, `appProcedure` adds the default app-access gate, and `requirePermission` should be used for resource-specific checks.
71
67
 
72
- For resource permissions that should only be available inside the app, include `system->access_app` in the Zed expression or make the permission depend on another permission that does. If a permission is intentionally usable without app enrollment, list it in `externallyShareablePermissions` in the manifest.
68
+ For resource permissions that should only be available inside the app, keep the API route behind `appProcedure` or another `canAccessApplication()` check, then let the resource permission model the object-specific rule. If a permission is intentionally usable without app enrollment, list it in `externallyShareablePermissions` in the manifest.
73
69
 
74
70
  ## App-Defined Roles
75
71
 
76
- App roles are static roles authored by the app builder in Zed and assigned by app owners/admins at runtime.
72
+ App roles are static roles authored by the app builder in Zed and assigned by app admins at runtime.
77
73
 
78
74
  To add a role:
79
75
 
80
76
  1. Add a relation to `<appNamespace>/system` in `schema.zed`.
81
- 2. Include the role in `system.assignableRoles` in `access.manifest.ts` if app owners may assign it.
77
+ 2. Include the role in `system.assignableRoles` in `access.manifest.ts` if app admins may assign it.
82
78
  3. Run `pnpm access:validate`.
83
79
 
84
80
  Example:
85
81
 
86
82
  ```zed
87
83
  definition __APP_NAME_SNAKE__/system {
88
- relation application: core/application
89
- relation admin: core/user | core/group#member
90
84
  relation hr_admin: core/user | core/group#member
91
85
 
92
- permission access_app = application->access
93
- permission manage_roles = access_app & (application->owner + admin)
86
+ permission manage_hr = hr_admin
94
87
  }
95
88
  ```
96
89
 
@@ -101,12 +94,12 @@ export const accessManifest = defineAccessManifest({
101
94
  // ...
102
95
  system: {
103
96
  // ...
104
- assignableRoles: ["admin", "hr_admin"],
97
+ assignableRoles: ["hr_admin"],
105
98
  },
106
99
  } as const);
107
100
  ```
108
101
 
109
- Role assignment is additive. Assigning `admin` and then `hr_admin` leaves both grants in place until each one is revoked.
102
+ Role assignment is additive. Assigning one app role and then another leaves both grants in place until each one is revoked.
110
103
 
111
104
  ## Permissioned Resources
112
105
 
@@ -120,8 +113,8 @@ definition __APP_NAME_SNAKE__/employee {
120
113
  relation employee_user: core/user
121
114
  relation manager: __APP_NAME_SNAKE__/employee
122
115
 
123
- permission view_basic = system->access_app
124
- permission view_private = system->access_app & (employee_user + manager->view_private + system->hr_admin)
116
+ permission view_basic = employee_user + manager->view_basic + system->hr_admin
117
+ permission view_private = employee_user + manager->view_private + system->hr_admin
125
118
  }
126
119
  ```
127
120
 
@@ -256,7 +249,7 @@ The `users.role` column is not the app permission source of truth. If Better Aut
256
249
  ## Debugging A Denied Check
257
250
 
258
251
  1. Confirm the user can authenticate and has the expected shared `users.id` in the auth DB.
259
- 2. Confirm application access first: the user or one of their groups needs `core/application:<app>#member` or `#owner`.
252
+ 2. Confirm application access first: the user or one of their groups needs `core/application:<app>#user` or `#admin`.
260
253
  3. Confirm local topology exists:
261
254
 
262
255
  ```bash
@@ -1,13 +1,13 @@
1
1
  // @ts-check
2
+ import eslint from "@eslint/js";
2
3
  import nextPlugin from "@next/eslint-plugin-next";
3
- import createEslintConfig from "@percepta/build/eslint";
4
4
  import nodePlugin from "eslint-plugin-n";
5
+ import reactPlugin from "eslint-plugin-react";
6
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
7
+ import globals from "globals";
8
+ import tseslint from "typescript-eslint";
5
9
 
6
- export default [
7
- ...createEslintConfig({
8
- type: "react",
9
- dirname: import.meta.dirname,
10
- }),
10
+ export default tseslint.config(
11
11
  {
12
12
  ignores: [
13
13
  "pnpm-lock.yaml",
@@ -18,15 +18,43 @@ export default [
18
18
  "terraform/**",
19
19
  ],
20
20
  },
21
+ eslint.configs.recommended,
22
+ ...tseslint.configs.recommended,
23
+ {
24
+ files: ["**/*.{js,mjs,cjs,ts,tsx}"],
25
+ languageOptions: {
26
+ ecmaVersion: "latest",
27
+ globals: {
28
+ ...globals.browser,
29
+ ...globals.node,
30
+ },
31
+ parserOptions: {
32
+ ecmaFeatures: {
33
+ jsx: true,
34
+ },
35
+ },
36
+ sourceType: "module",
37
+ },
38
+ },
21
39
  {
22
40
  plugins: {
23
41
  "@next/next": nextPlugin,
42
+ react: reactPlugin,
43
+ "react-hooks": reactHooksPlugin,
24
44
  },
25
45
  rules: {
26
46
  ...nextPlugin.configs.recommended.rules,
27
47
  ...nextPlugin.configs["core-web-vitals"].rules,
48
+ ...reactPlugin.configs.recommended.rules,
49
+ ...reactHooksPlugin.configs.recommended.rules,
50
+ "react/jsx-uses-react": "off",
28
51
  "react/react-in-jsx-scope": "off",
29
52
  },
53
+ settings: {
54
+ react: {
55
+ version: "detect",
56
+ },
57
+ },
30
58
  },
31
59
  {
32
60
  files: ["src/**/*"],
@@ -63,4 +91,4 @@ export default [
63
91
  "n/no-process-env": "off",
64
92
  },
65
93
  },
66
- ];
94
+ );
@@ -55,7 +55,7 @@
55
55
  "@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
56
56
  "@opentelemetry/sdk-node": "^0.203.0",
57
57
  "@__REPO_NAME__/auth": "workspace:*",
58
- "@percepta/access-control": "0.3.2",
58
+ "@percepta/access-control": "0.6.0",
59
59
  "@percepta/design": "0.3.2",
60
60
  "@percepta/logger": "0.0.6",
61
61
  "@percepta/next-utils": "0.1.0",
@@ -102,6 +102,7 @@
102
102
  "zod": "^4.1.5"
103
103
  },
104
104
  "devDependencies": {
105
+ "@eslint/js": "^9.18.0",
105
106
  "@next/eslint-plugin-next": "^15.3.5",
106
107
  "@percepta/build": "0.4.0",
107
108
  "@tailwindcss/postcss": "^4.1.11",
@@ -121,9 +122,11 @@
121
122
  "eslint-plugin-n": "^17.23.1",
122
123
  "eslint-plugin-react": "^7.37.4",
123
124
  "eslint-plugin-react-hooks": "^5.2.0",
125
+ "globals": "^15.14.0",
124
126
  "husky": "^9.1.7",
125
127
  "tailwindcss": "^4.0.12",
126
128
  "typescript": "^5.7.3",
129
+ "typescript-eslint": "^8.33.0",
127
130
  "vitest": "^3.2.1",
128
131
  "yargs": "^17.7.2"
129
132
  }
@@ -8,21 +8,36 @@
8
8
  */
9
9
 
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { execFileSync } from "node:child_process";
11
12
  import { loadEnvConfig } from "@next/env";
13
+ import type { SubjectRef } from "@percepta/access-control";
12
14
 
13
15
  const SEEDED_USERS = [
14
16
  {
15
- access: "owner",
16
- appRole: "admin",
17
- email: "admin@example.com",
18
- name: "Admin User",
17
+ access: "customer_admin",
18
+ email: "customer-admin@example.com",
19
+ name: "Customer Admin",
19
20
  password: "password",
20
21
  role: "admin",
21
22
  },
22
23
  {
23
- access: "member",
24
- email: "user@example.com",
25
- name: "Regular User",
24
+ access: "app_admin",
25
+ email: "app-admin@example.com",
26
+ name: "App Admin",
27
+ password: "password",
28
+ role: "admin",
29
+ },
30
+ {
31
+ access: "app_user",
32
+ email: "app-user@example.com",
33
+ name: "App User",
34
+ password: "password",
35
+ role: "user",
36
+ },
37
+ {
38
+ access: "none",
39
+ email: "non-user@example.com",
40
+ name: "App Non User",
26
41
  password: "password",
27
42
  role: "user",
28
43
  },
@@ -30,7 +45,7 @@ const SEEDED_USERS = [
30
45
 
31
46
  async function main(): Promise<void> {
32
47
  loadEnvConfig(process.cwd());
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
49
  (globalThis as any).AsyncLocalStorage = AsyncLocalStorage;
35
50
 
36
51
  const { auth } = await import("@__REPO_NAME__/auth");
@@ -38,13 +53,16 @@ async function main(): Promise<void> {
38
53
  const { users } = await import("@__REPO_NAME__/auth/schema");
39
54
  const { getAccessControl, toUserSubject } =
40
55
  await import("../src/services/access/AppAccessControl");
56
+ const { getEnvConfig } = await import("../src/config/getEnvConfig");
41
57
  const { createCustomerAccessControl } =
42
58
  await import("@percepta/access-control");
43
59
  const { eq, sql } = await import("drizzle-orm");
44
60
 
61
+ const envConfig = getEnvConfig();
45
62
  const access = getAccessControl();
63
+ const appNamespace = access.manifest.appNamespace;
46
64
  const customerAccess = createCustomerAccessControl({
47
- applications: [{ appNamespace: access.manifest.appNamespace }],
65
+ applications: [{ appNamespace }],
48
66
  client: access.client,
49
67
  });
50
68
 
@@ -89,25 +107,54 @@ async function main(): Promise<void> {
89
107
  }
90
108
 
91
109
  const subject = toUserSubject(userId);
92
- if (seededUser.access === "owner") {
93
- await customerAccess.assignApplicationOwner(
94
- access.manifest.appNamespace,
95
- subject,
96
- );
97
- } else {
98
- await customerAccess.assignApplicationMember(
99
- access.manifest.appNamespace,
100
- subject,
101
- );
102
- }
103
-
104
- if ("appRole" in seededUser) {
105
- await access.app.assignAppRole(seededUser.appRole, subject);
110
+ switch (seededUser.access) {
111
+ case "customer_admin":
112
+ bootstrapCustomerAdmin(subject, envConfig);
113
+ await customerAccess.revokeApplicationAdmin(appNamespace, subject);
114
+ await customerAccess.revokeApplicationUser(appNamespace, subject);
115
+ break;
116
+ case "app_admin":
117
+ await customerAccess.assignApplicationAdmin(appNamespace, subject);
118
+ await customerAccess.revokeApplicationUser(appNamespace, subject);
119
+ break;
120
+ case "app_user":
121
+ await customerAccess.assignApplicationUser(appNamespace, subject);
122
+ await customerAccess.revokeApplicationAdmin(appNamespace, subject);
123
+ break;
124
+ case "none":
125
+ await customerAccess.revokeApplicationAdmin(appNamespace, subject);
126
+ await customerAccess.revokeApplicationUser(appNamespace, subject);
127
+ break;
106
128
  }
107
129
  }
108
130
 
109
- console.log("Ensured local app access grants.");
131
+ console.log("Ensured local customer and app access grants.");
110
132
  process.exit(0);
111
133
  }
112
134
 
113
135
  void main();
136
+
137
+ function bootstrapCustomerAdmin(
138
+ subject: SubjectRef,
139
+ envConfig: {
140
+ readonly SPICEDB_ENDPOINT: string;
141
+ readonly SPICEDB_INSECURE: boolean;
142
+ readonly SPICEDB_PRESHARED_KEY: string;
143
+ },
144
+ ): void {
145
+ const args = [
146
+ "bootstrap-customer-admin",
147
+ "--subject",
148
+ subject,
149
+ "--endpoint",
150
+ envConfig.SPICEDB_ENDPOINT,
151
+ "--key",
152
+ envConfig.SPICEDB_PRESHARED_KEY,
153
+ ];
154
+
155
+ if (envConfig.SPICEDB_INSECURE) {
156
+ args.push("--insecure");
157
+ }
158
+
159
+ execFileSync("percepta-access-control", args, { stdio: "inherit" });
160
+ }
@@ -1,5 +1,18 @@
1
1
  import { defineAccessManifest } from "@percepta/access-control";
2
2
 
3
+ const appRoles = [] as const;
4
+
5
+ export const accessRoleDefinitions = [] as const satisfies ReadonlyArray<{
6
+ readonly description: string;
7
+ readonly editableBy: "app_admin" | "customer_admin";
8
+ readonly label: string;
9
+ readonly permissions: ReadonlyArray<{
10
+ readonly description: string;
11
+ readonly permission: string;
12
+ }>;
13
+ readonly role: (typeof appRoles)[number];
14
+ }>;
15
+
3
16
  export const accessManifest = defineAccessManifest({
4
17
  adminUI: {
5
18
  enabled: true,
@@ -10,6 +23,6 @@ export const accessManifest = defineAccessManifest({
10
23
  urlEnv: "APP_BASE_URL",
11
24
  },
12
25
  system: {
13
- assignableRoles: ["admin"],
26
+ assignableRoles: appRoles,
14
27
  },
15
28
  } as const);
@@ -1,7 +1,3 @@
1
1
  definition __APP_NAME_SNAKE__/system {
2
- relation application: core/application
3
- relation admin: core/user | core/group#member
4
-
5
- permission access_app = application->access
6
- permission manage_roles = access_app & (application->owner + admin)
2
+ // Add application-specific roles and permissions here.
7
3
  }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { Tabs, TabsList, TabsTrigger } from "@percepta/design";
4
+ import Link from "next/link";
5
+
6
+ export type AdminTab = "assignments" | "roles";
7
+
8
+ const tabs = [
9
+ { href: "/admin?tab=roles", label: "Roles", tab: "roles" },
10
+ {
11
+ href: "/admin?tab=assignments",
12
+ label: "Assignments",
13
+ tab: "assignments",
14
+ },
15
+ ] as const satisfies ReadonlyArray<{
16
+ readonly href: string;
17
+ readonly label: string;
18
+ readonly tab: AdminTab;
19
+ }>;
20
+
21
+ export function AdminTabs({ activeTab }: { readonly activeTab: AdminTab }) {
22
+ return (
23
+ <Tabs value={activeTab}>
24
+ <TabsList aria-label="RBAC views">
25
+ {tabs.map((tab) => (
26
+ <TabsTrigger asChild={true} key={tab.tab} value={tab.tab}>
27
+ <Link href={tab.href}>{tab.label}</Link>
28
+ </TabsTrigger>
29
+ ))}
30
+ </TabsList>
31
+ </Tabs>
32
+ );
33
+ }