使用者表單(User Form)
功能群:系統管理|三層 RBAC 模型與租戶/部門組織模型先讀 功能群總覽 §2、§3|上層導航頁見 使用者管理
事實基準:2026-07-05 從 FE
src/views/user-manage/UserMTRBACForm.vue(641 行)+ BEapi/auth/routes/user_route.py/change_password_route.py+ jedi-authuser_service.py/user_domain_service.py+ routes.json / db_schema.json 掃出。
變更紀錄
| 日期 | FR | 變更 |
|---|---|---|
| 2026-07-06 | FR-047 | 自 user-manage.md
拆出:帳號新增/編輯表單頁(含授權組合指派)獨立成子頁 |
1. 功能描述
使用者表單是 使用者管理
列表「建立」/「編輯」導向的獨立表單頁(route
user-form,實掛 /auth/user-form-mtrbac →
UserMTRBACForm.vue)。新增與編輯共用同一表單:上半填帳號基本欄位(登入名稱
/ 姓名 / Email / 職稱 / 電話 / 生效到期日
/(編輯限定)代改密碼),下半以 DataTable 維護「租戶 × 部門 ×
角色」的授權組合(多筆,恰一筆為預設)。儲存時基本欄位與授權組合陣列一起送出,user_roles
/ tenants / org_units
三表全量取代。
主要使用者:系統 / 租戶管理員。權限治理(角色如何綁能力)由 角色權限管理 負責;本頁只做「使用者 ↔︎ 角色/租戶/部門」的指派。
角色速覽(完整定義見 _overview §2、§3):
| 角色 | 一句話 |
|---|---|
| 系統 / 租戶管理員(可見本選單者) | 帳號 CRUD + 授權組合指派 |
is_admin(超級管理員 JWT 旗標) |
額外可在編輯他人帳號時代改密碼;admin
帳號本身的敏感欄位(生效/到期日、刪除授權)鎖死 |
| 一般使用者 | 無此選單(ui_routes 可見性未命中) |
⚠️ route 命名陷阱:FE
views/user-manage/UserForm.vue(463 行)是死碼——router 中對應的/auth/user-formroute 已整段註解掉(config/router/index.js:138-145)。目前user-form(name)這個路由名實際指向/auth/user-form-mtrbac→UserMTRBACForm.vue(641 行,multi-tenant RBAC 版表單),本 spec 全部以此為準,不描述UserForm.vue的行為。
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 帳號基本欄位 | 登入名稱*(新增後不可改)/ 姓名* / Email* / 職稱 / 電話 | 表單頁上半 | §6.2 |
| 2 | 帳號生效 / 到期日 | 日期選擇器;admin 帳號固定禁用 |
表單頁上半 | §4 |
| 3 | 變更密碼(管理員代改) | 僅登入者本身 is_admin 時顯示開關;開啟後顯示密碼 /
確認密碼欄 |
表單頁上半(編輯模式限定) | §4 |
| 4 | 授權組合(租戶 × 部門 × 角色)清單 | DataTable 呈現該使用者全部授權列,逐筆編輯 / 刪除,恰一筆標記「預設權限」 | 表單頁下半 | UC-USER-02 |
| 5 | 新增 / 編輯授權組合 Dialog | 選租戶 → 動態帶出該租戶部門與角色下拉 → 勾選是否預設 | 表單頁下半 Dialog | UC-USER-02 |
| 6 | 儲存帳號 | 送出基本欄位 + 授權組合陣列(user_roles / tenants / org_units 皆全量取代) | 表單頁 | UC-USER-01 |
| 7 | 取消編輯 | 二次確認後返回列表 | 表單頁 | — |
展開成 UC:UC-USER-01(建立 / 編輯帳號主流程)、UC-USER-02(授權組合指派)。停用 / 刪除 / 列表操作屬 使用者管理 hub。
2. Use Case
角色速覽(完整定義見 _overview §2、§3):
| 角色 | 一句話 |
|---|---|
| 系統 / 租戶管理員(可見本選單者) | 帳號 CRUD + 授權組合指派 |
is_admin(超級管理員 JWT 旗標) |
額外可在編輯他人帳號時代改密碼;admin
帳號本身的敏感欄位鎖死 |
| 一般使用者 | 無此選單 |
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點。
UC-USER-01 建立 / 編輯帳號
| 項目 | 內容 |
|---|---|
| 角色 | 具本頁可見性的管理員 |
| 前置條件 | 新增:login_name / email
未被使用;編輯:帳號存在 |
| 產出 / 後置條件 | public.users 新增 / 更新一列 +
user_tenants / user_org_units /
user_roles 全量重建 |
UC-USER-02 授權組合(租戶 × 部門 × 角色)指派
| 項目 | 內容 |
|---|---|
| 角色 | 同上 |
| 前置條件 | 表單已載入 tenantMenu /
roleMenu;至少一筆授權組合(必填) |
| 產出 / 後置條件 | 表單本地 user_roles[] 陣列變化;實際寫入在 UC-USER-01
儲存時發生 |
3. 權限矩陣
專案層 RBAC 基調見 _overview §5。本頁的實際檢查點:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 建立帳號 | 有建立按鈕即可 | ⚠️ 僅 @jwt_required,無角色 /
能力守門 |
user_route.py:75-88(UserRoute.post) |
| 編輯帳號 | 同上 | ⚠️ 僅 @jwt_required |
user_route.py:90-101(UserRoute.put) |
| 代改他人密碼 | 僅登入者 userInfo.is_admin
時顯示開關(UserMTRBACForm.vue:502) |
有:非 is_admin 改他人密碼會被
can_change_password 擋下 |
jedi-auth
user_domain_service.py:259(can_change_password)→
user_service.py:270-272 |
| 改自己密碼 | 個人資料頁另一條路徑(見 個人資料),非本頁 | — | — |
⚠️ 本頁寫入端點缺角色守門:
UserRoute全部 method 皆只掛@jwt_required(),jedi-authuser_service.add_user/update_user內部也無角色或能力檢查——任何已登入帳號皆可透過直打 API 新增 / 編輯他人帳號(含admin,因「不可動 admin」只是 FE 隱藏按鈕)。唯一的例外是密碼代改:update_user內有can_change_password明確擋非管理員改他人密碼。與 _overview §5 及 role-manage.md §3 一致(本群多頁通病)。收進 §12,也見 使用者管理 §12。
4. 狀態機與前置條件
帳號本身狀態(users.status)與列表操作 gate 見 使用者管理 §4。本表單頁 gate:
| 條件 | 效果 | 出處 |
|---|---|---|
login_name === 'admin'(表單內) |
生效日 / 到期日 Calendar 禁用;新增授權按鈕禁用;授權列的編輯 / 刪除鈕禁用 | UserMTRBACForm.vue:485,498,544,551-552 |
mode === 'EDIT' |
登入名稱欄位鎖死(不可改) | UserMTRBACForm.vue:434 |
userInfo.is_admin && mode === 'EDIT' |
顯示「變更密碼」開關;非 admin 登入者看不到此區塊(僅能改自己密碼,走 user-profile 頁) | UserMTRBACForm.vue:502 |
canEdit(currUser) =
mode==ADD || userInfo.is_admin || userInfo.uid===currUser.uid |
姓名 / Email 欄位可編輯性 | UserMTRBACForm.vue:299-310 |
| 授權組合陣列長度 = 1 | 該筆自動設為 is_default |
UserMTRBACForm.vue:361-364 |
設某筆 is_default=true |
其餘全部改 is_default=false(前端保證唯一預設) |
UserMTRBACForm.vue:365-370 |
刪除授權列後無任何 is_default |
自動補第一筆為預設 | UserMTRBACForm.vue:400-404 |
| BE 寫入無狀態前置檢查 | add_user / update_user 本身不檢查
status,任何狀態的帳號都可被改(唯一狀態轉換在
update_user_status 專職) |
jedi-auth user_service.py |
5. UI 設計
📸 實機截圖待補(亮色模式)。待補清單:表單頁上半基本欄位、表單頁下半授權組合列表、新增/編輯授權組合 Dialog、admin 帳號鎖死狀態(生效/到期日禁用)。
表單頁骨架
狀態呈現:表單頁儲存 / 讀取中蓋一層全螢幕遮罩 +
ProgressSpinner(submitted /
loading 兩個獨立旗標,避免進頁誤顯示「儲存中」)。
6. API 規格
Envelope:成功 {"code": 1, "data": …}、失敗
{"code": 0, "msg": "…"}。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 明細 | GET /api/1.0/user/<uid> |
單一使用者(含 auth_providers,排除 salt/password) | §6.2 |
| 建立 | POST /api/1.0/user |
建立使用者 + 授權組合 | §6.2 |
| 更新 | PUT /api/1.0/user/<uid> |
更新使用者 + 授權組合全量重建(可選帶密碼) | §6.2 |
| 下拉資料 | GET
/tenants/menu・/org-units/menu・/roles/menu・/capabilities/menu |
授權組合 Dialog 的租戶 / 部門 / 角色下拉 | GAI-SD-02 |
列表 / 狀態變更 / 刪除 / Excel 匯入端點見 使用者管理 §6 與 Excel 批次匯入 §6。
6.2 使用者 CRUD(request / response)
[GET] /api/1.0/user/
Response:UserResponse(exclude
salt/password),額外含
auth_providers[](UserAuthProviderResponse)。欄位卡見
使用者管理 §6.2
列表回傳。出處:user_route.py:57-73。
[POST]
/api/1.0/user(建立)/ [PUT] /api/1.0/user/(更新)
Request(UserRequest,api/auth/serializers/user.py:5-19):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
login_name |
string | 是 | 登入代碼(建立後不可改,FE 鎖死) |
email |
string | 是 | Email(重複會 409) |
nickname |
string | 否(FE 端 required) | 顯示名稱 |
job_title / tel |
string | 否 | 職稱 / 電話 |
password |
string | 否 | 有值才觸發改密碼流程 |
effective_date / expire_date |
datetime YYYY-MM-DD HH:MM:SS |
否 | 帳號生效 / 到期日 |
user_roles |
list[UserRoleRequest] |
是(FE 端 required,至少 1 筆) | 授權組合:{role_uid, tenant_id, org_unit_id, scope, is_default, start_at, end_at} |
tenants |
list[int] | 否 | 由 FE 從 user_roles[].tenant.id
去重組出,全量取代該使用者的
user_tenants |
org_units |
list[int] | 否 | 同上邏輯,全量取代 user_org_units |
status |
int | 否(僅編輯回填顯示,非本頁編輯欄位) |
Response:同 GET(exclude
salt/password/last_login_success_time/last_login_failed_time)。
BE
鏈路:UserRoute.post/put(user_route.py:75-101)→
jedi-auth UserService.add_user /
update_user(user_service.py:100-338)。關鍵行為:
- 授權組合陣列的第一筆或標
is_default的那筆決定user_entity.tenant_id/org_unit_id(使用者的預設租戶/部門欄位) tenants/org_units/user_roles三者在update_user時先刪後插(全量取代):user_tenant_domain_service.delete_by_user→ 重建;user_org_unit_domain_service.delete_by_user→ 重建;user_role_domain_service.delete_user_role_by_user_id→ 重建- 新增時若
password未帶,generate_complex_password(12)自動產生亂碼密碼,寄送帳號建立通知信(含明碼密碼,僅信件內、不落 log;user_service.py:102-104明確禁止把含密碼的 kwargs 整包 dump 進 log) - 編輯改密碼:非
is_admin呼叫者會先過can_change_password(curr_user.uid, uid)(只能改自己),並寫入user_change_pwd_logs - 檢核順序:
login_name重複 →email重複 → 租戶存在 → 部門存在 → 角色存在(uid 清單)
出處:user_service.py:99-214(add_user)、:216-338(update_user)。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/user-manage/UserMTRBACForm.vue |
route user-form(實際掛在
/auth/user-form-mtrbac):建立 / 編輯共用表單 + 授權組合
Dialog |
src/views/user-manage/UserForm.vue |
⚠️ 死碼:無任何 route 指向,router 對應項已整段註解(見 §1) |
src/config/router/index.js:126-172 |
/auth 群組:user-manage /
user-form(→UserMTRBACForm.vue)/
role-manage / role-form |
src/stores/menuStore.js |
fetchRoleMenu() / tenantMenu /
orgUnitMenu / capabilityMenu(授權組合 Dialog
下拉來源) |
src/utils/userUtil.js |
userInfo(含 is_admin / uid /
login_name,供本頁多處權限判斷) |
src/config/api/api.js:32-42 |
USER / ROLE_MENU 等端點常數 |
8. 後端檔案地圖
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 使用者 CRUD | user_route.py(UserRoute) |
直接委派(無主專案 app service 包裝) | jedi-auth user_service.py →
user_domain_service.py → repo |
| 忘記密碼 / 改密請求 | api/auth/routes/change_password_route.py |
直接委派 | jedi-auth user_change_password_service.py |
DDD 提醒:使用者 CRUD 的 route 直接注入 jedi-auth 的
UserService,主專案在這條鏈路無 app service 包裝。大半
domain 邏輯在 jedi-auth
套件(~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/),改行為要分清邊界(CLAUDE.md
外部套件異動規範)。此鏈路模式與 role-manage.md
§8 相同(本群通用模式)。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 詳述 |
|---|---|---|---|
public.users |
讀 / 寫 | 帳號本體(含 salt/password,敏感欄位僅描述不列值) | §9.3 |
public.user_roles |
讀 / 寫(全量取代) | 使用者 ↔︎ 角色的授權組合(帶 tenant_id/org_unit_id 授權邊界) | §9.3 |
public.user_tenants |
讀 / 寫(全量取代) | 使用者 ↔︎ 租戶(is_default) |
_overview §3 |
public.user_org_units |
讀 / 寫(全量取代) | 使用者 ↔︎ 部門(is_primary) |
_overview §3 |
public.user_change_password_request |
寫 | 新增帳號時建立首次註冊改密請求(type=FIRST_REGISTER) |
§9.3 |
public.user_change_pwd_logs |
寫 | 管理員代改密碼時寫入歷史 | §9.3 |
public.user_auth_providers |
讀 | 使用者第三方登入綁定(GET 明細帶出,本頁不維護寫入) | §9.3 |
public.roles |
讀 | 角色下拉、verify_role_exist_by_uids 檢核 |
role-manage.md §9 |
public.tenants |
讀 | 租戶下拉、verify_tenant_exist_by_id 檢核 |
_overview §6 |
public.org_units |
讀 | 部門下拉、verify_org_unit_exist_by_id 檢核 |
_overview §6 |
9.2 ER 圖
9.3 核心表欄位(取自 DEV DB dump,含 DB comment)
public.users — 帳號本體
| 欄位 | 型別 | 說明 |
|---|---|---|
| id / uid | integer / varchar(36) | 內部序號 / 對外識別碼 |
| login_name | varchar(255) | 登入名稱(唯一,建立後不可改) |
| nickname | varchar(255) | 顯示名稱 |
| salt / password | varchar(250) | 密碼鹽值 /
加密密碼(敏感欄位,僅描述存在,不列值;API response
一律 exclude=["salt","password"]) |
| varchar(100) | 電子郵件(唯一) | |
| job_title / tel | varchar(100) / varchar(50) | 職稱 / 電話(皆選填) |
| effective_date / expire_date | timestamp | 帳號生效 / 到期日 |
| status | integer | 0 未啟用 / 1 啟用 / -1 停用 /
-2 刪除 |
| last_login_success_time / last_login_failed_time / login_failed_count | timestamp / timestamp / integer | 登入軌跡(列表可排序顯示) |
| mfa_type | varchar(50) | 多因素驗證類型(TOTP 設定見 個人資料) |
| topt_secret | text | TOTP 密鑰(敏感欄位;命名沿用套件既有拼法,非本頁 typo) |
| is_super_admin | boolean | 超級管理員旗標(DB 欄位;JWT context 的 is_admin
由此衍生) |
| tenant_id / org_unit_id | integer | 使用者的預設租戶 / 部門(由 user_roles
中標 is_default 的那筆回填,非獨立輸入) |
| employee_id | varchar(20) | 員工編號(選填,表單無對應輸入欄,僅 API 層可帶) |
| created_user / created_at / updated_user / updated_at | varchar(50) / timestamp | 審計欄 |
public.user_roles — 授權組合(本頁核心寫入表)
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | bigint | 主鍵 |
| user_id / role_id | integer | → users / roles |
| scope | varchar(10) | global / tenant /
org(依是否帶
tenant_id/org_unit_id
推導,非使用者直接選) |
| tenant_id / org_unit_id | integer(nullable) | 授權邊界:該角色只在此租戶 / 部門內生效 |
| starts_at / ends_at | timestamp | 角色生效 / 結束時間(表單目前未提供輸入,API 層可帶) |
| is_default | boolean | 是否為使用者的預設授權(唯一,決定
users.tenant_id/org_unit_id) |
public.user_tenants / public.user_org_units — 租戶 / 部門歸屬
user_tenants:user_id / tenant_id / is_default / starts_at / ends_at;user_org_units:user_id / org_unit_id / is_primary / starts_at / ends_at。兩表在
update_user 時皆 delete_by_user 後依
user_roles
推導的租戶/部門集合重建(全量取代,非差異更新)。
public.user_change_password_request / public.user_change_pwd_logs
user_change_password_request:uid / user_id / type(0
首次註冊 / 1 密碼重設 / 2 忘記密碼 / 3 密碼過期 / 9 自行變更)/
status(0 未使用 / 1 已使用)/
expired;新增使用者時固定建一筆
type=FIRST_REGISTER、7
天後過期。user_change_pwd_logs:user_id / type / password(加密後);僅管理員代改密碼成功時寫入一筆。
public.user_auth_providers(唯讀)
uid / user_id / provider / provider_user_id:第三方登入綁定,本頁
GET 明細會回傳但無編輯 UI。
10. 頁面邏輯與資料對應
表單載入時序:
關鍵欄位對應(API ↔︎ 畫面):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 登入名稱 / 姓名 / Email / 職稱 / 電話 | currUser.login_name/nickname/email/job_title/tel |
UserRequest 同名欄位 |
| 生效日 / 到期日 | currUser.effective_date/expire_date(Calendar,自訂格式化) |
同名,送出前補 00:00:00 |
| 授權組合表格 | currUser.user_roles[] |
payload.user_roles[]({role_uid, tenant_id, org_unit_id, is_default})
+ 衍生 payload.tenants /
payload.org_units(去重 id 陣列) |
| 授權組合 Dialog:租戶/部門/角色下拉 | currAuth.tenant/org_unit/role(物件) |
存入陣列前轉成 *_id / *_name /
*_uid 扁平欄位 |
| 變更密碼開關 + 密碼欄 | currSecret.secret/confirm_secret |
payload.password(僅有值才附帶) |
儲存流程:表單整包送出(非逐欄位獨立儲存,不同於專案規劃頁);onSaveClick
先跑 vuelidate(基本欄位必填 + 密碼強度規則:≥12
碼、含大小寫與數字),通過才組 payload 呼叫
POST /user(新增)或
PUT /user/<uid>(編輯);成功後導回列表(router.push({name:'user-manage'}))。
錯誤對應:BE error envelope → toast
顯示;常見碼:AUTH_409001(login_name
已存在)、AUTH_409002(email
已存在)、AUTH_404005(租戶不存在)、AUTH_404004(部門不存在)、AUTH_404003(角色不存在)。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 新增帳號成功 → 寄送帳號建立信(含登入名稱 +
明碼密碼,僅信件內容,不落 log);notification_service 為
optional 注入,未設定時靜默跳過 |
| Job / 排程 | 無 |
| Socket | 無 |
| jedi-* 套件 | jedi-auth 擁有使用者 / 角色 / 租戶 / 部門的 service / domain / repo / ORM 全部邏輯;本頁 CRUD 直接委派套件,無主專案 wrapper |
| 密碼規則 | 新增未帶密碼 → generate_complex_password(12)
產生亂碼;FE 端管理員代改密碼強制 ≥12 碼 + 大小寫 + 數字(vuelidate
secretRules),BE 本身無獨立密碼強度檢查(僅 hash
儲存) |
12. 邊界情況與已知坑
UserForm.vue是死碼:463 行完整表單元件,但 router 對應/auth/user-formroute 整段被註解、無任何 import 引用;實際生效的是UserMTRBACForm.vue(multi-tenant RBAC 版)。維護時勿誤改死碼。- 本頁寫入端點缺角色守門(同 角色權限管理 模式):
UserRoute全部 method 僅@jwt_required(),jedi-auth service 層也無角色 / 能力檢查——任何登入帳號可直打 API 新增 / 編輯任何人(含admin)。唯一的例外:改他人密碼有can_change_password明確擋非is_admin者。本 spec 只記錄不修。 user_roles/tenants/org_units三表皆全量取代:update_user對這三個關聯先delete_by_user再重建;FE 表單編輯時務必先讀回完整既有授權組合再送出,漏帶等於刪除該筆授權。users.tenant_id/org_unit_id(預設值)由user_roles反推,非獨立輸入:新增/編輯時,若送出的授權組合都沒有標is_default,BE 會強制把第一筆設為預設(user_service.py:122-126,239-243),可能與使用者原意不符(FE 已同步這條規則於本地陣列操作,但兩端各自實作、非共用單一 source)。- 敏感欄位(
salt/password/topt_secret)一律 exclude:GET/POST/PUT response 都手動UserResponse(exclude=["salt","password", ...]),新增回傳欄位或改 serializer 務必保留這個 exclude list,避免意外外洩雜湊密碼。 nickname在 BE serializer 非 required,但 FE vuelidate 端強制必填:兩端必填集合不完全對稱(BEUserRequest.nickname無required=True),直打 API 可送出無nickname的使用者。admin帳號的鎖死規則分散在多處判斷(生效/到期日禁用、新增授權禁用、既有授權列編輯/刪除禁用):皆為 FEisTargetAdmin/ 字串比對login_name==='admin',BE 無對應保護(見坑 2)——非 FE 判斷點若新增功能,記得同步排除admin。
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:管理員帳號見 memory
reference_dev_login/.env(憑證不寫入本文件)。 - 導航路徑:登入 →
側選單「系統管理」→「使用者管理」(
/auth/user-manage)→ 建立 / 點某列「編輯」進表單頁。 - 前置資料:至少一個租戶 + 一個部門 +
一個角色(
tenant_id NULL的系統角色亦可)才能完成授權組合指派。 - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以使用者管理/user-form搜尋。 - 相關文件:上層導航 使用者管理;Excel
批次匯入;權限系統設計說明書
docs/system-design/permission/;角色權限管理;功能群總覽。