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
+15 -17
View File
@@ -46,10 +46,7 @@ CREATE TABLE IF NOT EXISTS public.studies (
phase character varying(50),
status character varying(20) NOT NULL DEFAULT 'DRAFT',
is_locked boolean NOT NULL DEFAULT false,
visit_interval_days integer,
visit_total integer,
visit_window_start_offset integer,
visit_window_end_offset integer,
visit_schedule jsonb NOT NULL DEFAULT '[]'::jsonb,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_studies_code UNIQUE (code),
@@ -112,6 +109,7 @@ CREATE TABLE IF NOT EXISTS public.subjects (
screening_date date,
consent_date date,
enrollment_date date,
baseline_date date,
completion_date date,
drop_reason text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
@@ -591,7 +589,7 @@ ON CONFLICT (email) DO UPDATE SET
updated_at = EXCLUDED.updated_at;
INSERT INTO public.studies (
id, code, name, sponsor, protocol_no, phase, status, visit_interval_days, visit_total, visit_window_start_offset, visit_window_end_offset, created_by, created_at
id, code, name, sponsor, protocol_no, phase, status, visit_schedule, created_by, created_at
) VALUES (
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'DEMO-CTMS',
@@ -600,10 +598,12 @@ INSERT INTO public.studies (
'DP-001',
'Phase II',
'ACTIVE',
7,
3,
-2,
2,
'[
{"visit_code": "基线访视", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0},
{"visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 2, "window_after_days": 2},
{"visit_code": "V2", "baseline_offset_days": 15, "window_before_days": 2, "window_after_days": 2},
{"visit_code": "V3", "baseline_offset_days": 23, "window_before_days": 3, "window_after_days": 3}
]'::jsonb,
(SELECT id FROM public.users WHERE email = 'admin@example.com'),
'2025-01-06 08:00:00+00'
) ON CONFLICT (code) DO UPDATE SET
@@ -612,10 +612,7 @@ INSERT INTO public.studies (
protocol_no = EXCLUDED.protocol_no,
phase = EXCLUDED.phase,
status = EXCLUDED.status,
visit_interval_days = EXCLUDED.visit_interval_days,
visit_total = EXCLUDED.visit_total,
visit_window_start_offset = EXCLUDED.visit_window_start_offset,
visit_window_end_offset = EXCLUDED.visit_window_end_offset,
visit_schedule = EXCLUDED.visit_schedule,
created_by = EXCLUDED.created_by;
INSERT INTO public.sites (
@@ -753,17 +750,18 @@ INSERT INTO public.training_authorizations (
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.subjects (
id, study_id, site_id, subject_no, status, screening_date, consent_date, enrollment_date, completion_date, drop_reason, created_at, updated_at
id, study_id, site_id, subject_no, status, screening_date, consent_date, enrollment_date, baseline_date, completion_date, drop_reason, created_at, updated_at
) VALUES
('11111111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-001', 'ENROLLED', '2025-01-05', '2025-01-06', '2025-01-10', NULL, NULL, '2025-01-10 10:00:00+00', '2025-01-10 10:00:00+00'),
('22222222-3333-4444-5555-666666666666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'SUBJ-002', 'SCREENING', '2025-01-12', '2025-01-13', NULL, NULL, NULL, '2025-01-12 10:00:00+00', '2025-01-12 10:00:00+00'),
('33333333-4444-5555-6666-777777777777', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-003', 'COMPLETED', '2024-12-20', '2024-12-21', '2024-12-28', '2025-02-05', NULL, '2025-02-05 10:00:00+00', '2025-02-05 10:00:00+00')
('11111111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-001', 'ENROLLED', '2025-01-05', '2025-01-06', '2025-01-10', '2025-01-10', NULL, NULL, '2025-01-10 10:00:00+00', '2025-01-10 10:00:00+00'),
('22222222-3333-4444-5555-666666666666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'SUBJ-002', 'SCREENING', '2025-01-12', '2025-01-13', NULL, NULL, NULL, NULL, '2025-01-12 10:00:00+00', '2025-01-12 10:00:00+00'),
('33333333-4444-5555-6666-777777777777', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-003', 'COMPLETED', '2024-12-20', '2024-12-21', '2024-12-28', '2024-12-28', '2025-02-05', NULL, '2025-02-05 10:00:00+00', '2025-02-05 10:00:00+00')
ON CONFLICT (study_id, subject_no) DO UPDATE SET
site_id = EXCLUDED.site_id,
status = EXCLUDED.status,
screening_date = EXCLUDED.screening_date,
consent_date = EXCLUDED.consent_date,
enrollment_date = EXCLUDED.enrollment_date,
baseline_date = EXCLUDED.baseline_date,
completion_date = EXCLUDED.completion_date,
drop_reason = EXCLUDED.drop_reason,
updated_at = EXCLUDED.updated_at;