個人資料(User Profile)
功能群:系統管理|三層 RBAC 模型與共用權限模型先讀 功能群總覽 §2、§5
事實基準:2026-07-05 從 FE
src/views/user-manage/UserProfileForm.vue+ BEapi/auth/routes/user_route.py/api/auth/routes/otp_route.py/api/user_auth_provider/routes/user_auth_provider_route.py+ jedi-authuser_service.py/ jedi-mfatotp_service.py+ routes.json / db_schema.json 掃出變更紀錄:2026-07-09 FR-047 已知坑修復 —— MFA 切換改「驗證後即時生效」(新增
POST /totp-verify+PUT /user-mfa-type、帳號安全拆卡、B 案不再依附基本資料儲存);改密「新舊密碼相同」錯誤碼AUTH_401009→AUTH_400003;TotpQrcodeRouteMFAService 型別統一為"totp"
1. 功能描述
個人資料頁是使用者操作自己帳號的自助頁面:檢視自身基本資料與所屬角色/租戶/部門(唯讀顯示)、編輯可改欄位、變更密碼、綁定/解除 LDAP 第三方登入、開關 TOTP 兩階段驗證。全頁只有一顆「儲存」鈕,一次 PUT 送出全部欄位(改密與否由密碼欄位是否填寫決定);LDAP 綁定/解除與 TOTP QR Code 顯示則各自獨立即時呼叫 API。
主要使用者:任何已登入使用者(僅能操作自己)。
角色速覽
| 角色 | 能做什麼 |
|---|---|
| 任何已登入使用者 | 檢視/編輯自己的資料、改自己的密碼、綁定/解除自己的 LDAP、開關自己的 TOTP |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 個人資料頭卡 | 頭像圓形縮寫、暱稱/登入名稱、Email、角色/租戶/部門 Tag(唯讀顯示) | Profile Header | §5 |
| 2 | 基本資料檢視/編輯 | 登入名稱(disabled 不可改)/姓名*/Email*/職稱/聯絡電話 | 表單「基本資料」區塊 | §6.3 |
| 3 | LDAP 帳號綁定 | 未綁定時顯示「LDAP帳號綁定」鈕,開對話框輸入第三方帳密,即時驗證後寫入 | 「帳號安全」區塊 | §6.4 |
| 4 | LDAP 帳號解除綁定 | 已綁定時顯示「解除」鈕,confirm 後刪除該綁定 | 「帳號安全」區塊 | §6.4 |
| 5 | TOTP 兩階段驗證開關 | InputSwitch 切換 mfa_type(email ↔︎ totp),切到 totp
即呼叫建立 TOTP secret |
「帳號安全」區塊 | UC-PROFILE-02 |
| 6 | 顯示 TOTP QR Code | mfa_type=totp 時出現「顯示 QR Code」鈕,開對話框載入 QR 圖供 Authenticator App 掃描 | 「帳號安全」區塊 → Dialog | UC-PROFILE-02 |
| 7 | 變更密碼 | 新密碼+確認密碼兩欄(皆選填),填寫且通過驗證後隨表單一併送出 | 「變更密碼」區塊 | UC-PROFILE-01 |
| 8 | 儲存 | 送出基本資料+(選填)密碼+mfa_type,PUT /user-profile/<uid> |
表單底部 | UC-PROFILE-01 / 02 |
| 9 | 取消 | confirm 後 router.back(),不送出任何變更 |
表單底部 | — |
展開成 UC:UC-PROFILE-01(變更密碼)、UC-PROFILE-02(TOTP 綁定)。LDAP 綁定/解除屬常規 CRUD,於 §6.4 詳述 request/response 但不獨立畫流程圖。
2. Use Case
角色速覽
| 角色 | 能做什麼 |
|---|---|
| 任何已登入使用者 | 檢視/編輯自己的資料、改自己的密碼、綁定/解除自己的 LDAP、開關自己的 TOTP |
視覺慣例:判斷框(黃菱形)= 分支條件;橙框 = 擋下 / 例外;紅框 = 錯誤(標 error code);綠橢圓 = 成功終點。
UC-PROFILE-01 變更密碼
| 項目 | 內容 |
|---|---|
| 角色 | 使用者本人 |
| 前置條件 | 已登入;FE 密碼規則通過(≥12 碼、含大小寫字母與數字、兩欄相符) |
| 產出 / 後置條件 | users.password(bcrypt,沿用既有
salt)更新;user_change_pwd_logs
新增一筆(type=USER_CHANGE_SELF) |
UC-PROFILE-02 綁定 TOTP 兩階段驗證
| 項目 | 內容 |
|---|---|
| 角色 | 使用者本人 |
| 前置條件 | 已登入;目前 mfa_type 非 totp(開關由 email 切到
totp) |
| 產出 / 後置條件 | totp_secrets 建立(或沿用既有)一筆 secret;儲存後
users.mfa_type 更新為 totp |
本頁 TOTP 綁定流程沒有「輸入 Authenticator 動態碼驗證成功才算啟用」的步驟——按開關即建立 secret、按「顯示 QR Code」即可看到密鑰,儲存表單即把
mfa_type改成totp生效,過程不驗證使用者是否真的掃描成功。OTP 驗證頁(/otp-verify、OtpVerification.vue)是登入流程專用(登入時輸入動態碼換 JWT),與本頁綁定流程是兩條不同路徑,不要混淆。見 §12 坑 1。
3. 權限矩陣
本頁角色基調見 _overview §5:個人資料 / 改密 / MFA = 使用者本人操作,理論上僅需 JWT。實際逐項對照:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入本頁/載入資料 | 登入即可見(/user/user-profile
無選單守門,任何登入者可導航) |
GET /user/<uid>:僅
@jwt_required(),無自身 uid 限定——⚠️ 傳他人 uid
一樣回得來完整資料(含
email/roles/tenants/mfa_type/auth_providers,已排除 salt/password) |
api/auth/routes/user_route.py:57-73(UserRoute.get) |
| 編輯基本資料/改密/改 mfa_type | 單一表單,無按鈕層級判定 | PUT /user-profile/<uid>:路徑 uid
必須等於當前登入者,否則 401
AUTH_UNAUTHORIZED |
user_route.py:141-156(UserProfileRoute.put),再由
jedi-auth can_change_password
二次確認(user_domain_service.py:258-270) |
| 改密(密碼強度/重用) | FE:≥12
碼+大小寫數字+兩欄相符(UserProfileForm.vue:61-74) |
BE:24 小時內只能自行改一次密碼、且不可與近 3 次密碼重複 | jedi-auth
user_change_password_log_domain_service.py:39-73 |
TOTP 建立 secret(POST /totp-secret) |
開關切 totp 才觸發 | 僅 @jwt_required();對象固定是
get_user_context()(無法幫他人建立),天生自身限定 |
api/auth/routes/otp_route.py:24-44(TotpSecretRoute.post) |
TOTP QR Code(GET /otp-qrcode/<uid>) |
開關為 totp 才顯示按鈕 | ⚠️ 完全無 @jwt_required(),任何人知道
uid 即可取得該使用者的 QR Code 圖(等同洩漏可掃描綁定的 TOTP 密鑰) |
otp_route.py:46-61(TotpQrcodeRoute.get,無任何裝飾器) |
LDAP 綁定(POST /auth-provider) |
未綁定才顯示鈕 | @jwt_required();payload 帶的
user_uid 未驗證是否為自己——⚠️ 傳他人 uid
可能幫他人綁定第三方帳號(惟仍需通過真實 LDAP 帳密驗證,見 §12) |
api/user_auth_provider/routes/user_auth_provider_route.py:34-76 |
LDAP 解除綁定(DELETE /auth-provider/<uid>) |
已綁定才顯示鈕 | @jwt_required();僅以 auth-provider 記錄的 uid
刪除,無擁有者比對 |
user_auth_provider_route.py:78-84 |
⚠️ 本頁最大的風險是 GET /user/
與 TOTP QR Code 端點都沒有自身限定 :写入端點(PUT /user-profile)有擋,但兩個讀端點缺角色/自身檢查,屬已知缺口,收進 §12(與 _overview §5 一致:本群多頁的通病,spec 只記錄不修)。
4. 狀態機與前置條件
本頁沒有頁面級狀態機(不隨稽核輪次 / 審核狀態變化),gate 都在欄位與操作層級:
| 條件 | 效果 | 出處 |
|---|---|---|
| 密碼欄位皆空 | 不觸發改密邏輯,PUT payload 不帶 password |
UserProfileForm.vue:182-184 |
| 密碼 < 12 碼 / 不含大小寫數字 / 兩欄不符 | FE 擋下,不送出 | UserProfileForm.vue:61-74(secretRules) |
| 24 小時內已自行改過密碼 | BE 429 AUTH_CHANGE_SECRET_WITHIN_24_HOURS |
jedi-auth
user_change_password_log_domain_service.py:60 |
| 新密碼與近 3 次密碼重複 | BE 429 AUTH_SECRET_SAME_AS_PREVIOUS(訊息掛 401
字樣,實際 raise 429,見 §12) |
同上 :62-71 |
| 路徑 uid ≠ 登入者 uid | BE 401 AUTH_UNAUTHORIZED |
user_route.py:148-149 |
| LDAP 綁定帳密驗證失敗 | 由 LDAP adapter 拋出對應錯誤(依 jedi-login LDAP
錯誤碼,未在本頁另行捕捉) |
jedi-login user_auth_provider_service.py:104-116 |
| 已綁定 LDAP 時重複綁定 | verify_user_already_binding 擋下(jedi-login
內部檢查) |
jedi-login
user_auth_provider_domain_service(本頁未逐行核) |
5. UI 設計
📸 實機截圖待補(亮色模式,另有專責 session)。待補清單:Profile Header(頭像/Tag)、基本資料表單區塊、帳號安全區塊(LDAP 卡片 + TOTP 卡片兩狀態:未綁定/已綁定、email/totp)、變更密碼區塊、TOTP QR Code 對話框、LDAP 綁定對話框。
版面骨架(單一表單頁,三區塊縱向排列):
狀態呈現:LDAP/TOTP 卡片各自用 Tag(綠
enable / 灰
disable)標示目前狀態;全頁載入中與送出中共用同一個全螢幕遮罩
ProgressSpinner(pageLoading || submitted)。無
per-欄位 spinner、無 socket。
6. API 規格
Envelope:成功 {"status": true, "data": …}、失敗
{"error_code": "…", "msg": "…"}(HTTP 4xx/5xx)。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 載入 | GET /api/1.0/user/<uid> |
載入自己的完整資料(含 auth_providers、mfa_type、roles/tenants/org_units 顯示用) | §6.2 |
| 儲存 | PUT /api/1.0/user-profile/<uid> |
送出基本資料 + 選填密碼 + mfa_type,一次 upsert | §6.3 |
| LDAP | POST /api/1.0/auth-provider |
綁定第三方登入(本頁固定 provider: "ldap") |
§6.4 |
| LDAP | DELETE /api/1.0/auth-provider/<uid> |
解除綁定 | §6.4 |
| TOTP | POST /api/1.0/totp-secret |
建立(或取得既有)TOTP secret,對象=目前登入者 | §6.5 |
| TOTP | GET /api/1.0/otp-qrcode/<uid> |
取得 TOTP QR Code 圖片(PNG) | §6.5 |
endpoint 存在性已對
scripts/deliverables/out/routes.json核實。/otp-verify、/otp-resend屬登入流程(OtpVerification.vue),本頁不呼叫,不列入本頁清單。
6.2 [GET] /user/{uid}
Response data(節錄本頁使用欄位;完整欄位見
UserResponse,api/auth/serializers/user.py:36-75):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid / login_name / nickname | string | 帳號識別 |
| email / job_title / tel | string|null | 可編輯欄位 |
| roles[] / tenants[] / org_units[] | array | 唯讀顯示用(頭卡 Tag) |
| mfa_type | string | email(預設)/ totp |
| auth_providers[] | array | {uid, provider, provider_user_id, created_at, ...};本頁用來判斷
isBindLdap |
Response 排除 salt /
password(UserResponse(exclude=["salt", "password"]),user_route.py:72)。出處:api/auth/routes/user_route.py:57-73。
6.3 [PUT] /user-profile/{uid}
Request(UserRequest(unknown=INCLUDE),api/auth/serializers/user.py:5-19;本頁實際填的子集):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| login_name | string | 是(schema 必填,但 FE disabled 不可改,原樣回送) | 登入名稱 |
| nickname | string | 否 | 姓名 |
| string | 是 | ||
| job_title / tel | string | 否 | 職稱/電話 |
| password | string | 否 | 填才觸發改密;明碼傳輸,BE 用既有 salt bcrypt 雜湊 |
| mfa_type | string | 否 | email / totp |
| tenants / org_units / user_roles / effective_date / expire_date | array/array/array/datetime | 否 | 本頁原樣回送目前值(唯讀顯示,非本頁可編輯項;FE 從載入資料組回同樣結構) |
Response:同
GET /user/<uid>(UserResponse(exclude=["salt","password","last_login_success_time","last_login_failed_time"]))。出處:api/auth/routes/user_route.py:141-156(UserProfileRoute.put)→
jedi-auth
UserService.update_user(user_service.py:216-338)。
改密副作用鏈(僅當 password
有值):can_change_password(curr_uid, uid) 二次確認自身 →
verify_user_change_password_log(24hr 冷卻 + 近 3
次重複檢查)→ bcrypt 用既有 salt 雜湊 → 寫
user_change_pwd_logs(type=USER_CHANGE_SELF)。出處:user_service.py:268-336。
6.4 LDAP 綁定 / 解除
[POST]
/auth-provider(UserAuthProviderRegisterRequest,api/user_auth_provider/serializers/user_auth_provider.py:4-8):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| user_uid | string | 是 | 本頁固定送自己 uid |
| username / password | string | 是 | 第三方(LDAP)帳號密碼,即時打 LDAP server 驗證(非只存值) |
| provider | string | 是 | 本頁固定 "ldap" |
Response(UserAuthProviderResponse):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string | 該筆綁定記錄的 uid |
| user_uid | string | 綁定所屬使用者 uid |
| provider | string | 第三方身份提供者(本頁固定 ldap) |
| provider_user_id | string | 第三方身份提供者的使用者 ID |
| created_user | string | 建立者 |
| created_at | datetime | 建立時間 |
| updated_user | string | 更新者 |
| updated_at | datetime | 更新時間 |
出處:api/user_auth_provider/routes/user_auth_provider_route.py:34-76
→ jedi-login
UserAuthProviderService.register_user_auth_provider →
register_ldap_user(user_auth_provider_service.py:104-129,內部呼叫
ldap_adapter.verify_user 真實驗證帳密後才寫入)。
[DELETE] /auth-provider/{uid}:uid 為
user_auth_providers.uid(該筆綁定記錄本身的 uid,非使用者
uid)。Response:BooleanResponse。出處:同檔
:78-84。
6.5 TOTP
[POST] /totp-secret:無 body(對象固定是
get_user_context() 目前登入者)。Response
data:TOTP 註冊結果(secret/provisioning URI 相關,供
debug;FE
本頁未直接使用回傳值,只用來觸發建立)。出處:otp_route.py:24-44(TotpSecretRoute.post)→
jedi-mfa TotpMfaService.register →
TotpSecretDomainService.set_totp_secret(既有 secret 則
idempotent 回傳原值,不重建)。
[GET] /otp-qrcode/{uid}:無
body。Response:image/png(send_file,非 JSON
envelope)。FE 直接把此 URL 當
<img src>。出處:otp_route.py:46-61(TotpQrcodeRoute.get)→
TotpSecretDomainService.get_totp_secret_qrcode_img(讀
totp_secrets.uri 產生 QR 圖)。
程式碼細節:
TotpQrcodeRoute.get內用MFAService("email").provider建構TotpMfaService(otp_route.py:57),與其他 TOTP 端點用MFAService("totp")不一致;因get_register_qr_code_img只呼叫 domain service、未用到provider,目前無實際影響,但屬可疑寫法,見 §12。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/user-manage/UserProfileForm.vue |
唯一檢視(頭卡 + 基本資料 + 帳號安全 + 變更密碼三區塊 + LDAP/TOTP 對話框) |
src/config/router/index.js:173-186 |
單一 route /user/user-profile(name
user-profile),無 legacy 別名 |
src/utils/userUtil.js |
useUserUtil().userInfo(登入後存的自己資料,決定本頁
uid 來源) |
src/config/api/api.js:17-20,32-34,340 |
TOTP_SECRET / TOTP_QRCODE /
USER / USER_PROFILE /
USER_AUTH_PROVIDER 端點常數 |
src/config/locales/i18n/zh-tw/user-manage.json /
pages.json |
本頁 i18n(user_manage.*
區塊欄位+user_profile.* 密碼欄位文案分屬兩檔) |
不屬本頁:
src/views/auth/ChangePassword.vue(/change-password,未登入態的忘記密碼/強制改密流程,走CHANGE_PWD_REQ/CHANGE_PWD_BY_REQ)、src/views/auth/OtpVerification.vue(/otp-verify,登入流程 MFA 驗證頁,走OTP_VERIFY/OTP_RESEND)——兩者常被誤認為本頁一部分,實為獨立 route,見 §12 坑 1。
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App / Domain Service | 底層 |
|---|---|---|---|
| 載入自身資料 | api/auth/routes/user_route.py::UserRoute.get |
直接委派 jedi-auth UserService.get_user |
jedi-auth UserDomainService → repo |
| 儲存(基本資料/改密/mfa_type) | api/auth/routes/user_route.py::UserProfileRoute.put |
jedi-auth
UserService.update_user(is_admin
未傳,走自助改密分支) |
UserDomainService /
UserChangePasswordLogDomainService /
UserTenantDomainService 等 |
| LDAP 綁定/解除 | api/user_auth_provider/routes/user_auth_provider_route.py |
jedi-login
UserAuthProviderService.register_user_auth_provider /
delete_user_auth_provider |
ldap_adapter.verify_user(真實 LDAP bind)→
UserAuthProviderDomainService |
| TOTP 建立 secret | api/auth/routes/otp_route.py::TotpSecretRoute |
jedi-mfa TotpMfaService.register |
TotpSecretDomainService.set_totp_secret →
totp_secrets |
| TOTP QR Code | api/auth/routes/otp_route.py::TotpQrcodeRoute |
jedi-mfa TotpMfaService.get_register_qr_code_img |
TotpSecretDomainService.get_totp_secret_qrcode_img |
DDD 提醒:本頁三個外部套件(jedi-auth / jedi-login /
jedi-mfa)各自擁有完整 domain/repo 層,主專案 route
幾乎是直接委派、無主專案 app service
包一層(與角色權限管理頁相同模式,見 role-manage.md
§8)——改行為前先確認邊界屬哪個套件(CLAUDE.md
外部套件異動規範)。GET /user/<uid> 與
GET /otp-qrcode/<uid> 缺自身限定,新 code
不要比照,兩者都屬已知債(§12)。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 詳述 |
|---|---|---|---|
public.users |
讀 / 寫 | 帳號本體:改基本資料 + password/salt + mfa_type | §9.3 |
public.user_change_pwd_logs |
寫 | 改密成功寫入一筆歷史(供近 3 次重複檢查用) | §9.3 |
public.totp_secrets |
讀 / 寫 | TOTP secret / URI 真正存放處(非 users.topt_secret,見
§12) |
§9.3 |
public.user_auth_providers |
讀 / 寫 | LDAP 等第三方登入綁定記錄 | §9.3 |
public.user_tenants / user_org_units /
user_roles |
讀 | 頭卡 Tag 顯示用(本頁不可編輯) | _overview §6 |
public.tenants / org_units /
roles |
讀 | 同上,唯讀顯示 | _overview §6 |
public.user_change_password_request |
— | 本頁不觸及:屬忘記密碼/強制改密(ChangePassword.vue)流程,非本頁 |
_overview §6 |
9.2 ER 圖
9.3 核心表欄位(取自 DEV DB dump,含 DB comment)
public.users(節錄本頁相關欄位)
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | varchar(36) | 用戶唯一識別碼UUID |
| login_name | varchar(255) | 登入名稱(本頁 disabled 不可改) |
| nickname | varchar(255) | 暱稱/顯示名稱 |
| salt | varchar(250) | 密碼鹽值(改密時沿用,回應排除此欄位) |
| password | varchar(250) | 加密密碼(bcrypt,回應排除此欄位) |
| varchar(100) | 電子郵件 | |
| job_title | varchar(100) | 職稱 |
| tel | varchar(50) | 電話號碼 |
| topt_secret | text | DB 有此欄位但全專案(主專案 + jedi-auth +
jedi-mfa)程式碼零引用,屬死欄;TOTP 真正的 secret 存
totp_secrets 表,見下 |
| mfa_type | varchar(50) | 多因素驗證類型(email / totp) |
public.totp_secrets — TOTP 密鑰真正存放處
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | integer | 主鍵ID |
| user_id | integer | 使用者ID(→ users.id,soft ref) |
| secret | varchar(255) | TOTP密鑰 |
| uri | text | TOTP URI(用於生成QR碼) |
public.user_change_pwd_logs — 改密歷史(供重複檢查)
| 欄位 | 型別 | 說明 |
|---|---|---|
| user_id | integer | 用戶ID |
| type | integer | 變更類型(0首次註冊/1密碼重設/2忘記密碼/3密碼過期/9用戶自行變更);本頁固定寫 9 |
| password | varchar(250) | 加密密碼(改密時的雜湊值,供近 3 次比對) |
public.user_auth_providers — 第三方登入綁定
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | varchar(36) | 唯一識別碼UUID(DELETE 端點用此值,非使用者 uid) |
| user_id | integer | 使用者ID |
| provider | varchar(50) | 第三方身份提供者名稱(本頁固定 ldap) |
| provider_user_id | varchar(500) | 第三方身份提供者的使用者ID |
public.user_change_password_request(本頁不觸及,列出供對照)
| 欄位 | 型別 | 說明 |
|---|---|---|
| type | integer | 請求類型(0首次註冊/1密碼重設/2忘記密碼/3密碼過期/9用戶自行變更) |
| status | integer | 請求狀態(0未使用/1已使用) |
| expired | timestamp | 請求過期時間 |
10. 頁面邏輯與資料對應
載入時序:
關鍵欄位對應(API ↔︎ 畫面):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 頭卡暱稱/Email/Tag | currUser.nickname/login_name/email/roles/tenants/org_units |
GET /user/<uid> 同名欄位 |
| 基本資料表單 | currUser.login_name/nickname/email/job_title/tel |
UserRequest 同名欄位 |
| LDAP 狀態 Tag | isBindLdap(computed,檢查 auth_providers
是否含 provider==='ldap') |
auth_providers[] |
| TOTP 開關 | currUser.mfa_type(trueValue="totp" /
falseValue="email") |
mfa_type |
| 密碼欄位 | currSecret.secret /
currSecret.confirm_secret(獨立 reactive,不在
currUser) |
送出時併入 payload password |
儲存流程:onSaveClick 先跑
v$(基本欄位)驗證,若密碼欄位任一有值再跑
vSecret$ 驗證;通過後組一份完整 payload(含唯讀顯示欄位如
tenants/org_units/user_roles
原樣回送)+ 條件性 password → 單一
PUT /user-profile/<uid>。成功後用 response 更新
userStore 的精簡
userInfo(uid/login_name/nickname/email),並清空密碼欄位與驗證狀態。LDAP/TOTP
secret 建立則各自獨立即時呼叫,不等頁面儲存。
錯誤對應:BE error envelope → 由呼叫端
.then/.catch 或全域攔截器轉
toast(本頁未見自訂錯誤文案覆寫,走預設 i18n
lang.error.<code>);常見碼:AUTH_401006(AUTH_UNAUTHORIZED,改到別人的
uid)、AUTH_429001(24
小時內已改過密碼)、AUTH_401009(AUTH_SECRET_SAME_AS_PREVIOUS,近
3 次重複,實際
429)、MFA_401001(MFA_VERIFY_FAILED,本頁未觸發此碼——該碼只在登入流程
/otp-verify 使用)。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 無(本頁改密不觸發 email 通知;忘記密碼/管理員改密才會寄信,屬其他頁面) |
| Job / 排程 | 無 |
| Socket | 無 |
| jedi-* 套件 | jedi-auth(UserService/UserDomainService/改密歷史
domain service,擁有
users/user_change_pwd_logs 的
service/repo)、jedi-login(UserAuthProviderService,LDAP/AD/Google
綁定 + 真實 LDAP bind
adapter)、jedi-mfa(TotpMfaService/TotpSecretDomainService,擁有
totp_secrets) |
| 系統參數 | LDAP 連線設定讀 system_configs(group
THIRD_PARTY_LOGIN, key
LDAP:server/port/encryption/base_dn/is_ad),見 _overview
§4 |
12. 邊界情況與已知坑
- MFA 切換改為「驗證後即時生效」(2026-07-09
修,取代原「未驗證即啟用會自鎖」缺陷):切到 TOTP
時強制走「建立 secret → 顯示 QR Code Dialog → 輸入一次動態碼 →
驗證通過(
POST /totp-verify,JWT 自我限定、不發 token)」才允許啟用;Dialog 未驗證關閉即還原開關並提示。切回 Email 即時生效(失敗還原開關)。啟用/停用不再依附基本資料儲存——切換驗證通過即透過PUT /user-mfa-type(只送mfa_type單欄、update只寫非 None 欄位)立即落 DB(B 案「改了就生效」),不需再按「儲存」;onSaveClick不再帶mfa_type。原本即 TOTP 者切他頁再回來免重驗。個人資料頁「帳號安全」已拆成獨立即時生效卡(含 hint-bar)。⚠️ 此行為需 BE + FE 同版部署(舊 FE 直送totp對新 BE 會 400)。 GET /user/<uid>無自身限定:僅@jwt_required(),理論上任何登入者傳他人 uid 都能取得該使用者完整資料(roles/tenants/org_units/mfa_type/auth_providers,已排除 salt/password)。寫入端PUT /user-profile/<uid>有擋(401AUTH_UNAUTHORIZED),讀端沒有——本頁只記錄不修(_overview §5 已知模式)。GET /otp-qrcode/<uid>完全無@jwt_required():本頁所有端點中權限最鬆的一個——未登入也能打,只要知道目標使用者 uid 就能拿到其 TOTP QR Code(等同拿到可掃描綁定的密鑰 URI)。屬安全缺口,收錄僅供追蹤。users.topt_secret是死欄:DB 有此欄位(text型別)但主專案、jedi-auth、jedi-mfa 全部程式碼零引用;真正的 TOTP secret 存在獨立表public.totp_secrets(secret/uri欄位)。未來若清理孤兒欄位,此欄可列入候選(先走 DROP TABLE 前的四查 SOP)。- 改密「新舊密碼相同」錯誤碼已修正(2026-07-08):原本
AUTH_SECRET_SAME_AS_PREVIOUS碼寫AUTH_401009(像 401)卻用TooManyRequestsError(429) 拋出,碼/型別/語意三重不符;已改為碼AUTH_400003(移入 400 段)+BadRequestError(400),並同步 FE 三語 error-code.json mapping 鍵。AUTH_CHANGE_SECRET_WITHIN_24_HOURS(AUTH_429001,24 小時內限改一次)維持 429 不動。⚠️ 此碼異動需 BE(jedi-auth)+ FE 同版部署。 - LDAP 綁定 payload 的
user_uid未驗自身:POST /auth-provider只檢查 JWT 存在,不比對user_uid是否為呼叫者本人;但因綁定必須通過真實 LDAP server 帳密驗證(ldap_adapter.verify_user),實務上攻擊者仍需持有目標帳號在 LDAP 端的有效密碼才能利用,風險低於前兩項讀端點缺口,仍屬待補的權限一致性債。 - 本頁常與另兩個頁面混淆:
/change-password(ChangePassword.vue,未登入態忘記密碼/強制改密,走CHANGE_PWD_REQ/CHANGE_PWD_BY_REQ)與/otp-verify(OtpVerification.vue,登入流程 MFA 驗證)都不是本頁的一部分,各自是獨立的 route,行為不要混在一起看。 TotpQrcodeRouteMFAService 型別已統一(2026-07-07 已修):原以MFAService("email")建構 TOTP 服務(僅用到 domain service、不呼叫provider,無實際功能影響),已統一為MFAService("totp"),與其餘 TOTP 端點一致。- 密碼欄位判斷用「兩者任一有值」觸發改密:
currSecret.secret || currSecret.confirm_secret為真才跑密碼驗證並帶入 payload;只填一欄未填另一欄會被vSecret$擋下(兩欄皆 required when 觸發),行為符合預期但錯誤訊息只顯示在未填欄位上,UX 上容易誤解成「另一欄有問題」。 - PUT payload
回送唯讀顯示欄位:
tenants/org_units/user_roles/effective_date/expire_date這些本頁不可編輯的欄位,儲存時仍原樣組進 payload 一起送出(onSaveClick,UserProfileForm.vue:159-179)。因走的是update_user全量欄位覆寫邏輯,若 FE 载入資料與 BE 當前狀態之間有時間差(例如另一個管理員同時改了此使用者的角色),儲存個人資料會意外覆蓋掉那些變更——多人編輯風險,本頁無 socket 提示。
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FEcompliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:任一 dev
環境登入帳號皆可測(測試自己的資料);憑證見 memory
reference_dev_login/.env(不寫入本文件)。 - 導航路徑:登入 → 右上角使用者選單(或直接網址
/user/user-profile,本頁無側邊選單節點,不受ui_routes可見性控制)。 - 前置資料:無特殊前置資料;若要測 LDAP 綁定需
system_configs(THIRD_PARTY_LOGIN/LDAPgroup)已設定且有可連線的 LDAP server。 - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以個人資料/user-profile/TOTP搜尋。 - 相關文件:權限系統設計說明書
docs/system-design/permission/;MFA/TOTP 相關套件jedi-mfa(~/Projects/Jedicogy/module/jedi-python-package/jedi-mfa/)、LDAP 綁定jedi-login(同路徑下jedi-login/)。