You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(project): reconcile plan and review with the shipped credential design
Address CodeRabbit review findings on the planning artifacts of PR #5141:
- The plan still documented the client-supplied `metadata` contract that
was the forgery vector removed by #5173, so an implementer following it
could reintroduce the vulnerability. Describe the shipped server-computed
snapshot design instead.
- Point file references at the shipped layout (verify/index.tsx,
CourseVerifiableCredentialsModal.tsx); the old ones do not exist.
- Replace machine-local file:///Users/... links with repo-relative links.
- Add the Prisma client regeneration step and require agent-browser
before/after screenshots for the frontend slices.
- Correct the Slice 2 progress entry that claimed verified tests while the
suite was failing.
- Label the 2026-07-07 review as a pre-remediation baseline and annotate its
checklist and sign-off gate with current status, preserving the original
findings as historical record.
Copy file name to clipboardExpand all lines: project/2026-07-06-export-assessment-performance-insights-plan.md
+27-21Lines changed: 27 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,13 +12,16 @@ MR/PR: unknown
12
12
## Non-Goals
13
13
* No PDF server-side generation using Puppeteer (local print-to-PDF / standalone HTML meets medical faculty needs).
14
14
* No support for decentralized W3C DID wallets in v1.
15
-
* No additional database models for achievements or course certificates (schema supports them generically via `metadata` JSON, but views are deferred until required).
15
+
* No additional database models for achievements or course certificates (the `VerifiableCredential` model supports them generically via a versioned `snapshot` JSON column, but views are deferred until required).
16
+
17
+
> **Note (post-#5173 remediation):** the original version of this plan described a design where the client supplied a `metadata` payload for the issued credential. That was a critical forgery vector — a client-supplied payload cannot be trusted as verified data — and was replaced by PR #5173 (commit `2c7acbbfa`) before this branch shipped. The sections below describe the **shipped** server-computed design. Do not reintroduce a client-supplied metadata/snapshot argument on the issuance mutation.
16
18
17
19
---
18
20
19
21
## Resolved Questions & Grill Findings
20
-
***GDPR Compliance**: The credential model cascades on participant deletion. When a student deletes their account or requests revocation, all active verification records are deleted or marked revoked.
21
-
***Tamper Proofing**: Employers verify records against the live database at `https://klicker.uzh.ch/verify/[token]`. Any modification of the printed HTML/PDF values by the student is detected.
22
+
***GDPR Compliance (resolved)**: `VerifiableCredential.participantId` has `onDelete: Cascade` to `Participant` (see `packages/prisma/src/prisma/schema/verification.prisma`). When a student's account is deleted, all of their issued credentials are deleted with it. This is the one part of the original GDPR claim that is settled by the schema.
23
+
* **Revocation policy (do not overclaim — see shipped behavior below)**: whether a lecturer revoking a credential should permanently block the student from getting an equivalent report again, or only invalidate that one snapshot, was flagged as an open product decision during the PR #5141 review (`project/2026-07-07-pr5141-review-export-assessment-performance-insights.md`, §2.6). The remediation in PR #5173 (`2c7acbbfa`) shipped a specific policy in `issueAssessmentReportInTransaction` (`packages/graphql/src/services/assessmentReports.ts`): re-issuing while the same claims (score/course, excluding the peer-comparison histogram) match a **revoked** record throws `ASSESSMENT_REPORT_REVOKED` and refuses to issue; re-issuing with **changed** underlying data (e.g. new results) is still allowed and supersedes the prior ACTIVE record. This is the mechanism actually running in production. There is no evidence in this repo of a separate, explicit product-owner sign-off on this exact policy beyond the engineering remediation — treat it as the current shipped behavior, not as a formally ratified product decision, until confirmed otherwise.
24
+
***Tamper Proofing**: Employers verify records against the live database via the public verification page (`apps/frontend-pwa/src/pages/verify/index.tsx`). The verification token travels in the URL **fragment** (`https://pwa.klicker.uzh.ch/verify#<token>`, read from `window.location.hash`), not a path segment, so it is never sent to the server as part of a plain page request; the lookup itself is a POST-based GraphQL call. Any modification of the printed HTML/PDF values by the student is detected because the server serves its own stored, hashed snapshot, not anything from the file.
22
25
***Offline Access**: The HTML export remains standalone and offline-functional. The QR code is rendered on the client canvas as a base64 Data URL and embedded directly into the HTML payload.
23
26
24
27
---
@@ -27,50 +30,51 @@ MR/PR: unknown
27
30
28
31
### Slice 1: Database Schema & Migration
29
32
***Do**:
30
-
* Create [`packages/prisma/src/prisma/schema/verification.prisma`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/packages/prisma/src/prisma/schema/verification.prisma) to define the `VerifiableCredential` model and`CredentialType` enum.
33
+
* Create [`packages/prisma/src/prisma/schema/verification.prisma`](../packages/prisma/src/prisma/schema/verification.prisma) to define the `VerifiableCredential` model, the`CredentialType` enum, and the `CredentialStatus` enum (`ACTIVE` / `REVOKED` / `SUPERSEDED`).
31
34
* Update `packages/prisma/src/prisma/schema/course.prisma` and `packages/prisma/src/prisma/schema/participant.prisma` to declare the back-relations.
32
35
***Check**:
33
36
* Run `pnpm run prisma:migrate` to create and apply the local database migration.
34
-
* Run `pnpm run prisma:sync` to mirror the schema to the analytics package.
35
-
* Run `pnpm run check` to verify Prisma client compiles successfully.
37
+
* Run `pnpm run prisma:sync` to mirror the schema to the analytics package (`apps/analytics`).
38
+
* Regenerate the Prisma client (part of `prisma:migrate`/`postinstall`, or `pnpm --filter @klicker-uzh/prisma generate` explicitly) — a schema edit is not complete until the generated client reflects it.
39
+
* Run `pnpm run check` to verify the codebase compiles against the regenerated client.
* Implement service functions in [`packages/graphql/src/services/verification.ts`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/packages/graphql/src/services/verification.ts) for credential issuance, retrieval, and revocation.
44
+
* Implement service functions for **server-side** snapshot computation, issuance, retrieval, and revocation in [`packages/graphql/src/services/assessmentReports.ts`](../packages/graphql/src/services/assessmentReports.ts) and [`packages/graphql/src/services/verification.ts`](../packages/graphql/src/services/verification.ts). The service computes the immutable `AssessmentReportSnapshotV1` payload itself (course, subject, scores, histogram) from the participant's own data — it never accepts a client-supplied snapshot/metadata argument — and hashes it with SHA-256 (`snapshotHash`) for tamper detection.
41
45
* Implement GraphQL schema types and resolvers in `packages/graphql/src/schema/verification.ts`:
42
-
*`Query.verifiableCredential(token)`: Public lookup (unauthenticated).
43
-
*`Query.courseVerificationRecords(courseId)`: Lecturer-only lookup (authenticated, checked against course ownership).
*`Mutation.revokeCredential(id)`: Lecturer revocation (authenticated, checked against course ownership).
46
+
*`Query.assessmentReportVerification(token)`: Public lookup (unauthenticated), redacts to `{status, issuedAt, snapshot: null}` unless the record is `ACTIVE`.
47
+
*`Query.courseAssessmentReportRecords(courseId, ...)` / `Query.courseAssessmentReportRecordCount(courseId)`: Lecturer-only lookup, `t.withAuth(asUserFullAccess)` plus a course-ADMIN `checkAccess` check.
48
+
*`Mutation.issueAssessmentReport(courseId)`: Participant issuance, `t.withAuth(asParticipant)`. No client-supplied payload — the resolver computes the snapshot itself. Idempotent: an unchanged snapshot returns the existing `ACTIVE` record; a changed snapshot supersedes it and issues a new `ACTIVE` record. Runs in a `Serializable` transaction with bounded retries on Prisma `P2034`.
49
+
*`Mutation.revokeAssessmentReport(id)`: Lecturer revocation, `t.withAuth(asUserFullAccess)` plus a course-ADMIN `checkAccess` check performed **before** existence is revealed (unauthorized and non-existent both return `NOT_FOUND`).
46
50
* Run codegen to update GraphQL client bindings.
47
-
* Create unit tests in `packages/graphql/test/verification.test.ts`.
51
+
* Create unit tests in `packages/graphql/test/assessmentReports.test.ts` and `packages/graphql/test/verification.test.ts`.
48
52
***Check**:
49
53
* Run `pnpm --filter @klicker-uzh/graphql generate` (codegen).
50
54
* Run `pnpm --filter @klicker-uzh/graphql test` to verify all tests pass.
51
55
***Commit**: `feat(graphql): implement verifiable credentials queries and mutations`
* Update [`SuspendedAssessmentResults.tsx`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/apps/frontend-pwa/src/components/insights/assessmentResults/SuspendedAssessmentResults.tsx): Trigger the mutation to issue a credential upon export, generate the verification QR code using a client-side library (`react-qrcode-logo` or simple canvas), and pass it to `exportReport.ts`.
56
-
* Update [`exportReport.ts`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/apps/frontend-pwa/src/components/insights/assessmentResults/exportReport.ts) to append a UZH-branded footer containing the QR code image and the clickable verification URL.
* Update [`SuspendedAssessmentResults.tsx`](../apps/frontend-pwa/src/components/insights/assessmentResults/SuspendedAssessmentResults.tsx): Trigger the `issueAssessmentReport(courseId)`mutation to issue a credential upon export, generate the verification QR code, and pass the returned token to `exportReport.ts`.
60
+
* Update [`exportReport.ts`](../apps/frontend-pwa/src/components/insights/assessmentResults/exportReport.ts) to append a UZH-branded footer containing the QR code image and the clickable verification URL.
61
+
* Create page [`verify/index.tsx`](../apps/frontend-pwa/src/pages/verify/index.tsx) (the token is read from the URL **fragment** via `window.location.hash`, not a `[token]` path segment or query string, so it is never sent to the server in a plain page request):
58
62
* Displays a large success banner: **"Status: Verifiziert / Verified"** matching UZH Corporate Design.
59
63
* Renders the scores cards, percentile box, and dynamic Recharts points histogram.
60
-
* If revoked, displays a prominent warning: **"Ungültig / Invalid (Widerrufen)"**.
64
+
* If revoked or superseded, displays a prominent warning banner instead of the redacted scores.
61
65
***Check**:
62
66
* Run `pnpm --filter @klicker-uzh/frontend-pwa build` to verify compiling.
63
-
*Export a performance report, open the HTML file, scan the QR code, and verify it directs to the correct URL.
67
+
***Required**: verify with `agent-browser` (per repo rules, `.agents/skills/agent-browser/SKILL.md`) — capture before/after screenshots of the export flow and the verification page (valid, revoked/superseded, malformed-token states). A successful build and a manual QR scan are not sufficient verification on their own.
64
68
***Commit**: `feat(frontend-pwa): implement student export footer and verification portal`
65
69
66
70
### Slice 4: Lecturer UI (Manage Portal)
67
71
***Do**:
68
72
* Add translations in `de.ts` and `en.ts` for all verification strings.
69
-
* Create `IssuedCredentialsModal.tsx` in `apps/frontend-manage/src/components/courses/modals/IssuedCredentialsModal.tsx`.
70
-
* Add the "Issued Credentials" button in [`results.tsx`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/apps/frontend-manage/src/pages/courses/%5Bid%5D/assessment/results.tsx) to trigger the modal.
73
+
* Create [`CourseVerifiableCredentialsModal.tsx`](../apps/frontend-manage/src/components/courses/CourseVerifiableCredentialsModal.tsx) and [`CourseVerifiableCredentialsList.tsx`](../apps/frontend-manage/src/components/courses/CourseVerifiableCredentialsList.tsx) in `apps/frontend-manage/src/components/courses/`.
74
+
* Add the "Issued Credentials" button in [`results.tsx`](../apps/frontend-manage/src/pages/courses/%5Bid%5D/assessment/results.tsx) to trigger the modal.
71
75
***Check**:
72
76
* Run `pnpm --filter @klicker-uzh/frontend-manage build` to verify compiling.
73
-
*Open the lecturer dashboard, verify the modal lists issued credentials, click "Revoke", and verify the status updates in the UI and DB immediately.
77
+
*Verify with `agent-browser`: open the lecturer dashboard, confirm the modal lists issued/revoked records, click "Revoke", and confirm the status updates in the UI and DB. Capture before/after screenshots per repo rules.
74
78
***Commit**: `feat(frontend-manage): add lecturer credentials dashboard and revocation UI`
75
79
76
80
### Slice 5: Quality Assurance & Verification
@@ -87,7 +91,9 @@ MR/PR: unknown
87
91
88
92
***2026-07-06**: Plan written, reviewed, simplified, and committed to project directory.
89
93
***2026-07-06**: Slice 1 completed. Added `VerifiableCredential` model and `CredentialType` enum, ran dev database migrations, synced schemas, and verified successful compilation.
90
-
***2026-07-06**: Slice 2 completed. Created backend verification services and GraphQL resolvers/queries/mutations for issuing, resolving, listing, and revoking credentials. Added and verified comprehensive Vitest unit tests.
94
+
***2026-07-06**: Slice 2 completed. Created backend verification services and GraphQL resolvers/queries/mutations for issuing, resolving, listing, and revoking credentials. Added Vitest unit tests.
95
+
***Correction (added 2026-07-17)**: this entry originally read "Added and verified comprehensive Vitest unit tests." That overclaimed — the 2026-07-07 review (`project/2026-07-07-pr5141-review-export-assessment-performance-insights.md`, §1, §2.3) ran the suite live and found 1 of 4 tests failing at that time (`does not resolve a revoked credential`). The tests were added on 2026-07-06 but had not actually been run green before that claim was written.
96
+
***2026-07-17**: PR #5173 (`2c7acbbfa`) remediated the design (client-supplied metadata → server-computed, hashed, versioned snapshot; added auth/enrollment checks; redacted public verification; fragment-based tokens) and rewrote the test suites (`packages/graphql/test/assessmentReports.test.ts`, `packages/graphql/test/verification.test.ts`, `playwright/tests/Z-credential-verification.spec.ts`). On the current PR head commit (`f769bec30`), CI's `test-graphql` and `test-playwright` jobs both pass (verified via `gh pr checks 5141`, 2026-07-17). This supersedes the 2026-07-06 test-status claim above; do not read the correction note as still describing the current state.
0 commit comments