Skip to content

Commit bab745b

Browse files
committed
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.
1 parent f769bec commit bab745b

2 files changed

Lines changed: 58 additions & 30 deletions

File tree

project/2026-07-06-export-assessment-performance-insights-plan.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ MR/PR: unknown
1212
## Non-Goals
1313
* No PDF server-side generation using Puppeteer (local print-to-PDF / standalone HTML meets medical faculty needs).
1414
* 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.
1618
1719
---
1820

1921
## 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.
2225
* **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.
2326

2427
---
@@ -27,50 +30,51 @@ MR/PR: unknown
2730

2831
### Slice 1: Database Schema & Migration
2932
* **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`).
3134
* Update `packages/prisma/src/prisma/schema/course.prisma` and `packages/prisma/src/prisma/schema/participant.prisma` to declare the back-relations.
3235
* **Check**:
3336
* 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.
3640
* **Commit**: `feat(prisma): add verifiable credential schema`
3741

3842
### Slice 2: Backend Service & GraphQL API
3943
* **Do**:
40-
* 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.
4145
* 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).
44-
* `Mutation.issueCredential(courseId, type, metadata)`: Student issuance (authenticated).
45-
* `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`).
4650
* 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`.
4852
* **Check**:
4953
* Run `pnpm --filter @klicker-uzh/graphql generate` (codegen).
5054
* Run `pnpm --filter @klicker-uzh/graphql test` to verify all tests pass.
5155
* **Commit**: `feat(graphql): implement verifiable credentials queries and mutations`
5256

5357
### Slice 3: Student PWA Export & Verification Portal
5458
* **Do**:
55-
* 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.
57-
* Create page [`verify/[token].tsx`](file:///Users/roland/.gemini/antigravity/worktrees/klicker-uzh/export-assessment-performance-insights/apps/frontend-pwa/src/pages/verify/%5Btoken%5D.tsx):
59+
* 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):
5862
* Displays a large success banner: **"Status: Verifiziert / Verified"** matching UZH Corporate Design.
5963
* 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.
6165
* **Check**:
6266
* 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.
6468
* **Commit**: `feat(frontend-pwa): implement student export footer and verification portal`
6569

6670
### Slice 4: Lecturer UI (Manage Portal)
6771
* **Do**:
6872
* 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.
7175
* **Check**:
7276
* 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.
7478
* **Commit**: `feat(frontend-manage): add lecturer credentials dashboard and revocation UI`
7579

7680
### Slice 5: Quality Assurance & Verification
@@ -87,7 +91,9 @@ MR/PR: unknown
8791

8892
* **2026-07-06**: Plan written, reviewed, simplified, and committed to project directory.
8993
* **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.
9197

9298
---
9399

0 commit comments

Comments
 (0)