POST /users/forgot_password.json

快速結論

  • 狀態:已補 action / PHPUnit / staging diff。
  • 2026-06-24 查 legacy 正式機 access log,POST /users/forgot_password.json 有 3900 筆 200,另有 1226 筆 OPTIONS 200
  • legacy JSON path 會進 forgot_password_from_api(),不是畫面版 forgot_password() 的 validation / captcha 流程。
  • 成功時會補 users.mobile_app_hash(僅空值時)、寫入 event_queue,並回 {"error":0,"message":"Reset password email sent"}
  • 找不到 email 時不寫 DB / queue,回 {"error":1,"message":"Invalid email address"}
  • new 專案已補 Users::forgot_password(),用 EmailSrc::appendForgotPasswordDynamicUrl()EventQueue::insertEvent(),並用 legacy-shaped event payload,避免 new DO 多帶 legacy 沒有的 null 欄位。
  • migration decision:本次經 reviewer 確認,legacy 有 forgot_password_from_api endpoint 專用 request log,new 不再補這份 duplicate full request log;保留 RouterV3 generic request log。

Access Log

環境:

項目
hostmei-te@ip-10-5-2-242
log/var/log/apache2/*access.log*
查詢日期2026-06-24

本 API 命中:

MethodPathStatus筆數
POST/users/forgot_password.json2003900
OPTIONS/users/forgot_password.json2001226

Legacy 對照

檔案行號行為
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php2715-2719JSON request 轉進 forgot_password_from_api()
同上2645-2658email、寫 application log、用 User.email = $provided_email 查 user,contain => ['UserProfile']
同上2660-2668user 存在且 mobile_app_hash 空值時,產生 hash 並 updateAll()
同上2670-2683組 email replace content 與 dynamic reset URL
同上2690-2701建立 email task,templateKey = 420 User Change Password,寫 EventQueue::insertEvent('User_Change_Password', ...)
同上2703-2711成功 / 失敗 JSON response
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/AppController.php544-552_unum() 產生 8 位數字亂碼,參與 mobile_app_hash
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/UserProfile.php197-208getFullName();沒有姓名時 fallback 到 username,再 fallback User
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php1977-1989getMultiLangId();查不到時回 default lang

legacy hash 來源:

md5($this->_unum() . $user['User'][Configure::read('user.using_to_login')] . $user['User']['password'] . Configure::read('Security.salt'))

legacy config 來源:

keylegacy 來源
user.using_to_login/Users/mattsu/Documents/Site/get-lancer-php56/app/Config/settings.yml:332username
Security.salt/Users/mattsu/Documents/Site/get-lancer-php56/app/Config/core.phpc:191migration 實作使用 legacy salt 產生 mobile_app_hash

Input / Response

項目legacy 行為
methodPOST
auth全域 API key;不需要 user session
inputform data email
user lookupusers.email = trim(email);沒有 is_active / is_deleted / role filter
success response{"error":0,"message":"Reset password email sent"}
not found response{"error":1,"message":"Invalid email address"}
missing emaillegacy PHP 5.6 會 notice 後等同空字串 lookup;實作時需避免 PHP 8.2 trim(null) warning,不可自行加 required validation

Side Effects

類型legacy 行為
user update只在 mobile_app_hash 空值時更新;已有 hash 時重用舊值
event queueevent_queue.event_key = User_Change_Password
email template420 User Change Password
email to使用 request 送入的 trimmed email,不是重新取 user row email 後格式化
replace content##USER_NAME####DISPLAY_URL####RESET_PASSWORD_URL####REQUESTS_URL##、空白 action placeholders
languageUser::getMultiLangId(user_id)
logginglegacy 有 application log channel forgot_password_from_api,message 含 email 與 request data;本次 migration decision:經 reviewer 確認不補 endpoint duplicate log,只保留 RouterV3 generic request log

New 對照

項目狀態
Endpoint/V1/Users.php:770-834forgot_password() action;API key、email lookup、hash update、event queue、response
Endpoint/V1/Users.php:1293-1313findForgotPasswordUser()LEFT JOIN user_profiles,只用 users.email = ?
Endpoint/V1/Users.php:1315-1323buildLegacyMobileAppHash();沿用 legacy hash 組成與 username
Endpoint/V1/Users.php:1325-1333legacyUnum();8 位數字亂碼
Endpoint/V1/Users.php:1335-1351getForgotPasswordUserName()last_name first_name,再 fallback username / User
Lib/Common/RouterV3.php:210,266-289generic request log;password/passwd key 過濾由 router 層統一處理
routingOrigin routing 可依 method name 分派;不需新增 route mapping
Lib/Util/EmailSrc.php::appendForgotPasswordDynamicUrl()已存在,補 ?pk=<user_id>&path=forgot_password&hash=<hash>;現行 DynamicLinkUtil 不短網址時回原址
Lib/Model/EventQueue.php::insertEvent()已存在,寫 event_queue
event payload使用局部 legacy-shaped object;只含 user_idquote_service_idtasks,task 不含 new shopList
Lib/Model/User.php::getMultiLangId()已存在
tests/UsersForgotPasswordTest.php覆蓋空 hash、既有 hash、unknown email

Staging / Local Diff

2026-06-25 用 luckynaming@gmail.com 驗證:

項目結果
legacy / staging response{"error":0,"message":"Reset password email sent"}
new local response{"error":0,"message":"Reset password email sent"}
user rowusers.id = 8617mobile_app_hash = 633c9e45829df5bb9f975b7481fb84fd;已有 hash 時不重生
event rowworker 已搬到 event_queue_log_3
event payload keysuser_id, quote_service_id, tasks
task keystemplateType, templateKey, to, from, replaceContent, notifyOptions, withPhone, formSummary, requestList, unsubscribeUrl, langId

注意:new local 使用本地 site.url = http://localhost:12347,所以 reset / request URL host 與 staging 的 https://staging.pro360.com.tw 不同;payload shape 與欄位語意一致。

實作注意

  • 不可加入 legacy 沒有的 guard,例如 is_active = 1、email blacklist、rate limit、captcha、session required。
  • 不可改用 security_hashes 取代 users.mobile_app_hash,除非先標 migration decision;legacy reset_password path 是用 mobile_app_hash
  • mobile_app_hash 已存在時不可重新產生。
  • PHP 8.2 實作要避免 trim(null) warning;missing / non-string email 等同空字串 lookup,不自行加 required validation。
  • event payload 要維持 legacy shape;不可因 new EventTaskDO / EmailTaskDO 有額外 public property 就讓 quote_request_idquote_bid_idshopList 出現在這支 API。

測試結果

2026-06-25 Docker PHP 8.2:

php -l Endpoint/V1/Users.php
php -l tests/UsersForgotPasswordTest.php
./vendor/bin/phpunit tests/UsersForgotPasswordTest.php

結果:

OK (3 tests, 39 assertions)

覆蓋:

  • existing email 且 mobile_app_hash 空值:response success、補 hash、寫 User_Change_Password queue。
  • negative assertion:fixture user 暫設 is_active = 0 時仍成功,避免未來加上 legacy 沒有的 active guard。
  • negative assertion:queue payload 不含 legacy 沒有的 quote_request_idquote_bid_idshopList
  • existing email 且已有 mobile_app_hash:response success、hash 不變、仍寫 queue。
  • unknown email:response invalid,不寫 queue。