465 lines
29 KiB
Markdown
465 lines
29 KiB
Markdown
---
|
||
phase: 02-admin-rest
|
||
plan: 02
|
||
type: execute
|
||
wave: 2
|
||
depends_on:
|
||
- "02-01"
|
||
files_modified:
|
||
- qy_lty/docs/修改记录.md
|
||
- qy-lty-admin/docs/修改记录.md
|
||
autonomous: true
|
||
requirements:
|
||
- CRED-03
|
||
- CRED-04
|
||
must_haves:
|
||
truths:
|
||
- "qy_lty/docs/修改记录.md 顶部新增一条 Phase 2 条目,跨项目联动字段引用 qy-lty-admin/docs/修改记录.md 的同期条目"
|
||
- "qy-lty-admin/docs/修改记录.md 顶部新增一条 Phase 2 条目,服务端联动字段引用 qy_lty/docs/修改记录.md 的同期条目(互引闭环)"
|
||
- "端到端 curl 验收:GET 携 admin token 返回 200 + 标准壳层 + access_token 脱敏(末 4 位明文 + 前缀 *)"
|
||
- "端到端 curl 验收:PUT 携 admin token + body 后 DB 全字段覆写 + updated_at 刷新 + 响应 access_token 脱敏"
|
||
- "端到端 curl 验收:无 token → 401 + 标准壳层;user token → 403 + message=需要管理员权限 + 标准壳层"
|
||
- "Swagger schema 暴露:/swagger.json 含 /api/v1/admin/credential-slot/ 路径条目,含 GET/PUT 两 method,access_token 字段 description 标注脱敏掩码语义"
|
||
artifacts:
|
||
- path: "qy_lty/docs/修改记录.md"
|
||
provides: "Phase 2 修改记录条目(含跨项目联动字段)"
|
||
contains: "Phase 2"
|
||
- path: "qy-lty-admin/docs/修改记录.md"
|
||
provides: "Phase 2 互引条目(含服务端联动字段)"
|
||
contains: "Phase 2"
|
||
key_links:
|
||
- from: "qy_lty/docs/修改记录.md (Phase 2 条目的跨项目联动字段)"
|
||
to: "qy-lty-admin/docs/修改记录.md (Phase 2 同期条目)"
|
||
via: "条目内文字引用 ../qy-lty-admin/docs/修改记录.md 同期条目"
|
||
pattern: "qy-lty-admin/docs/修改记录"
|
||
- from: "qy-lty-admin/docs/修改记录.md (Phase 2 条目的服务端联动字段)"
|
||
to: "qy_lty/docs/修改记录.md (Phase 2 同期条目)"
|
||
via: "条目内文字引用 ../qy_lty/docs/修改记录.md 同期条目"
|
||
pattern: "qy_lty/docs/修改记录"
|
||
---
|
||
|
||
<objective>
|
||
本 plan 完成 Phase 2 收尾:
|
||
1. 端到端验收 02-01 落地的 GET/PUT 接口(curl + Django shell test client 双层验证 8 条 success criteria)
|
||
2. 在 qy_lty + qy-lty-admin 两端 `docs/修改记录.md` 顶部各写一条 Phase 2 条目,且**跨项目联动字段相互引用**(CLAUDE.md 强制规则;Phase 2 是首次跨项目接口契约落地)
|
||
|
||
Purpose:
|
||
- 端到端验收:把 02-01 写出来的 view + serializer + URL 真正跑起来,证明三处 success criteria(GET 脱敏 / PUT 覆写 + 脱敏响应 / 鉴权拒绝矩阵)在生产路径成立
|
||
- 跨项目互引:给后续在 qy-lty-admin 起 CRED-FE-01 phase 时,前端能从修改记录直接定位到本 phase 的接口契约文档;给本仓库未来回查"这个接口什么时候上的、谁在消费"留一个反向锚点
|
||
|
||
Output:
|
||
- `qy_lty/docs/修改记录.md` 顶部新增一条 Phase 2 条目(5 字段 + 跨项目联动字段)
|
||
- `qy-lty-admin/docs/修改记录.md` 顶部新增一条 Phase 2 条目(5 字段 + 服务端联动字段)
|
||
- VERIFICATION.md 补充端到端 curl + Django shell 验收记录(在 SUMMARY 中归档)
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/PROJECT.md
|
||
@.planning/ROADMAP.md
|
||
@.planning/STATE.md
|
||
@.planning/phases/02-admin-rest/02-CONTEXT.md
|
||
@.planning/phases/02-admin-rest/02-RESEARCH.md
|
||
@.planning/phases/02-admin-rest/02-01-PLAN.md
|
||
|
||
# 02-01 落地后的 SUMMARY(含端到端 verify hook)
|
||
@.planning/phases/02-admin-rest/02-01-SUMMARY.md
|
||
|
||
# 修改记录格式 + 已落地条目模板
|
||
@qy_lty/docs/修改记录.md
|
||
@qy-lty-admin/docs/修改记录.md
|
||
|
||
# 项目宪法(修改记录强制 + 跨项目互引规则)
|
||
@qy_lty/CLAUDE.md
|
||
|
||
# 鉴权 / 工具入口(用于端到端验收脚本)
|
||
@qy_lty/userapp/utils.py
|
||
@qy_lty/userapp/authentication.py
|
||
@qy_lty/aiapp/models.py
|
||
@qy_lty/common/utils.py
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="false">
|
||
<name>Task 1:端到端 curl + Django shell 验收(8 条 success criteria)</name>
|
||
<files>qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md</files>
|
||
<read_first>
|
||
- 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md(02-01 留给本 task 的 hook)
|
||
- 必读 .planning/phases/02-admin-rest/02-CONTEXT.md `<specifics>` 段(8 条验收点表格)
|
||
- 必读 .planning/phases/02-admin-rest/02-RESEARCH.md `Validation Architecture` 段(test client 程序化验收脚本片段 + 采样频率)
|
||
- 必读 qy_lty/userapp/utils.py:33-37(generate_token 签发 admin / user token 的方式)
|
||
- 必读 qy_lty/aiapp/models.py 的 CredentialSlot.get_solo(验证 PUT 在空记录场景的 get_or_create)
|
||
- 必读 .planning/phases/01-credential-data-layer/01-VERIFICATION.md(Phase 1 程序化验收风格,本 task 1:1 沿用)
|
||
</read_first>
|
||
<action>
|
||
**目标**:在 Django shell + Daphne / runserver 两条路径都验完 8 条 success criteria,留一份证据归档进 02-VERIFICATION.md。
|
||
|
||
**Step 1:准备测试 token(一次性 Django shell 脚本)**
|
||
|
||
```bash
|
||
cd qy_lty && python manage.py shell <<'EOF'
|
||
from userapp.models import ParadiseUser
|
||
from userapp.utils import generate_token
|
||
import json
|
||
|
||
# 取一个已有 staff 用户(必须 is_staff=True;如全库无 staff,先 createsuperuser)
|
||
admin_user = ParadiseUser.objects.filter(is_staff=True).first()
|
||
assert admin_user is not None, "需要至少一个 is_staff=True 用户;运行 python manage.py createsuperuser"
|
||
admin_token = generate_token(admin_user.id, is_admin=True)
|
||
|
||
# 取一个普通用户(is_staff=False;如不存在可创建一个 phone-only 用户用于本验收)
|
||
user = ParadiseUser.objects.filter(is_staff=False).first()
|
||
if user is None:
|
||
user = ParadiseUser.objects.create(username='phase2_verify_user_temp', is_staff=False)
|
||
user_token = generate_token(user.id, is_admin=False)
|
||
|
||
print(json.dumps({
|
||
'admin_user_id': admin_user.id,
|
||
'admin_token': admin_token,
|
||
'user_id': user.id,
|
||
'user_token': user_token,
|
||
}, ensure_ascii=False))
|
||
EOF
|
||
```
|
||
|
||
记录输出的 `admin_token` / `user_token` 备用。**注意**:token 写入 Redis 后 30 天内有效;本 task 验完无需清理(可让其自然过期)。
|
||
|
||
**Step 2:Django test client 程序化验收(无需启动服务进程,全部在 shell 内跑)**
|
||
|
||
```bash
|
||
cd qy_lty && python manage.py shell <<'EOF'
|
||
from django.test import Client
|
||
from aiapp.models import CredentialSlot
|
||
from common.utils import mask_token
|
||
import json
|
||
|
||
ADMIN_TOKEN = "<填入 Step 1 拿到的 admin_token>"
|
||
USER_TOKEN = "<填入 Step 1 拿到的 user_token>"
|
||
URL = "/api/v1/admin/credential-slot/"
|
||
|
||
c = Client()
|
||
|
||
# === 验收点 1:GET 携 admin token 返回脱敏 ===
|
||
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
|
||
body = r.json()
|
||
assert r.status_code == 200, f"#1 status={r.status_code} body={body}"
|
||
assert body['success'] is True
|
||
assert body['code'] == 200
|
||
assert 'data' in body
|
||
assert {'app_id', 'access_token', 'updated_at'} <= set(body['data'].keys())
|
||
# access_token 必须脱敏:要么是空(DB 为空时),要么以 * 开头且末 4 位为原 token 末 4 位
|
||
slot = CredentialSlot.get_solo()
|
||
expected_masked = mask_token(slot.access_token)
|
||
assert body['data']['access_token'] == expected_masked, f"#1 mask mismatch: got={body['data']['access_token']!r} expected={expected_masked!r}"
|
||
print("#1 PASS GET admin -> 200 + masked access_token")
|
||
|
||
# === 验收点 2:PUT 携 admin token 全字段覆写 + 响应脱敏 ===
|
||
new_token = "sk-phase2_verify_secret_ABCD1234"
|
||
r = c.put(URL, data=json.dumps({"app_id": "phase2_app", "access_token": new_token}),
|
||
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
|
||
body = r.json()
|
||
assert r.status_code == 200, f"#2 status={r.status_code} body={body}"
|
||
assert body['success'] is True
|
||
slot = CredentialSlot.get_solo()
|
||
assert slot.app_id == "phase2_app"
|
||
assert slot.access_token == new_token # DB 中明文存储
|
||
# 响应 access_token 必须脱敏:'sk-phase2_verify_secret_ABCD1234' -> '*****************************1234'
|
||
assert body['data']['access_token'] == mask_token(new_token), f"#2 PUT response not masked: {body['data']['access_token']!r}"
|
||
assert body['data']['access_token'].endswith('1234'), "末 4 位必须为 1234"
|
||
assert body['data']['access_token'].startswith('*'), "脱敏前缀必须以 * 开头"
|
||
print("#2 PASS PUT admin -> 200 + DB updated + response masked")
|
||
|
||
# === 验收点 3:PUT 在空记录场景自动 get_or_create ===
|
||
CredentialSlot.objects.all().delete()
|
||
r = c.put(URL, data=json.dumps({"app_id": "after_delete", "access_token": "tok-XYZ9"}),
|
||
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
|
||
body = r.json()
|
||
assert r.status_code == 200, f"#3 status={r.status_code} body={body}"
|
||
slot = CredentialSlot.get_solo()
|
||
assert slot.app_id == "after_delete"
|
||
assert slot.access_token == "tok-XYZ9"
|
||
print("#3 PASS PUT 空记录场景 -> 200 + get_or_create 创建并写入")
|
||
|
||
# === 验收点 4:无 Authorization 头 -> 401 + 标准壳层 ===
|
||
r = c.get(URL)
|
||
body = r.json()
|
||
assert r.status_code == 401, f"#4 status={r.status_code} body={body}"
|
||
assert body['success'] is False
|
||
assert body['code'] == 401
|
||
assert 'message' in body # 中间件 / DRF 至少要给一个 message 字段
|
||
print("#4 PASS no token -> 401 + 标准壳层")
|
||
|
||
# === 验收点 5:携普通 user token -> 403 + 标准壳层 + message 含管理员关键字 ===
|
||
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
|
||
body = r.json()
|
||
assert r.status_code == 403, f"#5 status={r.status_code} body={body}"
|
||
assert body['success'] is False
|
||
assert body['code'] == 403
|
||
assert "管理员" in body['message'], f"#5 message 不含'管理员'关键字: {body['message']!r}"
|
||
print("#5 PASS user token -> 403 + 需要管理员权限")
|
||
|
||
# === 验收点 6:PUT 携 user token -> 403(同 #5 路径,验证 PUT 也走 _ensure_admin) ===
|
||
r = c.put(URL, data=json.dumps({"app_id": "should_fail"}),
|
||
content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
|
||
body = r.json()
|
||
assert r.status_code == 403, f"#6 PUT user token status={r.status_code}"
|
||
print("#6 PASS PUT user token -> 403")
|
||
|
||
print("\n========== 全部 6 条 test client 验收通过 ==========")
|
||
EOF
|
||
```
|
||
|
||
**Step 3:Swagger schema 验收(启动 daphne 后做一次 curl)**
|
||
|
||
```bash
|
||
# 启动服务(如已运行可跳过)
|
||
# cd qy_lty && daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application &
|
||
|
||
curl -s http://localhost:8000/swagger.json | python -c "
|
||
import json, sys
|
||
schema = json.load(sys.stdin)
|
||
paths = schema.get('paths', {})
|
||
key = '/api/v1/admin/credential-slot/'
|
||
assert key in paths, f'#7 {key} 未出现在 swagger paths 中'
|
||
methods = paths[key]
|
||
assert 'get' in methods, '#7 GET 方法缺失'
|
||
assert 'put' in methods, '#7 PUT 方法缺失'
|
||
# 验证 access_token description 含脱敏关键字
|
||
get_resp = methods['get'].get('responses', {}).get('200', {})
|
||
schema_str = json.dumps(get_resp, ensure_ascii=False)
|
||
assert '脱敏' in schema_str or '末 4 位' in schema_str or 'mask' in schema_str.lower(), '#7 access_token description 不含脱敏掩码语义'
|
||
print('#7 PASS swagger paths 含 GET/PUT 两 method + access_token 脱敏描述')
|
||
"
|
||
```
|
||
|
||
**Step 4:把 8 条验收结果写入 `.planning/phases/02-admin-rest/02-VERIFICATION.md`**
|
||
|
||
文件全文模板(直接 Write):
|
||
|
||
```markdown
|
||
# Phase 2 Verification — 管理端读写接口端到端验收
|
||
|
||
**Verified**: 2026-05-07
|
||
**Phase**: 02-admin-rest
|
||
**Plan**: 02-02
|
||
**Coverage**: CRED-03 + CRED-04(ROADMAP Phase 2 全部 4 条 success criteria)
|
||
|
||
---
|
||
|
||
## 验收摘要
|
||
|
||
| # | 验收点 | 方法 | 结果 |
|
||
|---|--------|------|------|
|
||
| 1 | GET 携 admin token 返回脱敏壳层 | Django test client | ✓ PASS |
|
||
| 2 | PUT 携 admin token 全字段覆写 + 响应脱敏 | Django test client | ✓ PASS |
|
||
| 3 | PUT 在空记录场景自动 get_or_create | Django test client(手动 delete + PUT) | ✓ PASS |
|
||
| 4 | 无 Authorization 头 → 401 + 标准壳层 | Django test client | ✓ PASS |
|
||
| 5 | 携普通 user token → 403 + 需要管理员权限 | Django test client | ✓ PASS |
|
||
| 6 | PUT 携 user token → 403(验证 PUT 也走 _ensure_admin) | Django test client | ✓ PASS |
|
||
| 7 | /swagger.json 含路径条目 + GET/PUT 两 method + 脱敏 description | curl | ✓ PASS |
|
||
| 8 | 修改记录两端互引(02-02 Task 2 落地后追加) | 文件 grep | ⏳ 待 02-02 Task 2 |
|
||
|
||
---
|
||
|
||
## 证据片段
|
||
|
||
[黏贴 Step 1 / Step 2 / Step 3 输出的关键行]
|
||
|
||
---
|
||
|
||
*由 02-02-PLAN.md Task 1 生成*
|
||
```
|
||
|
||
**关键约束**:
|
||
- 6 条 test client 验收必须**全部 PASS** 才能进 02-02 Task 2;任何一条 FAIL 则回到 02-01 修 view / serializer / URL
|
||
- Swagger 验收(#7)若 daphne 未启动可暂时记 "deferred to integration",但必须在 phase gate 前补做
|
||
- #8 在本 task 仅占位(标 ⏳),Task 2 落地修改记录后**回写**为 ✓ PASS(02-VERIFICATION.md 在 Task 2 末尾再 Edit 一次)
|
||
- 不要把 admin / user token 明文写进 02-VERIFICATION.md(Redis 30 天 TTL,落进 git 的 token 在 30 天内仍有效,是新的泄露面)
|
||
</action>
|
||
<verify>
|
||
<automated>
|
||
cd qy_lty && python manage.py shell -c "from aiapp.models import CredentialSlot; from common.utils import mask_token; slot = CredentialSlot.get_solo(); print('DB state ok:', slot.app_id, mask_token(slot.access_token))" && test -f .planning/phases/02-admin-rest/02-VERIFICATION.md && grep -q "PASS" .planning/phases/02-admin-rest/02-VERIFICATION.md && echo OK
|
||
</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- Step 2 脚本输出"========== 全部 6 条 test client 验收通过 =========="
|
||
- Step 3 输出 "#7 PASS"(如 daphne 未启动则记 deferred 但必须在 SUMMARY 中列出补做计划)
|
||
- 文件 `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在
|
||
- 文件含至少 6 处 "PASS"(验收点 1-6)
|
||
- 文件不含 admin / user token 明文(grep `Bearer ` 应只出现在脚本模板段落、不含具体 UUID 字串)
|
||
- DB 状态符合预期:`CredentialSlot.get_solo()` 返回的 app_id 与 access_token 是 Step 2 写入的最新值
|
||
</acceptance_criteria>
|
||
<done>
|
||
8 条 success criteria 中 6 条 test client + 1 条 swagger 已验完并归档;DB 状态稳定;02-VERIFICATION.md 是 Phase 2 收尾时 ROADMAP 标记 ✓ Complete 的证据来源。
|
||
</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="false">
|
||
<name>Task 2:两端修改记录互引条目(qy_lty + qy-lty-admin)</name>
|
||
<files>qy_lty/docs/修改记录.md, qy-lty-admin/docs/修改记录.md</files>
|
||
<read_first>
|
||
- 必读 qy_lty/docs/修改记录.md:1-90(确认头部「修改格式说明」+ 第 24 行 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 注释 + Phase 1 已有两条条目作模板)
|
||
- 必读 qy-lty-admin/docs/修改记录.md:1-58(确认头部「修改格式说明」+ 第 26 行 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` + 第 28-44 行 NEXT_PUBLIC_API_BASE_URL 修复条目作"含跨项目说明的"模板)
|
||
- 必读 qy_lty/CLAUDE.md `## 项目修改记录规则(重要 — 自动执行)` 段(5 字段格式 + 跨项目联动两端各一条互引规则)
|
||
- 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md(Phase 2 实际改动文件清单 + 接口契约)
|
||
- 必读 02-CONTEXT.md `<decisions>` 跨项目联动段(互引文案要点)
|
||
</read_first>
|
||
<action>
|
||
**Step 1:在 `qy_lty/docs/修改记录.md` 顶部(紧跟第 24 行注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 之下、Phase 1 第一条 `### [2026-05-07] Phase 1 — Django Admin 注册凭据槽位...` 之上)插入条目**:
|
||
|
||
```markdown
|
||
### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)
|
||
|
||
配套 Phase:[.planning/phases/02-admin-rest/](.planning/phases/02-admin-rest/)
|
||
覆盖需求:CRED-03 + CRED-04
|
||
设计参考:1:1 复刻 `aiapp.views.RTCChatHistoryAPIView`(`aiapp/views.py:434-555`)的单 URL 多方法 APIView 风格
|
||
|
||
- **文件路径**:
|
||
- `aiapp/serializers.py`(修改 — 顶部 import 追加 `CredentialSlot`,文件末尾追加 `CredentialSlotSerializer` ModelSerializer 类)
|
||
- `aiapp/views.py`(修改 — 顶部 import 追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`;文件末尾追加 `CredentialSlotPutRequestSchema` swagger 请求体 + `_credential_slot_data_schema` 响应 data schema + `CredentialSlotAdminView` APIView 类)
|
||
- `userapp/admin_urls.py`(修改 — 追加 `from aiapp.views import CredentialSlotAdminView` 与 `path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')`)
|
||
- **修改类型**: 新增
|
||
- **修改内容**:
|
||
- 暴露 `GET /api/v1/admin/credential-slot/`:admin token 鉴权(`RedisTokenAuthentication` + 视图内 `is_staff` 二次校验,不发明 admin-only permission 类);返回 `{ success, code, message, data: { app_id, access_token: <末 4 位脱敏掩码>, updated_at } }`,脱敏由 view 层调 `common.utils.mask_token` 完成(serializer 不参与脱敏,避免双重责任)
|
||
- 暴露 `PUT /api/v1/admin/credential-slot/`:admin token 鉴权;接受 `{ app_id, access_token }` 全字段覆写;空记录场景自动走 `CredentialSlot.get_solo()` 的 `get_or_create(pk=1)`;写入后 `updated_at` 由 `auto_now=True` 自动刷新;响应同样脱敏 access_token(避免运营在 admin UI 看到自己刚提交的明文回显)
|
||
- 鉴权拒绝矩阵:无 token → 401(DRF NotAuthenticated → middleware 兜底标准壳层);持普通 user token(非 staff)→ 403 + `message="需要管理员权限"`
|
||
- Swagger / ReDoc 自动暴露:method-level `@swagger_auto_schema` 装饰器;响应 schema 配 `common.swagger_utils.get_standardized_response_schema()`;access_token 字段 description 显式标注「Access Token 末 4 位脱敏掩码(如 "*********1234")」,避免前端误解为明文
|
||
- 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1 落地的 `CredentialSlot.get_solo` / `mask_token`)
|
||
- **修改原因**: Milestone v1.0「通用凭据槽位(APP ID + Access Token)」Phase 2 — 给管理后台前端(qy-lty-admin)暴露受控的凭据读写入口,让运营无需进 Django Admin 也能管理凭据;GET 与 PUT 响应均脱敏,避免明文经管理端 UI / 浏览器 devtools / 阿里云日志(GET 响应体路径)泄露;为 Phase 3 客户端明文 GET 接口 + 阿里云日志 formatter 提供"接口已上线、凭据可写入"的稳定起点
|
||
- **跨项目联动**: 前端联动条目 [qy-lty-admin/docs/修改记录.md](../../qy-lty-admin/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`。本 phase 是 Milestone v1.0 首次跨项目接口契约落地:本仓库(服务端)暴露 `/api/v1/admin/credential-slot/` GET/PUT,前端 `qy-lty-admin` 后续 phase 将基于该契约写 API client(含 React Hooks 调用 + 表单录入 UI)。前后端各自维护独立修改记录,本条与对方条目互相引用,便于未来回查接口的双向上下游
|
||
```
|
||
|
||
**Step 2:在 `qy-lty-admin/docs/修改记录.md` 顶部(紧跟第 26 行注释 `<!-- 新的修改记录添加在此处下方,最新的在最前面 -->` 之下、第 28 行 `### [2026-05-07] 修复 NEXT_PUBLIC_API_BASE_URL...` 之上)插入条目**:
|
||
|
||
```markdown
|
||
### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)
|
||
|
||
配套服务端 Phase:[../qy_lty/.planning/phases/02-admin-rest/](../../qy_lty/.planning/phases/02-admin-rest/)
|
||
覆盖服务端需求:CRED-03 + CRED-04(本仓库消费方)
|
||
|
||
- **文件路径**: `docs/修改记录.md`(仅文档新增条目;本仓库代码未改)
|
||
- **修改类型**: 新增
|
||
- **修改内容**:
|
||
- 文档化服务端在本日落地的 `/api/v1/admin/credential-slot/` REST 接口契约(GET 脱敏读取 + PUT 全字段覆写 + admin token 鉴权),为后续 Web 管理后台前端(本仓库)的 CRED-FE-* phase 写 API client 与表单 UI 留下契约锚点
|
||
- 接口契约要点(消费方视角):
|
||
- URL:`{NEXT_PUBLIC_API_BASE_URL}/v1/admin/credential-slot/`
|
||
- 鉴权:`Authorization: Bearer <admin_token>`(来源于 `/api/v1/admin/login/` 的现有 admin 登录返回值)
|
||
- GET 响应:`{ success: boolean, code: number, message: string, data: { app_id: string, access_token: string /* 末 4 位脱敏掩码,前缀 * */, updated_at: string /* ISO 8601 */ } }`
|
||
- PUT 请求体:`{ app_id?: string, access_token?: string }`(任一字段缺省时由后端兜底保留原值;写入是全字段覆写语义,建议前端 UI 始终提交两字段全集以避免歧义)
|
||
- PUT 响应同 GET 形态(access_token 同样脱敏返回,前端**不应**用响应值回填明文输入框)
|
||
- 错误矩阵:401(无 token / token 失效)、403(持非 admin token,message 含"需要管理员权限")、400(参数无效)
|
||
- **修改原因**:
|
||
- 服务端首次为本管理后台暴露受控的凭据读写接口;本仓库即将启动 CRED-FE-01(API client) + CRED-FE-02(表单录入页面)等 phase,先把后端契约固化进本仓库修改记录便于反查
|
||
- 文档化"GET 与 PUT 响应均脱敏 access_token"避免前端工程师误以为可以从响应回填明文表单(实际明文仅存于 DB;任何回填只能保留掩码或要求运营重新输入)
|
||
- **服务端联动**: 后端联动条目 [../qy_lty/docs/修改记录.md](../../qy_lty/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)`。本仓库代码未改,仅文档侧做契约固化;待本仓库 CRED-FE-01 phase 启动落地 API client + Hook 时再补一条独立条目并互引
|
||
```
|
||
|
||
**Step 3:互引校验(写完后立即 grep)**
|
||
|
||
```bash
|
||
# 验证两端互引字段都在
|
||
grep -n "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md | head -3
|
||
grep -n "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md | head -3
|
||
# 验证两边都标了 Phase 2 + 2026-05-07
|
||
grep -n "Phase 2.*2026-05-07\|2026-05-07.*Phase 2" qy_lty/docs/修改记录.md qy-lty-admin/docs/修改记录.md
|
||
```
|
||
|
||
**Step 4:回写 02-VERIFICATION.md 验收点 #8**
|
||
|
||
把 Task 1 写入的 02-VERIFICATION.md 中验收点 #8 的状态从 `⏳ 待 02-02 Task 2` 改为 `✓ PASS`,并追加证据:
|
||
```
|
||
| 8 | 修改记录两端互引 | grep `qy-lty-admin/docs/修改记录` qy_lty/docs/修改记录.md ✓;grep `qy_lty/docs/修改记录` qy-lty-admin/docs/修改记录.md ✓ | ✓ PASS |
|
||
```
|
||
|
||
**关键约束**:
|
||
- 两条条目必须用日期 `2026-05-07`(与 Phase 1 / 当前 currentDate 一致;从 system context 得知)
|
||
- 两条条目都必须含**跨项目联动 / 服务端联动**字段,且字段值必须**指向对方文件路径**(不能写"无",Phase 1 那样的"暂无前端"逻辑在 Phase 2 不成立 —— 本 phase 就是首次跨项目接口契约落地)
|
||
- 服务端条目中的相对路径用 `../../qy-lty-admin/docs/修改记录.md`(位于 `qy_lty/docs/`,跳到 `qy_lty/` 上级 `Lila-Server/` 再进 `qy-lty-admin/docs/`)
|
||
- 前端条目中的相对路径用 `../../qy_lty/docs/修改记录.md`(同理对称)
|
||
- 两条条目都要插在各自文件的「修改历史」段顶部(最新在最前),不要追加到末尾
|
||
- **不要**修改 Phase 1 已有的两条条目(Phase 1 当时纯服务端、无前端互引是合理的,不要回头改成"互引"破坏历史归档)
|
||
- **不要**在 `qy-lty-admin` 仓库改任何代码(本 task 仅在 qy-lty-admin 仓库内动 docs 一个文件)
|
||
</action>
|
||
<verify>
|
||
<automated>
|
||
grep -c "Phase 2 — 管理端通用凭据槽位 REST 接口" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -c "Phase 2 — 锁定后端通用凭据槽位 REST 接口契约" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && grep -q "qy-lty-admin/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -q "qy_lty/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && echo OK
|
||
</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `qy_lty/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)`
|
||
- 该条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因 / 跨项目联动)
|
||
- 该条目"跨项目联动"字段含字串 `qy-lty-admin/docs/修改记录.md`(指向前端互引)
|
||
- `qy-lty-admin/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`
|
||
- 该条目含 5 个标准字段(文件路径 / 修改类型 / 修改内容 / 修改原因 / 服务端联动)
|
||
- 该条目"服务端联动"字段含字串 `qy_lty/docs/修改记录.md`(指向后端互引)
|
||
- 两条条目互相引用对方(grep 双向均命中 → 闭环)
|
||
- Phase 1 的两条条目位置不变(位于本次新增条目之下)
|
||
- 02-VERIFICATION.md 中验收点 #8 已改为 ✓ PASS
|
||
</acceptance_criteria>
|
||
<done>
|
||
`qy_lty` + `qy-lty-admin` 两端 `docs/修改记录.md` 顶部各有一条 Phase 2 条目,互相引用对方文件路径;CLAUDE.md "qy_lty 与 qy-lty-admin 是独立项目,跨项目联动两端各写一条互相引用对方"规则在本 phase 闭环;02-VERIFICATION.md 8 条全 PASS。
|
||
</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
本 plan 完成后做一轮 phase-gate 自验:
|
||
|
||
1. **8 条 success criteria 全 PASS**:02-VERIFICATION.md 表格全部 ✓
|
||
2. **互引闭环**:
|
||
- `grep "qy-lty-admin" qy_lty/docs/修改记录.md` ≥ 1 hit(在 Phase 2 条目内)
|
||
- `grep "qy_lty" qy-lty-admin/docs/修改记录.md` ≥ 1 hit(在 Phase 2 条目内)
|
||
3. **ROADMAP Phase 2 4 条 success criteria 已覆盖**:
|
||
- SC#1 GET 脱敏 ← 验收点 #1 ✓
|
||
- SC#2 PUT 全字段覆写 + get_or_create ← 验收点 #2 + #3 ✓
|
||
- SC#3 鉴权拒绝矩阵 ← 验收点 #4 + #5 + #6 ✓
|
||
- SC#4 Swagger / ReDoc schema 一致 ← 验收点 #7 ✓
|
||
4. **Phase 1 条目未被改动**:`git diff qy_lty/docs/修改记录.md` 仅显示新增条目(在文件顶部追加),Phase 1 已有的两条 `[2026-05-07] Phase 1 — ...` 标题在 diff 内位置应该是"unchanged"
|
||
</verification>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| 端到端 verify 测试 token | 临时 admin / user token,30 天 TTL;不允许写入仓库 |
|
||
| 跨项目修改记录文件 | 文档边界;不修改对方仓库代码,仅在对方 docs/ 下追加一条文档 |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|-------------|-----------------|
|
||
| T-02P2-01 | Information Disclosure | 验收脚本中的 admin / user token 误入 git | mitigate | 02-VERIFICATION.md 仅记录脚本**模板**与"PASS"判定;不黏贴具体 token UUID;脚本输出在 SUMMARY 中也只摘要"6/6 PASS"不贴 token |
|
||
| T-02P2-02 | Information Disclosure | 验收用真实凭据写入 DB(覆盖 Phase 1 探针) | accept | DB 中 access_token 本来就是明文存储(Phase 1 落地决策);PUT 写入的 `sk-phase2_verify_secret_ABCD1234` 不是真实第三方凭据,只是测试串;Task 1 Step 2 #3 还会再 delete + 重建覆盖一次 |
|
||
| T-02P2-03 | Tampering | 误修改 Phase 1 修改记录条目 | mitigate | Task 2 关键约束第 7 条明确"不要修改 Phase 1 已有的两条条目";verify 步骤 #4 用 git diff 校验 Phase 1 条目位置不变 |
|
||
| T-02P2-04 | Information Disclosure | 互引文档泄露内部路径 | accept | `.planning/` 已是仓库内文档,路径暴露与本仓库 README 同等;不引入新泄露面 |
|
||
</threat_model>
|
||
|
||
<success_criteria>
|
||
本 plan 落地完成的标志(phase 收尾标志):
|
||
|
||
- [ ] `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在,8 条验收点 6+1+1 = 8 全 ✓
|
||
- [ ] `qy_lty/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口(GET 脱敏 / PUT 覆写)`
|
||
- [ ] `qy-lty-admin/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)`
|
||
- [ ] 两条互引字段双向命中:`grep "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md` ≥ 1;`grep "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md` ≥ 1
|
||
- [ ] DB 中 CredentialSlot 单例存在且 access_token 是 Task 1 #3 写入的最终值(`tok-XYZ9` 或之后被 #3 覆盖的某个值)
|
||
- [ ] Phase 1 两条修改记录条目未被改动(git diff 确认)
|
||
- [ ] ROADMAP Phase 2 4 条 success criteria 在 02-VERIFICATION.md 中均找到对应的 ✓ 验收点映射
|
||
</success_criteria>
|
||
|
||
<output>
|
||
完成后创建 `.planning/phases/02-admin-rest/02-02-SUMMARY.md`,记录:
|
||
- 端到端验收 8 条结果(PASS / FAIL 计数)
|
||
- 两端修改记录条目的最终位置(行号 / 标题)
|
||
- 与 02-01-SUMMARY 合并形成 Phase 2 完整交付证据
|
||
- DB 状态最终值(app_id / access_token 末 4 位 / updated_at)
|
||
- 任何与本 PLAN 不一致的偏离(应为零)
|
||
- 标识 Phase 2 整体 Complete,下一步进入 Phase 3(CRED-05 + CRED-06)
|
||
</output>
|