POST /users/login.json

快速結論

  • 狀態:已補 legacy JSON path parity、response allowlist、cookie / last-login side effect 與 PHPUnit;staging 部署後 full diff 待補。
  • 正式機 access log 統計見 API 路由與遷移/API Migration/pro360_api_82/api_migration/users.mdPOST /users/login.json 29407 筆 200OPTIONS 14095 筆。
  • legacy JSON path 不是頁面版 login 流程;controller 只 dispatch Controller.User.validate_user,實際邏輯在 MobileAppEventHandler::validate_user()
  • 2026-06-29 已補 Google sub lookup、Facebook Limited login、Apple transfer fallback、UserProfile.user_id response、email OTP consume id、login_logsuser_login_infos、device-id 無密碼失敗分支。
  • 2026-06-30 已補 success response User legacy allowlist、CakeCookie[user_language] / il=1 / pro360_u_s_token cookie、users.mobile_app_time_modified legacy 12 小時制、users.last_login_ip_id / last_logged_in_timeuser_logins.device_id = NULL for device_id="undefined"
  • 2026-07-01 staging Google login 確認 platform=ios 需同步 legacy google.oauth_client_id_ios = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com;new 若仍用舊值 847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.com,會因 token aud=cvvd...Pro360LoginUtil client id mismatch error。
  • 2026-07-21 migration decision:一般 login 成功時不再重用同 user_id + device_id token;舊 row 先逐欄寫入 api_session_logsaction=login_rotate)並刪除,再於同一 transaction 建立新 token。其他 device 與其他 ApiSession::upsert() caller 不受影響。

Legacy 對照

檔案行號行為
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php2025-2032JSON request logout session 後 dispatch Controller.User.validate_user
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/MobileApp/Event/MobileAppEventHandler.php3293-3456password / Facebook / Google / Apple / email OTP / phone OTP login lookup
同上3353-3359Google login 用 token subusers.google_user_id
同上3367-3373Facebook Limited login 用 token subusers.facebook_user_id
同上3374-3392Apple login 先查 apple_user_id = sub,找不到再用 apple_user_transfers.old_sub -> new_sub
同上3415-3421email OTP 成功後 consume email_verifications.id
同上3424-3430username 無密碼時不允許 device-id 直接登入,固定走失敗分支
同上3464-3488、3516-3529、3567-3581成功 / 失敗寫 login_logs;成功且有 seon_session / passwduser_login_infos
同上3490-3559成功後寫 session、user_logins、response User / UserProfile
同上3498-3504更新 users.mobile_app_hash / mobile_app_time_modified;legacy mobile_app_time_modified 使用 date('Y-m-d h:i:s') 12 小時制
同上3513-3514api_sessionsuser_logins;legacy UserLogin::insertUserLogin()device_id="undefined"NULL
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/ApiSession.php44-79upsert() 找到同 user_id + device_id row 時重用原 id,只更新 IP、expires、api key;不寫 api_session_logs
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/UserLogin.php23-31、35-51、53-62user_logins 欄位、device_id="undefined"NULL、有 UserProfile.Language.iso2 時設定 CakeCookie[user_language]、設定 il=1 cookie;afterSave() 更新 users.last_login_ip_id / last_logged_in_time
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php1223-1229login response User legacy brief allowlist
同上2061-2087成功後設定 pro360_u_s_token cookie

New 對照

檔案行號行為
Endpoint/V1/Users.php51-58、741-779Users::login() action、response mapping;User 使用本 API legacy allowlist,不直接使用 shared User::$SELF_FIELDS_BRIEF
Endpoint/V1/Users.php765-768UserProfileuser_id 查詢,對齊 legacy contain
Lib/Util/Pro360LoginUtil.php49-108Google id_token 驗證;client id mismatch log 會帶 platformexpected_config_keyexpected_client_idactual_aud
Lib/Util/Pro360LoginUtil.php181-385login 驗證主流程,依 legacy 順序解析 social token 再找 user
Lib/Util/Pro360LoginUtil.php225-263、400-405Google / Facebook / Facebook Limited / Apple lookup;Apple 含 transfer fallback
Lib/Util/Pro360LoginUtil.php269-276email OTP consume EmailVerification::getByVerifyCode() 回傳的 flat row id
Lib/Util/Pro360LoginUtil.php334-350device-id 無密碼與密碼錯誤維持 legacy failure response / log
Lib/Util/Pro360LoginUtil.php355-382、407-533成功後呼叫 login 專用 session rotation,再寫 user_loginslogin_logsuser_login_infosdevice_id="undefined" 只在 user_loginsNULL,cookie / response 使用新 token
Lib/Model/ApiSession.php20、120-211ACTION_LOGIN_ROTATErotateForLogin();鎖定同 user/device、archive、delete、insert new session 共用同一 transaction
Lib/DataObject/ApiSessionLogsDO.php13-56對齊實際 schema,包含 session_iddevice_idapi_key 欄位 mapping
Lib/Model/LoginLog.php12-28login_logs,payload 與 legacy model addLog() 對齊
Lib/Model/UserLoginInfo.php24-34user_login_infos,對齊 legacy addInfo()

差異清單

項目LegacyNew 目前判斷
Google user lookupgoogle_user_id = token.sub已補 sub lookup已對齊
Facebook Limited loginfacebook_user_id = token.sub已補 authentication_token path已對齊
UserProfile responsecontain by user_profiles.user_id已改查 user_profiles.user_id已對齊
email OTP consumeconsume EmailVerification.id已改用 flat row id已對齊
login_logs成功 / 失敗都寫已補成功 / no user / empty password / wrong password log已對齊
user_login_infosseon_session / passwd 時寫已補 INFO_TYPE_SEON 與 HMAC 後的 INFO_TYPE_PASS已對齊
Apple transfer fallbackold_sub -> new_sub fallback已補已對齊
無密碼 + device idlegacy 實際固定不通過已移除成功分支,回 error=3 / checkauth=YES已對齊
success response User 欄位User::$SELF_FIELDS_BRIEF legacy 清單,不含 apple_user_id / is_apple_connected / multi_lang_idUsers::LEGACY_LOGIN_USER_FIELDS_BRIEF 專用 allowlist已對齊
language cookieuser_profiles.language_id -> languages.iso2 有值,UserLogin::insertUserLogin()CakeCookie[user_language]已補 Pro360LoginUtil::sendLegacyUserLanguageCookie(),無 language 時不送已對齊
login indicator cookieUserLogin::insertUserLogin()il=1、1 小時Pro360LoginUtil::sendLegacyLoginIndicatorCookie() 設同名 cookie已對齊
session cookieUser::setCookieLogin($session_id)pro360_u_s_tokenPro360LoginUtil::sendLegacySessionCookie() 設同名 cookie已對齊
mobile_app_time_modifieddate('Y-m-d h:i:s'),12 小時制已補本 API success path 同格式已對齊
last login 欄位UserLogin::afterSave() 更新 last_login_ip_id / last_logged_in_timePro360LoginUtil::updateLegacyLastLogin() 僅本 API success path 更新已對齊
device_id="undefined"api_sessions.device_id 保留字串;user_logins.device_idNULLapi_sessions 保留字串;user_logins 呼叫點 normalize 為 NULL已對齊
同 device 重登 token重用同 user_id + device_idapi_sessions.id舊 row 寫 api_session_logs 後刪除,建立新 idmigration decision;2026-07-21 reviewer 明確接受

Session rotation migration decision

範圍與順序

只套用 POST /users/login.json 成功路徑,且只處理同一 user_id + device_id row。失敗登入不讀寫 session log;其他 device 保留;login_oncesign_upsearch_add、Fetnet、55688 與其他直接呼叫 ApiSession::upsert() 的流程維持既有行為。

驗證 login credential 成功
-> SELECT api_sessions WHERE user_id/device_id FOR UPDATE
-> 有舊 row 時 INSERT api_session_logs(action=login_rotate)
-> DELETE 該舊 row
-> INSERT 新 api_sessions row
-> COMMIT
-> response / pro360_u_s_token cookie 使用新 session_id

archive、delete、new insert 任一步失敗會 rollback,不回傳一個沒有落 DB 的新 token。api_session application log 沿用原 channel;一般首次登入 context status 為 new,有輪替時為 login_rotate

api_session_logs insert 欄位分類

欄位來源說明
createdapplication current datetimearchive 當下時間
user_idapi_sessions.user_id同已驗證登入 user
paramsjson_encode($existingSession)完整舊 session snapshot,包含 data / expires;不放 login request、password、social token 或 id_token
session_idapi_sessions.id被撤銷 token
device_idapi_sessions.device_id僅撤銷相同 device
api_keyapi_sessions.api_key可為 NULL
actionconstant login_rotate表示成功重登造成 token rotation
admin_user_idconstant 0非 admin 操作

api_sessions insert 欄位分類

欄位來源
idPHP web session regenerate;CLI/test 使用 random_bytes()
user_id已驗證 login user id
device_idrequest device_id;未傳時沿用 legacy my-device-id
dataHTTP_X_FORWARDED_FOR,fallback REMOTE_ADDR / 127.0.0.1
expirescurrent epoch + 86400
api_keyHTTP_X_PRO360_REST_API_KEY,未提供時空字串

Schema / deployment 狀態

  • 2026-07-21 於本地目前連線的 database-staging-84-testSHOW COLUMNS / SHOW CREATE TABLE 確認 api_session_logs 已有 session_iddevice_idapi_keyactionadmin_user_id
  • reviewer 決定 log 的 session_id / device_id / api_key 維持 varchar(190);來源欄位雖為 varchar(255),目前 api_sessions 5202 rows 的實際最大長度分別為 49 / 85 / 32,沒有超過 190 的資料。
  • staging / production 實際 schema 尚待部署前確認。若缺 table 或三個新增 mapping 欄位,同 device 第二次 login archive 會失敗並 rollback。
  • 本次沒有新增 config key,Configuration.php 不需修改;既有 cookie / social login config checklist 仍照下節處理。

Config 對照

要補哪個

這次 /users/login.json migration 的 cookie domain 在 new 專案讀 api.url。legacy User::setCookieLogin()site.api_url,但 new Pro360LoginUtil::buildLegacySessionCookieHeader() 已改用既有 api.url,所以部署時要確認 api.url 對應 API host。其他 key 是「確認既有設定」,不是這次 cookie parity 的必要新增。

動作key何時需要目前本地值要怎麼處理
必確認api.url每次 login success 都要用來組 pro360_u_s_token cookie domainhttps://api-staging.pro360.com.twstaging / production deployment config 都要確認。staging 應為 https://api-staging.pro360.com.tw;production 要填 production API host。
缺才補user_login_seon_pass_hashrequest 有 passwd 時會寫 user_login_infos.INFO_TYPE_PASS HMACNULL若 staging / production 也缺,需補 legacy 同值;否則 HMAC 會用空字串,雖不影響登入成功,但 DB side effect 不對齊。
只確認google.oauth_client_id*只有 Google id_token login 會用本地多數為 NULL這是既有 Google / sign_up 共用設定。若要測 Google login,staging / production 必須已有對應 platform client id;password login 不需要。
只確認Apple sign-in config只有 Apple code login 會用本地已有 apple_sign_in_pro / apple_sign_in_consumer / apple_sign_in_client_id_web這是既有 Apple 共用設定。若要測 Apple login,確認 staging / production 有同組 key;password login 不需要。

2026-07-01 Google client id 實測

  • legacy staging /var/www/html/app/Config/settings.yml 實際值:google.oauth_client_id_ios = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com
  • new PHP82 staging Lib/Common/Configuration.php 當時值:google.oauth_client_id_ios = 847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.com
  • new 實測錯誤:monolog_error.id = 121763channel = Pro360LoginUtilmessage = Failed to verify google id_token:false client_id
  • monolog_error.context 新增診斷欄位後可直接看到:platform = iosexpected_config_key = google.oauth_client_id_iosexpected_client_id = 847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.comactual_aud = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com
  • migration decision:不新增多 client id allowlist;依 legacy deployment config 同步 google.oauth_client_id_ioscvvd...Configuration.php.gitignore 忽略,staging / production 需人工確認實際部署設定。
  • config 同步後 staging 重測:login_logs.id = 11923created = 2026-07-01 03:52:46platform = iosis_login = 1result = {"l":"google"}monolog_error 最新 false client_id 仍停在 id = 121763 / 2026-07-01 03:49:15,未再新增。

Config 同步提醒

  • 最小必確認:api.url
  • 建議一起確認:user_login_seon_pass_hash
  • legacy User::setCookieLogin() 讀的是 site.api_url;new Pro360LoginUtil::buildLegacySessionCookieHeader() 讀的是 api.url。兩者都要是對應環境的 API host,否則 pro360_u_s_token cookie domain 會錯。
  • 同步範圍:Lib/Common/Configuration.php 或等效 deployment config;該檔被 .gitignore 忽略,切 staging / production 前要人工確認實際環境有同步。

測試結果

docker exec -w /project-data cd63f9147e8d php -l Endpoint/V1/Users.php
docker exec -w /project-data cd63f9147e8d php -l Lib/DataObject/ApiSessionLogsDO.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/ApiSession.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Util/Pro360LoginUtil.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/LoginLog.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/UserLoginInfo.php
docker exec -w /project-data cd63f9147e8d php -l tests/UsersLoginTest.php
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersLoginTest.php
OK (11 tests, 234 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/Pro360LoginUtilTest.php
OK (2 tests, 22 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersLoginOnceTest.php
OK (5 tests, 50 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersForgotPasswordTest.php
OK (3 tests, 39 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersSignUpTest.php
OK (11 tests, 129 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/SearchAddStep8IntegrationTest.php
OK (9 tests, 93 assertions)

UsersLoginTest.php 覆蓋:

  • password login 成功時 UserProfile 來自 user_profiles.user_id
  • success response 型別:error / credit_card_debt 為 number,user_id / session_id / legacy id、flag、count 欄位維持 string;nullable DB 欄位允許 null
  • failure response 型別:error 為 number,checkauth / message 為 string,not-found 類錯誤不回 checkauth
  • password login 成功時寫 login_logsuser_login_infos.INFO_TYPE_SEONuser_login_infos.INFO_TYPE_PASS
  • password login success response User 不回 legacy 沒有的 apple_user_id / is_apple_connected / multi_lang_id
  • password login 成功時設定 legacy CakeCookie[user_language]il=1pro360_u_s_token cookie header;沒有 profile language 時不送 language cookie。
  • login 成功時更新 users.last_login_ip_id / last_logged_in_time,且 users.user_login_count 加 1。
  • login 成功時 users.mobile_app_time_modified 維持 legacy Y-m-d h:i:s 12 小時制。
  • email OTP login 成功時 consume flat email_verifications.id
  • Google login 使用 token subusers.google_user_id,不使用 id
  • Facebook Limited login 使用 authentication_token 驗出的 subusers.facebook_user_id
  • Apple login 找不到 apple_user_id = old_sub 時,用 apple_user_transfers.old_sub -> new_sub fallback。
  • username + matching device_id 但無密碼時仍回 legacy failure,不建立 session / user_logins
  • inactive user 維持 legacy is_active = 1 guard,不建立 session。
  • device_id="undefined" 成功登入時,api_sessions.device_id 保留 undefineduser_logins.device_idNULL
  • 同 device 成功重登會產生不同的新 token;舊 token 不再能 load user,archive 欄位與 params snapshot 完整,其他 device session 保留。
  • 同 device 登入失敗不刪除既有 token,也不寫 action=login_rotate

部署後驗證

  • 部署前必須在 staging / production 分別確認 api_session_logs schema;Configuration.php 沒有本次新增 key。
  • staging 部署後需補同 device 連續成功登入、錯誤密碼重登、不同 device 保留與舊 token invalid 的 response / DB side effect 實測。
  • password / Google / Facebook Limited / Apple / email OTP / failure path full diff 仍待補。