feat(certifications): server actions + page guard
certifications.ts: create/update/delete/list cert types (gated by certifications.manage; delete refused once granted, archive instead), and grant/revoke/renew/listMemberCertifications/getMyCertifications (gated by certifications.grant, the Instructor capability). Grant verifies cert and target member are in-space and the cert is active, computes expiry from validity_months unless overridden, and relies on the partial unique index for one active grant per member+cert. Revoke is soft; nothing is ever deleted. getMyCertifications takes no params (member resolved from session; RLS restricts to own rows). certifications-guard.ts mirrors the forms guard for the admin pages. Barrel re-export and API_REFERENCE updated.
This commit is contained in:
@@ -899,3 +899,13 @@ Management actions are gated by `forms.manage` (checked via the `user_has_permis
|
||||
Onboarding integration (Phase 4, migration 027): a `space_onboarding_steps` step can have `step_type = 'form'` with `config.form_id`. The onboarding flow renders the form/waiver and submits via `submitForm`; `finishOnboarding` treats a required form step as satisfied if a submission for that form by the member already exists (any version — re-sign is non-blocking), and fails open if the configured form is missing/unpublished so a misconfiguration cannot trap members.
|
||||
|
||||
Retro-link (Phase 5): `claimMyAnonymousSubmissions()` is a safe, self-only, verified-email-gated action — it resolves the member/email from the caller's session (no trusted params), so a client can only ever link its own anonymous submissions, and only with a confirmed email (the locked decision). It is invoked best-effort from `joinSpace` (new membership, verified email) and `finishOnboarding`. `addMember`/`importMembers` are deliberately NOT auto-linked: those emails are admin-asserted, not verified, so auto-linking would violate the verified-email rule and risk mis-attributing a waiver; the admin uses the forms.manage `linkSubmissionsForMember` for those (the "admin manual-link" path).
|
||||
|
||||
### Certifications + Instructor: `certifications.ts` (migration 030)
|
||||
Cert-type actions are gated by `certifications.manage`; grant actions by `certifications.grant` (the Instructor capability). Both checked via the `user_has_permission` RPC; the `certifications` / `member_certifications` RLS independently enforces the same permission.
|
||||
- `createCertification(input)` / `updateCertification(input)` / `deleteCertification(input)` — manage. Name is unique per space (case-insensitive); friendly error on collision. `validity_months` null = never expires. `deleteCertification` is refused once the cert has been granted to anyone (archive via `updateCertification {is_active:false}` instead — grant history is immutable).
|
||||
- `listCertifications()` — readable by a manager OR a granter (the admin list and the per-member award panel both need the catalog); returns all cert types for the caller's space (RLS-scoped), active first.
|
||||
- `grantCertification(input)` — granter. Verifies the cert is in-space and active and the target member is in-space; computes `expires_at` from the cert's `validity_months` at grant time unless an explicit `expires_at` override is supplied; the partial unique index rejects a second active grant of the same cert to the same member with a friendly message.
|
||||
- `revokeCertification(input)` — granter. Soft revoke (`revoked_at`/`revoked_by`/`revoked_reason`); refused if already revoked. No row is ever deleted (no DELETE policy; history immutable).
|
||||
- `renewCertification(input)` — granter. Resets `granted_at` to now and recomputes expiry from the cert's current validity; a revoked grant is terminal (issue a fresh grant instead).
|
||||
- `listMemberCertifications({ memberId })` — manager or granter; all grants for one member (joined with the cert) for the per-member award panel.
|
||||
- `getMyCertifications()` — the signed-in member's own grants. No params: the member is resolved from the session and RLS independently restricts a member to their own rows (the `/me` view).
|
||||
|
||||
Reference in New Issue
Block a user