ProjectMed ProjectMed Wiki

Technical Documentation

A detailed look at the architecture, security model, and infrastructure behind ProjectMed. Written for IT staff, technical partners, and stakeholders.

技術文件

深入了解 ProjectMed 背後的架構、安全模型和基礎設施。為 IT 人員、技術合作夥伴和利害關係人撰寫。

Documentación Técnica

Una mirada detallada a la arquitectura, el modelo de seguridad y la infraestructura de ProjectMed. Escrita para personal de TI, socios técnicos y partes interesadas.

1. Architecture Overview

System Diagram

BrowserUser's device
VercelReact + Vite
RailwayFastAPI (Python)
SupabasePostgreSQL

ProjectMed follows a standard three-tier architecture: a single-page application (SPA) in the browser communicates with a REST API backend, which in turn reads and writes data to a managed PostgreSQL database.

Tech Stack

LayerTechnologyHosting
FrontendReact + Vite + Ant DesignVercel
BackendPython + FastAPIRailway
DatabasePostgreSQL (Supabase)Supabase Cloud
Email (Sending)Resend APIResend
Email (Routing)Cloudflare Email RoutingCloudflare
PDF GenerationReportLab + PyPDF2Server-side
AuthenticationJWT (PyJWT) + bcryptServer-side

How a Request Flows

  1. The user interacts with the React frontend in their browser.
  2. The frontend sends an HTTPS request to the FastAPI backend hosted on Railway. Every request includes a JWT token in the Authorization header.
  3. The backend validates the JWT, checks the user's role and permissions, then queries Supabase (PostgreSQL) using the service role key.
  4. Supabase returns the data. The backend formats it and sends the JSON response back to the frontend.
  5. The frontend renders the data using Ant Design components.

1. 架構概覽

系統架構圖

瀏覽器用戶設備
VercelReact + Vite
RailwayFastAPI (Python)
SupabasePostgreSQL

ProjectMed 採用標準的三層架構:瀏覽器中的單頁應用程式 (SPA) 與 REST API 後端通訊,後端再讀寫託管的 PostgreSQL 資料庫。

技術棧

層級技術託管
前端React + Vite + Ant DesignVercel
後端Python + FastAPIRailway
資料庫PostgreSQL (Supabase)Supabase Cloud
電子郵件(發送)Resend APIResend
電子郵件(轉發)Cloudflare Email RoutingCloudflare
PDF 生成ReportLab + PyPDF2伺服器端
身份驗證JWT (PyJWT) + bcrypt伺服器端

請求流程

  1. 用戶在瀏覽器中與 React 前端互動。
  2. 前端向託管在 Railway 上的 FastAPI 後端發送 HTTPS 請求。每個請求都在 Authorization 標頭中包含 JWT 令牌。
  3. 後端驗證 JWT,檢查用戶的角色和權限,然後使用服務角色金鑰查詢 Supabase (PostgreSQL)。
  4. Supabase 返回資料。後端格式化後將 JSON 回應發送回前端。
  5. 前端使用 Ant Design 元件渲染資料。

1. Arquitectura General

Diagrama del Sistema

NavegadorDispositivo del usuario
VercelReact + Vite
RailwayFastAPI (Python)
SupabasePostgreSQL

ProjectMed sigue una arquitectura estándar de tres capas: una aplicación de página única (SPA) en el navegador se comunica con una API REST en el backend, que a su vez lee y escribe datos en una base de datos PostgreSQL gestionada.

Stack Tecnológico

CapaTecnologíaAlojamiento
FrontendReact + Vite + Ant DesignVercel
BackendPython + FastAPIRailway
Base de datosPostgreSQL (Supabase)Supabase Cloud
Correo (Envío)Resend APIResend
Correo (Reenvío)Cloudflare Email RoutingCloudflare
Generación PDFReportLab + PyPDF2Servidor
AutenticaciónJWT (PyJWT) + bcryptServidor

Flujo de una Solicitud

  1. El usuario interactúa con el frontend React en su navegador.
  2. El frontend envía una solicitud HTTPS al backend FastAPI alojado en Railway. Cada solicitud incluye un token JWT en el encabezado Authorization.
  3. El backend valida el JWT, verifica el rol y los permisos del usuario, y luego consulta Supabase (PostgreSQL) usando la clave del rol de servicio.
  4. Supabase devuelve los datos. El backend los formatea y envía la respuesta JSON al frontend.
  5. El frontend renderiza los datos usando componentes de Ant Design.

2. Authentication & Security

JWT Tokens

JSON Web Tokens (JWT) are the mechanism ProjectMed uses to keep users logged in. When a user logs in successfully, the server creates a signed token containing the user's ID, role, and group. This token is sent to the browser and stored in localStorage.

Every subsequent API request includes this token in the Authorization: Bearer <token> header. The server verifies the signature on each request to ensure the token has not been tampered with. Tokens expire after 8 hours, at which point the user must log in again.

bcrypt Password Hashing

Passwords are never stored in plain text. ProjectMed uses bcrypt, an industry-standard password hashing algorithm designed to be intentionally slow, making brute-force attacks impractical.

How bcrypt Works

  1. When a user sets a password, bcrypt automatically generates a unique random salt (random data mixed into the hash).
  2. The password and salt are combined and run through the Blowfish cipher 4,096 times (212 iterations, controlled by the cost factor).
  3. The result is a single string that contains the algorithm identifier, cost factor, salt, and hash — all in one.
Example bcrypt hash (safe to show — this is not reversible):

$2b$12$LJ3m4sDpR5XKVO9qG2Wieu.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS

$2b$ — Algorithm identifier (bcrypt)
$12$ — Cost factor (212 = 4,096 iterations)
LJ3m4sDpR5XKVO9qG2Wieu — The salt (22 characters)
.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS — The hash (31 characters)

Because each password gets its own unique random salt, even two users with the exact same password will have completely different hashes. This prevents attackers from using precomputed "rainbow tables" to crack passwords.

Password Strength Requirements

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one digit
  • At least one special character

Password History

The last 5 password hashes are stored. When a user changes their password, the system checks the new password against all 5 previous hashes. If any match, the change is rejected. This prevents users from cycling back to an old, potentially compromised password.

PIN Security

Every user has a 6-digit PIN used to verify identity before sensitive operations (like exporting a PDF). The PIN is hashed with bcrypt using the same security as passwords. It receives the same protections: hashing, salting, and brute-force protection.

Two-Factor Authentication (TOTP)

ProjectMed requires mandatory TOTP (Time-based One-Time Password) two-factor authentication for all users. After entering a correct password, the server issues a short-lived pending token (5 minutes, marked with purpose: "pending_2fa") that cannot access any API endpoint. The user must enter a 6-digit code from their authenticator app (Google Authenticator, Microsoft Authenticator, etc.) to exchange the pending token for a real JWT session token.

  • TOTP secrets are stored as base32 strings in the database (one per user)
  • Backup codes — 10 single-use 8-character codes, bcrypt-hashed, for recovery when the authenticator app is unavailable
  • Rate limiting — 5 failed TOTP attempts locks the verification, requiring a fresh login
  • Biometric bypass — WebAuthn (fingerprint/Face ID) login skips TOTP since biometrics are already a second factor
  • Admin reset — superadmins can disable a user's TOTP in emergencies, logged in the audit trail

2. 身份驗證與安全

JWT 令牌

JSON Web Token (JWT) 是 ProjectMed 用來維持用戶登入狀態的機制。當用戶成功登入時,伺服器會建立一個已簽名的令牌,其中包含用戶的 ID、角色和群組。這個令牌會發送到瀏覽器並儲存在 localStorage 中。

之後每次 API 請求都會在 Authorization: Bearer <token> 標頭中包含此令牌。伺服器在每次請求時驗證簽名,確保令牌未被篡改。令牌在 8 小時後過期,屆時用戶必須重新登入。

bcrypt 密碼雜湊

密碼永遠不會以明文儲存。ProjectMed 使用 bcrypt,這是一種業界標準的密碼雜湊演算法,設計上故意很慢,使暴力破解攻擊變得不切實際。

bcrypt 如何運作

  1. 當用戶設定密碼時,bcrypt 會自動產生一個唯一的隨機鹽值(混入雜湊中的隨機資料)。
  2. 密碼和鹽值結合後,通過 Blowfish 加密演算法運行 4,096 次(212 次迭代,由成本因子控制)。
  3. 結果是一個包含演算法識別碼、成本因子、鹽值和雜湊值的單一字串。
bcrypt 雜湊範例(可以安全展示 — 這是不可逆的):

$2b$12$LJ3m4sDpR5XKVO9qG2Wieu.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS

$2b$ — 演算法識別碼(bcrypt)
$12$ — 成本因子(212 = 4,096 次迭代)
LJ3m4sDpR5XKVO9qG2Wieu — 鹽值(22 個字元)
.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS — 雜湊值(31 個字元)

因為每個密碼都有自己唯一的隨機鹽值,即使兩個用戶設定完全相同的密碼,也會產生完全不同的雜湊值。這可以防止攻擊者使用預先計算的「彩虹表」來破解密碼。

密碼強度要求

  • 最少 8 個字元
  • 至少一個大寫字母
  • 至少一個小寫字母
  • 至少一個數字
  • 至少一個特殊字元

密碼歷史

系統會儲存最近 5 個密碼的雜湊值。當用戶更改密碼時,系統會將新密碼與所有 5 個先前的雜湊值進行比對。如果任何一個匹配,更改將被拒絕。這可以防止用戶重複使用可能已被洩露的舊密碼。

PIN 安全

每位用戶都有一個 6 位數的 PIN,用於在敏感操作(如匯出 PDF)之前驗證身份。PIN 使用與密碼相同的 bcrypt 安全機制進行雜湊,享有相同的保護:雜湊、加鹽和暴力破解防護。

兩步驟驗證 (TOTP)

ProjectMed 要求所有使用者設定強制性 TOTP(基於時間的一次性密碼)兩步驟驗證。輸入正確密碼後,伺服器會發送一個短效待驗證令牌(5 分鐘,標記為 purpose: "pending_2fa"),該令牌無法存取任何 API 端點。使用者必須輸入驗證器應用程式中的 6 位數代碼,才能將待驗證令牌換成真正的 JWT 會話令牌。

  • TOTP 密鑰以 base32 字串儲存在資料庫中(每位使用者一個)
  • 備用代碼 — 10 組一次性 8 字元代碼,經 bcrypt 雜湊處理,用於驗證器應用程式不可用時的復原
  • 速率限制 — 5 次失敗的 TOTP 嘗試會鎖定驗證,需要重新登入
  • 生物辨識跳過 — WebAuthn(指紋/Face ID)登入跳過 TOTP,因為生物辨識本身已是第二因素
  • 管理員重設 — 超級管理員可以在緊急情況下停用使用者的 TOTP,並記錄在稽核軌跡中

2. Autenticación y Seguridad

Tokens JWT

Los JSON Web Tokens (JWT) son el mecanismo que ProjectMed usa para mantener a los usuarios conectados. Cuando un usuario inicia sesión exitosamente, el servidor crea un token firmado que contiene el ID del usuario, su rol y su grupo. Este token se envía al navegador y se almacena en localStorage.

Cada solicitud API posterior incluye este token en el encabezado Authorization: Bearer <token>. El servidor verifica la firma en cada solicitud para asegurar que el token no ha sido manipulado. Los tokens expiran después de 8 horas, momento en el cual el usuario debe iniciar sesión nuevamente.

Hashing de Contraseñas con bcrypt

Las contraseñas nunca se almacenan en texto plano. ProjectMed usa bcrypt, un algoritmo de hashing de contraseñas estándar de la industria, diseñado para ser intencionalmente lento, lo que hace que los ataques de fuerza bruta sean imprácticos.

Cómo Funciona bcrypt

  1. Cuando un usuario establece una contraseña, bcrypt genera automáticamente una sal aleatoria única (datos aleatorios mezclados en el hash).
  2. La contraseña y la sal se combinan y pasan por el cifrado Blowfish 4,096 veces (212 iteraciones, controlado por el factor de costo).
  3. El resultado es una cadena única que contiene el identificador del algoritmo, el factor de costo, la sal y el hash — todo en uno.
Ejemplo de hash bcrypt (seguro de mostrar — esto no es reversible):

$2b$12$LJ3m4sDpR5XKVO9qG2Wieu.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS

$2b$ — Identificador del algoritmo (bcrypt)
$12$ — Factor de costo (212 = 4,096 iteraciones)
LJ3m4sDpR5XKVO9qG2Wieu — La sal (22 caracteres)
.g8N3BFTZ0VpSJnMxqK5kDaE7wVrS — El hash (31 caracteres)

Dado que cada contraseña obtiene su propia sal aleatoria única, incluso dos usuarios con exactamente la misma contraseña tendrán hashes completamente diferentes. Esto evita que los atacantes usen "tablas arcoíris" precalculadas para descifrar contraseñas.

Requisitos de Fortaleza de Contraseña

  • Mínimo 8 caracteres
  • Al menos una letra mayúscula
  • Al menos una letra minúscula
  • Al menos un dígito
  • Al menos un carácter especial

Historial de Contraseñas

Se almacenan los últimos 5 hashes de contraseñas. Cuando un usuario cambia su contraseña, el sistema verifica la nueva contraseña contra los 5 hashes anteriores. Si alguno coincide, el cambio es rechazado. Esto evita que los usuarios vuelvan a usar una contraseña antigua potencialmente comprometida.

Seguridad del PIN

Cada usuario tiene un PIN de 6 dígitos usado para verificar su identidad antes de operaciones sensibles (como exportar un PDF). El PIN se hashea con bcrypt usando la misma seguridad que las contraseñas. Recibe las mismas protecciones: hashing, sal y protección contra fuerza bruta.

Autenticación de Dos Factores (TOTP)

ProjectMed requiere autenticación TOTP (Contraseña de Un Solo Uso Basada en Tiempo) obligatoria para todos los usuarios. Después de ingresar la contraseña correcta, el servidor emite un token pendiente de corta duración (5 minutos, marcado con purpose: "pending_2fa") que no puede acceder a ningún endpoint de la API. El usuario debe ingresar un código de 6 dígitos de su aplicación de autenticación para intercambiar el token pendiente por un token de sesión JWT real.

  • Secretos TOTP se almacenan como cadenas base32 en la base de datos (uno por usuario)
  • Códigos de respaldo — 10 códigos de un solo uso de 8 caracteres, hasheados con bcrypt, para recuperación cuando la aplicación de autenticación no está disponible
  • Límite de intentos — 5 intentos fallidos de TOTP bloquean la verificación, requiriendo un nuevo inicio de sesión
  • Excepción biométrica — El inicio de sesión WebAuthn (huella/Face ID) omite TOTP ya que la biometría ya es un segundo factor
  • Restablecimiento de admin — los superadministradores pueden desactivar el TOTP de un usuario en emergencias, registrado en el historial de auditoría

3. Rate Limiting & Account Protection

ProjectMed implements progressive account lockout to protect against brute-force attacks:

Failed AttemptsWhat Happens
5 wrong passwordsPassword login disabled — must use PIN instead
5 wrong PINs (after password lockout)Account fully locked
10 total failuresMust reset both password and PIN via OTP

Login Attempt Tracking

All failed login attempts are stored in the login_attempts database table (not in server memory). This means that the counter survives server restarts and deployments — an attacker cannot simply wait for the server to restart to get fresh attempts.

OTP (One-Time Password) Codes

  • 6-digit numeric code sent via email
  • Expires after 15 minutes
  • Single-use — consumed immediately after verification
  • Stored as a bcrypt hash in the database (the raw code is never stored)
  • Email address is verified to exist in the system before sending

3. 速率限制與帳戶保護

ProjectMed 實施漸進式帳戶鎖定以防止暴力破解攻擊:

失敗次數發生什麼
5 次密碼錯誤密碼登入停用 — 必須改用 PIN
5 次 PIN 錯誤(密碼鎖定後)帳戶完全鎖定
總共 10 次失敗必須通過 OTP 重設密碼和 PIN

登入嘗試追蹤

所有失敗的登入嘗試都儲存在 login_attempts 資料庫表中(而非伺服器記憶體中)。這意味著計數器在伺服器重啟和部署後仍然存在 — 攻擊者無法通過等待伺服器重啟來獲得新的嘗試次數。

OTP(一次性密碼)

  • 6 位數數字驗證碼,通過電子郵件發送
  • 15 分鐘後過期
  • 單次使用 — 驗證後立即作廢
  • 以 bcrypt 雜湊形式儲存在資料庫中(原始驗證碼絕不儲存)
  • 發送前會驗證電子郵件地址是否存在於系統中

3. Límite de Intentos y Protección de Cuentas

ProjectMed implementa un bloqueo progresivo de cuentas para proteger contra ataques de fuerza bruta:

Intentos FallidosQué Ocurre
5 contraseñas incorrectasInicio con contraseña deshabilitado — debe usar PIN
5 PINs incorrectos (tras bloqueo de contraseña)Cuenta completamente bloqueada
10 fallos en totalDebe restablecer contraseña y PIN mediante OTP

Seguimiento de Intentos de Inicio de Sesión

Todos los intentos fallidos de inicio de sesión se almacenan en la tabla login_attempts de la base de datos (no en la memoria del servidor). Esto significa que el contador sobrevive a reinicios y despliegues del servidor — un atacante no puede simplemente esperar a que el servidor se reinicie para obtener nuevos intentos.

Códigos OTP (Contraseña de Un Solo Uso)

  • Código numérico de 6 dígitos enviado por correo electrónico
  • Expira después de 15 minutos
  • Un solo uso — se consume inmediatamente después de la verificación
  • Almacenado como hash bcrypt en la base de datos (el código sin procesar nunca se almacena)
  • Se verifica que la dirección de correo exista en el sistema antes de enviar

4. Data Security & Cloud Storage

Is it safe to store medical records in the cloud?

Yes. Cloud storage, when done correctly, is significantly more secure than paper records or a local server. Here is why.

How ProjectMed Protects Data

  • Encryption at rest: Supabase uses PostgreSQL with AES-256 encryption. Data is encrypted on disk — even if someone physically stole the storage hardware, they could not read the data.
  • Encryption in transit: All data between the browser, backend, and database travels over TLS/HTTPS. No data is ever sent in plain text.
  • Row Level Security (RLS): Supabase enforces RLS on all tables. Only the backend's service role key can access the data — not even Supabase's own dashboard users (unless they have the service key).
  • Service role key isolation: The key is stored as an encrypted environment variable on Railway. It never appears in source code, logs, or the frontend.
  • Automatic backups: Supabase performs automatic daily backups of all data.

Cloud vs. Paper Records

Paper Records

  • Can be stolen physically
  • Vulnerable to fire, flood, natural disasters
  • Anyone who finds them can read them
  • No audit trail — who read what?
  • Single copy — if lost, gone forever

ProjectMed (Cloud)

  • Encrypted at rest and in transit
  • Distributed across redundant servers
  • Access-controlled with roles and JWT
  • Full audit trail of every access
  • Automatic daily backups

Cloud vs. Local Server

Local Server

  • Single point of failure
  • No redundancy unless manually configured
  • Physical theft risk
  • Requires dedicated IT staff for security patches
  • Power outage = downtime

ProjectMed (Cloud)

  • Distributed infrastructure with redundancy
  • Managed by professional security teams
  • No physical access risk
  • Automatic security updates
  • 99.9% uptime SLA

Data Residency

Supabase allows you to choose the data center region when creating your project. Data stays in the selected region and is subject to that region's data protection laws. ProjectMed's data resides in the region selected during initial Supabase project setup.

4. 資料安全與雲端儲存

在雲端儲存醫療記錄安全嗎?

是的。正確實施的雲端儲存比紙本記錄或本地伺服器安全得多。以下是原因。

ProjectMed 如何保護資料

  • 靜態加密:Supabase 使用 PostgreSQL 搭配 AES-256 加密。資料在磁碟上是加密的 — 即使有人實際竊取了儲存硬體,也無法讀取資料。
  • 傳輸加密:瀏覽器、後端和資料庫之間的所有資料都通過 TLS/HTTPS 傳輸。資料永遠不會以明文發送。
  • 行級安全 (RLS):Supabase 在所有表上強制執行 RLS。只有後端的服務角色金鑰才能存取資料 — 連 Supabase 自己的儀表板用戶也不能(除非他們擁有服務金鑰)。
  • 服務角色金鑰隔離:金鑰作為加密的環境變數儲存在 Railway 上。它永遠不會出現在原始碼、日誌或前端中。
  • 自動備份:Supabase 每天自動備份所有資料。

雲端 vs. 紙本記錄

紙本記錄

  • 可被實際竊取
  • 容易受到火災、洪水、自然災害影響
  • 任何找到的人都能閱讀
  • 無稽核追蹤 — 誰讀了什麼?
  • 只有一份 — 遺失即永遠失去

ProjectMed(雲端)

  • 靜態和傳輸中均加密
  • 分佈在冗餘伺服器上
  • 使用角色和 JWT 進行存取控制
  • 完整的每次存取稽核追蹤
  • 每日自動備份

雲端 vs. 本地伺服器

本地伺服器

  • 單點故障
  • 除非手動設定,否則無冗餘
  • 實體竊取風險
  • 需要專門的 IT 人員進行安全修補
  • 停電 = 停機

ProjectMed(雲端)

  • 具有冗餘的分佈式基礎設施
  • 由專業安全團隊管理
  • 無實體存取風險
  • 自動安全更新
  • 99.9% 正常運行時間 SLA

資料駐留

Supabase 允許在建立專案時選擇資料中心區域。資料留在所選區域,並受該區域的資料保護法律約束。ProjectMed 的資料駐留在初始 Supabase 專案設定時選擇的區域。

4. Seguridad de Datos y Almacenamiento en la Nube

¿Es seguro almacenar registros médicos en la nube?

Sí. El almacenamiento en la nube, cuando se hace correctamente, es significativamente más seguro que los registros en papel o un servidor local. He aquí por qué.

Cómo ProjectMed Protege los Datos

  • Cifrado en reposo: Supabase usa PostgreSQL con cifrado AES-256. Los datos están cifrados en disco — incluso si alguien robara físicamente el hardware de almacenamiento, no podría leer los datos.
  • Cifrado en tránsito: Todos los datos entre el navegador, el backend y la base de datos viajan por TLS/HTTPS. Ningún dato se envía en texto plano.
  • Seguridad a Nivel de Fila (RLS): Supabase aplica RLS en todas las tablas. Solo la clave del rol de servicio del backend puede acceder a los datos — ni siquiera los usuarios del panel de Supabase pueden (a menos que tengan la clave de servicio).
  • Aislamiento de la clave de servicio: La clave se almacena como variable de entorno cifrada en Railway. Nunca aparece en el código fuente, los logs ni el frontend.
  • Copias de seguridad automáticas: Supabase realiza copias de seguridad diarias automáticas de todos los datos.

Nube vs. Registros en Papel

Registros en Papel

  • Pueden ser robados físicamente
  • Vulnerables a incendios, inundaciones, desastres naturales
  • Cualquiera que los encuentre puede leerlos
  • Sin registro de auditoría — ¿quién leyó qué?
  • Una sola copia — si se pierde, se pierde para siempre

ProjectMed (Nube)

  • Cifrado en reposo y en tránsito
  • Distribuido en servidores redundantes
  • Acceso controlado con roles y JWT
  • Registro completo de auditoría de cada acceso
  • Copias de seguridad diarias automáticas

Nube vs. Servidor Local

Servidor Local

  • Punto único de fallo
  • Sin redundancia a menos que se configure manualmente
  • Riesgo de robo físico
  • Requiere personal de TI dedicado para parches de seguridad
  • Corte de energía = tiempo de inactividad

ProjectMed (Nube)

  • Infraestructura distribuida con redundancia
  • Gestionada por equipos de seguridad profesionales
  • Sin riesgo de acceso físico
  • Actualizaciones de seguridad automáticas
  • SLA de 99.9% de tiempo de actividad

Residencia de Datos

Supabase permite elegir la región del centro de datos al crear el proyecto. Los datos permanecen en la región seleccionada y están sujetos a las leyes de protección de datos de esa región. Los datos de ProjectMed residen en la región seleccionada durante la configuración inicial del proyecto de Supabase.

5. PDF Security

PDF export is one of the most sensitive operations in ProjectMed. Multiple layers of security protect the exported document:

How PDF Export Works

  1. The doctor clicks "Export PDF" on a patient record.
  2. A dialog prompts the doctor to enter their 6-digit PIN.
  3. The backend verifies the PIN against the stored bcrypt hash.
  4. If valid, the PDF is generated server-side using ReportLab.
  5. The PDF is encrypted with AES-128 and protected with two passwords.
  6. The export is logged in the audit trail.

Password Protection

Password TypePurposeValue
User password (to open)Required to view the PDFPatient's last 4 digits of ID number, OR date of birth in MMDD format (e.g., Jan 1 = 0101)
Owner password (to edit)Prevents copying, editing, printing restrictionsStored in PDF_OWNER_PASSWORD environment variable on the server
The user password ensures that even if a PDF file is intercepted or forwarded, only someone who knows the patient's ID or date of birth can open it. The owner password prevents unauthorized modifications to the document.

5. PDF 安全

PDF 匯出是 ProjectMed 中最敏感的操作之一。多層安全保護匯出的文件:

PDF 匯出流程

  1. 醫師在病患記錄上點擊「匯出 PDF」。
  2. 對話框提示醫師輸入其 6 位數 PIN
  3. 後端驗證 PIN 與儲存的 bcrypt 雜湊值。
  4. 如果有效,PDF 將使用 ReportLab 在伺服器端生成。
  5. PDF 使用 AES-128 加密,並以兩個密碼保護。
  6. 匯出操作記錄在稽核追蹤中。

密碼保護

密碼類型用途
用戶密碼(開啟用)查看 PDF 時需要病患身份證號碼後 4 位,或出生日期 MMDD 格式(例如 1 月 1 日 = 0101
擁有者密碼(編輯用)防止複製、編輯、列印限制儲存在伺服器的 PDF_OWNER_PASSWORD 環境變數中
用戶密碼確保即使 PDF 檔案被攔截或轉發,也只有知道病患身份證號碼或出生日期的人才能開啟。擁有者密碼防止未經授權的文件修改。

5. Seguridad de PDF

La exportación de PDF es una de las operaciones más sensibles en ProjectMed. Múltiples capas de seguridad protegen el documento exportado:

Cómo Funciona la Exportación de PDF

  1. El médico hace clic en "Exportar PDF" en un registro de paciente.
  2. Un diálogo solicita al médico que ingrese su PIN de 6 dígitos.
  3. El backend verifica el PIN contra el hash bcrypt almacenado.
  4. Si es válido, el PDF se genera en el servidor usando ReportLab.
  5. El PDF se cifra con AES-128 y se protege con dos contraseñas.
  6. La exportación se registra en el registro de auditoría.

Protección con Contraseña

Tipo de ContraseñaPropósitoValor
Contraseña de usuario (para abrir)Requerida para ver el PDFÚltimos 4 dígitos del número de ID del paciente, O fecha de nacimiento en formato MMDD (ej., 1 de enero = 0101)
Contraseña del propietario (para editar)Previene copiar, editar, restricciones de impresiónAlmacenada en la variable de entorno PDF_OWNER_PASSWORD en el servidor
La contraseña de usuario asegura que incluso si un archivo PDF es interceptado o reenviado, solo alguien que conozca el ID o la fecha de nacimiento del paciente pueda abrirlo. La contraseña del propietario previene modificaciones no autorizadas al documento.

6. Email Infrastructure

Sending Emails: Resend API

All system-generated emails are sent through the Resend API. This includes:

  • Welcome emails when a new doctor or admin is created
  • OTP codes for password/PIN reset
  • Account approval notifications
  • Temporary password emails when an account is unlocked

All emails are trilingual — they contain the message in English, Traditional Chinese, and Spanish simultaneously, so every recipient can read in their preferred language.

Email Routing: Cloudflare

ProjectMed uses Cloudflare Email Routing to manage the official @crmedicalteam.com email addresses. This works as follows:

  1. Each doctor or admin is assigned a professional email address (e.g., dr.smith@crmedicalteam.com).
  2. Cloudflare routes incoming emails to that address to the user's personal email inbox.
  3. Each personal email must be verified through Cloudflare's destination verification process.
Why this matters: Doctors can give patients their @crmedicalteam.com address. Patients never see the doctor's personal email. If a doctor leaves the team, the professional email can be reassigned.

6. 電子郵件基礎設施

發送電子郵件:Resend API

所有系統產生的電子郵件都通過 Resend API 發送。包括:

  • 建立新醫師或管理員時的歡迎郵件
  • 密碼/PIN 重設的 OTP 驗證碼
  • 帳戶核准通知
  • 帳戶解鎖時的臨時密碼郵件

所有電子郵件都是三語的 — 同時包含英文、繁體中文和西班牙文的訊息,讓每位收件人都能以其偏好的語言閱讀。

郵件路由:Cloudflare

ProjectMed 使用 Cloudflare Email Routing 管理官方 @crmedicalteam.com 電子郵件地址。運作方式如下:

  1. 每位醫師或管理員會被分配一個專業電子郵件地址(例如 dr.smith@crmedicalteam.com)。
  2. Cloudflare 將該地址的來信路由到用戶的個人電子郵件信箱。
  3. 每個個人電子郵件都必須通過 Cloudflare 的目標驗證程序進行驗證。
為什麼重要:醫師可以給病患他們的 @crmedicalteam.com 地址。病患永遠看不到醫師的個人電子郵件。如果醫師離開團隊,專業電子郵件可以重新分配。

6. Infraestructura de Correo Electrónico

Envío de Correos: Resend API

Todos los correos generados por el sistema se envían a través de la API de Resend. Esto incluye:

  • Correos de bienvenida cuando se crea un nuevo médico o administrador
  • Códigos OTP para restablecimiento de contraseña/PIN
  • Notificaciones de aprobación de cuenta
  • Correos con contraseña temporal cuando se desbloquea una cuenta

Todos los correos son trilingües — contienen el mensaje en inglés, chino tradicional y español simultáneamente, para que cada destinatario pueda leer en su idioma preferido.

Enrutamiento de Correo: Cloudflare

ProjectMed usa Cloudflare Email Routing para gestionar las direcciones oficiales de @crmedicalteam.com. Funciona de la siguiente manera:

  1. A cada médico o administrador se le asigna una dirección de correo profesional (ej., dr.smith@crmedicalteam.com).
  2. Cloudflare enruta los correos entrantes a esa dirección hacia la bandeja de entrada personal del usuario.
  3. Cada correo personal debe ser verificado a través del proceso de verificación de destino de Cloudflare.
Por qué importa: Los médicos pueden dar a los pacientes su dirección @crmedicalteam.com. Los pacientes nunca ven el correo personal del médico. Si un médico deja el equipo, el correo profesional puede reasignarse.

7. Role-Based Access Control

ProjectMed has three roles. Unlike a simple hierarchy, admins do not inherit clinical write permissions from doctors. Patient data modification is doctor-only:

PermissionDoctorAdminSuperadmin
View patientsOwn groupOwn groupAll groups
Create / edit / delete patientsYesView onlyView only
Create / edit / delete visitsYesView onlyView only
Upload / delete imagesYesView onlyView only
Export PDFsYes (PIN required)Yes (PIN required)Yes (PIN required)
Manage own profileYesYesYes
Request group transferYesNoNo
Manage doctorsNoOwn group onlyAll groups
Approve group transfersNoOwn group onlyAll groups
View audit logsNoYesYes
View System DashboardNoNoYes
Manage adminsNoNoYes
Manage all groupsNoNoYes
Permanently delete patientsNoNoYes

How Access Control is Enforced

Access control is enforced at two levels:

  • Frontend (React): Route guards check the user's role from the JWT token. Pages like Doctor Management are only rendered for admins and superadmins. Patient edit/delete buttons and visit forms only appear for doctors. The System Dashboard navigation link only appears for superadmins.
  • Backend (FastAPI): Every API endpoint uses Depends() guards that verify the JWT role before processing the request. The require_doctor guard is used on all patient, visit, and image mutation endpoints (create, update, delete) to ensure only doctors can modify clinical data. Even if someone bypasses the frontend, the backend will reject unauthorized requests.

The require_doctor Guard

A dedicated require_doctor dependency is applied to all endpoints that create, update, or delete patients, visits, and images. This guard checks that the authenticated user has the doctor role. Admins and superadmins calling these endpoints receive a 403 Forbidden response.

Group Transfer Requests

Doctors can request to transfer to a different group via the group_transfer_requests table. Each request stores the doctor ID, source group, target group, status (pending/approved/rejected), and timestamps. Admins can approve or reject transfer requests for doctors in their group. The transfer is logged in the audit trail.

Admin Group Cascade

When a superadmin changes an admin's group assignment, the system warns that all doctors managed by that admin will also be affected. A confirmation checkbox is required before proceeding with the group change.

Group Isolation

Doctors and admins can only see patients within their own group. Superadmins can see all patients across all groups. Admins can only manage doctors within their own group. This maintains both clinical and administrative boundaries.

7. 角色存取控制

ProjectMed 有三個角色。與簡單的層級結構不同,管理員繼承醫師的臨床寫入權限。病患資料修改僅限醫師:

權限醫師管理員超級管理員
查看病患自己群組自己群組所有群組
建立/編輯/刪除病患僅檢視僅檢視
建立/編輯/刪除看診僅檢視僅檢視
上傳/刪除影像僅檢視僅檢視
匯出 PDF是(需要 PIN)是(需要 PIN)是(需要 PIN)
管理個人檔案
申請群組轉移
管理醫師僅限自己的群組所有群組
核准群組轉移僅限自己的群組所有群組
查看稽核日誌
查看系統儀表板
管理管理員
管理所有群組
永久刪除病患

存取控制如何執行

存取控制在兩個層級執行:

  • 前端 (React):路由守衛從 JWT 令牌中檢查用戶角色。像醫師管理這樣的頁面只為管理員和超級管理員渲染。病患編輯/刪除按鈕和看診表單只對醫師顯示。系統儀表板導航連結只對超級管理員顯示。
  • 後端 (FastAPI):每個 API 端點使用 Depends() 守衛在處理請求前驗證 JWT 角色。require_doctor 守衛用於所有病患、看診和影像的變更端點(建立、更新、刪除),確保只有醫師可以修改臨床資料。即使有人繞過前端,後端也會拒絕未經授權的請求。

require_doctor 守衛

專用的 require_doctor 依賴項應用於所有建立、更新或刪除病患、看診和影像的端點。此守衛檢查已認證用戶是否具有 doctor 角色。管理員和超級管理員調用這些端點將收到 403 Forbidden 回應。

群組轉移申請

醫師可以透過 group_transfer_requests 表申請轉移至不同群組。每個申請記錄醫師 ID、來源群組、目標群組、狀態(待審核/已核准/已拒絕)和時間戳。管理員可以核准或拒絕其群組內醫師的轉移申請。轉移操作會記錄在稽核軌跡中。

管理員群組級聯

當超級管理員更改管理員的群組分配時,系統會警告該管理員管理的所有醫師也會受影響。在繼續群組變更前需要勾選確認核取方塊。

群組隔離

醫師和管理員只能查看自己群組內的病患。超級管理員可以查看所有群組的所有病患。管理員只能管理自己群組內的醫師。這同時維持了臨床和行政管理邊界。

7. Control de Acceso Basado en Roles

ProjectMed tiene tres roles. A diferencia de una jerarquía simple, los administradores no heredan los permisos de escritura clínica de los médicos. La modificación de datos de pacientes es exclusiva de médicos:

PermisoMédicoAdminSuperadmin
Ver pacientesSu grupoSu grupoTodos los grupos
Crear / editar / eliminar pacientesSolo lecturaSolo lectura
Crear / editar / eliminar consultasSolo lecturaSolo lectura
Subir / eliminar imágenesSolo lecturaSolo lectura
Exportar PDFsSí (PIN requerido)Sí (PIN requerido)Sí (PIN requerido)
Gestionar perfil propio
Solicitar transferencia de grupoNoNo
Gestionar médicosNoSolo su grupoTodos los grupos
Aprobar transferencias de grupoNoSolo su grupoTodos los grupos
Ver registros de auditoríaNo
Ver Panel del SistemaNoNo
Gestionar administradoresNoNo
Gestionar todos los gruposNoNo
Eliminar pacientes permanentementeNoNo

Cómo se Aplica el Control de Acceso

El control de acceso se aplica en dos niveles:

  • Frontend (React): Los guardias de ruta verifican el rol del usuario desde el token JWT. Páginas como Gestión de Médicos solo se renderizan para administradores y superadministradores. Los botones de editar/eliminar pacientes y los formularios de consulta solo aparecen para médicos. El enlace de navegación del Panel del Sistema solo aparece para superadministradores.
  • Backend (FastAPI): Cada endpoint de la API usa guardias Depends() que verifican el rol del JWT antes de procesar la solicitud. El guardia require_doctor se usa en todos los endpoints de mutación de pacientes, consultas e imágenes (crear, actualizar, eliminar) para asegurar que solo los médicos puedan modificar datos clínicos. Incluso si alguien evita el frontend, el backend rechazará las solicitudes no autorizadas.

El Guardia require_doctor

Una dependencia dedicada require_doctor se aplica a todos los endpoints que crean, actualizan o eliminan pacientes, consultas e imágenes. Este guardia verifica que el usuario autenticado tenga el rol doctor. Los administradores y superadministradores que llamen a estos endpoints recibirán una respuesta 403 Forbidden.

Solicitudes de Transferencia de Grupo

Los médicos pueden solicitar transferencia a un grupo diferente mediante la tabla group_transfer_requests. Cada solicitud almacena el ID del médico, grupo de origen, grupo destino, estado (pendiente/aprobado/rechazado) y marcas de tiempo. Los administradores pueden aprobar o rechazar solicitudes de transferencia de médicos en su grupo. La transferencia se registra en el rastro de auditoría.

Cascada de Grupo del Administrador

Cuando un superadministrador cambia la asignación de grupo de un administrador, el sistema advierte que todos los médicos gestionados por ese administrador también serán afectados. Se requiere una casilla de confirmación antes de continuar con el cambio de grupo.

Aislamiento de Grupos

Los médicos y administradores solo pueden ver pacientes dentro de su propio grupo. Los superadministradores pueden ver todos los pacientes de todos los grupos. Los administradores solo pueden gestionar médicos dentro de su propio grupo. Esto mantiene tanto los límites clínicos como administrativos.

8. Audit Trail

Every significant action in ProjectMed is recorded in the audit_logs table. This creates a complete, tamper-evident history of all system activity.

What Gets Logged (35 Action Types)

  • User login (successful and failed)
  • Patient record creation, editing, and deletion
  • Doctor and admin account creation, editing, and deletion
  • PDF export, FHIR export, email record
  • Password and PIN changes
  • Account unlock operations
  • Audit log deletion (logged separately for accountability)

Filtering & Performance

Audit logs support filtering by: date range, user email, target name, target type, and action type. Sortable columns and configurable page sizes (20–1000). Database indexes on timestamp, actor_id, and action ensure fast queries even with large volumes.

What Each Log Entry Contains

FieldDescription
timestampExact date and time of the action (UTC)
actor_idID of the user who performed the action
actor_roleRole of the user (doctor, admin, superadmin)
actionType of action (e.g., create_patient, export_pdf)
targetWho or what was affected (e.g., patient name, doctor email)
detailsAdditional context about the action

Who Can View Audit Logs

Only admins and superadmins can view audit logs. They can filter by date range to narrow down the records. Doctors cannot access the audit log viewer.

8. 稽核追蹤

ProjectMed 中的每個重要操作都記錄在 audit_logs 表中。這建立了一個完整、防篡改的系統活動歷史。

記錄哪些操作(35 種操作類型)

  • 用戶登入(成功和失敗)
  • 病患記錄的建立、編輯和刪除
  • 醫師和管理員帳戶的建立、編輯和刪除
  • PDF 匯出、FHIR 匯出、電子郵件寄送紀錄
  • 密碼和 PIN 變更
  • 帳戶解鎖操作
  • 稽核日誌刪除(另行記錄以確保問責)

篩選與效能

稽核日誌支援以下篩選條件:日期範圍、用戶電子郵件、目標名稱、目標類型和操作類型。可排序欄位和可設定的每頁筆數(20–1000)。資料庫在 timestampactor_idaction 上建立索引,確保大量資料下的查詢效能。

每個日誌條目包含什麼

欄位描述
timestamp操作的確切日期和時間 (UTC)
actor_id執行操作的用戶 ID
actor_role用戶角色(醫師、管理員、超級管理員)
action操作類型(例如 create_patientexport_pdf
target受影響的對象(例如病患姓名、醫師電子郵件)
details關於操作的附加上下文

誰可以查看稽核日誌

只有管理員超級管理員可以查看稽核日誌。他們可以按日期範圍篩選以縮小記錄範圍。醫師無法存取稽核日誌查看器。

8. Registro de Auditoría

Cada acción significativa en ProjectMed se registra en la tabla audit_logs. Esto crea un historial completo y a prueba de manipulaciones de toda la actividad del sistema.

Qué se Registra (35 Tipos de Acciones)

  • Inicio de sesión de usuario (exitoso y fallido)
  • Creación, edición y eliminación de registros de pacientes
  • Creación, edición y eliminación de cuentas de médicos y administradores
  • Exportación de PDF, exportación FHIR, envío de registro por correo
  • Cambios de contraseña y PIN
  • Operaciones de desbloqueo de cuenta
  • Eliminación de registros de auditoría (registrado por separado para responsabilidad)

Filtrado y Rendimiento

Los registros de auditoría admiten filtros por: rango de fechas, correo del usuario, nombre del objetivo, tipo de objetivo y tipo de acción. Columnas ordenables y tamaños de página configurables (20–1000). Índices en la base de datos sobre timestamp, actor_id y action aseguran consultas rápidas incluso con grandes volúmenes.

Qué Contiene Cada Entrada de Registro

CampoDescripción
timestampFecha y hora exacta de la acción (UTC)
actor_idID del usuario que realizó la acción
actor_roleRol del usuario (médico, admin, superadmin)
actionTipo de acción (ej., create_patient, export_pdf)
targetQuién o qué fue afectado (ej., nombre del paciente, correo del médico)
detailsContexto adicional sobre la acción

Quién Puede Ver los Registros de Auditoría

Solo los administradores y superadministradores pueden ver los registros de auditoría. Pueden filtrar por rango de fechas para acotar los registros. Los médicos no pueden acceder al visor de registros de auditoría.

9. Inactivity & Session Management

To protect unattended sessions, ProjectMed implements a multi-layered session management strategy:

Inactivity Timer

  • After 15 minutes of inactivity, the user is automatically logged out.
  • A 2-minute visual countdown warning appears before the auto-logout, giving the user a chance to stay logged in.

What Counts as Activity

The inactivity timer resets on any of the following user interactions:

  • Mouse movement
  • Keyboard input
  • Touch events (mobile)
  • Scrolling
  • Clicking

JWT Expiry

Regardless of user activity, the JWT token expires after 8 hours. At that point, the user must log in again. This is a hard limit that cannot be extended by staying active.

Session Storage

The JWT token is stored in localStorage. On logout (manual or automatic), the token is cleared from storage and the user is redirected to the login page.

Visit Form Auto-Save

To prevent data loss during inactivity timeouts, visit forms auto-save to localStorage on every field change and every 30 seconds. When the doctor logs back in and opens the same patient, the draft is restored. Drafts are keyed per patient (visit-draft-{patientId}) and cleared on successful submission.

9. 閒置與工作階段管理

為了保護無人看管的工作階段,ProjectMed 實施了多層次的工作階段管理策略:

閒置計時器

  • 閒置 15 分鐘後,用戶將自動登出。
  • 自動登出前會顯示 2 分鐘的視覺倒數計時警告,讓用戶有機會選擇繼續登入。

什麼算作活動

以下任何用戶互動都會重設閒置計時器:

  • 滑鼠移動
  • 鍵盤輸入
  • 觸控事件(行動裝置)
  • 滾動
  • 點擊

JWT 過期

無論用戶是否活動,JWT 令牌都會在 8 小時後過期。届時用戶必須重新登入。這是一個硬性限制,無法通過保持活動來延長。

工作階段儲存

JWT 令牌儲存在 localStorage 中。登出時(手動或自動),令牌將從儲存中清除,用戶將被重新導向到登入頁面。

看診表單自動儲存

為了防止閒置逾時造成資料遺失,看診表單會在每次欄位變更及每 30 秒自動儲存至 localStorage。當醫師重新登入並打開同一位病患時,草稿會自動恢復。草稿以病患為單位儲存(visit-draft-{patientId}),並在成功提交後清除。

9. Inactividad y Gestión de Sesión

Para proteger las sesiones desatendidas, ProjectMed implementa una estrategia de gestión de sesiones de múltiples capas:

Temporizador de Inactividad

  • Después de 15 minutos de inactividad, el usuario se desconecta automáticamente.
  • Aparece una advertencia visual con cuenta regresiva de 2 minutos antes de la desconexión automática, dando al usuario la oportunidad de permanecer conectado.

Qué Cuenta como Actividad

El temporizador de inactividad se restablece con cualquiera de las siguientes interacciones del usuario:

  • Movimiento del mouse
  • Entrada de teclado
  • Eventos táctiles (móvil)
  • Desplazamiento
  • Clics

Expiración del JWT

Independientemente de la actividad del usuario, el token JWT expira después de 8 horas. En ese momento, el usuario debe iniciar sesión nuevamente. Este es un límite estricto que no puede extenderse manteniéndose activo.

Almacenamiento de Sesión

El token JWT se almacena en localStorage. Al cerrar sesión (manual o automáticamente), el token se elimina del almacenamiento y el usuario es redirigido a la página de inicio de sesión.

Auto-guardado del Formulario de Visita

Para prevenir la pérdida de datos durante los tiempos de espera por inactividad, los formularios de visita se guardan automáticamente en localStorage con cada cambio de campo y cada 30 segundos. Cuando el médico vuelve a iniciar sesión y abre el mismo paciente, el borrador se restaura. Los borradores se almacenan por paciente (visit-draft-{patientId}) y se eliminan tras el envío exitoso.

10. Infrastructure Security

ProjectMed's infrastructure is built on managed services that handle security at every layer:

ServiceSecurity Features
Vercel (Frontend)Automatic HTTPS, DDoS protection, global CDN, immutable deployments
Railway (Backend)Container isolation, automatic HTTPS, environment variable encryption, private networking
Supabase (Database)Managed PostgreSQL, AES-256 encryption at rest, automatic backups, Row Level Security
Cloudflare (DNS/Email)DNS protection, email routing, SSL certificates, DDoS mitigation
Resend (Email)DKIM/SPF/DMARC email authentication, TLS delivery

CORS (Cross-Origin Resource Sharing)

The backend restricts API access to specific allowed origins only. This means that only the official ProjectMed frontend (hosted on Vercel) can make requests to the backend. Requests from any other website are rejected by the browser.

Secret Management

No secrets are ever stored in source code. All sensitive values are stored as environment variables:

  • JWT_SECRET — for signing tokens (raises error if not set)
  • SUPABASE_SERVICE_KEY — for database access
  • RESEND_API_KEY — for sending emails
  • PDF_OWNER_PASSWORD — for PDF protection
  • BOOTSTRAP_SECRET — for initial superadmin setup

These are stored encrypted on Railway and are never exposed to the frontend or included in logs.

10. 基礎設施安全

ProjectMed 的基礎設施建立在每個層級都處理安全性的託管服務上:

服務安全功能
Vercel(前端)自動 HTTPS、DDoS 防護、全球 CDN、不可變部署
Railway(後端)容器隔離、自動 HTTPS、環境變數加密、私有網路
Supabase(資料庫)託管 PostgreSQL、AES-256 靜態加密、自動備份、行級安全
Cloudflare(DNS/郵件)DNS 保護、郵件路由、SSL 憑證、DDoS 緩解
Resend(郵件)DKIM/SPF/DMARC 郵件認證、TLS 傳送

CORS(跨來源資源共享)

後端將 API 存取限制在特定的允許來源。這意味著只有官方的 ProjectMed 前端(託管在 Vercel 上)可以向後端發出請求。來自任何其他網站的請求都會被瀏覽器拒絕。

密鑰管理

密鑰永遠不會儲存在原始碼中。所有敏感值都作為環境變數儲存:

  • JWT_SECRET — 用於簽署令牌(未設定時會引發錯誤)
  • SUPABASE_SERVICE_KEY — 用於資料庫存取
  • RESEND_API_KEY — 用於發送電子郵件
  • PDF_OWNER_PASSWORD — 用於 PDF 保護
  • BOOTSTRAP_SECRET — 用於初始超級管理員設定

這些都以加密形式儲存在 Railway 上,永遠不會暴露給前端或包含在日誌中。

10. Seguridad de Infraestructura

La infraestructura de ProjectMed está construida sobre servicios gestionados que manejan la seguridad en cada capa:

ServicioCaracterísticas de Seguridad
Vercel (Frontend)HTTPS automático, protección DDoS, CDN global, despliegues inmutables
Railway (Backend)Aislamiento de contenedores, HTTPS automático, cifrado de variables de entorno, red privada
Supabase (Base de datos)PostgreSQL gestionado, cifrado AES-256 en reposo, copias de seguridad automáticas, Seguridad a Nivel de Fila
Cloudflare (DNS/Correo)Protección DNS, enrutamiento de correo, certificados SSL, mitigación DDoS
Resend (Correo)Autenticación de correo DKIM/SPF/DMARC, entrega TLS

CORS (Intercambio de Recursos de Origen Cruzado)

El backend restringe el acceso a la API solo a orígenes específicos permitidos. Esto significa que solo el frontend oficial de ProjectMed (alojado en Vercel) puede hacer solicitudes al backend. Las solicitudes de cualquier otro sitio web son rechazadas por el navegador.

Gestión de Secretos

Ningún secreto se almacena en el código fuente. Todos los valores sensibles se almacenan como variables de entorno:

  • JWT_SECRET — para firmar tokens (genera error si no está configurado)
  • SUPABASE_SERVICE_KEY — para acceso a la base de datos
  • RESEND_API_KEY — para enviar correos electrónicos
  • PDF_OWNER_PASSWORD — para protección de PDF
  • BOOTSTRAP_SECRET — para la configuración inicial del superadmin

Estos se almacenan cifrados en Railway y nunca se exponen al frontend ni se incluyen en los logs.

11. Disaster Recovery

Dual-Host Frontend Deployment

  • Primary: Vercel (crmedicalteam.com) — auto-deploys from GitHub
  • Backup: Cloudflare Pages (medicalteam.cc) — auto-deploys from same GitHub repo
  • Both point to the same Railway backend and Supabase database
  • If Vercel goes down, medicalteam.cc continues to function
  • CORS configured to allow both domains

Data Export Endpoint

  • GET /admins/export (superadmin only)
  • Exports all tables: patients, doctors, admins, groups, audit_logs
  • Strips sensitive fields: password_hash, pin_hash, password_history
  • Returns JSON file with Content-Disposition header for download
  • Export action logged in audit trail

What's Protected Against

  • DNS failure on primary domain → backup domain on different DNS
  • Vercel outage → Cloudflare Pages serves the same site
  • Data loss → manual JSON export available anytime
  • Domain expiration → backup domain independent

Single Points of Failure (Not Yet Mitigated)

  • Railway outage → both sites lose API access (same backend)
  • Supabase outage → database unavailable for both sites
These are mitigated by Supabase's automatic daily backups and Railway's container redundancy.

SSL/TLS Security

  • Both sites use TLS 1.3 encryption
  • SSL Labs grade: A+ (medicalteam.cc confirmed)
  • Certificates managed automatically by Cloudflare and Vercel
  • HSTS preload configured (max-age 2 years)

Security Headers

  • SecurityHeadersMiddleware on backend + identical headers in vercel.json
  • HSTS preload (2-year max-age), COOP/CORP same-origin, strict CSP
  • CORS restricted: explicit origins, methods, and headers; localhost excluded in production
  • PDF_OWNER_PASSWORD required from environment (no hardcoded fallback)
  • 15-minute idle session timeout with mobile visibility check
  • Target: A+ on SSL Labs, SecurityHeaders.com, and Mozilla Observatory

11. 災難復原

雙主機前端部署

  • 主站: Vercel(crmedicalteam.com)— 從 GitHub 自動部署
  • 備用: Cloudflare Pages(medicalteam.cc)— 從同一個 GitHub 儲存庫自動部署
  • 兩者皆指向相同的 Railway 後端和 Supabase 資料庫
  • 如果 Vercel 故障,medicalteam.cc 仍可繼續運作
  • CORS 已設定允許兩個網域

資料匯出端點

  • GET /admins/export(僅限超級管理員)
  • 匯出所有資料表:patients、doctors、admins、groups、audit_logs
  • 移除敏感欄位:password_hashpin_hashpassword_history
  • 回傳帶有 Content-Disposition 標頭的 JSON 檔案供下載
  • 匯出操作會記錄在稽核軌跡中

已防護的風險

  • 主要網域 DNS 故障 → 備用網域使用不同的 DNS
  • Vercel 服務中斷 → Cloudflare Pages 提供相同的網站
  • 資料遺失 → 隨時可手動匯出 JSON
  • 網域過期 → 備用網域獨立運作

單點故障(尚未完全緩解)

  • Railway 服務中斷 → 兩個網站都會失去 API 存取(共用同一後端)
  • Supabase 服務中斷 → 兩個網站的資料庫都無法使用
這些風險透過 Supabase 的每日自動備份和 Railway 的容器冗餘來緩解。

SSL/TLS 安全性

  • 兩個網站皆使用 TLS 1.3 加密
  • SSL Labs 評級:A+(medicalteam.cc 已確認)
  • 憑證由 Cloudflare 和 Vercel 自動管理
  • 已設定 HSTS 預載(max-age 2 年)

安全標頭

  • 兩個平台使用完全相同的標頭(vercel.json + _headers 檔案)
  • 已通過 Mozilla Observatory 測試
  • 已設定 CSP、HSTS、X-Frame-Options、Referrer-Policy、Permissions-Policy、COOP、CORP、X-XSS-Protection

11. Recuperación ante Desastres

Despliegue Frontend en Doble Host

  • Principal: Vercel (crmedicalteam.com) — despliegue automático desde GitHub
  • Respaldo: Cloudflare Pages (medicalteam.cc) — despliegue automático desde el mismo repositorio de GitHub
  • Ambos apuntan al mismo backend de Railway y base de datos de Supabase
  • Si Vercel se cae, medicalteam.cc sigue funcionando
  • CORS configurado para permitir ambos dominios

Endpoint de Exportación de Datos

  • GET /admins/export (solo super administrador)
  • Exporta todas las tablas: patients, doctors, admins, groups, audit_logs
  • Elimina campos sensibles: password_hash, pin_hash, password_history
  • Devuelve archivo JSON con encabezado Content-Disposition para descarga
  • La acción de exportación se registra en el registro de auditoría

Riesgos Protegidos

  • Fallo de DNS en el dominio principal → dominio de respaldo en DNS diferente
  • Caída de Vercel → Cloudflare Pages sirve el mismo sitio
  • Pérdida de datos → exportación manual JSON disponible en cualquier momento
  • Expiración de dominio → dominio de respaldo independiente

Puntos Únicos de Fallo (Aún No Mitigados)

  • Caída de Railway → ambos sitios pierden acceso a la API (mismo backend)
  • Caída de Supabase → base de datos no disponible para ambos sitios
Estos riesgos se mitigan con las copias de seguridad diarias automáticas de Supabase y la redundancia de contenedores de Railway.

Seguridad SSL/TLS

  • Ambos sitios utilizan cifrado TLS 1.3
  • Calificación SSL Labs: A+ (medicalteam.cc confirmado)
  • Certificados gestionados automáticamente por Cloudflare y Vercel
  • HSTS preload configurado (max-age 2 años)

Encabezados de Seguridad

  • Encabezados idénticos en ambas plataformas (vercel.json + archivo _headers)
  • Probado con Mozilla Observatory
  • CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, CORP, X-XSS-Protection configurados

12. Multi-Specialty Architecture

ProjectMed supports multiple medical specialties through a flexible, JSONB-based architecture. Doctors can register with one or more specialties, and each visit is routed to the appropriate form template.

Specialty Registration

During registration, doctors select one or more specialties from a predefined list. Each specialty requires its own license number. All 12 specialties now have dedicated visit form templates:

  • General / Family Medicine
  • Internal Medicine (ROS, chronic disease panels, medication reconciliation)
  • Sports Medicine
  • Psychology / Mental Health
  • Pediatrics
  • Pediatric Psychology
  • OB/GYN (prenatal + gynecological visits)
  • Nutrition / Dietetics
  • Dermatology
  • Rehabilitation / Physical Therapy
  • Traditional Chinese Medicine (中醫)
  • Bone-Setting (整骨)

Data Model

The doctors table contains a specialties JSONB column that stores an array of objects, each with a specialty name and corresponding license number:

[{"specialty": "psychology", "license_number": "LIC123"}, {"specialty": "pediatrics", "license_number": "PED456"}]

The legacy specialty string field is maintained for backward compatibility with older records.

Visit Type Routing

Each visit is stamped with a visit_type field (defaults to "general"). Visits also record the doctor_name and doctor_id from the JWT token at creation time. Single-specialty doctors have their visit type auto-assigned. Multi-specialty doctors select the visit type per visit.

Visit Storage

All visit types are stored in the same JSONB visits array on the patient record. Each visit has a different internal structure based on its visit_type, but the container is universal — no schema changes are needed when adding a new specialty.

Frontend Routing

A visit form component selector renders the appropriate form based on visit_type. All 12 specialties now have dedicated form templates.

Specialty-Specific Forms (All Implemented)

Each specialty has a tailored visit form:

  • General / Family Medicine: Standard SOAP format with vitals, exam, assessment, plan
  • Internal Medicine: Review of Systems (ROS), chronic disease management panels, medication reconciliation
  • Sports Medicine: Injury assessment, sport clearance, return-to-play protocols
  • Psychology: Mental Status Examination (MSE), risk assessment, session notes, treatment goals
  • Pediatrics: Growth tracking (height/weight percentiles), developmental milestones, vaccination records
  • Pediatric Psychology: Child-specific behavioral assessments, caregiver input, play therapy notes
  • OB/GYN: Prenatal visits (gestational age, fundal height, fetal heart rate) and gynecological visits
  • Nutrition: Dietary evaluation, meal plans, body composition tracking, nutritional lab results
  • Dermatology: Lesion documentation, body map, photographic records, treatment protocols
  • Rehabilitation / PT: Functional assessments, range of motion tracking, exercise prescriptions, progress measurements
  • Traditional Chinese Medicine: Four examinations (四診), tongue/pulse assessment, herbal prescriptions, acupuncture point records
  • Bone-Setting: Structural assessment, manipulation records, alignment tracking

Architecture Principle

Universal patient record + specialty-specific visit templates. Every patient has a single record with demographics, medical history, and allergies shared across all specialties. Each visit stores its specialty-specific data as JSONB, so no database schema changes are required when adding new specialties. The architecture is intentionally flexible and extensible.

12. 多專科架構

ProjectMed 透過彈性的 JSONB 架構支援多種醫療專科。醫師可以註冊一個或多個專科,每次看診會路由至適當的表單範本。

專科註冊

在註冊過程中,醫師從預設清單中選擇一個或多個專科。每個專科需要其對應的執照號碼。全部 12 個專科現已配備專屬看診表單範本:

  • 一般 / 家庭醫學
  • 內科(系統回顧 ROS、慢性病管理面板、用藥核對)
  • 運動醫學
  • 心理學 / 心理健康
  • 兒科
  • 兒童心理學
  • 婦產科(產前 + 婦科看診)
  • 營養學 / 飲食學
  • 皮膚科
  • 復健 / 物理治療
  • 中醫
  • 整骨

資料模型

doctors 資料表包含一個 specialties JSONB 欄位,儲存物件陣列,每個物件包含專科名稱和對應的執照號碼:

[{"specialty": "psychology", "license_number": "LIC123"}, {"specialty": "pediatrics", "license_number": "PED456"}]

舊版 specialty 字串欄位仍然保留,以維持與舊記錄的向後相容性。

看診類型路由

每次看診都標記一個 visit_type 欄位(預設為 "general")。看診記錄同時記載來自 JWT 權杖的 doctor_namedoctor_id。單一專科醫師的看診類型會自動指派。多專科醫師則在每次看診時選擇看診類型。

看診儲存

所有看診類型都儲存在病患記錄的同一個 JSONB visits 陣列中。每次看診根據其 visit_type 有不同的內部結構,但容器是通用的 — 新增專科時不需要變更資料庫結構。

前端路由

看診表單元件選擇器根據 visit_type 渲染適當的表單。全部 12 個專科現已配備專屬表單範本。

專科特定表單(全部完成)

每個專科都有量身定制的看診表單:

  • 一般 / 家庭醫學:標準 SOAP 格式含生命徵象、檢查、評估、計畫
  • 內科:系統回顧(ROS)、慢性病管理面板、用藥核對
  • 運動醫學:傷害評估、運動許可、重返運動方案
  • 心理學:精神狀態檢查(MSE)、風險評估、治療紀錄、治療目標
  • 兒科:生長追蹤(身高/體重百分位)、發展里程碑、疫苗接種紀錄
  • 兒童心理學:兒童行為評估、照顧者輸入、遊戲治療紀錄
  • 婦產科:產前看診(孕週、子宮底高、胎心率)和婦科看診
  • 營養學:飲食評估、餐食計劃、體組成追蹤、營養實驗室結果
  • 皮膚科:病灶記錄、身體部位圖、影像記錄、治療方案
  • 復健 / 物理治療:功能評估、關節活動度追蹤、運動處方、進度測量
  • 中醫:四診(望聞問切)、舌脈評估、中藥處方、針灸穴位記錄
  • 整骨:結構評估、手法治療記錄、對位追蹤

架構原則

通用病患記錄 + 專科特定看診範本。每位病患都有一份記錄,包含基本資料、病史和過敏資訊,所有專科共享。每次看診將專科特定資料以 JSONB 格式儲存,因此新增專科時不需要變更資料庫結構。架構設計上具有靈活性和可擴展性。

12. Arquitectura Multi-Especialidad

ProjectMed soporta múltiples especialidades médicas a través de una arquitectura flexible basada en JSONB. Los médicos pueden registrarse con una o más especialidades, y cada visita se dirige a la plantilla de formulario apropiada.

Registro de Especialidades

Durante el registro, los médicos seleccionan una o más especialidades de una lista predefinida. Cada especialidad requiere su propio número de licencia. Las 12 especialidades ahora tienen plantillas de formulario dedicadas:

  • Medicina General / Familiar
  • Medicina Interna (ROS, paneles de enfermedades crónicas, conciliación de medicamentos)
  • Medicina Deportiva
  • Psicología / Salud Mental
  • Pediatría
  • Psicología Infantil
  • Ginecología y Obstetricia (visitas prenatales + ginecológicas)
  • Nutrición / Dietética
  • Dermatología
  • Rehabilitación / Fisioterapia
  • Medicina Tradicional China (中醫)
  • Osteopatía (整骨)

Modelo de Datos

La tabla doctors contiene una columna JSONB specialties que almacena un arreglo de objetos, cada uno con el nombre de la especialidad y su número de licencia correspondiente:

[{"specialty": "psychology", "license_number": "LIC123"}, {"specialty": "pediatrics", "license_number": "PED456"}]

El campo heredado specialty de tipo texto se mantiene para compatibilidad con registros anteriores.

Enrutamiento por Tipo de Visita

Cada visita se marca con un campo visit_type (por defecto "general"). Las visitas también registran el doctor_name y doctor_id del token JWT al momento de la creación. Los médicos con una sola especialidad tienen el tipo de visita asignado automáticamente. Los médicos con múltiples especialidades seleccionan el tipo por visita.

Almacenamiento de Visitas

Todos los tipos de visita se almacenan en el mismo arreglo JSONB visits del registro del paciente. Cada visita tiene una estructura interna diferente según su visit_type, pero el contenedor es universal — no se requieren cambios en el esquema de la base de datos al agregar una nueva especialidad.

Enrutamiento en el Frontend

Un selector de componentes de formulario renderiza el formulario apropiado según el visit_type. Las 12 especialidades ahora tienen plantillas de formulario dedicadas.

Formularios por Especialidad (Todos Implementados)

Cada especialidad tiene un formulario de visita adaptado:

  • Medicina General / Familiar: Formato SOAP estándar con signos vitales, examen, evaluación, plan
  • Medicina Interna: Revisión por Sistemas (ROS), paneles de enfermedades crónicas, conciliación de medicamentos
  • Medicina Deportiva: Evaluación de lesiones, autorización deportiva, protocolos de retorno al juego
  • Psicología: Examen del Estado Mental (MSE), evaluación de riesgo, notas de sesión, objetivos de tratamiento
  • Pediatría: Seguimiento de crecimiento (percentiles de talla/peso), hitos del desarrollo, registros de vacunación
  • Psicología Infantil: Evaluaciones conductuales específicas para niños, aportes del cuidador, notas de terapia de juego
  • Ginecología y Obstetricia: Visitas prenatales (edad gestacional, altura uterina, frecuencia cardíaca fetal) y visitas ginecológicas
  • Nutrición: Evaluación dietética, planes de comidas, seguimiento de composición corporal, resultados de laboratorio nutricional
  • Dermatología: Documentación de lesiones, mapa corporal, registros fotográficos, protocolos de tratamiento
  • Rehabilitación / Fisioterapia: Evaluaciones funcionales, seguimiento de rango de movimiento, prescripciones de ejercicio, mediciones de progreso
  • Medicina Tradicional China: Cuatro exámenes (四診), evaluación de lengua/pulso, prescripciones herbales, registros de puntos de acupuntura
  • Osteopatía: Evaluación estructural, registros de manipulación, seguimiento de alineación

Principio Arquitectónico

Registro universal del paciente + plantillas de visita por especialidad. Cada paciente tiene un único registro con datos demográficos, historial médico y alergias compartidos entre todas las especialidades. Cada visita almacena sus datos específicos como JSONB, por lo que no se requieren cambios en el esquema de la base de datos al agregar nuevas especialidades. La arquitectura es intencionalmente flexible y extensible.

13. ICD-10 Integration

ProjectMed includes a built-in ICD-10 code database for standardized diagnosis coding.

Code Database

The file icd10.json contains 1,532 ICD-10 codes with trilingual labels (English, Traditional Chinese, Spanish). Codes are loaded client-side for instant search.

ICD10AutoComplete Component

The ICD10AutoComplete React component provides debounced search as the doctor types a diagnosis. Doctors can search by disease name in any language or by ICD-10 code directly. Selecting a suggestion saves both the human-readable name and the ICD-10 code. Free text is still allowed if no matching code exists.

Display in Visits and PDF

When an ICD-10 code is associated with a diagnosis, it is displayed alongside the diagnosis name in the visit list view and in exported PDF reports. This provides standardized coding for clinical documentation and interoperability.

Backward Compatibility

Older visits created before ICD-10 integration still work normally. The code field is optional — visits without ICD-10 codes display the free-text diagnosis as before.

13. ICD-10 整合

ProjectMed 內建 ICD-10 代碼資料庫,用於標準化診斷編碼。

代碼資料庫

icd10.json 檔案包含 1,532 個 ICD-10 代碼,具有三語標籤(英文、繁體中文、西班牙文)。代碼在客戶端載入,可即時搜尋。

ICD10AutoComplete 元件

ICD10AutoComplete React 元件在醫師輸入診斷時提供防抖搜尋。醫師可以用任何語言的疾病名稱或直接用 ICD-10 代碼搜尋。選擇建議後會同時儲存人類可讀名稱和 ICD-10 代碼。如果沒有匹配的代碼,仍可輸入自由文字。

在看診和 PDF 中顯示

當 ICD-10 代碼與診斷關聯時,代碼會在看診列表和匯出的 PDF 報告中與診斷名稱一同顯示。這為臨床文件和互通性提供標準化編碼。

向後相容性

在 ICD-10 整合之前建立的舊看診紀錄仍可正常運作。代碼欄位為選填 — 沒有 ICD-10 代碼的看診紀錄會如同以前一樣顯示自由文字診斷。

13. Integración ICD-10

ProjectMed incluye una base de datos ICD-10 integrada para la codificación estandarizada de diagnósticos.

Base de Datos de Códigos

El archivo icd10.json contiene 1,532 códigos ICD-10 con etiquetas trilingües (inglés, chino tradicional, español). Los códigos se cargan en el cliente para búsqueda instantánea.

Componente ICD10AutoComplete

El componente React ICD10AutoComplete proporciona búsqueda con antirrebote mientras el médico escribe un diagnóstico. Los médicos pueden buscar por nombre de enfermedad en cualquier idioma o directamente por código ICD-10. Al seleccionar una sugerencia se guardan tanto el nombre legible como el código ICD-10. El texto libre sigue siendo permitido si no existe un código coincidente.

Visualización en Consultas y PDF

Cuando un código ICD-10 está asociado con un diagnóstico, se muestra junto al nombre del diagnóstico en la vista de lista de consultas y en los informes PDF exportados. Esto proporciona codificación estandarizada para documentación clínica e interoperabilidad.

Compatibilidad con Versiones Anteriores

Las visitas antiguas creadas antes de la integración ICD-10 siguen funcionando normalmente. El campo de código es opcional — las visitas sin códigos ICD-10 muestran el diagnóstico en texto libre como antes.

14. FHIR R4 Compliance

ProjectMed supports exporting patient records as FHIR R4 Bundles, the international standard for health data interoperability.

FHIR Service

fhir_service.py builds a FHIR R4 Bundle containing the following resources:

  • Patient — demographics, identifiers, contact information
  • Practitioner — the exporting doctor's information
  • Encounter — one per visit, linked to conditions and observations
  • Observation — vital signs and measurements, LOINC-coded
  • Condition — diagnoses with ICD-10 codes
  • MedicationRequest — prescribed medications with dosage instructions
  • AllergyIntolerance — patient allergies with severity

Endpoints

  • GET /patients/{id}/export-fhir — download FHIR R4 JSON Bundle
  • POST /patients/{id}/email-record — send record (PDF or FHIR) as email attachment to another doctor

Both endpoints require PIN verification. All exports are logged in the audit trail.

14. FHIR R4 合規

ProjectMed 支援將病患紀錄匯出為 FHIR R4 Bundle,這是健康資料互通性的國際標準。

FHIR 服務

fhir_service.py 建構包含以下資源的 FHIR R4 Bundle:

  • Patient — 基本資料、識別碼、聯絡資訊
  • Practitioner — 匯出醫師的資訊
  • Encounter — 每次看診一筆,連結至病況和觀察
  • Observation — 生命徵象和測量值,LOINC 編碼
  • Condition — 含 ICD-10 代碼的診斷
  • MedicationRequest — 處方藥物及劑量指示
  • AllergyIntolerance — 病患過敏及嚴重程度

端點

  • GET /patients/{id}/export-fhir — 下載 FHIR R4 JSON Bundle
  • POST /patients/{id}/email-record — 以電子郵件附件方式寄送紀錄(PDF 或 FHIR)給另一位醫師

兩個端點都需要 PIN 驗證。所有匯出都記錄在稽核日誌中。

14. Cumplimiento FHIR R4

ProjectMed soporta la exportación de registros de pacientes como Bundles FHIR R4, el estándar internacional para la interoperabilidad de datos de salud.

Servicio FHIR

fhir_service.py construye un Bundle FHIR R4 que contiene los siguientes recursos:

  • Patient — datos demográficos, identificadores, información de contacto
  • Practitioner — información del médico que exporta
  • Encounter — uno por visita, vinculado a condiciones y observaciones
  • Observation — signos vitales y mediciones, codificados con LOINC
  • Condition — diagnósticos con códigos ICD-10
  • MedicationRequest — medicamentos recetados con instrucciones de dosificación
  • AllergyIntolerance — alergias del paciente con severidad

Endpoints

  • GET /patients/{id}/export-fhir — descargar Bundle FHIR R4 en JSON
  • POST /patients/{id}/email-record — enviar registro (PDF o FHIR) como adjunto por correo a otro médico

Ambos endpoints requieren verificación de PIN. Todas las exportaciones se registran en el registro de auditoría.

Contact & Support

If you encounter any issues with the website, have questions, or need support, please contact David Chen (陳顥丰):

We will respond as soon as possible.

聯絡與支援

如果您在使用網站時遇到任何問題、有疑問或需要協助,請聯繫陳顥丰 (David Chen)

我們會盡快回覆。

Contacto y Soporte

Si encuentra algún problema con el sitio web, tiene preguntas o necesita asistencia, contacte a David Chen (陳顥丰):

Responderemos lo antes posible.