merge: sync dev into main
# Conflicts: # README.md # frontend/src/views/ia/ProjectMilestones.vue
This commit is contained in:
@@ -5,6 +5,7 @@ CTMS 文档按用途分为三类:当前操作手册、审计与治理记录、
|
||||
## 当前常用
|
||||
|
||||
- [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁
|
||||
- [`guides/branch-environment-installation.md`](guides/branch-environment-installation.md): `dev`、`main`、`release` 分支环境安装配置
|
||||
- [`guides/setup-config-api.md`](guides/setup-config-api.md): 立项配置接口、联调与冒烟说明
|
||||
- [`setup-config-curl-smoke.sh`](setup-config-curl-smoke.sh): 立项配置 curl 冒烟脚本
|
||||
- [`postman/setup-config.postman_collection.json`](postman/setup-config.postman_collection.json): Postman 联调集合
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# CTMS 分支环境安装配置指南
|
||||
|
||||
本文定义 `dev`、`main`、`release` 三类分支对应的推荐部署环境、配置文件、初始化步骤与验证命令。分支本身不会自动切换运行模式,实际模式由后端环境变量 `ENV` 决定。
|
||||
|
||||
## 分支与环境映射
|
||||
|
||||
| 分支 | 分支定位 | 推荐环境 | 后端 `ENV` | 用途 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `dev` | 日常开发与集成 | 开发环境 | `development` | 功能开发、联调、自测、内部集成 |
|
||||
| `main` | 下一版本候选 | 预发布 / 验收环境 | `production` | 回归测试、验收、发布候选验证 |
|
||||
| `release` | 当前稳定生产线 | 生产环境 | `production` | 正式生产部署、生产 hotfix |
|
||||
|
||||
推广路径保持为:
|
||||
|
||||
```text
|
||||
feature/* -> dev -> main -> release
|
||||
```
|
||||
|
||||
不要把 `release` 当作日常开发分支使用。`main` 应只接收已经准备进入候选版本范围的变更。
|
||||
|
||||
## 公共前置条件
|
||||
|
||||
所有环境都需要:
|
||||
|
||||
- Docker 与 Docker Compose
|
||||
- 可写的 `pg_data/` 数据目录
|
||||
- 端口 `8888`、`8000`、`5432` 未被占用
|
||||
- 后端镜像可安装 `backend/requirements.txt` 中的依赖
|
||||
- 前端镜像可执行 `npm ci` 与 `npm run build`
|
||||
|
||||
当前 compose 服务拓扑:
|
||||
|
||||
```text
|
||||
nginx -> backend -> db
|
||||
```
|
||||
|
||||
对外入口:
|
||||
|
||||
- 前端:`http://localhost:8888`
|
||||
- 后端 API:同域 `/api/v1/*`
|
||||
- 健康检查:`http://localhost:8888/health`
|
||||
|
||||
## dev 分支:开发环境
|
||||
|
||||
`dev` 默认用于本地开发和内部集成,推荐使用 `ENV=development`。
|
||||
|
||||
### 1. 切换分支
|
||||
|
||||
```bash
|
||||
git checkout dev
|
||||
git pull --rebase origin dev
|
||||
```
|
||||
|
||||
### 2. 配置 `.env`
|
||||
|
||||
根目录 `.env` 不提交到仓库。开发环境推荐:
|
||||
|
||||
```env
|
||||
COMPOSE_PROJECT_NAME=ctms_dev
|
||||
ENV=development
|
||||
JWT_SECRET_KEY=dev-secret
|
||||
LOGIN_RSA_KEY_ID=default
|
||||
LOGIN_RSA_PRIVATE_KEY=
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `ENV=development` 会启用开发行为。
|
||||
- 未配置 `LOGIN_RSA_PRIVATE_KEY` 时,后端启动后会生成临时 RSA 私钥。
|
||||
- 后端重启后临时公钥会变化,已有登录页应刷新后重新登录。
|
||||
- `JWT_SECRET_KEY=dev-secret` 只允许本地开发使用。
|
||||
|
||||
### 3. 启动
|
||||
|
||||
```bash
|
||||
docker compose up -d --build backend nginx
|
||||
```
|
||||
|
||||
如果需要首次启动完整栈:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### 4. 开发模式行为
|
||||
|
||||
`development` 模式下:
|
||||
|
||||
- FastAPI `debug=True`
|
||||
- 应用启动时会执行 `Base.metadata.create_all`
|
||||
- 应用启动时会确保默认管理员存在
|
||||
- 未配置 RSA 私钥时允许临时生成
|
||||
|
||||
这些行为只适合开发,不适合生产。
|
||||
|
||||
### 5. 验证
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV)"
|
||||
curl -i http://127.0.0.1:8888/health
|
||||
curl -i http://127.0.0.1:8888/api/v1/auth/login-key
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
ENV=development
|
||||
GET /health -> 200
|
||||
GET /api/v1/auth/login-key -> 200, key_id=default
|
||||
```
|
||||
|
||||
## main 分支:预发布 / 验收环境
|
||||
|
||||
`main` 是下一正式版本候选分支,应按生产模式运行,但不直接承载正式生产流量。
|
||||
|
||||
### 1. 切换分支
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull --rebase origin main
|
||||
```
|
||||
|
||||
### 2. 配置 `.env`
|
||||
|
||||
预发布环境应使用 `ENV=production`,并配置独立于生产的密钥:
|
||||
|
||||
```env
|
||||
COMPOSE_PROJECT_NAME=ctms_staging
|
||||
ENV=production
|
||||
JWT_SECRET_KEY=<staging-strong-random-secret>
|
||||
LOGIN_RSA_KEY_ID=staging-YYYYMMDD
|
||||
LOGIN_RSA_PRIVATE_KEY=<staging-rsa-private-key-pem-with-\n-escaped-newlines>
|
||||
```
|
||||
|
||||
生成密钥示例:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `JWT_SECRET_KEY` 不得使用 `dev-secret`。
|
||||
- `LOGIN_RSA_PRIVATE_KEY` 必须固定保存,不能每次部署重新生成。
|
||||
- staging 私钥不得复用 production 私钥。
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
生产模式不会自动建表或自动补管理员。首次部署或迁移前执行:
|
||||
|
||||
```bash
|
||||
docker compose run --rm backend-init
|
||||
```
|
||||
|
||||
该步骤应运行 Alembic migration,并确保固定管理员账号存在。
|
||||
|
||||
### 4. 启动
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### 5. 验证
|
||||
|
||||
```bash
|
||||
docker compose config
|
||||
docker compose ps
|
||||
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV); print(settings.JWT_SECRET_KEY == 'dev-secret'); print(bool(settings.LOGIN_RSA_PRIVATE_KEY))"
|
||||
curl -i http://127.0.0.1:8888/health
|
||||
curl -i http://127.0.0.1:8888/api/v1/auth/login-key
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
ENV=production
|
||||
JWT_SECRET_KEY == dev-secret -> False
|
||||
LOGIN_RSA_PRIVATE_KEY_SET -> True
|
||||
GET /health -> 200
|
||||
GET /api/v1/auth/login-key -> 200
|
||||
```
|
||||
|
||||
### 6. 验收门禁
|
||||
|
||||
在把 `main` 推进到 `release` 前,至少执行:
|
||||
|
||||
```bash
|
||||
docker compose run --rm -v "$PWD/backend/tests:/code/tests:ro" backend python -m pytest
|
||||
cd frontend && npm run test:unit
|
||||
cd frontend && npm run type-check
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
## release 分支:生产环境
|
||||
|
||||
`release` 是正式生产稳定分支。只有正式发布和生产 hotfix 应进入该分支。
|
||||
|
||||
### 1. 切换分支
|
||||
|
||||
```bash
|
||||
git checkout release
|
||||
git pull --rebase origin release
|
||||
```
|
||||
|
||||
### 2. 配置 `.env`
|
||||
|
||||
生产环境必须使用生产专用配置:
|
||||
|
||||
```env
|
||||
COMPOSE_PROJECT_NAME=ctms_prod
|
||||
ENV=production
|
||||
JWT_SECRET_KEY=<production-strong-random-secret>
|
||||
LOGIN_RSA_KEY_ID=prod-YYYYMMDD
|
||||
LOGIN_RSA_PRIVATE_KEY=<production-rsa-private-key-pem-with-\n-escaped-newlines>
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `.env` 必须只保存在部署机器或密钥管理系统中。
|
||||
- 不得提交 `.env`、私钥、JWT 密钥。
|
||||
- `JWT_SECRET_KEY` 轮换会使既有 token 失效,应安排维护窗口。
|
||||
- `LOGIN_RSA_PRIVATE_KEY` 轮换会影响新登录密钥获取,应同步更新 `LOGIN_RSA_KEY_ID`。
|
||||
|
||||
### 3. HTTPS 要求
|
||||
|
||||
登录加密依赖浏览器 WebCrypto。生产访问必须使用 HTTPS:
|
||||
|
||||
- `https://正式域名`
|
||||
- 本地 `localhost` 是浏览器安全上下文例外,但不能代表生产可用性
|
||||
|
||||
如果生产仍通过 `http://服务器IP:8888` 访问,前端会拒绝执行登录加密。
|
||||
|
||||
### 4. 初始化与迁移
|
||||
|
||||
首次部署或每次包含 migration 的发布:
|
||||
|
||||
```bash
|
||||
docker compose run --rm backend-init
|
||||
```
|
||||
|
||||
确认 migration 成功后再启动或滚动重启服务。
|
||||
|
||||
### 5. 启动
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### 6. 生产验证
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV); print(settings.JWT_SECRET_KEY == 'dev-secret'); print(bool(settings.LOGIN_RSA_PRIVATE_KEY)); print(settings.LOGIN_RSA_KEY_ID)"
|
||||
curl -i https://<production-domain>/health
|
||||
curl -i https://<production-domain>/api/v1/auth/login-key
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
ENV=production
|
||||
JWT_SECRET_KEY == dev-secret -> False
|
||||
LOGIN_RSA_PRIVATE_KEY_SET -> True
|
||||
GET /health -> 200
|
||||
GET /api/v1/auth/login-key -> 200
|
||||
```
|
||||
|
||||
### 7. 多实例约束
|
||||
|
||||
当前登录 challenge 默认保存在后端进程内:
|
||||
|
||||
- 单实例、单 worker:可直接使用
|
||||
- 多实例或多 worker:必须满足以下至少一项
|
||||
- 使用共享缓存保存 challenge,例如 Redis
|
||||
- 启用粘性会话,确保 `/login-key` 与 `/login` 命中同一后端进程
|
||||
- 将后端限制为单 worker 单副本
|
||||
|
||||
如果不满足,上线后可能出现偶发登录失败。
|
||||
|
||||
## 环境变量说明
|
||||
|
||||
| 变量 | dev | main/staging | release/production | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `COMPOSE_PROJECT_NAME` | `ctms_dev` | `ctms_staging` | `ctms_prod` | 防止不同环境容器名、网络名冲突 |
|
||||
| `ENV` | `development` | `production` | `production` | 后端运行模式 |
|
||||
| `JWT_SECRET_KEY` | `dev-secret` | 强随机 | 强随机 | JWT 签名密钥 |
|
||||
| `LOGIN_RSA_KEY_ID` | `default` | `staging-YYYYMMDD` | `prod-YYYYMMDD` | 登录 RSA 密钥版本 |
|
||||
| `LOGIN_RSA_PRIVATE_KEY` | 空 | staging 私钥 | production 私钥 | RSA 私钥,生产模式必填 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 登录页提示当前浏览器环境不支持安全登录加密
|
||||
|
||||
原因:非 HTTPS、非 localhost 的访问环境不满足 WebCrypto 安全上下文要求。
|
||||
|
||||
处理:
|
||||
|
||||
- 本地使用 `http://localhost:8888`
|
||||
- staging/production 使用 HTTPS 域名
|
||||
|
||||
### production 模式启动失败,提示必须配置 LOGIN_RSA_PRIVATE_KEY
|
||||
|
||||
原因:`ENV=production` 时必须提供固定 RSA 私钥。
|
||||
|
||||
处理:
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048
|
||||
```
|
||||
|
||||
将输出 PEM 写入 `.env` 的 `LOGIN_RSA_PRIVATE_KEY`,换行使用 `\n` 转义。
|
||||
|
||||
### 切换分支后环境不符合预期
|
||||
|
||||
分支不会自动修改 `.env`。切换分支后应重新确认:
|
||||
|
||||
```bash
|
||||
docker compose config
|
||||
docker compose exec backend python -c "from app.core.config import settings; print(settings.ENV)"
|
||||
```
|
||||
|
||||
必要时修改 `.env` 并重启:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build backend nginx
|
||||
```
|
||||
@@ -104,13 +104,21 @@
|
||||
"plan_end_date": "2026-12-31",
|
||||
"planned_site_count": 12,
|
||||
"planned_enrollment_count": 120,
|
||||
"summary_note": "",
|
||||
"objective_note": "",
|
||||
"status": "DRAFT",
|
||||
"visit_interval_days": null,
|
||||
"visit_total": null,
|
||||
"visit_window_start_offset": null,
|
||||
"visit_window_end_offset": null
|
||||
"visit_schedule": [
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"saved_by": "11111111-1111-1111-1111-111111111111",
|
||||
"saved_by_name": "System Admin",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Business Table Width Unification Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 统一用户实际浏览的业务列表/详情表格列宽,使表格横向铺满容器、避免横向滚动,并保持操作列紧凑。
|
||||
|
||||
**Architecture:** 仅处理业务浏览类表格页面,不改管理配置矩阵和计划编辑类表格。通过为目标表格统一启用固定布局、移除大部分硬编码列宽、保留少量操作列宽度,并为长文本列补充溢出提示来实现一致体验。
|
||||
|
||||
**Tech Stack:** Vue 3, Element Plus, Vite, scoped CSS
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 锁定业务表格范围
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/admin/Users.vue`
|
||||
- Modify: `frontend/src/views/admin/AdminUserApproval.vue`
|
||||
- Modify: `frontend/src/views/admin/AuditLogs.vue`
|
||||
- Modify: `frontend/src/views/admin/Projects.vue`
|
||||
- Modify: `frontend/src/views/ia/FinanceContracts.vue`
|
||||
- Modify: `frontend/src/views/ia/FinanceSpecial.vue`
|
||||
- Modify: `frontend/src/views/documents/DocumentList.vue`
|
||||
- Modify: `frontend/src/views/ia/SubjectManagement.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssueSae.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssuePd.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssueMonitoringVisits.vue`
|
||||
- Modify: `frontend/src/views/ia/ProjectMilestones.vue`
|
||||
- Modify: `frontend/src/views/ia/StartupFeasibilityEthics.vue`
|
||||
- Modify: `frontend/src/views/ia/StartupMeetingAuth.vue`
|
||||
- Modify: `frontend/src/views/ia/KnowledgeNotes.vue`
|
||||
- Modify: `frontend/src/views/ia/DrugShipments.vue`
|
||||
- Modify: `frontend/src/views/ia/MaterialEquipment.vue`
|
||||
- Modify: `frontend/src/views/subjects/SubjectDetail.vue`
|
||||
- Modify: `frontend/src/views/fees/ContractFeeDetail.vue`
|
||||
- Modify: `frontend/src/views/documents/DocumentDetail.vue`
|
||||
- Modify: `frontend/src/views/startup/KickoffDetail.vue`
|
||||
- Modify: `frontend/src/components/attachments/AttachmentList.vue`
|
||||
- Modify: `frontend/src/components/fees/FeeAttachmentPanel.vue`
|
||||
- Modify: `frontend/src/components/FaqList.vue`
|
||||
- Modify: `frontend/src/views/workbench/components/CenterSummary.vue`
|
||||
|
||||
**Step 1: 仅保留业务浏览页**
|
||||
|
||||
明确不修改 `frontend/src/views/admin/ProjectDetail.vue` 及其各类配置矩阵。
|
||||
|
||||
**Step 2: 统一策略**
|
||||
|
||||
- 业务表格统一启用 `table-layout="fixed"`
|
||||
- 取消大多数 `width` / `min-width`
|
||||
- 操作列保留较小固定宽度
|
||||
- 长文本列补 `show-overflow-tooltip`
|
||||
|
||||
### Task 2: 改列表页表格
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/admin/Users.vue`
|
||||
- Modify: `frontend/src/views/admin/AdminUserApproval.vue`
|
||||
- Modify: `frontend/src/views/admin/AuditLogs.vue`
|
||||
- Modify: `frontend/src/views/admin/Projects.vue`
|
||||
- Modify: `frontend/src/views/ia/FinanceContracts.vue`
|
||||
- Modify: `frontend/src/views/ia/FinanceSpecial.vue`
|
||||
- Modify: `frontend/src/views/documents/DocumentList.vue`
|
||||
- Modify: `frontend/src/views/ia/SubjectManagement.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssueSae.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssuePd.vue`
|
||||
- Modify: `frontend/src/views/ia/RiskIssueMonitoringVisits.vue`
|
||||
- Modify: `frontend/src/views/ia/ProjectMilestones.vue`
|
||||
- Modify: `frontend/src/views/ia/StartupFeasibilityEthics.vue`
|
||||
- Modify: `frontend/src/views/ia/StartupMeetingAuth.vue`
|
||||
- Modify: `frontend/src/views/ia/KnowledgeNotes.vue`
|
||||
- Modify: `frontend/src/views/ia/DrugShipments.vue`
|
||||
- Modify: `frontend/src/views/ia/MaterialEquipment.vue`
|
||||
|
||||
**Step 1: 为目标表格添加固定布局**
|
||||
|
||||
在 `el-table` 上显式加入 `table-layout="fixed"`,必要时补 `fit`。
|
||||
|
||||
**Step 2: 收缩操作列,释放内容列**
|
||||
|
||||
操作列保留 `width="110"` 或 `width="120"`;其余列尽量移除 `width` / `min-width`。
|
||||
|
||||
**Step 3: 为易溢出文本补 tooltip**
|
||||
|
||||
在标题、备注、描述、对象、变更明细等列使用 `show-overflow-tooltip`。
|
||||
|
||||
### Task 3: 改详情页子表和通用表格组件
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/subjects/SubjectDetail.vue`
|
||||
- Modify: `frontend/src/views/fees/ContractFeeDetail.vue`
|
||||
- Modify: `frontend/src/views/documents/DocumentDetail.vue`
|
||||
- Modify: `frontend/src/views/startup/KickoffDetail.vue`
|
||||
- Modify: `frontend/src/components/attachments/AttachmentList.vue`
|
||||
- Modify: `frontend/src/components/fees/FeeAttachmentPanel.vue`
|
||||
- Modify: `frontend/src/components/FaqList.vue`
|
||||
- Modify: `frontend/src/views/workbench/components/CenterSummary.vue`
|
||||
|
||||
**Step 1: 对齐详情子表策略**
|
||||
|
||||
详情子表同样改成固定布局,压缩状态/操作列宽度,文本列使用 tooltip。
|
||||
|
||||
**Step 2: 通用组件同步**
|
||||
|
||||
附件表、FAQ 表和中心摘要表也改成同一列宽语言,避免跨页面体验不一致。
|
||||
|
||||
### Task 4: 验证
|
||||
|
||||
**Files:**
|
||||
- Test: `frontend`
|
||||
|
||||
**Step 1: 运行构建**
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
**Expected:** 构建成功,无 TypeScript / Vue 模板错误。
|
||||
|
||||
**Step 2: 人工复查目标**
|
||||
|
||||
确认业务列表/详情表格:
|
||||
- 横向铺满容器
|
||||
- 默认不出现横向滚动条
|
||||
- 操作列不挤压主体信息
|
||||
@@ -6,7 +6,35 @@
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 登录(获取 Token)",
|
||||
"name": "1. 获取登录公钥",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/login-key",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "login-key"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('status is 200', function () { pm.response.to.have.status(200); });",
|
||||
"var json = pm.response.json();",
|
||||
"pm.collectionVariables.set('login_key_id', json.key_id || '');",
|
||||
"pm.collectionVariables.set('login_challenge', json.challenge || '');",
|
||||
"pm.collectionVariables.set('login_public_key', json.public_key || '');",
|
||||
"pm.collectionVariables.set('login_ciphertext', '<请生成 AES-GCM 密文,并用 login_public_key 通过 RSA-OAEP-SHA256 加密 AES key 后填写外层 Base64 envelope>');"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. 加密登录(获取 Token)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
@@ -14,7 +42,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"{{email}}\",\n \"password\": \"{{password}}\"\n}"
|
||||
"raw": "{\n \"key_id\": \"{{login_key_id}}\",\n \"challenge\": \"{{login_challenge}}\",\n \"ciphertext\": \"{{login_ciphertext}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/login",
|
||||
|
||||
@@ -7,10 +7,65 @@ PASSWORD="${PASSWORD:-admin123}"
|
||||
STUDY_ID="${STUDY_ID:-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}"
|
||||
|
||||
echo "[1/6] login: $EMAIL"
|
||||
TOKEN=$(curl -sS -X POST "$BASE_URL/api/v1/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | \
|
||||
python3 -c 'import json,sys; print(json.load(sys.stdin).get("access_token",""))')
|
||||
TOKEN=$(BASE_URL="$BASE_URL" EMAIL="$EMAIL" PASSWORD="$PASSWORD" python3 - <<'PY'
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
base_url = os.environ["BASE_URL"]
|
||||
email = os.environ["EMAIL"]
|
||||
password = os.environ["PASSWORD"]
|
||||
|
||||
with urllib.request.urlopen(f"{base_url}/api/v1/auth/login-key", timeout=20) as resp:
|
||||
login_key = json.loads(resp.read().decode())
|
||||
|
||||
public_key = serialization.load_pem_public_key(login_key["public_key"].encode("utf-8"))
|
||||
plaintext = json.dumps(
|
||||
{"email": email, "password": password, "challenge": login_key["challenge"]},
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
aes_key = AESGCM.generate_key(bit_length=256)
|
||||
iv = os.urandom(12)
|
||||
encrypted_data = AESGCM(aes_key).encrypt(iv, plaintext, None)
|
||||
encrypted_key = public_key.encrypt(
|
||||
aes_key,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None,
|
||||
),
|
||||
)
|
||||
payload = json.dumps(
|
||||
{
|
||||
"key_id": login_key["key_id"],
|
||||
"challenge": login_key["challenge"],
|
||||
"ciphertext": base64.b64encode(
|
||||
json.dumps(
|
||||
{
|
||||
"encrypted_key": base64.b64encode(encrypted_key).decode("ascii"),
|
||||
"iv": base64.b64encode(iv).decode("ascii"),
|
||||
"data": base64.b64encode(encrypted_data).decode("ascii"),
|
||||
},
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).decode("ascii"),
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/api/v1/auth/login",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=payload,
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
print(json.loads(resp.read().decode()).get("access_token", ""))
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "login failed: access_token empty"
|
||||
|
||||
Reference in New Issue
Block a user