# FR-048 Phase 2a — 授權守門盤點：survey-group

> 事實盤點（no code change）。分支 `fix/v1.8.0-bugs`。
> 涵蓋模組：`api/survey`、`api/task_survey`。
> 路徑前綴 `/api/1.0`（兩個 blueprint 都是）。
> 每個 WRITE method（post/put/delete/patch）一列；敏感 GET（export/download/report）一列。
>
> **總結論**：兩模組全站**無任何角色 / participant / admin 守門**。所有 service（本地 `app/survey`、`app/task_survey` + 外部 `jedi_survey` 套件）trace 到底皆無 `assert_project_manager` / `require_platform_admin` / `require_super_admin` / `SspPermissionChecker` / `assert_workflow_project_participant` / 手動 `ForbiddenError`。task_survey_service 內唯一的 `TaskAssignee` 引用（`task_survey_service.py:461`）是「通知收件人查詢」，非授權。**已驗證 survey 為裸區屬實。** 另有 2 個敏感 GET（範本下載）連 `@jwt_required()` 都沒有 → 完全未認證。

## api/survey

| Route path | Method | Route file:line | @jwt_required? | 呼叫的 service method | 現況守門 | 備註 |
|---|---|---|---|---|---|---|
| `/api/1.0/surveys` | POST | api/survey/routes/survey_route.py:59 | 是 | `SurveyAppService.get_surveys_and_pager` | 僅 JWT | POST 實為分頁查詢(讀) |
| `/api/1.0/survey` / `/survey/<uid>` | POST | api/survey/routes/survey_route.py:103 | 是 | `SurveyService.add_survey`（jedi_survey） | 僅 JWT | 新增問卷；裸區 |
| `/api/1.0/survey/<uid>` | PUT | api/survey/routes/survey_route.py:127 | 是 | `SurveyService.update_survey`（jedi_survey） | 僅 JWT | 更新問卷；裸區 |
| `/api/1.0/survey/<uid>` | DELETE | api/survey/routes/survey_route.py:146 | 是 | `SurveyService.delete_survey`（jedi_survey） | 僅 JWT | 刪除問卷；裸區 |
| `/api/1.0/survey/excel-import` | POST | api/survey/routes/survey_route.py:157 | 是 | `SurveyService.import_survey`（jedi_survey） | 僅 JWT | Excel 匯入問卷（寫入）；裸區 |
| `/api/1.0/survey/import-template` | GET | api/survey/routes/survey_route.py:198 | **否** | 無（`send_from_directory` 靜態檔） | **無 JWT** | 敏感 GET（範本下載）；連 JWT 都缺 |
| `/api/1.0/survey/move` | POST | api/survey/routes/survey_route.py:209 | 是 | `SurveyService.move_survey`（jedi_survey） | 僅 JWT | 移動問卷（寫入）；裸區 |
| `/api/1.0/survey/<uid>/clone` | POST | api/survey/routes/survey_route.py:229 | 是 | `SurveyCloneService.clone_survey` | 僅 JWT | 深度複製問卷（寫入）；裸區 |
| `/api/1.0/survey/<uid>/release` | PUT | api/survey/routes/survey_route.py:252 | 是 | `SurveyDomainService.release_survey`（jedi_survey） | 僅 JWT | 發布問卷（publish）；裸區 |
| `/api/1.0/survey/<uid>/release` | DELETE | api/survey/routes/survey_route.py:266 | 是 | `SurveyDomainService.unrelease_survey`（jedi_survey） | 僅 JWT | 取消發布（unpublish）；裸區 |
| `/api/1.0/survey-folders` | POST | api/survey/routes/survey_folder_route.py:39 | 是 | `SurveyFolderService.get_folders`（jedi_survey） | 僅 JWT | POST 實為分頁查詢(讀) |
| `/api/1.0/survey-folder` / `/survey-folder/<uid>` | POST | api/survey/routes/survey_folder_route.py:67 | 是 | `SurveyFolderService.add_folder`（jedi_survey） | 僅 JWT | 新增資料夾；裸區 |
| `/api/1.0/survey-folder/<uid>` | PUT | api/survey/routes/survey_folder_route.py:82 | 是 | `SurveyFolderService.update_folder`（jedi_survey） | 僅 JWT | 更新資料夾；裸區 |
| `/api/1.0/survey-folder/<uid>` | DELETE | api/survey/routes/survey_folder_route.py:95 | 是 | `SurveyFolderService.delete_folder`（jedi_survey） | 僅 JWT | 刪除資料夾；裸區 |
| `/api/1.0/survey-question` / `/survey-question/<uid>` | POST | api/survey/routes/survey_question_route.py:34 | 是 | `SurveyQuestionDomainService.add_question`（jedi_survey） | 僅 JWT | 新增題目；裸區 |
| `/api/1.0/survey-question/<uid>` | PUT | api/survey/routes/survey_question_route.py:61 | 是 | `SurveyQuestionDomainService.update_question`（jedi_survey） | 僅 JWT | 更新題目；裸區 |
| `/api/1.0/survey-question/<uid>` | DELETE | api/survey/routes/survey_question_route.py:91 | 是 | `SurveyQuestionDomainService.delete_question`（jedi_survey） | 僅 JWT | 刪除題目；裸區 |
| `/api/1.0/survey-questions/reorder` | POST | api/survey/routes/survey_question_route.py:106 | 是 | `SurveyQuestionDomainService.reorder_questions`（jedi_survey） | 僅 JWT | 題目重排序（寫入）；裸區 |
| `/api/1.0/survey-question/move` | POST | api/survey/routes/survey_question_route.py:125 | 是 | `SurveyQuestionDomainService.move_question`（jedi_survey） | 僅 JWT | 移動題目（寫入）；裸區 |
| `/api/1.0/survey-page` / `/survey-page/<uid>` | POST | api/survey/routes/survey_page_route.py:34 | 是 | `SurveyPageDomainService.add_page`（jedi_survey） | 僅 JWT | 新增頁面；裸區 |
| `/api/1.0/survey-page/<uid>` | PUT | api/survey/routes/survey_page_route.py:60 | 是 | `SurveyPageDomainService.update_page`（jedi_survey） | 僅 JWT | 更新頁面；裸區 |
| `/api/1.0/survey-page/<uid>` | DELETE | api/survey/routes/survey_page_route.py:83 | 是 | `SurveyPageDomainService.delete_page`（jedi_survey） | 僅 JWT | 刪除頁面；裸區 |
| `/api/1.0/survey-pages/reorder` | POST | api/survey/routes/survey_page_route.py:97 | 是 | `SurveyPageDomainService.reorder_pages`（jedi_survey） | 僅 JWT | 頁面重排序（寫入）；裸區 |
| `/api/1.0/survey-page/move` | POST | api/survey/routes/survey_page_route.py:116 | 是 | `SurveyPageDomainService.move_page`（jedi_survey） | 僅 JWT | 移動頁面（寫入）；裸區 |
| `/api/1.0/survey-discussions` | POST | api/survey/routes/survey_discussion_route.py:26 | 是 | `SurveyDiscussionService.get_survey_discussions` | 僅 JWT | POST 實為查詢(讀) |
| `/api/1.0/survey-discussion` / `/survey-discussion/<uid>` | POST | api/survey/routes/survey_discussion_route.py:44 | 是 | `SurveyDiscussionService.add_discussion` | 僅 JWT | 新增討論；裸區 |
| `/api/1.0/survey-discussion/<uid>` | PUT | api/survey/routes/survey_discussion_route.py:64 | 是 | `SurveyDiscussionService.update_discussion` | 僅 JWT | 更新討論；裸區 |
| `/api/1.0/survey-discussion/<uid>` | DELETE | api/survey/routes/survey_discussion_route.py:83 | 是 | `SurveyDiscussionService.delete_discussion` | 僅 JWT | 刪除討論；裸區 |

## api/task_survey

| Route path | Method | Route file:line | @jwt_required? | 呼叫的 service method | 現況守門 | 備註 |
|---|---|---|---|---|---|---|
| `/api/1.0/task-surveys` | POST | api/task_survey/routes/task_survey_route.py:31 | 是 | `TaskSurveyService.get_task_surveys` | 僅 JWT | POST 實為查詢(讀) |
| `/api/1.0/task-survey/<uid>` | PUT | api/task_survey/routes/task_survey_route.py:50 | 是 | `TaskSurveyService.update_task_survey` | 僅 JWT | 更新 task survey（可換問卷）；有 project/round context 但**未**檢查 participant |
| `/api/1.0/task-survey/import-template/<task_uid>` | GET | api/task_survey/routes/task_survey_route.py:62 | **否** | `TaskSurveyService.generate_task_survey_question_import_excel_template_data` | **無 JWT** | 敏感 GET（範本下載 xlsx，含 task 題目內容）；連 JWT 都缺 |
| `/api/1.0/task-survey/import-answers` | POST | api/task_survey/routes/task_survey_route.py:130 | 是 | `TaskSurveyService.batch_import_task_survey_questions` | 僅 JWT | Excel 批次上傳專案調查結果（寫入）；未檢查 participant |
| `/api/1.0/task-survey/configured/<uid>` | PUT | api/task_survey/routes/task_survey_route.py:169 | 是 | `TaskSurveyService.update_task_survey_configure` | 僅 JWT | 更新 configured（approval_required/換卷，寫入）；未檢查 participant |
| `/api/1.0/project-survey/answers/<uid>` | PUT | api/task_survey/routes/question_answer_route.py:38 | 是 | `QuestionAnswerService.update_task_survey_answer` | 僅 JWT | 更新答案（寫入）；填答者面(assignee) 但未驗 assignee/participant |
| `/api/1.0/project-survey/answers/<uid>/patch` | POST | api/task_survey/routes/question_answer_route.py:55 | 是 | `QuestionAnswerService.patch_task_survey_answer` | 僅 JWT | 單題批次 patch（socket/keepalive flush 寫入）；填答者面(assignee) 但未驗 |
| `/api/1.0/project-survey/answers/<uid>/checkpoint` | POST | api/task_survey/routes/question_answer_route.py:85 | 是 | `QuestionAnswerService.checkpoint_task_survey_answer` | 僅 JWT | 暫存/提交答案（寫入）；填答者面(assignee) 但未驗 |
| `/api/1.0/project-survey/histories` | POST | api/task_survey/routes/question_answer_history_route.py:35 | 是 | `QuestionAnswerHistoryService.get_all_question_answer_history` | 僅 JWT | POST 實為查詢(讀) |
| `/api/1.0/project-survey/history/revert` | POST | api/task_survey/routes/question_answer_history_route.py:46 | 是 | `QuestionAnswerHistoryService.revert_question_answer_from_history` | 僅 JWT | 還原歷史答案（寫入，會 socket emit reload）；未驗 participant |
| `/api/1.0/project-survey/history-details` | POST | api/task_survey/routes/question_answer_history_detail_route.py:21 | 是 | `QuestionAnswerHistoryDetailService.get_all_question_answer_history_detail` | 僅 JWT | POST 實為查詢(讀) |

### 註記
- **敏感 GET（未認證，最高優先）**：
  - `GET /api/1.0/survey/import-template`（survey_route.py:198）— 純靜態檔下載，無 JWT。
  - `GET /api/1.0/task-survey/import-template/<task_uid>`（task_survey_route.py:62）— **動態產生含該 task 實際題目/答案選項的 xlsx**，無 JWT，任意人帶 task_uid 可下載題庫內容。
- **task_survey 有 project/round context 卻不檢查**：`update_task_survey` / `update_task_survey_configure` / answer 系列 / `revert` 皆能經 task_survey → task_id 反查所屬專案輪次，但**無一處**驗證呼叫者是否為該專案 participant 或 assignee。
- 未列入表格的純讀 GET（`SurveyMenuRoute` GET、`SurveyRoute` GET、`SurveyFolderMenuRoute`/`SurveyFolderRoute` GET、`SurveysRoute` 已列、`TaskSurveyRoute` GET、`TaskSurveysAnswersRoute` GET、`QuestionAnswerHistoryMenuRoute` GET）皆 `@jwt_required()` 且非 export/download/report，故不納敏感 GET 清單。
