feat: harden auth and study workflows

- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads

- add login challenge replay protection, production RSA key validation, and auth tests

- wire compose to environment-driven dev/prod settings without committing local secrets

- update setup-config smoke scripts and Postman docs for encrypted login

- add visit schedule migrations/tests and update study/subject setup workflows
This commit is contained in:
Cheng Zhou
2026-05-08 22:13:12 +08:00
parent a7bbcaa5dc
commit 74feca4467
47 changed files with 2423 additions and 534 deletions
+2 -7
View File
@@ -1,7 +1,7 @@
import uuid
from datetime import date, datetime
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, JSON, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -32,14 +32,9 @@ class Study(Base):
planned_enrollment_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
enrollment_monthly_goal_note: Mapped[str | None] = mapped_column(Text, nullable=True)
enrollment_stage_breakdown: Mapped[str | None] = mapped_column(Text, nullable=True)
summary_note: Mapped[str | None] = mapped_column(Text, nullable=True)
objective_note: Mapped[str | None] = mapped_column(Text, nullable=True)
phase: Mapped[str | None] = mapped_column(String(50), nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT")
is_locked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
visit_interval_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
visit_total: Mapped[int | None] = mapped_column(Integer, nullable=True)
visit_window_start_offset: Mapped[int | None] = mapped_column(Integer, nullable=True)
visit_window_end_offset: Mapped[int | None] = mapped_column(Integer, nullable=True)
visit_schedule: Mapped[list[dict]] = mapped_column(JSON, nullable=False, default=list)
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
+1
View File
@@ -20,6 +20,7 @@ class Subject(Base):
screening_date: Mapped[date | None] = mapped_column(Date, nullable=True)
consent_date: Mapped[date | None] = mapped_column(Date, nullable=True)
enrollment_date: Mapped[date | None] = mapped_column(Date, nullable=True)
baseline_date: Mapped[date | None] = mapped_column(Date, nullable=True)
completion_date: Mapped[date | None] = mapped_column(Date, nullable=True)
drop_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())