POST /users/change_password.json
快速結論
- 狀態:已補 action / routing / PHPUnit;2026-07-03 staging curl、DB side effect 與 log 已驗證。
- 2026-06-24 legacy 正式機 access log:
POST /users/change_password.json104 筆200。 - web-app
https://staging.pro360.com.tw/dashboard/settings點選「變更密碼」會呼叫這支 API。 - legacy JSON path 實際進
UsersController::change_password()後立刻轉呼叫change_password_from_api();不走 web/adminchange_password()後半段 validation、logout、admin email 分支。 - 成功主流程更新
users.password,response 為{"error":0,"message":"Success"};session helper 仍保留 legacy access 更新副作用。 - 這支不可直接沿用 new
Lib/Util/UserUtil::change_password(),因為該 helper 要求 8 碼、回 shared string error,且不支援 legacy 的「目前密碼空字串 + DB password 空字串」建立密碼分支。web 前端目前只要求新密碼至少 6 碼。
Legacy 對照
| 檔案 | 行號 | 行為 |
|---|---|---|
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php | 3151-3155 | change_password():JSON request 直接呼叫 change_password_from_api() 後 return |
| 同上 | 2911-2916 | change_password_from_api():需 valid API session;invalid session 回 {"error":"Invalid session"} |
| 同上 | 2918-2923 | 依 RestApiHelper::getUserId($this->request) 查 session user |
| 同上 | 2931-2934 | 讀 current_password,用 crypt(current_password, users.password) 比對;若 current password 與 DB password 都是空字串也允許 |
| 同上 | 2935-2944 | 讀 new_password,用 getCryptHash(new_password) 更新 users.password,成功回 {"error":0,"message":"Success"} |
| 同上 | 2945-2955 | 舊密碼錯誤回 {"error":2,"code":200,"message":"Incorrect current password"} |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php | 75-152 | hasValidApiSession():valid API key + session row + user row;不加 is_active guard |
| 同上 | 100-105 | blocked user 回 HTTP 403 + {"error":5,"message":"Blocked User"} 並停止流程 |
| 同上 | 108-131 | session cache miss 時可能寫 returnedQuoteService task queue,並更新 users.iphone_last_access / last_access_client |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php | 162-182 | getUserId() 會寫 full request info log,channel {url}::{method} |
New 對照
| 項目 | 狀態 |
|---|---|
Endpoint/V1/Users.php:837-1030 | 已補 change_password()、shared legacy session resolver、blocked user、legacy response shape、password verify/update、legacy auth side effect |
Lib/Common/RouterV3.php:210,266-289 | 保留 RouterV3 generic request log,但 request post data 先經 password/passwd key 過濾 |
Lib/Common/RouterRule/Mapping.php:96-99 | 已補 users/change_password.json explicit routing |
Lib/Util/UserUtil.php:108-135 | 有 shared change_password(),但不符合 legacy JSON path;不建議直接沿用 |
Lib/Util/Pro360LoginUtil.php:151-176 | 可用既有 verify_password() / getCryptHash() 對齊 legacy crypt hash |
tests/UsersChangePasswordTest.php:53-218 | 已補 success、setup password、missing current password、wrong current password、inactive user、blocked user、invalid session、RouterV3 password log filter 測試 |
前端入口
| 位置 | 行為 |
|---|---|
web-app/modules/components/dashboard/account/AccountDetail.js:129-143 | 帳號基本資料頁依 password_empty 顯示「建立密碼」或「變更密碼」 |
web-app/modules/components/dashboard/account/ChangePassword.js:145-164 | 前端表單驗證;有密碼帳號需 current password,new password 至少 6 碼,確認密碼需一致 |
web-app/modules/containers/AccountContainer.js:109-116 | 呼叫 changePassword(currentPassword, newPassword);建立密碼成功後把 noPassword 設回 false |
web-app/modules/actions/index.js:366-375 | dispatch CHANGE_PASSWORD,payload 為 API response |
web-app/modules/utils/api-manager.js:1097-1106 | 送 POST /users/change_password.json,body 為 current_password / new_password |
web-app/modules/containers/AccountContainer.js:371-381 | 前端依 error=0/1/2 顯示成功、帳戶錯誤、舊密碼錯誤 |
操作路徑:
https://staging.pro360.com.tw/dashboard/settings
-> 點選「變更密碼」
-> POST /users/change_password.jsonInput / Response
| 項目 | legacy 行為 | new 搬移方向 |
|---|---|---|
| method | POST | POST |
| path | /users/change_password.json | 同 legacy |
| auth | valid API key + valid user session;blocked user 403;不加 is_active guard | 用專用 legacy resolver,避免 new shared validator 的 active guard 改變 legacy 行為 |
| body | current_password, new_password | 不新增其他 required 欄位 |
| success | {"error":0,"message":"Success"} | 欄位與型別照 legacy |
| invalid session | {"error":"Invalid session"} | 已對齊 legacy;不丟 new common exception response |
| blocked user | HTTP 403 + {"error":5,"message":"Blocked User"} | 已對齊 legacy;blocked 時不改 password;RouterV3 generic request log 仍會先寫入且 password 已過濾 |
| missing user | 通常在 session helper 階段回 {"error":"Invalid session"};error=1 只會在 helper 已設 user id 後第二次查 user 才可能出現 | new 目前比照 session helper 查不到 user 時回 invalid session |
| wrong current password | {"error":2,"code":200,"message":"Incorrect current password"} | 同 legacy |
DB Write
主流程明確 DB write:
| table | 欄位 | 來源 / 規則 |
|---|---|---|
users | password | Pro360LoginUtil::getCryptHash(new_password),對齊 legacy getCryptHash() |
session helper legacy side effect:
| table | 欄位 / row | 來源 / 規則 |
|---|---|---|
users | iphone_last_access, last_access_client | legacy RestApiHelper::hasValidApiSession() 在 session DB load 後更新;new 保留此副作用 |
task_queue | action=returnedQuoteService | user 有 active service、quote_service_count > 0 且 iphone_last_access 超過 180 天時,對齊 legacy 寫入 |
不應更新:
| 類型 | 欄位例 |
|---|---|
| session | api_sessions 不刪、不重建 |
| user status | is_active、email/phone、social id/token 不改 |
| profile | user_profiles 不改 |
| security token | mobile_app_hash 不改 |
| counters/wallet | wallet、counter、payment 欄位不改 |
| queue/activity | 除 session helper 的 returnedQuoteService 條件式 task queue 外,change password 主流程不寫 activity / queue / event |
重要分支
有既有密碼
current_hash = crypt(current_password, users.password)
current_hash === users.password
-> 更新 users.password錯誤 current password 回 error=2。
尚未建立密碼
legacy 允許:
current_password === ''
users.password === ''
-> 更新 users.password這對應 web-app 的「建立密碼」流程。搬移時不可要求 old password 非空。
新密碼格式
legacy API path 沒有後端長度 / regex validation;前端目前只擋新密碼少於 6 碼。new 不應直接用 UserUtil::change_password() 的 8 碼 regex,否則會比 legacy 更嚴。
Config / Side Effect
legacy web/admin change_password() 後半段有 user.is_logout_after_change_password 與 admin email 分支,但 JSON path 在 UsersController.php:3151-3155 已直接轉 change_password_from_api() 並 return,不會執行那些分支。
因此本 API 搬移不需要:
- 讀
user.is_logout_after_change_password - 登出 / 刪 session
- 寫 admin changed password email
- 寫 change password event queue;task queue 只有 session helper 的
returnedQuoteService條件式副作用
Payment / Activity / Notification
| 類型 | legacy JSON path | new 搬移狀態 |
|---|---|---|
| payment / transaction | 無 | 不寫 payment、wallet、transaction |
| activity | 無 QuoteActivity / user activity | 不寫 activity |
| queue | 主流程無;session helper 可能寫 returnedQuoteService | 只保留 session helper 條件式 task queue |
| notification / email | web/admin email 分支不執行 | 不寄 email、不寫通知 queue |
| session / cookie | 成功不刪 session;invalid cookie session 會清 cookie | 成功不刪 session;invalid header session 不額外清 cookie |
Log 對照
legacy RestApiHelper::getUserId() 會寫 full request info log;此 API request body 會包含 current_password / new_password。
本次 migration decision:不在 Users::change_password() 另補 endpoint 專用 log,避免和 RouterV3 generic request log 重複;保留 RouterV3 既有 request log,但在 RouterV3 寫 log 前遞迴過濾 password 類 key。
- 保留 RouterV3 generic channel:
users/change_password.json::POST{RequestClass} - 保留 generic log context 的 API key、path、subDir、request post data 與 request id
current_password/new_password/ nestedUser.passwd寫入 log 前改成[FILTERED]- 過濾規則在
RouterV3層,key name 包含password或passwd時套用;因此也會保護其他 endpoint 的同類 request log
PHP 5.6 -> 8.2 注意
| 項目 | legacy | new 注意 |
|---|---|---|
missing current_password | PHP 5.6 undefined index notice 後值接近 null;null === '' 不成立 | new 需保留「missing 不等於空字串」;DB password 空值時,缺欄位不可通過 setup password 分支 |
missing new_password | PHP 5.6 會把 null 傳進 getCryptHash(),實際等同用空字串產生 hash | new 用 (string)$newPassword 對齊 |
| empty password setup | current_password === '' && users.password === '' 可成功 | 不可被 shared helper 的 old password required 擋掉 |
| password compare | crypt($current_password, $stored_hash) === $stored_hash | 用 Pro360LoginUtil::verify_password() 或等價 hash_equals() |
| response code field | wrong password / missing user 有 code=200 | success 沒有 code |
測試規劃
至少補 PHPUnit:
| case | 驗證 |
|---|---|
| invalid session | 已補:回 legacy invalid session |
| blocked user | 已補:回 HTTP 403 legacy body;不改 password;generic request log password 已過濾 |
| inactive user | 已補:valid session 可改密碼,不新增 legacy 沒有的 is_active = 1 guard |
| wrong current password | 已補:回 error=2, code=200, message=Incorrect current password;不改 password |
| success existing password | 已補:current password 正確時更新 users.password,新密碼可登入/verify;不動 session/profile/mobile_app_hash |
| setup password | 已補:users.password='' 且 current_password='' 時可建立密碼 |
missing current_password | 已補:DB password 空值時缺欄位不等於空字串,回 error=2 |
| response shape | 已補:success 無 code,錯誤 current password 有 code=200 |
| request log | 已補:只保留 RouterV3 generic request log,並確認 password/passwd key 會變成 [FILTERED] |
測試結果:
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/UsersChangePasswordTest.php
OK (7 tests, 61 assertions)
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/UsersDeleteTest.php
OK (3 tests, 99 assertions)
docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests/UsersLoginTest.php
OK (9 tests, 210 assertions)Staging 實測
2026-07-03,新版 staging api-staging.pro360.com.tw:
| 項目 | 結果 |
|---|---|
| curl response | {"error":0,"message":"Success"} |
| session | request session 對到 user_id=8616 |
users.password | 已更新為 bcrypt $2y$12$ hash;legacy / new 都用 crypt(input, stored_hash) 驗證,可互用;更新後密碼可驗證,更新前密碼驗證失敗 |
users.modified | 未更新;符合 legacy User->updateAll() 只改 User.password |
| session side effect | users.iphone_last_access 更新到本次 request 時間 |
| 不應新增 side effect | 未新增 user_logins、quote_activities、event_queue、task_event_queue、queue log |
| request log | monolog_202607.channel = users/change_password.json::POST\RequestHttp;current_password / new_password 皆為 [FILTERED] |
| duplicate log | 本次時間後無 exact channel users/change_password.json::POST |
| error log | monolog_error 無本次相關錯誤 |
待確認
- 因已依 reviewer 要求在 RouterV3 過濾 password/passwd,這點與 legacy raw request log 不同,屬明確 migration decision。