@percepta/create 3.3.0 → 3.4.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 +3 -3
- package/package.json +2 -2
- package/templates/monorepo/README.md +5 -3
- package/templates/monorepo/access/dev-grants.yaml.example +3 -8
- package/templates/monorepo/auth/package.json +2 -1
- package/templates/monorepo/auth/src/principals.ts +11 -0
- package/templates/monorepo/package.json.template +1 -1
- package/templates/webapp/AGENTS.md +6 -7
- package/templates/webapp/README.md +31 -3
- package/templates/webapp/agent-skills/access-control.md +15 -22
- package/templates/webapp/e2e/rbac.spec.ts +136 -0
- package/templates/webapp/eslint.config.mjs +41 -7
- package/templates/webapp/gitignore.template +2 -0
- package/templates/webapp/package.json.template +8 -1
- package/templates/webapp/playwright.config.ts +33 -0
- package/templates/webapp/scripts/seed.ts +71 -24
- package/templates/webapp/src/access/access.manifest.ts +9 -2
- package/templates/webapp/src/access/schema.zed +1 -5
- package/templates/webapp/src/app/(admin)/admin/_components/AdminTabs.tsx +33 -0
- package/templates/webapp/src/app/(admin)/admin/_lib/accessAdmin.ts +62 -0
- package/templates/webapp/src/app/(admin)/admin/page.tsx +598 -0
- package/templates/webapp/src/app/(admin)/layout.tsx +43 -0
- package/templates/webapp/src/app/(app)/layout.tsx +13 -3
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +1 -1
- package/templates/webapp/src/app/global-error.tsx +1 -2
- package/templates/webapp/src/components/Header.tsx +23 -2
- package/templates/webapp/src/server/api/routers/access.ts +2 -2
- package/templates/webapp/src/services/access/AppAccessControl.ts +23 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +2 -2
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +6 -10
- package/templates/webapp/src/app/(app)/admin/_lib/PrincipalRoleTable.tsx +0 -113
- package/templates/webapp/src/app/(app)/admin/_lib/accessAdmin.ts +0 -85
- package/templates/webapp/src/app/(app)/admin/groups/page.tsx +0 -117
- 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
|
|
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
|
+
"version": "3.4.1",
|
|
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.
|
|
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
|
|
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,
|
|
57
|
-
|
|
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
|
-
|
|
8
|
+
admins:
|
|
9
9
|
- userId: "00000000-0000-0000-0000-000000000000"
|
|
10
|
-
|
|
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: []
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./db": "./src/drizzle/db.ts",
|
|
10
|
+
"./principals": "./src/principals.ts",
|
|
10
11
|
"./schema": "./src/drizzle/schema/index.ts"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
"db:setup-and-migrate": "pnpm db:setup && pnpm db:migrate"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@percepta/auth": "0.1.
|
|
21
|
+
"@percepta/auth": "0.1.1",
|
|
21
22
|
"drizzle-orm": "^0.45.2"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { listAuthPrincipals } from "@percepta/auth/drizzle";
|
|
2
|
+
import { db } from "./drizzle/db";
|
|
3
|
+
import { groups, users } from "./drizzle/schema";
|
|
4
|
+
|
|
5
|
+
export function listPrincipals() {
|
|
6
|
+
return listAuthPrincipals({
|
|
7
|
+
db,
|
|
8
|
+
groupsTable: groups,
|
|
9
|
+
usersTable: users,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -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
|
|
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
|
|
112
|
+
Provides centralized formatter, TypeScript, bundler, and Vitest configuration.
|
|
113
113
|
|
|
114
114
|
```js
|
|
115
115
|
// eslint.config.mjs
|
|
116
|
-
import
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
@@ -98,6 +98,32 @@ src/
|
|
|
98
98
|
| `pnpm db:setup-and-migrate` | Setup and migrate database |
|
|
99
99
|
| `pnpm db:studio` | Setup/migrate the database, then open Drizzle Studio |
|
|
100
100
|
| `pnpm db:seed` | Seed default shared-auth dev users and local access grants |
|
|
101
|
+
| `pnpm test:e2e:install` | Install the Chromium browser used by Playwright |
|
|
102
|
+
| `pnpm test:e2e` | Run Playwright e2e tests after local setup |
|
|
103
|
+
| `pnpm test:e2e:ui` | Run Playwright e2e tests in UI mode after local setup |
|
|
104
|
+
|
|
105
|
+
## End-to-End Tests
|
|
106
|
+
|
|
107
|
+
This template includes focused Playwright coverage for seeded RBAC flows. Before
|
|
108
|
+
the first run, install the browser binary:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm test:e2e:install
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then run:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm test:e2e
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The e2e script runs local setup first, then starts the Next.js dev server via
|
|
121
|
+
Playwright. To point the tests at an already-running app, set
|
|
122
|
+
`PLAYWRIGHT_BASE_URL`, for example:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
PLAYWRIGHT_BASE_URL=http://localhost:3000 pnpm test:e2e
|
|
126
|
+
```
|
|
101
127
|
|
|
102
128
|
## Logging
|
|
103
129
|
|
|
@@ -134,11 +160,13 @@ BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
|
|
|
134
160
|
BETTER_AUTH_URL=http://localhost:3000
|
|
135
161
|
```
|
|
136
162
|
|
|
137
|
-
To create
|
|
163
|
+
To create dev users:
|
|
138
164
|
```bash
|
|
139
165
|
pnpm db:seed
|
|
140
|
-
# Creates admin@example.com / password as
|
|
141
|
-
# Creates
|
|
166
|
+
# Creates customer-admin@example.com / password as a customer admin
|
|
167
|
+
# Creates app-admin@example.com / password as an app admin
|
|
168
|
+
# Creates app-user@example.com / password as an app user
|
|
169
|
+
# Creates non-user@example.com / password with no app access
|
|
142
170
|
```
|
|
143
171
|
|
|
144
172
|
## 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
|
-
|
|
|
19
|
-
| App roles/resources | This app's
|
|
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.
|
|
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
|
-
|
|
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
|
|
60
|
+
The core application permission is:
|
|
65
61
|
|
|
66
62
|
```zed
|
|
67
|
-
permission
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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: ["
|
|
97
|
+
assignableRoles: ["hr_admin"],
|
|
105
98
|
},
|
|
106
99
|
} as const);
|
|
107
100
|
```
|
|
108
101
|
|
|
109
|
-
Role assignment is additive. Assigning
|
|
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->
|
|
124
|
-
permission view_private =
|
|
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>#
|
|
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
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { expect, test, type Page } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const password = "password";
|
|
7
|
+
|
|
8
|
+
const users = {
|
|
9
|
+
appAdmin: "app-admin@example.com",
|
|
10
|
+
appUser: "app-user@example.com",
|
|
11
|
+
customerAdmin: "customer-admin@example.com",
|
|
12
|
+
nonAppUser: "non-user@example.com",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
test.describe("RBAC access", () => {
|
|
16
|
+
test("customer admin can manage role mappings but cannot enter the main app", async ({
|
|
17
|
+
page,
|
|
18
|
+
}) => {
|
|
19
|
+
test.setTimeout(60_000);
|
|
20
|
+
await signIn(page, users.customerAdmin, "/admin?tab=assignments");
|
|
21
|
+
|
|
22
|
+
await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
|
|
23
|
+
await expect(page.getByRole("link", { name: "Admin" })).toBeVisible();
|
|
24
|
+
|
|
25
|
+
const userAssignments = page.getByRole("button", {
|
|
26
|
+
name: "User assignments",
|
|
27
|
+
});
|
|
28
|
+
const adminAssignments = page.getByRole("button", {
|
|
29
|
+
name: "Admin assignments",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(userAssignments).toHaveAttribute("aria-disabled", "false");
|
|
33
|
+
await expect(adminAssignments).toHaveAttribute("aria-disabled", "false");
|
|
34
|
+
|
|
35
|
+
await userAssignments.click();
|
|
36
|
+
await expect(page.getByPlaceholder("Search users or groups")).toBeVisible();
|
|
37
|
+
|
|
38
|
+
let shouldResetSeedData = false;
|
|
39
|
+
try {
|
|
40
|
+
await page.getByRole("option", { name: /App Non User/ }).click();
|
|
41
|
+
shouldResetSeedData = true;
|
|
42
|
+
await expect(userAssignments).toContainText("App Non User");
|
|
43
|
+
await expect(userAssignments).toHaveAttribute("aria-busy", "false");
|
|
44
|
+
|
|
45
|
+
await page.reload();
|
|
46
|
+
await expect(
|
|
47
|
+
page.getByRole("button", { name: "User assignments" }),
|
|
48
|
+
).toContainText("App Non User");
|
|
49
|
+
} finally {
|
|
50
|
+
if (shouldResetSeedData) {
|
|
51
|
+
await resetSeedData();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await page.reload();
|
|
56
|
+
await expect(
|
|
57
|
+
page.getByRole("button", { name: "User assignments" }),
|
|
58
|
+
).not.toContainText("App Non User");
|
|
59
|
+
|
|
60
|
+
await page.goto("/");
|
|
61
|
+
await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
|
|
62
|
+
0,
|
|
63
|
+
);
|
|
64
|
+
await expectNotFound(page);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("app admin can see RBAC and main app but cannot edit default role mappings", async ({
|
|
68
|
+
page,
|
|
69
|
+
}) => {
|
|
70
|
+
await signIn(page, users.appAdmin, "/admin?tab=assignments");
|
|
71
|
+
|
|
72
|
+
await expect(page.getByRole("heading", { name: "RBAC" })).toBeVisible();
|
|
73
|
+
await expect(
|
|
74
|
+
page
|
|
75
|
+
.getByText("Only customer admins can edit default application roles.")
|
|
76
|
+
.first(),
|
|
77
|
+
).toBeVisible();
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
page.getByRole("button", { name: "User assignments" }),
|
|
81
|
+
).toHaveAttribute("aria-disabled", "true");
|
|
82
|
+
await expect(
|
|
83
|
+
page.getByRole("button", { name: "Admin assignments" }),
|
|
84
|
+
).toHaveAttribute("aria-disabled", "true");
|
|
85
|
+
|
|
86
|
+
await page.goto("/");
|
|
87
|
+
await expect(
|
|
88
|
+
page.getByRole("heading", { name: /Welcome to/ }),
|
|
89
|
+
).toBeVisible();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("app user can enter the main app but cannot reach admin", async ({
|
|
93
|
+
page,
|
|
94
|
+
}) => {
|
|
95
|
+
await signIn(page, users.appUser, "/");
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
page.getByRole("heading", { name: /Welcome to/ }),
|
|
99
|
+
).toBeVisible();
|
|
100
|
+
await expect(page.getByRole("link", { name: "Admin" })).toHaveCount(0);
|
|
101
|
+
|
|
102
|
+
await page.goto("/admin");
|
|
103
|
+
await expect(page.getByRole("heading", { name: "RBAC" })).toHaveCount(0);
|
|
104
|
+
await expectNotFound(page);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("non app user cannot enter the app", async ({ page }) => {
|
|
108
|
+
await signIn(page, users.nonAppUser, "/");
|
|
109
|
+
|
|
110
|
+
await expect(page.getByRole("heading", { name: /Welcome to/ })).toHaveCount(
|
|
111
|
+
0,
|
|
112
|
+
);
|
|
113
|
+
await expect(page.getByRole("heading", { name: "RBAC" })).toHaveCount(0);
|
|
114
|
+
await expectNotFound(page);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function signIn(page: Page, email: string, callbackUrl: string) {
|
|
119
|
+
await page.goto(
|
|
120
|
+
`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`,
|
|
121
|
+
);
|
|
122
|
+
await page.getByLabel("Email").fill(email);
|
|
123
|
+
await page.getByLabel("Password").fill(password);
|
|
124
|
+
await page.getByRole("button", { name: "Sign In" }).click();
|
|
125
|
+
await page.waitForURL((url) => url.pathname !== "/auth/signin");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function expectNotFound(page: Page) {
|
|
129
|
+
await expect(
|
|
130
|
+
page.getByText(/This page could not be found|404/i).first(),
|
|
131
|
+
).toBeVisible();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function resetSeedData() {
|
|
135
|
+
await execFileAsync("pnpm", ["db:seed"], { cwd: process.cwd() });
|
|
136
|
+
}
|
|
@@ -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,10 @@ export default [
|
|
|
63
91
|
"n/no-process-env": "off",
|
|
64
92
|
},
|
|
65
93
|
},
|
|
66
|
-
|
|
94
|
+
{
|
|
95
|
+
files: ["playwright.config.ts"],
|
|
96
|
+
rules: {
|
|
97
|
+
"n/no-process-env": "off",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
);
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"deploy:percepta-test": "tsx ./scripts/deploy-percepta-test.ts",
|
|
28
28
|
"deploy:percepta-test:pr": "tsx ./scripts/open-ryvn-deploy-pr.ts",
|
|
29
29
|
"test": "vitest run",
|
|
30
|
+
"test:e2e": "pnpm run setup && playwright test",
|
|
31
|
+
"test:e2e:install": "playwright install chromium",
|
|
32
|
+
"test:e2e:ui": "pnpm run setup && playwright test --ui",
|
|
30
33
|
"test:watch": "vitest"
|
|
31
34
|
},
|
|
32
35
|
"dependencies": {
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
|
|
56
59
|
"@opentelemetry/sdk-node": "^0.203.0",
|
|
57
60
|
"@__REPO_NAME__/auth": "workspace:*",
|
|
58
|
-
"@percepta/access-control": "0.
|
|
61
|
+
"@percepta/access-control": "0.6.1",
|
|
59
62
|
"@percepta/design": "0.3.2",
|
|
60
63
|
"@percepta/logger": "0.0.6",
|
|
61
64
|
"@percepta/next-utils": "0.1.0",
|
|
@@ -102,7 +105,9 @@
|
|
|
102
105
|
"zod": "^4.1.5"
|
|
103
106
|
},
|
|
104
107
|
"devDependencies": {
|
|
108
|
+
"@eslint/js": "^9.18.0",
|
|
105
109
|
"@next/eslint-plugin-next": "^15.3.5",
|
|
110
|
+
"@playwright/test": "^1.58.2",
|
|
106
111
|
"@percepta/build": "0.4.0",
|
|
107
112
|
"@tailwindcss/postcss": "^4.1.11",
|
|
108
113
|
"@types/formidable": "^3.4.5",
|
|
@@ -121,9 +126,11 @@
|
|
|
121
126
|
"eslint-plugin-n": "^17.23.1",
|
|
122
127
|
"eslint-plugin-react": "^7.37.4",
|
|
123
128
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
129
|
+
"globals": "^15.14.0",
|
|
124
130
|
"husky": "^9.1.7",
|
|
125
131
|
"tailwindcss": "^4.0.12",
|
|
126
132
|
"typescript": "^5.7.3",
|
|
133
|
+
"typescript-eslint": "^8.33.0",
|
|
127
134
|
"vitest": "^3.2.1",
|
|
128
135
|
"yargs": "^17.7.2"
|
|
129
136
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000);
|
|
4
|
+
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`;
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: "./e2e",
|
|
8
|
+
fullyParallel: false,
|
|
9
|
+
forbidOnly: Boolean(process.env.CI),
|
|
10
|
+
retries: process.env.CI ? 2 : 0,
|
|
11
|
+
reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : "list",
|
|
12
|
+
use: {
|
|
13
|
+
baseURL,
|
|
14
|
+
trace: "on-first-retry",
|
|
15
|
+
},
|
|
16
|
+
webServer: process.env.PLAYWRIGHT_BASE_URL
|
|
17
|
+
? undefined
|
|
18
|
+
: {
|
|
19
|
+
command: `pnpm dev -- --hostname 127.0.0.1 --port ${port}`,
|
|
20
|
+
env: {
|
|
21
|
+
SKIP_INNGEST_SYNC: "true",
|
|
22
|
+
},
|
|
23
|
+
reuseExistingServer: !process.env.CI,
|
|
24
|
+
timeout: 120_000,
|
|
25
|
+
url: baseURL,
|
|
26
|
+
},
|
|
27
|
+
projects: [
|
|
28
|
+
{
|
|
29
|
+
name: "chromium",
|
|
30
|
+
use: { ...devices["Desktop Chrome"] },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|