- 新增 .planning/phases/02-admin-rest/02-01-SUMMARY.md(含 frontmatter / decisions / metrics / 偏差 / Plan 02-02 端到端 verify hook) - STATE.md:当前位置 1→2、Plan 02 of 02→01 of 02、progress 75%、性能指标加 Plan 02-01 行、 累积决策追加 5 条 [Plan 02-01] 标签项、下一步切到 /gsd-execute-plan 02-02 - ROADMAP.md:Phase 2 plan 进度 0/2 → 1/2 - REQUIREMENTS.md:CRED-03 / CRED-04 标记 complete + traceability 表更新 - config.json:gsd-tools init 写入 workflow._auto_chain_active flag(不影响本期执行) Plan 02-01 三个 task commit: 6820fe7 / 192d0a1 / 9d02021
299 lines
15 KiB
Markdown
299 lines
15 KiB
Markdown
---
|
||
phase: 02-admin-rest
|
||
plan: 01
|
||
subsystem: aiapp + userapp(admin namespace 路由)
|
||
tags: [credential-slot, admin-api, drf, mask, swagger, rest]
|
||
requirements_completed:
|
||
- CRED-03
|
||
- CRED-04
|
||
dependency_graph:
|
||
requires:
|
||
- aiapp.models.CredentialSlot(Phase 1 / Plan 01-01 落地)
|
||
- aiapp.models.CredentialSlot.get_solo()(Phase 1 / Plan 01-01 落地)
|
||
- common.utils.mask_token(Phase 1 / Plan 01-01 落地)
|
||
- common.responses.success_response / error_response(已存在)
|
||
- common.swagger_utils.get_standardized_response_schema(已存在)
|
||
- userapp.authentication.RedisTokenAuthentication(已存在)
|
||
provides:
|
||
- aiapp.serializers.CredentialSlotSerializer(DRF ModelSerializer,3 字段)
|
||
- aiapp.views.CredentialSlotAdminView(APIView,GET/PUT 两端点)
|
||
- URL: /api/v1/admin/credential-slot/(name='admin_credential_slot')
|
||
affects:
|
||
- 下一 plan:02-02-PLAN(端到端 verify + 修改记录两端互引)
|
||
tech_stack:
|
||
added: []
|
||
patterns:
|
||
- DRF 自定义 APIView 单 URL 多方法(1:1 复刻 RTCChatHistoryAPIView)
|
||
- permission_classes=[IsAuthenticated] + view 内 _ensure_admin 二次校验 is_staff
|
||
(沿用 AdminEmailLoginView / AdminLogoutView 模式,不发明 IsAdminTokenAuthenticated)
|
||
- 脱敏在 view 层 _build_response_data helper 完成,不在 serializer 层
|
||
- GET 与 PUT 响应都走 _build_response_data,避免 PUT 明文回显
|
||
- method-level @swagger_auto_schema 装饰器 + access_token 字段 description
|
||
显式标注脱敏掩码语义
|
||
key_files:
|
||
created: []
|
||
modified:
|
||
- qy_lty/aiapp/serializers.py
|
||
- qy_lty/aiapp/views.py
|
||
- qy_lty/userapp/admin_urls.py
|
||
decisions:
|
||
- "View 1:1 复刻 RTCChatHistoryAPIView 风格:不走 RetrieveUpdateAPIView(仓库零先例)"
|
||
- "permission_classes=[IsAuthenticated] + 视图内 _ensure_admin 二次校验:与 AdminEmailLoginView/AdminLogoutView 一致;不发明 IsAdminTokenAuthenticated permission 类"
|
||
- "脱敏放 view 层不放 serializer:PUT 路径需明文走 is_valid + save,serializer 只做字段校验,避免双重责任"
|
||
- "GET 与 PUT 响应都强制走 _build_response_data:避免 PUT 直接 return success_response(data=serializer.data) 导致刚提交的明文回显(Pitfall 3)"
|
||
- "drf-yasg request body 用独立 CredentialSlotPutRequestSchema serializer 类(与 userapp/views.py:705-708 AdminEmailLoginRequestSchema 模式一致):与实际写入校验的 CredentialSlotSerializer 解耦"
|
||
- "method-level @swagger_auto_schema:在 GET 与 PUT 各挂一份;access_token 响应字段 description 明示末 4 位脱敏掩码"
|
||
metrics:
|
||
duration_seconds: 216
|
||
tasks_completed: 3
|
||
files_modified: 3
|
||
commits:
|
||
- 6820fe7 feat(02-01): 新增 CredentialSlotSerializer
|
||
- 192d0a1 feat(02-01): 新增 CredentialSlotAdminView(GET 脱敏 / PUT 全字段覆写)
|
||
- 9d02021 feat(02-01): 注册 /api/v1/admin/credential-slot/ 路由
|
||
completed_date: 2026-05-07
|
||
---
|
||
|
||
# Phase 2 Plan 02-01:管理端 REST 接口(serializer + view + URL + Swagger)Summary
|
||
|
||
在 `/api/v1/admin/credential-slot/` 暴露 GET(脱敏读取)+ PUT(全字段覆写)两个 admin token 鉴权端点,全部沿用仓库现有 RTCChatHistoryAPIView / AdminEmailLoginView 模式,零新依赖。
|
||
|
||
## 一句话概述
|
||
|
||
新增 `CredentialSlotSerializer` + `CredentialSlotAdminView`(GET/PUT),在 `userapp/admin_urls.py` 注册到 `/api/v1/admin/credential-slot/`,view 层 `mask_token` 脱敏 access_token,覆盖 CRED-03 / CRED-04。
|
||
|
||
## 改动文件清单
|
||
|
||
| 文件 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| `qy_lty/aiapp/serializers.py` | 修改 | import 区追加 `CredentialSlot`;文件末尾追加 `CredentialSlotSerializer`(ModelSerializer,3 字段) |
|
||
| `qy_lty/aiapp/views.py` | 修改 | import 区追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`;文件末尾追加 `CredentialSlotPutRequestSchema`(drf-yasg 请求体 schema)+ `_credential_slot_data_schema`(响应 data 子 schema)+ `CredentialSlotAdminView`(含 `_ensure_admin` / `_build_response_data` / GET / PUT 4 个方法) |
|
||
| `qy_lty/userapp/admin_urls.py` | 修改 | 顶部 import `CredentialSlotAdminView`;urlpatterns 追加 `path('credential-slot/', ..., name='admin_credential_slot')` |
|
||
|
||
## 实际落地的关键产物
|
||
|
||
### Serializer
|
||
|
||
```python
|
||
# qy_lty/aiapp/serializers.py
|
||
class CredentialSlotSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = CredentialSlot
|
||
fields = ['app_id', 'access_token', 'updated_at']
|
||
read_only_fields = ['updated_at']
|
||
extra_kwargs = {
|
||
'app_id': {'allow_blank': True, 'allow_null': False, 'required': False},
|
||
'access_token': {'allow_blank': True, 'allow_null': False, 'required': False},
|
||
}
|
||
```
|
||
|
||
### View
|
||
|
||
`qy_lty/aiapp/views.py` 文件末尾:
|
||
|
||
- `CredentialSlotPutRequestSchema(serializers.Serializer)` — drf-yasg 请求体 schema
|
||
- `_credential_slot_data_schema = openapi.Schema(...)` — 响应 data 子 schema(access_token description 明示末 4 位脱敏掩码)
|
||
- `CredentialSlotAdminView(APIView)`:
|
||
- `authentication_classes = [RedisTokenAuthentication]`
|
||
- `permission_classes = [IsAuthenticated]`
|
||
- `_ensure_admin(request)` — `if not request.user.is_staff: return error_response(code=403, status_code=403)`
|
||
- `_build_response_data(instance)` — 调 `mask_token(instance.access_token)` 覆盖明文
|
||
- `get(request)` / `put(request)` — 各挂 `@swagger_auto_schema`
|
||
|
||
### URL
|
||
|
||
```python
|
||
# qy_lty/userapp/admin_urls.py
|
||
from aiapp.views import CredentialSlotAdminView
|
||
urlpatterns = [
|
||
...,
|
||
path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot'),
|
||
]
|
||
```
|
||
|
||
完整路径:`/api/v1/admin/credential-slot/`(拼接自 `qy_lty/urls.py:59` 的 `path('v1/admin/', include('userapp.admin_urls'))` + `path('credential-slot/', ...)`)。
|
||
|
||
### 调用 mask_token 的位置(脱敏调用点)
|
||
|
||
`qy_lty/aiapp/views.py:637`:
|
||
|
||
```python
|
||
data['access_token'] = mask_token(instance.access_token)
|
||
```
|
||
|
||
`_build_response_data` 是 GET 与 PUT 唯一的响应数据构造点,强制脱敏。
|
||
|
||
## Plan 内自验证据
|
||
|
||
| 验证点 | 命令 | 结果 |
|
||
|--------|------|------|
|
||
| import 链路 | `from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer` | OK 无 ImportError |
|
||
| URL 解析 | `reverse('admin_credential_slot')` | `/api/v1/admin/credential-slot/` |
|
||
| Django check | `python manage.py check` | 仅 1 条 W004(STATICFILES_DIRS)— 预先存在,与本 plan 无关 |
|
||
| Serializer 字段 | 实例化 `pk=1` 后 `.data.keys()` | `['app_id', 'access_token', 'updated_at']` |
|
||
| Serializer read_only_fields | `.fields['updated_at'].read_only` | `True` |
|
||
| Serializer allow_null/allow_blank | `.fields['app_id'/'access_token']` | `allow_blank=True, allow_null=False` |
|
||
| View 鉴权 / 权限链 | `CredentialSlotAdminView.authentication_classes / permission_classes` | `[RedisTokenAuthentication]` / `[IsAuthenticated]` |
|
||
| View 4 个 method 完整性 | `hasattr(v, 'get'/'put'/'_ensure_admin'/'_build_response_data')` | 全部 True |
|
||
| Swagger 装饰器 | `hasattr(get_method/put_method, '_swagger_auto_schema')` | 全部 True |
|
||
| 探针脱敏 | `mask_token('probe_secret_xxxx')` | `*************xxxx`(13 stars + xxxx;len=17) |
|
||
|
||
### Goal-backward reachability self-check
|
||
|
||
- truth #1(GET 脱敏 200)→ `views.py CredentialSlotAdminView.get` + `serializers.py CredentialSlotSerializer` + `admin_urls.py path` → reachable ✓
|
||
- truth #2(PUT 全字段覆写 + updated_at 刷新 + 响应脱敏)→ `views.py CredentialSlotAdminView.put`(`serializer.save` + `_build_response_data`)→ reachable ✓
|
||
- truth #3(PUT 在空记录场景 get_or_create)→ `CredentialSlot.get_solo()`(Phase 1 已在)→ reachable ✓
|
||
- truth #4(无 token → 401)→ `RedisTokenAuthentication` + DRF `NotAuthenticated` → reachable ✓
|
||
- truth #5(user token → 403)→ `_ensure_admin` `is_staff` 校验 → reachable ✓
|
||
- truth #6(swagger 路径条目 + access_token 脱敏 description)→ `@swagger_auto_schema` + `_credential_slot_data_schema` description → reachable ✓
|
||
|
||
端到端 curl 验收(含 admin token 签发 / user token 拒绝 / swagger.json 校验)由 Plan 02-02 完成。
|
||
|
||
## Deviations from Plan
|
||
|
||
### 偏差 1:Plan import 行号偏移(Rule 1 等价 — 现实修正)
|
||
|
||
- **Found during:** Task 2
|
||
- **Plan 假设:** `from drf_yasg.utils import swagger_auto_schema` 在第 13 行
|
||
- **实际仓库状态:** 该 import 实际位于第 14 行(第 11 行已是 `from common.swagger_utils import swagger_schema`)
|
||
- **Adjustment:** 不按行号定位,按字符串精确 Edit 替换 8 行 import 块(包含 `.models` / `.serializers` / `RedisTokenAuthentication` / `serializers` / `swagger_utils` / `responses`),追加 `mask_token` 1 行;新增 `get_standardized_response_schema` 通过扩展现有 `from common.swagger_utils import swagger_schema` 一行完成
|
||
- **Reason:** Plan 行号是参考值,不是契约;本仓库 import 区结构与 plan 描述完全等价(仅个别行偏移),按精确 token 串替换更安全
|
||
- **Files modified:** `qy_lty/aiapp/views.py`
|
||
- **Commit:** `192d0a1`
|
||
|
||
### 偏差 2:Task 1 自动化校验命令补 Django setup(Rule 3 — 阻塞修复)
|
||
|
||
- **Found during:** Task 1 verify
|
||
- **Issue:** Plan 提供的 `python -c "from aiapp.serializers import CredentialSlotSerializer..."` 命令在 windows 命令行下直接 import 会触发 `ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured`
|
||
- **Fix:** 在 verify 命令内显式 `os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'qy_lty.settings'); django.setup()`;功能等价但兼容裸 python -c 调用
|
||
- **Reason:** Plan 假设的 verify 命令行假设了某个隐式环境(如 `python manage.py shell -c`),裸 python 解释器需显式 setup;不影响功能正确性
|
||
- **Files modified:** 无(仅 verify 命令)
|
||
- **Commit:** 无(不涉及代码改动)
|
||
|
||
### 偏差 3:未写 docs/修改记录.md(per execution_context 强制约束)
|
||
|
||
- **Reason:** 用户在 `<sequential_execution>` 显式声明 "本 plan 不写 docs/修改记录.md(Plan 02-02 Task 2 一并写两端互引)"
|
||
- **Status:** 故意不写,由 Plan 02-02 一次性补完两端互引条目(CLAUDE.md 跨项目联动规则)
|
||
- **Risk:** 在 Plan 02-02 落地前,本仓库 `docs/修改记录.md` 不含 Phase 2 条目;可接受,因为 Plan 02-01 + 02-02 是同 session 同 phase 的连续动作
|
||
|
||
### 其余偏差
|
||
|
||
无。Plan 三个 task 的代码内容、acceptance criteria、anti-pattern 约束、reachability 验证全部按 Plan 1:1 落地。
|
||
|
||
## 留给 Plan 02-02 的端到端 verify hook
|
||
|
||
### Setup(Wave 0 — 签发测试 token)
|
||
|
||
```python
|
||
# Django shell:python manage.py shell
|
||
from userapp.models import ParadiseUser
|
||
from userapp.utils import generate_token
|
||
|
||
# 找一个 staff 用户(或现成的 admin)
|
||
admin_user = ParadiseUser.objects.filter(is_staff=True).first()
|
||
admin_token = generate_token(admin_user.id, is_admin=True)
|
||
print('ADMIN_TOKEN=', admin_token)
|
||
|
||
# 找一个普通用户
|
||
normal_user = ParadiseUser.objects.filter(is_staff=False).first()
|
||
user_token = generate_token(normal_user.id, is_admin=False)
|
||
print('USER_TOKEN=', user_token)
|
||
```
|
||
|
||
### Curl 矩阵
|
||
|
||
```bash
|
||
# 1. 无 token → 401
|
||
curl -i http://localhost:8000/api/v1/admin/credential-slot/
|
||
|
||
# 2. user token → 403 + "需要管理员权限"
|
||
curl -i -H "Authorization: Bearer ${USER_TOKEN}" http://localhost:8000/api/v1/admin/credential-slot/
|
||
|
||
# 3. admin token GET → 200 + access_token 脱敏(probe_secret_xxxx → *************xxxx)
|
||
curl -i -H "Authorization: Bearer ${ADMIN_TOKEN}" http://localhost:8000/api/v1/admin/credential-slot/
|
||
|
||
# 4. admin token PUT → 200 + DB 写入 + 响应同样脱敏
|
||
curl -i -X PUT -H "Authorization: Bearer ${ADMIN_TOKEN}" -H "Content-Type: application/json" \
|
||
-d '{"app_id":"new_app","access_token":"new_secret_token_5678"}' \
|
||
http://localhost:8000/api/v1/admin/credential-slot/
|
||
# 期望响应 data.access_token = "****************5678",updated_at 刷新
|
||
|
||
# 5. swagger.json 含 credential-slot 路径条目 + access_token 脱敏 description
|
||
curl -s http://localhost:8000/swagger.json | python -c "import json,sys; d=json.load(sys.stdin); p='/api/v1/admin/credential-slot/'; assert p in d['paths'], p; assert 'GET' in [m.upper() for m in d['paths'][p].keys()]; assert 'PUT' in [m.upper() for m in d['paths'][p].keys()]; print('OK swagger')"
|
||
```
|
||
|
||
### Django shell 程序化验收(test client)
|
||
|
||
```python
|
||
# python manage.py shell
|
||
from rest_framework.test import APIClient
|
||
from userapp.models import ParadiseUser
|
||
from userapp.utils import generate_token
|
||
|
||
c = APIClient()
|
||
|
||
# admin token GET
|
||
admin_user = ParadiseUser.objects.filter(is_staff=True).first()
|
||
admin_token = generate_token(admin_user.id, is_admin=True)
|
||
c.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}')
|
||
resp = c.get('/api/v1/admin/credential-slot/')
|
||
print(resp.status_code, resp.json())
|
||
assert resp.status_code == 200
|
||
assert resp.json()['success'] == True
|
||
assert '*' in resp.json()['data']['access_token'] # 脱敏特征
|
||
|
||
# admin token PUT
|
||
resp2 = c.put('/api/v1/admin/credential-slot/',
|
||
data={'app_id': 'put_app', 'access_token': 'put_secret_5678'},
|
||
format='json')
|
||
print(resp2.status_code, resp2.json())
|
||
assert resp2.status_code == 200
|
||
assert resp2.json()['data']['access_token'].endswith('5678') # 末 4 位
|
||
assert '*' in resp2.json()['data']['access_token'] # 脱敏
|
||
|
||
# user token → 403
|
||
normal_user = ParadiseUser.objects.filter(is_staff=False).first()
|
||
user_token = generate_token(normal_user.id, is_admin=False)
|
||
c.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}')
|
||
resp3 = c.get('/api/v1/admin/credential-slot/')
|
||
print(resp3.status_code, resp3.json())
|
||
assert resp3.status_code == 403
|
||
assert resp3.json()['success'] == False
|
||
|
||
# 无 token → 401
|
||
c.credentials()
|
||
resp4 = c.get('/api/v1/admin/credential-slot/')
|
||
print(resp4.status_code, resp4.json())
|
||
assert resp4.status_code == 401
|
||
```
|
||
|
||
## Threat Flags
|
||
|
||
无。本 plan 改动严格落在 02-01-PLAN 的 `<threat_model>` 8 条已声明威胁内(T-02-01 ~ T-02-08),未引入新 trust boundary 或新攻击面。
|
||
|
||
## Self-Check: PASSED
|
||
|
||
### Files
|
||
|
||
- FOUND: `qy_lty/aiapp/serializers.py`(已修改,含 CredentialSlotSerializer)
|
||
- FOUND: `qy_lty/aiapp/views.py`(已修改,含 CredentialSlotAdminView)
|
||
- FOUND: `qy_lty/userapp/admin_urls.py`(已修改,含 admin_credential_slot URL)
|
||
- FOUND: `.planning/phases/02-admin-rest/02-01-SUMMARY.md`(本文件)
|
||
|
||
### Commits
|
||
|
||
- FOUND: `6820fe7` feat(02-01): 新增 CredentialSlotSerializer
|
||
- FOUND: `192d0a1` feat(02-01): 新增 CredentialSlotAdminView(GET 脱敏 / PUT 全字段覆写)
|
||
- FOUND: `9d02021` feat(02-01): 注册 /api/v1/admin/credential-slot/ 路由
|
||
|
||
### Imports
|
||
|
||
- VERIFIED: `from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer` 同行无 ImportError
|
||
- VERIFIED: `reverse('admin_credential_slot')` = `/api/v1/admin/credential-slot/`
|
||
- VERIFIED: `mask_token('probe_secret_xxxx')` = `*************xxxx`
|
||
|
||
---
|
||
|
||
*Phase: 02-admin-rest / Plan: 01*
|
||
*Executed: 2026-05-07 by gsd-executor(顺序执行模式,无 worktree 隔离)*
|