Skip to content

19 Share With Doctor Architecture

Anubha Parashar edited this page Oct 10, 2026 · 1 revision

Share With Doctor lets a guardian hand a clinician exactly the records they choose and nothing else (internal code name: BabyCare). The guardian can share in three ways: download a ZIP package, download a clinician PDF summary, or create an expiring, revocable doctor link that may be PIN-protected. The doctor opens the link in a public page (web/doctor.html) that needs no account. That page can reach only the documents in the package, addressed by their position in the package, never by database id.

Responsibilities

  • Resolve a selection (preset, condition, categories, dates, manual list or complete) using documented links only, and show the exact document list before anything is generated.
  • Build packages whose file names contain only date, title and bundle reference: no database ids, storage names or tokens.
  • Label every summary statement with its provenance.
  • Issue links with a random token (only its SHA-256 is stored), an optional PBKDF2-hashed PIN, a fixed expiry and revocation.
  • Log every access without clinical content.

Inputs and outputs

Endpoint Caller Purpose
GET /api/share/presets guardian presets, categories, expiry choices
POST /api/patients/{pid}/share/preview guardian document list, record counts, unverified-row count, warnings
POST or GET …/share/package.zip guardian ZIP package (GET takes a base64url spec for mobile share sheets)
POST or GET …/share/summary.pdf guardian clinician summary PDF
POST …/share/links primary guardian create a link. Returns the token once and the path /doctor.html#t=<token>
GET …/share/links, GET /api/share/links/{sid}/log guardian link list with status, access log
DELETE /api/share/links/{sid} primary guardian revoke
GET /api/share/public/{token} doctor package metadata and document list
GET /api/share/public/{token}/document/{n} doctor the n-th package document
GET /api/share/public/{token}/summary.pdf, …/package.zip doctor summary, or ZIP when downloads are allowed

Scope (ShareSpec): mode is one of complete, condition, categories, dates, manual or preset; presets are ecoli, latest_hospitalization, antibiotics, latest_labs, vaccination and complete. The spec also accepts episode_ids, categories (validated against CATEGORY_CHOICES), date_from/date_to, explicit document_ids and exclude_document_ids, include_pending and daily_care_days (≤ 90). Link policy (LinkIn): expiry of 1h, 24h, 3d or 7d; a pin of 4–12 digits; view_only; allow_download.

Link lifecycle

flowchart TD
  O["Owner<br/>primary guardian"] --> S["Select scope<br/>preset, condition, categories, dates, manual"]
  S --> PV["Preview<br/>exact document list"]
  PV --> G["Generate package<br/>resolved selection stored encrypted"]
  G --> T["Expiring token<br/>256-bit, only SHA-256 stored"]
  T --> P{"Optional PIN"}
  P -->|"set"| PH["PBKDF2 hash with salt"]
  P -->|"not set"| L["Link sent<br/>doctor.html with token in fragment"]
  PH --> L
  L --> D["Doctor opens link"]
  D --> CHK{"Token valid, not revoked,<br/>not expired, PIN correct"}
  CHK -->|"no"| DEN["404 or 410 or 401 or 429<br/>logged as denied, expired_hit, pin_failed"]
  CHK -->|"yes"| SC["Scoped access<br/>documents by package index only"]
  SC --> AU["Audit<br/>access log and counter"]
  AU --> R["Revoke<br/>revoked_at set, later hits get 410"]
  O --> R
Loading

Token and PIN handling

  • secrets.token_urlsafe(32) makes the token. SharePackage.token_hash holds sha256(token), and the plaintext is returned once in the create response. The link list never includes it ('token' not in listing[0]).
  • The doctor page reads the token from the URL fragment (#t=), which browsers do not send with the page request. The page then calls /api/share/public/{token}, so the token does appear in API request paths.
  • The PIN is hashed with security.hash_password (PBKDF2-HMAC-SHA256, 390,000 iterations) and a 16-byte random salt. It is compared with hmac.compare_digest and sent in the X-Share-Pin header, never in the URL.
  • After 8 pin_failed events within 15 minutes, the link answers 429 until the window passes.
  • Tokens shorter than 20 or longer than 120 characters return 404 without a database lookup.

Expiry and revocation

expires_at = creation time + 1, 24, 72 or 168 hours. _open_package checks revocation first (410, logged denied), then expiry (410, logged expired_hit), then the PIN. package_json reports active, expired or revoked. Only the primary guardian (patient.user_id == user.id) can create or revoke links; caregivers with shared access cannot.

Package-relative document indexes

entries_for() numbers the selected documents 1..N and names each one Folder/<record_date>_<title-slug>[_<bundle ref>].<ext>, with folders Labs, Imaging, Prescriptions, Hospital, Vaccinations and Other. The public endpoint /document/{n} looks up that index inside the stored selection. An index outside the package returns 404 Not in this package. The public JSON has no id field and no storage names, and the tests check this explicitly. A doctor's token gives no access to the authenticated /api/patients/... or /api/documents/... routes (401).

ZIP package

build_zip() writes the following under HealthBook_Share_<date>/:

Entry Content
<Folder>/<file> decrypted originals of the selected documents only
Doctor_Summary.pdf clinician summary (when include_summary)
INDEX.html table of files with a noindex,nofollow meta tag
manifest.csv file, folder, date, title, type, bundle ref, pages, SHA-256 of each original

Status labels in summaries

build_summary_pdf() ("Medical Summary for Clinician") opens with a legend, and every line carries one of these labels plus the package file it comes from:

Label Meaning
DOCUMENTED on a clinician document, or verified against the original page
PARENT REPORTED entered by the caregiver
OCR UNVERIFIED transcribed, not yet checked
PRESCRIBED a prescription or discharge plan; not proof of administration
CONFIRMED ADMINISTERED a dated administration record exists
UNKNOWN administration not documented either way
PENDING VERIFICATION a lifetime-health row awaiting the caregiver check
REFERENCE ONLY, NOT RECEIVED (culture panel entry) reference schedules, and susceptibility entries that are not medicines

The daily-care section comes from care.care_export, and each of its lines is labelled PARENT REPORTED. The summary PDF is drawn with PyMuPDF base fonts, and its text is passed through _latin(), which replaces characters outside Latin-1.

Access log

ShareAccessLog events: created, opened, document, package, summary, pin_failed, revoked, expired_hit, denied. Each row stores the time, a SHA-256 hash of the client IP, and a short policy-only detail (for example "2 documents; expiry 24h; pin yes"). Tests confirm that no profile name or condition ever appears in the log. Successful opens increment access_count and set last_accessed_at. Guardian actions are also written to the main Audit table.

Security boundaries

Public responses set X-Robots-Tag: noindex, nofollow, noarchive, Cache-Control: no-store and Referrer-Policy: no-referrer. In view_only mode (or when downloads are off), documents are served inline and package.zip returns 403. scope_json, label and reason are stored with EncryptedText.

Clients

web/share.js and mobile/ShareScreen.tsx provide scope selection, preview, link creation, the link list with status and access log, and revoke. The mobile app copies the link with expo-clipboard or sends it through the native share sheet. web/doctor.js renders the doctor page: it asks for the PIN on 401, shows the error text on 404/410, and fetches files as blobs so the PIN header can be attached.

Implementation

api/share.py, api/share_models.py, api/security.py (hash_password), api/export_model.py (shares resolve), web/share.js, web/doctor.html, web/doctor.js, mobile/ShareScreen.tsx, tests/test_share.py.

Related pages

PDF Export Architecture · Security and Privacy Model · Authentication and Session Security · Encryption and Private Storage · API Design · Limitations

Clone this wiki locally