5.2 KiB
Digital Consent (Sprint 3)
This document describes the digital consent and student transfer approval implementation (Sprint 3).
Architecture overview
- Server: Node 20 + Express, SQLite via better-sqlite3.
- Frontend: React + Vite, SignatureModal component captures drawings to a PNG data URL.
- USSD: state machine + ussd service. PIN verification is bcrypt-based and persisted on the
usersrow (ussd_pin,ussd_failed_attempts,ussd_locked_until). - Audit: AuditService writes to
audit_logstable. Consent actions write audit entries.
Database schema
- New table:
transfer_consents(created by migrationserver/src/database/migrations/knex/2026073000000000_consent_columns.js). Key columns:- id (PK integer)
- uid (TEXT UNIQUE)
- student_id (FK -> users.id)
- parent_id (FK -> users.id)
- requested_by (TEXT)
- from_school_id (TEXT)
- target_school (TEXT)
- transfer_request_id (self FK)
- consent_status (TEXT) — values: 'pending','approved','rejected','expired'
- consent_method (TEXT) — 'WEB_SIGNATURE' | 'USSD_PIN'
- signed_at (DATETIME)
- signature_hash (TEXT)
- expires_at (DATETIME)
- created_at, updated_at
Indexes: student_id, parent_id, consent_status, plus a partial index for pending rows.
Notes: field names were chosen to align with existing code patterns (target_school used for the destination school, signed_at used for approval timestamp).
Web signature flow
- Admin initiates a transfer via POST /api/transfers/initiate — this creates a
transfer_consentspending row and sends instructions to the parent. - Parent (authenticated) opens
TransferConsentspage (GET /api/consents/pending) and opens theSignatureModalcomponent. - SignatureModal captures drawing to a PNG data URL and sends it to POST /api/transfers/:id/consent.
- Server builds a canonical payload and computes SHA-256 over the canonical string (see signature.service.canonicalize). The server stores only the hex hash in
signature_hashon the consent row along withsigned_at.
Security properties:
- The payload includes consent_id (uid), student_id, parent_id, both schools, and approved_at timestamp — so replaying an old PNG alone won't pass verification because the approved_at will differ.
- The signature hash is verified with constant-time comparison when needed (signature.service.verifyConsentHash).
- Parent/student relationship is validated against
parent_studentsbefore approval. - Atomic UPDATE ensures duplicate approvals are prevented (0 rows affected → conflict).
USSD approval flow
- Parent uses USSD and authenticates with student ID and 4-digit PIN.
- In the Main Menu choose "Transfer Approval"; the USSD state machine picks the next pending consent for the student.
- Parent enters PIN to approve. The PIN is verified using
ussd.service.verifyPin(bcrypt). On success the server computes the same SHA-256 payload and marks the consentapprovedwithconsent_method = 'USSD_PIN'. - The USSD flow returns
END Transfer approved successfully.on successful approval.
API endpoints
- POST /api/transfers/initiate — admin-only; creates pending consent.
- POST /api/transfers/:id/consent — authenticated user (parent or admin override); approves with web signature image in
signature_data_url. - GET /api/consents/pending — parent-only; lists pending consents for the authenticated parent.
- POST /api/consents/:id/reject — parent or admin; rejects a pending consent.
- Additional routes: GET /api/transfers/:id/consent and DELETE /api/transfers/:id/consent (for compatibility).
Signature hashing
- Canonical format: pipe-separated key=value pairs in this exact order:
consent_id, student_id, parent_id, from_school_id, to_school_id, approved_at. - Hash algorithm: SHA-256 hex digest.
- Generation and verification functions live in
server/src/services/signature.service.js.
Audit logging
- All consent actions write audit entries using
AuditServiceor the localwriteAudithelper. - Actions include:
consent_requested,consent_approved,consent_ussd_approved,consent_rejected,consent_expired_sweep. - Audit rows include user, entity_type, entity_id (consent uid), timestamp, and new_data metadata.
Testing instructions
-
Unit tests for signature hashing were added (Vitest) at
server/test/signature.spec.js. -
Run the server tests from the
server/folder:cd server npm install # if dependencies are not yet installed npm run test -
Integration tests for the full consent workflow are recommended (DB-backed); these require a test DB and fixtures. Create tests that run
node src/database/init.jsagainst a temporary DB path or use in-memory SQLite.
Open items / caveats
- A small USSD-to-service return-shape mismatch and an audit user reference bug were fixed in
server/src/services/consent.service.js(USSD approval now returns{ ok: true, consent }and audit user usesconsent.parent_id). - Full integration tests covering the transfer lifecycle, parent linkage negative cases, and PIN lockout flows are not yet present — recommended next work.
Contact
For changes to consent formats (canonicalization) or migration overwrite, coordinate with the ops team — existing signed consents rely on the exact canonicalization string.