跳轉到

使用者表單(User Form)

功能群:系統管理|三層 RBAC 模型與租戶/部門組織模型先讀 功能群總覽 §2、§3|上層導航頁見 使用者管理

事實基準:2026-07-05 從 FE src/views/user-manage/UserMTRBACForm.vue + BE api/auth/routes/user_route.py / change_password_route.py + jedi-auth user_service.py / user_domain_service.py + routes.json / db_schema.json 掃出;角色區塊、守門與 user_roles 寫入語意 2026-10-08 依 FE src/components/user-manage/UserRoleSummary.vue、UserInitialRolePicker.vue、UserMembershipEditor.vue + jedi-iam api/routes/user_route.py、app/service/user_service.py、role_assignment_service.py 重掃。

變更紀錄

日期 FR 變更
2026-07-06 FR-047 自 user-manage.md 拆出:帳號新增/編輯表單頁(含授權組合指派)獨立成子頁
2026-10-08 FR-132 角色區塊改為指派表(就地編輯期間/移除、新增角色),存帳號不再動指派;所屬租戶與部門另成一區;寫入端點掛權限守門;不顯示「主要權限」

1. 功能描述

使用者表單是 使用者管理 列表「建立」/「編輯」導向的獨立表單頁(route user-form,實掛 /auth/user-form-mtrbac → UserMTRBACForm.vue)。新增與編輯共用同一表單:上半填帳號基本欄位(登入名稱 / 姓名 / Email / 職稱 / 電話 / 生效到期日 /(編輯限定)代改密碼),中段是所屬租戶與部門,下段是角色。

角色區塊依模式不同:

  • 新增帳號:角色列(每列=角色+期間,可加可刪),隨「建立」一起送出;角色選項跟著第一個所屬租戶,換租戶時不屬於新租戶的列會被清掉並提示。
  • 編輯帳號:一張指派表(租戶/角色/部門/生效/到期/狀態),與 指派 頁同一支 API、同一套欄位與狀態標籤;每列可就地「編輯期間」「移除角色」,表頭「新增角色」開同一個指派對話框(人已選好)。這些動作各自存檔,不經帳號的「儲存」;帳號存檔不送角色,後端也就不動指派。

「主要權限」(user_roles.is_default)不顯示:權限判定、側欄、登入都不讀它。

主要使用者:系統 / 租戶管理員。角色本身有哪些權限在 角色明細 設定;跨使用者看指派在 指派。

角色速覽(完整定義見 _overview §2、§3):

角色 一句話
持 iam.user.create/iam.user.update 新增/編輯帳號基本資料、所屬租戶與部門
另持 iam.user.assign 角色區塊的新增、編輯期間、移除
is_admin(超級管理員 JWT 旗標) 額外可在編輯他人帳號時代改密碼;admin 帳號本身的生效/到期日與所屬鎖死
沒有 iam.user.read 無此選單

⚠️ route 命名陷阱:FE views/user-manage/UserForm.vue(463 行)是死碼——router 中對應的 /auth/user-form route 已整段註解掉(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 所屬租戶與部門 帳號屬於哪些租戶、部門;隨「儲存」整批替換 表單頁中段 §6.2
5 角色列(新增帳號) 角色+期間,可多列;隨「建立」送出 表單頁下段 UC-USER-02
6 角色指派表(編輯帳號) 租戶/角色/部門/生效/到期/狀態;每列「編輯期間」「移除角色」,各自存檔 表單頁下段 UC-USER-02
7 新增角色(編輯帳號) 開指派對話框(人已選好):角色、期間、可見選單預覽 角色區塊表頭 UC-USER-02
8 儲存帳號 送基本欄位+所屬租戶與部門;編輯時不送角色 表單頁 UC-USER-01
9 取消編輯 二次確認後返回列表 表單頁 —

展開成 UC:UC-USER-01(建立 / 編輯帳號主流程)、UC-USER-02(帳號頁的角色)。停用 / 刪除 / 列表操作屬 使用者管理 hub。

2. Use Case

角色速覽(完整定義見 _overview §2、§3):

角色 一句話
持 iam.user.create/iam.user.update 新增/編輯帳號基本資料、所屬租戶與部門
另持 iam.user.assign 角色區塊的新增、編輯期間、移除
is_admin(超級管理員 JWT 旗標) 額外可在編輯他人帳號時代改密碼;admin 帳號本身的生效/到期日與所屬鎖死
沒有 iam.user.read 無此選單

流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點。

UC-USER-01 建立 / 編輯帳號

項目 內容
角色 具本頁可見性的管理員
前置條件 新增:login_name / email 未被使用;編輯:帳號存在
產出 / 後置條件 public.users 新增 / 更新一列 + user_tenants / user_org_units 整批替換;新增時另建 user_roles,編輯時 user_roles 不動

UC-USER-01 流程:新增/編輯共用表單→驗證→存檔;login_name/email 重複與租戶/部門/角色不存在皆 409/404 擋下

UC-USER-02 帳號頁的角色

項目 內容
角色 持 iam.user.assign
前置條件 新增:先選所屬租戶才能挑角色;編輯:帳號已存在
產出 / 後置條件 新增:隨建立帳號寫入 user_roles(scope=tenant);編輯:各動作立即寫入 user_roles,不經帳號儲存
flowchart TD
  A{"新增或編輯?"} -- 新增 --> B["角色列:角色+期間<br/>選項限第一個所屬租戶"]
  B --> C["按 建立:POST /user 帶 user_roles"]
  A -- 編輯 --> D["指派表:POST /role-assignments(filters.user_uid)"]
  D --> E["新增角色/編輯期間/移除<br/>POST・PUT・DELETE /role-assignment"]
  E --> F["只刷新角色表,不覆蓋表單上未存的欄位"]
  D --> G{"能讀指派列表?"}
  G -- 否 --> H["退回帳號 API 的摘要:只列角色與租戶,沒有期間"]

規則與錯誤碼同 指派(期間不可顛倒、同角色期間不可重疊、須是租戶成員、原廠管理員至少留一筆有效指派)。

3. 權限矩陣

專案層 RBAC 基調見 _overview §5。本頁的實際檢查點:

操作 FE 判定 BE 強制 BE 檢查位置
進入(新增) user-form 綁 iam.user.create(ALL) — —
進入(編輯) user-form 綁 iam.user.update(ANY) 讀他人明細需 iam.user.read;讀自己放行 jedi-iam api/routes/user_route.py(UserRoute.get)
建立帳號 儲存鈕 v-can iam.user.create/update iam.user.create;帶 user_roles 另需 iam.user.assign user_route.py(UserCreateRoute.post)、role_assignment_service.py(assert_can_change_user_roles)
編輯帳號 同上 iam.user.update;user_roles 與現況不同才另要 iam.user.assign;原廠管理員帳號另有保護 user_route.py(UserRoute.put)
角色區塊新增/編輯/移除 v-can iam.user.assign iam.user.assign role_assignment_route.py
代改他人密碼 僅登入者 userInfo.is_admin 時顯示開關 有:非超級管理員改他人密碼會被 can_change_password 擋下 jedi-iam user_service.py(update_user)
改自己密碼 個人資料頁另一條路徑(見 個人資料),非本頁 — —

缺權限回 403 GRC_403022,畫面顯示「使用者不具備此操作所需的權限(需要權限:<權限名>)」。只有 iam.user.update 的人改暱稱、電話不會被要求 iam.user.assign——存帳號不送角色,送了且與現況相同也不檢查。

4. 狀態機與前置條件

帳號本身狀態(users.status)與列表操作 gate 見 使用者管理 §4。本表單頁 gate:

條件 效果 出處
login_name === 'admin'(表單內) 生效日 / 到期日 Calendar 禁用;所屬租戶與部門禁用 UserMTRBACForm.vue(isTargetAdmin)
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
新增帳號尚未選所屬租戶 角色列無法加(提示「先選所屬租戶,才能挑角色」) UserInitialRolePicker.vue
新增帳號換第一個所屬租戶 不屬於新租戶的角色列被移除並提示名稱 UserInitialRolePicker.vue
編輯帳號、沒有讀指派的權限 角色表退回帳號 API 的摘要(只有角色與租戶,沒有期間) UserRoleSummary.vue
授權到期唯讀 寫入鈕停用 useLicenseReadonly
BE 寫入無狀態前置檢查 add_user / update_user 本身不檢查 status,任何狀態的帳號都可被改(唯一狀態轉換在 update_user_status 專職) jedi-auth user_service.py

5. UI 設計

由上而下:基本欄位(編輯限定另有「變更密碼」開關)→ 所屬租戶與部門 → 角色(新增:角色列;編輯:指派表+「新增角色」)→ 取消/儲存。角色的新增與編輯期間是對話框。

📸 版面圖與實機截圖待補(亮色模式):新增帳號(含角色列)、編輯帳號(含角色指派表)、新增角色對話框、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> 更新使用者、所屬租戶與部門(可選帶密碼);不帶 user_roles §6.2
角色 POST /role-assignments・POST /role-assignment・PUT/DELETE /role-assignment/<id> 編輯帳號的角色指派表 指派 §6
下拉資料 GET /tenants/menu・/org-units/menu・/roles/menu 所屬租戶與部門、角色下拉 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] 否 新增時才送:{role_uid, tenant_id, scope:'tenant', starts_at, ends_at, is_default}。沒送=不動指派;送空清單=清空全部指派
tenants list[int] 是(一定要送) 所屬租戶,整批替換 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 鏈路:jedi-iam UserCreateRoute.post/UserRoute.put(api/routes/user_route.py)→ jedi-iam UserService.add_user / update_user(app/service/user_service.py)。關鍵行為:

  • 新增時第一個所屬租戶為使用者的預設租戶(user_entity.tenant_id)
  • tenants / org_units 在 update_user 時整批替換;user_roles 以「鍵在不在」決定:沒送不動,有送才同步(sync_user_roles)
  • 新增時若 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 清單)

出處:jedi-iam app/service/user_service.py(add_user、update_user)。

7. 前端檔案地圖(compliance-manager-fe/)

檔案 角色
src/views/user-manage/UserMTRBACForm.vue route user-form(實際掛在 /auth/user-form-mtrbac):建立 / 編輯共用表單
src/components/user-manage/UserMembershipEditor.vue 所屬租戶與部門
src/components/user-manage/UserInitialRolePicker.vue、UserInitialRoleDialog.vue 新增帳號的角色列
src/components/user-manage/UserRoleSummary.vue 編輯帳號的角色指派表(用 指派 的 API 與 AssignmentDialog.vue)
src/views/user-manage/UserForm.vue ⚠️ 死碼:無任何 route 指向,router 對應項已整段註解(見 §1)
src/config/router/index.js /auth 群組:user-manage / user-form(→UserMTRBACForm.vue);舊 role-manage / role-form 轉址到存取管理
src/stores/menuStore.js fetchRoleMenu() / tenantMenu / orgUnitMenu(下拉來源)
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 jedi-iam api/routes/user_route.py(UserCreateRoute、UserRoute) jedi-iam user_service.py;守門前置 role_assignment_service.assert_can_change_user_roles、宿主 user_app_service(原廠管理員保護) jedi-iam user_domain_service.py → repo
忘記密碼 / 改密請求 api/auth/routes/change_password_route.py 直接委派 jedi-auth user_change_password_service.py

DDD 提醒:使用者 CRUD 的 route 與 service 在 jedi-iam 套件(~/Projects/Jedicogy/module/jedi-python-package/jedi-iam/),改行為要分清邊界(CLAUDE.md 外部套件異動規範)。

9. DB

9.1 資料表總清單(本頁讀寫的全部表)

表 讀/寫 說明 詳述
public.users 讀 / 寫 帳號本體(含 salt/password,敏感欄位僅描述不列值) §9.3
public.user_roles 讀 / 寫(新增帳號時建;編輯帳號時由角色指派表逐筆寫) 使用者 ↔ 角色指派 §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 檢核 角色明細 §9
public.tenants 讀 租戶下拉、verify_tenant_exist_by_id 檢核 _overview §6
public.org_units 讀 部門下拉、verify_org_unit_exist_by_id 檢核 _overview §6

9.2 ER 圖

使用者表單讀寫範圍:綠=本頁寫入(users/user_roles/user_tenants/user_org_units/改密相關)、黃=唯讀依賴(roles/tenants/org_units)

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"])
email 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 使用者的預設租戶 / 部門
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 tenant/org_unit;畫面一律建 tenant(部門範圍判定上與整租戶相同)
tenant_id / org_unit_id integer(nullable) 範圍
starts_at / ends_at timestamptz 有效期間;ends_at 空=永久
is_default boolean 保留欄位,畫面不顯示、判定不讀

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 時依送來的 tenants/org_units 整批替換(非差異更新);指派頁只接受已是成員的租戶(AUTH_400014),加入新租戶只能從本頁。

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. 頁面邏輯與資料對應

表單載入時序:

onMounted → 載入租戶、部門、角色下拉 → 有 uid 則 GET /user/<uid>(tenants/org_units/user_roles 為 null 時補成空陣列)→ 編輯模式角色表另打 POST /role-assignments(filters.user_uid、含已到期)。

關鍵欄位對應(API ↔ 畫面):

畫面元素 FE state API 欄位
登入名稱 / 姓名 / Email / 職稱 / 電話 currUser.login_name/nickname/email/job_title/tel UserRequest 同名欄位
生效日 / 到期日 currUser.effective_date/expire_date(Calendar,自訂格式化) 同名,送出前補 00:00:00
所屬租戶與部門 currUser.tenants[] / currUser.org_units[] payload.tenants / payload.org_units(id 陣列)
角色列(新增) initialRoleRows[] payload.user_roles[]({role_uid, tenant_id, scope:'tenant', starts_at, ends_at, is_default},第一列 is_default=true)
角色指派表(編輯) UserRoleSummary 自己的列表 指派 API,不進帳號 payload
變更密碼開關 + 密碼欄 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(角色不存在)、GRC_403022(缺權限,附缺的權限名)。

11. 背景行為與外部依賴

類型 內容
通知 新增帳號成功 → 寄送帳號建立信(含登入名稱 + 明碼密碼,僅信件內容,不落 log);notification_service 為 optional 注入,未設定時靜默跳過
Job / 排程 無
Socket 無
jedi-* 套件 jedi-iam 擁有使用者 / 角色 / 指派 / 租戶 / 部門的 service / domain / repo / ORM
密碼規則 新增未帶密碼 → generate_complex_password(12) 產生亂碼;FE 端管理員代改密碼強制 ≥12 碼 + 大小寫 + 數字(vuelidate secretRules),BE 本身無獨立密碼強度檢查(僅 hash 儲存)

12. 邊界情況與已知坑

  1. UserForm.vue 是死碼:463 行完整表單元件,但 router 對應 /auth/user-form route 整段被註解、無任何 import 引用;實際生效的是 UserMTRBACForm.vue(multi-tenant RBAC 版)。維護時勿誤改死碼。
  2. user_roles 以「鍵在不在」判斷:沒送=不動指派;送空清單=清空全部指派。後端不可改成 kwargs.get("user_roles") or []——那會把「改電話」也當成清空角色,存一次帳號指派的到期日就被洗掉。
  3. tenants/org_units 一定要送:後端整批替換,漏送=把人移出所有租戶與部門。
  4. 帳號頁的角色動作不經「儲存」:新增角色、編輯期間、移除各自立即寫入;按「取消」不會撤回這些動作。
  5. Excel 批次匯入帳號維持每人一個角色、整個租戶、永久;要多角色或期限,匯入後到本頁或 指派 補。
  6. 敏感欄位(salt/password/topt_secret)一律 exclude:GET/POST/PUT response 都手動 UserResponse(exclude=["salt","password", ...]),新增回傳欄位或改 serializer 務必保留這個 exclude list,避免意外外洩雜湊密碼。
  7. nickname 在 BE serializer 非 required,但 FE vuelidate 端強制必填:兩端必填集合不完全對稱(BE UserRequest.nickname 無 required=True),直打 API 可送出無 nickname 的使用者。
  8. admin 帳號的鎖死規則:畫面以 login_name==='admin' 禁用生效/到期日與所屬;後端另以原廠管理員保護擋「改完後沒有有效的原廠管理員指派」與刪除。

13. 開發與驗證

  • 跑起來:BE python main.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)→ 建立 / 點某列「編輯」進表單頁。
  • 前置資料:至少一個租戶與一個該租戶的角色,才能在新增帳號時加角色列。
  • E2E:compliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 使用者管理 / user-form 搜尋。
  • 相關文件:上層導航 使用者管理;Excel 批次匯入;指派;角色明細;功能群總覽。