手冊首頁 / 租戶與組織 / 個人資料

個人資料(User Profile)

功能群:系統管理|三層 RBAC 模型與共用權限模型先讀 功能群總覽 §2、§5

事實基準:2026-07-05 從 FE src/views/user-manage/UserProfileForm.vue + BE api/auth/routes/user_route.py / api/auth/routes/otp_route.py / api/user_auth_provider/routes/user_auth_provider_route.py + jedi-auth user_service.py / jedi-mfa totp_service.py + routes.json / db_schema.json 掃出

變更紀錄:2026-07-09 FR-047 已知坑修復 —— MFA 切換改「驗證後即時生效」(新增 POST /totp-verify + PUT /user-mfa-type、帳號安全拆卡、B 案不再依附基本資料儲存);改密「新舊密碼相同」錯誤碼 AUTH_401009AUTH_400003TotpQrcodeRoute MFAService 型別統一為 "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_typePUT /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)
profile_changepwd start 進入個人資料頁 GET /user/<自己 uid> fill 填「變更密碼」區塊 新密碼+確認密碼(皆選填,不填則不改密) start->fill fe_val FE 驗證:≥12 碼、需含大小寫字母與數字、 兩欄需相同 fill->fe_val fe_err 表單顯示錯誤,聚焦該欄位 fe_val->fe_err 不通過 submit 按「儲存」 PUT /user-profile/<uid> body 含 password(明碼)+ 其他基本欄位 fe_val->submit 通過(或未填,跳過改密) be_uid BE 檢查:路徑 uid === 目前登入者 uid? submit->be_uid err401u 401 AUTH_401006 AUTH_UNAUTHORIZED be_uid->err401u be_self jedi-auth can_change_password(curr_uid, uid) (非 admin 路徑,再次確認同一人) be_uid->be_self err401s 401 AUTH_401006(理論分支, 路由層已擋,實務不會走到) be_self->err401s 否(理論上不可能) cool 24 小時內已自行改過密碼? be_self->cool err429a 429 AUTH_429001 AUTH_CHANGE_SECRET_WITHIN_24_HOURS cool->err429a reuse 新密碼與近 3 次密碼雜湊比對命中? cool->reuse err401r 401 AUTH_401009 AUTH_SECRET_SAME_AS_PREVIOUS (訊息語意 401,實際 raise 429,見 §12) reuse->err401r hash bcrypt 用既有 salt 雜湊新密碼 users.password 更新 reuse->hash log 寫入 user_change_pwd_logs (type=USER_CHANGE_SELF) hash->log ok 200 成功 → toast → 清空密碼欄位 log->ok
UC-PROFILE-01 流程:FE 規則驗證→BE 自身 uid 檢查→24 小時冷卻→近 3 次密碼重複檢查→bcrypt 雜湊寫入+寫歷史

UC-PROFILE-02 綁定 TOTP 兩階段驗證

項目 內容
角色 使用者本人
前置條件 已登入;目前 mfa_type 非 totp(開關由 email 切到 totp)
產出 / 後置條件 totp_secrets 建立(或沿用既有)一筆 secret;儲存後 users.mfa_type 更新為 totp
profile_mfa start 帳號安全區塊:TOTP 開關(InputSwitch) 目前 mfa_type = email toggle 使用者切換開關 → true (currUser.mfa_type = 'totp') start->toggle reg POST /totp-secret (僅 JWT,操作對象=目前登入者, jedi-mfa 若已存在 secret 則 idempotent 回傳既有) toggle->reg qrbtn 顯示「顯示 QR Code」按鈕 使用者點擊開啟對話框 reg->qrbtn qrload Dialog 開啟 GET /otp-qrcode/<uid> ⚠️ 無 @jwt_required、無 uid 自身檢查(見 §12) qrbtn->qrload qrshow 顯示 QR Code 圖片 (Authenticator App 掃描) qrload->qrshow save 按頁面「儲存」 PUT /user-profile/<uid> body.mfa_type = 'totp' qrshow->save saved users.mfa_type 更新為 totp save->saved notevf ⚠️ 本頁流程未強制「先驗證 OTP 碼再啟用」 (OTP 驗證頁 /otp-verify 是登入流程專用, 非本頁綁定流程的一部分,見 §12) saved->notevf login 下次登入:LoginService 判斷 mfa_type=totp 導向 /otp-verify 輸入 Authenticator 動態碼 notevf->login
UC-PROFILE-02 流程:開關切換→建立/取得 TOTP secret→顯示 QR Code 供掃描→按頁面儲存才真正生效 mfa_type

本頁 TOTP 綁定流程沒有「輸入 Authenticator 動態碼驗證成功才算啟用」的步驟——按開關即建立 secret、按「顯示 QR Code」即可看到密鑰,儲存表單即把 mfa_type 改成 totp 生效,過程不驗證使用者是否真的掃描成功。OTP 驗證頁(/otp-verifyOtpVerification.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-73UserRoute.get
編輯基本資料/改密/改 mfa_type 單一表單,無按鈕層級判定 PUT /user-profile/<uid>路徑 uid 必須等於當前登入者,否則 401 AUTH_UNAUTHORIZED user_route.py:141-156UserProfileRoute.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-44TotpSecretRoute.post
TOTP QR Code(GET /otp-qrcode/<uid> 開關為 totp 才顯示按鈕 ⚠️ 完全無 @jwt_required(),任何人知道 uid 即可取得該使用者的 QR Code 圖(等同洩漏可掃描綁定的 TOTP 密鑰) otp_route.py:46-61TotpQrcodeRoute.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-74secretRules
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 綁定對話框。

版面骨架(單一表單頁,三區塊縱向排列):

Profile Header:頭像/暱稱/登入名稱/Email/角色租戶部門 Tag(唯讀) GET /api/1.0/user/<uid>(載入自身資料) 基本資料:登入名稱(disabled)/姓名*/Email*/職稱/電話 單一表單,隨底部「儲存」一併送出 開啟:§6 API 規格 帳號安全:LDAP 綁定卡片 + TOTP 開關卡片(Tag 顯示 enable/disable) POST/DELETE /auth-provider(LDAP 綁定/解除,即時打 LDAP server 驗證) POST /totp-secret・GET /otp-qrcode/<uid>(⚠ 無 @jwt_required) → §6 API 規格(點我開啟) 開啟:UC-PROFILE-02 綁定 TOTP 兩階段驗證 TOTP 開關 → 建立 secret → 顯示 QR Code Dialog POST /totp-secret(idempotent) GET /otp-qrcode/<uid>(PNG) ⚠ 無驗證動態碼關卡;按儲存即生效 mfa_type → UC-PROFILE-02(點我開啟) 開啟:UC-PROFILE-01 變更密碼 變更密碼:新密碼+確認密碼(皆選填,兩欄皆填才觸發驗證) PUT /user-profile/<uid>(password 有值才觸發改密) 24hr 冷卻+近 3 次不可重複;≥12 碼含大小寫數字 路徑 uid≠登入者 → 401 AUTH_UNAUTHORIZED → UC-PROFILE-01(點我開啟) 底部:取消(confirm 後 router.back)/儲存(單一 PUT 送出全部欄位) 彩色=主要互動/明細區塊,整塊可點擊跳轉本頁 §6 或對應 UC|灰=本頁結構|虛線框=dialog/條件顯示層 API 僅列代表性 2~3 條,完整規格見 §6;本頁僅能操作自己(GET /user、GET /otp-qrcode 缺自身限定,見 §12)
個人資料頁版面:Header 唯讀資訊卡 + 基本資料 + 帳號安全(可跳 §6)+ TOTP QR Code Dialog(可跳 UC-PROFILE-02)+ 變更密碼(可跳 UC-PROFILE-01)+ 底部取消/儲存

狀態呈現:LDAP/TOTP 卡片各自用 Tag(綠 enable / 灰 disable)標示目前狀態;全頁載入中與送出中共用同一個全螢幕遮罩 ProgressSpinnerpageLoading || 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(節錄本頁使用欄位;完整欄位見 UserResponseapi/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 / passwordUserResponse(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 姓名
email string Email
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-156UserProfileRoute.put)→ jedi-auth UserService.update_useruser_service.py:216-338)。

改密副作用鏈(僅當 password 有值):can_change_password(curr_uid, uid) 二次確認自身 → verify_user_change_password_log(24hr 冷卻 + 近 3 次重複檢查)→ bcrypt 用既有 salt 雜湊 → 寫 user_change_pwd_logstype=USER_CHANGE_SELF)。出處:user_service.py:268-336

6.4 LDAP 綁定 / 解除

[POST] /auth-providerUserAuthProviderRegisterRequestapi/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_providerregister_ldap_useruser_auth_provider_service.py:104-129,內部呼叫 ldap_adapter.verify_user 真實驗證帳密後才寫入)。

[DELETE] /auth-provider/{uid}uiduser_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-44TotpSecretRoute.post)→ jedi-mfa TotpMfaService.registerTotpSecretDomainService.set_totp_secret(既有 secret 則 idempotent 回傳原值,不重建)。

[GET] /otp-qrcode/{uid}:無 body。Response:image/pngsend_file,非 JSON envelope)。FE 直接把此 URL 當 <img src>。出處:otp_route.py:46-61TotpQrcodeRoute.get)→ TotpSecretDomainService.get_totp_secret_qrcode_img(讀 totp_secrets.uri 產生 QR 圖)。

程式碼細節:TotpQrcodeRoute.get 內用 MFAService("email").provider 建構 TotpMfaServiceotp_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_useris_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_secrettotp_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 圖

profile_er users public.users (login_name/nickname/email/job_title/tel/ password+salt/mfa_type/topt_secret 已死欄未用) pwdlog public.user_change_pwd_logs (改密後寫入一筆,type=USER_CHANGE_SELF) users->pwdlog 改密成功寫入歷史 totp public.totp_secrets (secret / uri,TOTP 真正存放處) users->totp user_id 關聯 (POST /totp-secret 建立/取得) authprov public.user_auth_providers (provider=ldap,第三方登入綁定) users->authprov user_id 關聯 (LDAP 綁定/解除) pwdreq public.user_change_password_request (讀不到,本頁不用; 屬 forget-password / 強制改密流程) users->pwdreq 同表但本頁不觸及 tenants public.tenants / org_units / roles (僅唯讀顯示 Tag,不可在本頁編輯) users->tenants user_tenants/user_org_units/user_roles 唯讀顯示
個人資料頁讀寫範圍:users 為核心,改密寫 user_change_pwd_logs,TOTP 真相在 totp_secrets(非 users.topt_secret 死欄),LDAP 綁定寫 user_auth_providers;虛線=本頁不觸及或唯讀

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,回應排除此欄位)
email 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. 頁面邏輯與資料對應

載入時序

profile_load_seq mount UserProfileForm onMounted uid = userStore.userInfo.uid(登入時存的自己) get GET /user/<uid> ⚠️ 端點本身無自身限定, 靠 FE 只會帶自己的 uid mount->get fill 回填 currUser: 基本欄位 + roles/tenants/org_units(顯示用) + mfa_type + auth_providers get->fill ready 表單就緒 fill->ready
頁面載入時序:onMounted 直接 GET /user/ 回填整份 currUser

關鍵欄位對應(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_typetrueValue="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 的精簡 userInfouid/login_name/nickname/email),並清空密碼欄位與驗證狀態。LDAP/TOTP secret 建立則各自獨立即時呼叫,不等頁面儲存。

錯誤對應:BE error envelope → 由呼叫端 .then/.catch 或全域攔截器轉 toast(本頁未見自訂錯誤文案覆寫,走預設 i18n lang.error.<code>);常見碼:AUTH_401006AUTH_UNAUTHORIZED,改到別人的 uid)、AUTH_429001(24 小時內已改過密碼)、AUTH_401009AUTH_SECRET_SAME_AS_PREVIOUS,近 3 次重複,實際 429)、MFA_401001MFA_VERIFY_FAILED,本頁未觸發此碼——該碼只在登入流程 /otp-verify 使用)。

11. 背景行為與外部依賴

類型 內容
通知 無(本頁改密不觸發 email 通知;忘記密碼/管理員改密才會寄信,屬其他頁面)
Job / 排程
Socket
jedi-* 套件 jedi-authUserService/UserDomainService/改密歷史 domain service,擁有 users/user_change_pwd_logs 的 service/repo)、jedi-loginUserAuthProviderService,LDAP/AD/Google 綁定 + 真實 LDAP bind adapter)、jedi-mfaTotpMfaService/TotpSecretDomainService,擁有 totp_secrets
系統參數 LDAP 連線設定讀 system_configs(group THIRD_PARTY_LOGIN, key LDAP:server/port/encryption/base_dn/is_ad),見 _overview §4

12. 邊界情況與已知坑

  1. 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)。
  2. GET /user/<uid> 無自身限定:僅 @jwt_required(),理論上任何登入者傳他人 uid 都能取得該使用者完整資料(roles/tenants/org_units/mfa_type/auth_providers,已排除 salt/password)。寫入端 PUT /user-profile/<uid> 有擋(401 AUTH_UNAUTHORIZED),讀端沒有——本頁只記錄不修(_overview §5 已知模式)。
  3. GET /otp-qrcode/<uid> 完全無 @jwt_required():本頁所有端點中權限最鬆的一個——未登入也能打,只要知道目標使用者 uid 就能拿到其 TOTP QR Code(等同拿到可掃描綁定的密鑰 URI)。屬安全缺口,收錄僅供追蹤。
  4. users.topt_secret 是死欄:DB 有此欄位(text 型別)但主專案、jedi-auth、jedi-mfa 全部程式碼零引用;真正的 TOTP secret 存在獨立表 public.totp_secretssecret/uri 欄位)。未來若清理孤兒欄位,此欄可列入候選(先走 DROP TABLE 前的四查 SOP)。
  5. 改密「新舊密碼相同」錯誤碼已修正(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_HOURSAUTH_429001,24 小時內限改一次)維持 429 不動。⚠️ 此碼異動需 BE(jedi-auth)+ FE 同版部署。
  6. LDAP 綁定 payload 的 user_uid 未驗自身POST /auth-provider 只檢查 JWT 存在,不比對 user_uid 是否為呼叫者本人;但因綁定必須通過真實 LDAP server 帳密驗證(ldap_adapter.verify_user),實務上攻擊者仍需持有目標帳號在 LDAP 端的有效密碼才能利用,風險低於前兩項讀端點缺口,仍屬待補的權限一致性債。
  7. 本頁常與另兩個頁面混淆/change-passwordChangePassword.vue,未登入態忘記密碼/強制改密,走 CHANGE_PWD_REQ/CHANGE_PWD_BY_REQ)與 /otp-verifyOtpVerification.vue,登入流程 MFA 驗證)都不是本頁的一部分,各自是獨立的 route,行為不要混在一起看。
  8. TotpQrcodeRoute MFAService 型別已統一(2026-07-07 已修):原以 MFAService("email") 建構 TOTP 服務(僅用到 domain service、不呼叫 provider,無實際功能影響),已統一為 MFAService("totp"),與其餘 TOTP 端點一致。
  9. 密碼欄位判斷用「兩者任一有值」觸發改密currSecret.secret || currSecret.confirm_secret 為真才跑密碼驗證並帶入 payload;只填一欄未填另一欄會被 vSecret$ 擋下(兩欄皆 required when 觸發),行為符合預期但錯誤訊息只顯示在未填欄位上,UX 上容易誤解成「另一欄有問題」。
  10. PUT payload 回送唯讀顯示欄位tenants/org_units/user_roles/effective_date/expire_date 這些本頁不可編輯的欄位,儲存時仍原樣組進 payload 一起送出(onSaveClickUserProfileForm.vue:159-179)。因走的是 update_user 全量欄位覆寫邏輯,若 FE 载入資料與 BE 當前狀態之間有時間差(例如另一個管理員同時改了此使用者的角色),儲存個人資料會意外覆蓋掉那些變更——多人編輯風險,本頁無 socket 提示。

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE compliance-manager-fe/ 起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。
  • 測試帳號:任一 dev 環境登入帳號皆可測(測試自己的資料);憑證見 memory reference_dev_login / .env(不寫入本文件)。
  • 導航路徑:登入 → 右上角使用者選單(或直接網址 /user/user-profile,本頁無側邊選單節點,不受 ui_routes 可見性控制)。
  • 前置資料:無特殊前置資料;若要測 LDAP 綁定需 system_configsTHIRD_PARTY_LOGIN/LDAP group)已設定且有可連線的 LDAP server。
  • E2Ecompliance-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/)。