POST /users/connect_social/{type}.json

狀態

已補 action / routing / PHPUnit;2026-07-08 已完成 new staging Google / Apple 真實 token 驗證,Facebook normal / limited login 真實 token 驗證待補。

legacy 正式機 access log 已確認這支有流量。新專案已補 /users/connect_social/{type}.json explicit routing 與 Users::connect_social();搬移時保留 legacy social id conflict、empty-only update、response shape 與 session helper side effect。

Legacy 流量

2026-07-06 查 legacy 正式機 ip-10-5-2-242/var/log/apache2/get-lancer_access.log*

Method / pathstatuscount
POST /users/connect_social/apple.json200495
POST /users/connect_social/google.json200277
OPTIONS /users/connect_social/google.json200166
POST /users/connect_social/facebook.json20036
OPTIONS /users/connect_social/facebook.json20017
POST /users/connect_social/google.json40314
OPTIONS /users/connect_social/apple.json2009
POST /users/connect_social/apple.json4033
POST /users/connect_social/facebook.json4031

/users/check_account.json 同一批 log 查詢為 0 筆;下一支 migration 不以 check_account 為優先。

操作路徑

Web 帳號設定的「連結帳號」區塊會觸發這支 API。前端目前用 session token 呼叫:

POST /users/connect_social/google.json
POST /users/connect_social/facebook.json
POST /users/connect_social/apple.json

前端來源:

檔案行為
/Users/mattsu/Documents/Site/web-app/modules/utils/api-manager.js:3268-3283linkSocialAccount(type, user_id, social_token) 組出 /users/connect_social/{type}.json;Apple 送 platform=web + code,其他 social 送 access_token
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:39-52Google bind 成功後用 profile.tokenaccess_token 呼叫。
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:64-80Apple bind 成功後用 authorization code 呼叫,id_token 只當第二個參數傳入但 APIManager 未送出。
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:128-139Facebook bind 成功後用 Facebook access token 呼叫。
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:148-151前端用 facebook_user_id / google_user_id / apple_user_id 判斷是否已連結。

實際頁面路徑待實測確認;從既有 settings flow 判斷主要入口在 https://staging.pro360.com.tw/dashboard/settings 的帳號設定區。

Legacy 對照

項目legacy
controller/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php:5278-5372
session helper/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php:75-152
session user id + full request log/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php:162-182
session id source/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php:185-191
social user lookup/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php:841-849
Facebook access token verify/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/QuoteUtil.php:1085-1101
Facebook limited login verify/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/QuoteUtil.php:1103-1118
Facebook link update/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/QuoteUtil.php:1120-1141
Apple link update / verify/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/QuoteUtil.php:1143-1160
Google id token verify / link update/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/QuoteUtil.php:1170-1217
Apple cached code lookup/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/AppleVerifyLog.php:24-28
legacy config path list/Users/mattsu/Documents/Site/get-lancer-php56/app/Config/config.php:295-298

Legacy 流程:

  1. 只處理 JSON request。
  2. 預設 response 是 error=1 + err_connect_social_1 翻譯文字。
  3. 必須通過 RestApiHelper::hasValidApiSession($this)
  4. 從 session 取得 user_id
  5. {type} 驗證 social token 並取得 social_id
    • google:用 access_token 當 Google id token,呼叫 verify_google_id_token($access_token, $platform),成功時取 sub
    • facebook:有 access_token 時走 Graph API /me,成功時取 id
    • facebook:沒有 access_token 時走 authentication_token limited login verify,成功時取 sub
    • apple:先查 AppleVerifyLog::getRowByCode($code);找不到才呼叫 Apple verify。
  6. social_id 已屬於目前 user,回 success,不更新。
  7. social_id 已屬於其他 user,回 error=2
  8. 若 social token 有效但該 social_id 未被使用,讀目前 user:
    • 只有目前 user 對應 social 欄位為 empty 時才更新。
    • 不論是否真的更新欄位,最後都回 success。
  9. 若無法取得有效 social 資訊,回 error=3

重要 parity:

  • legacy getBySocialId() 只用 User.<type>_user_id = social_id 查詢,沒有 is_active = 1 guard。
  • 若目前 user 已有不同的同類 social id,而新的 social_id 未被其他 user 使用,legacy 會回 success 但不覆蓋舊值。
  • Google connect path 呼叫 update_user_google_link($user_id, $google_auth_data),未傳 $id_token;legacy helper 的 SQL expression 實際會把 google_access_token 更新成空字串。
  • Legacy RestApiHelper::getUserId() 會記 full request log;new 只保留 RouterV3 generic request log,不補 endpoint duplicate full request log。

New 對照

項目new
endpointEndpoint/V1/Users.php:950-1081 已補 connect_social()
helperEndpoint/V1/Users.php:1397-1575 已補 input helper、social owner lookup、Apple cached lookup、verify wrapper、DB update 與 legacy error message helper。
routingLib/Common/RouterRule/Mapping.php:104-107 已補 users/connect_social/([a-zA-Z_]+)\.json explicit mapping。
Google / Facebook / Apple verify helperLib/Util/Pro360LoginUtil.php:49-140,由 Users::connect_social() 的 protected wrapper 呼叫,方便測試替換外部 token verify。
Google client id mapping testtests/Pro360LoginUtilTest.php:22-54 已覆蓋與 legacy mapping 一致。
Facebook limited login verifyLib/Util/FacebookUtil.php:21-68
Apple verifyLib/Util/AppleSignInUtil.php:36-197,成功時會寫 apple_verify_logs
AppleVerifyLog modelLib/Model/AppleVerifyLog.php:14-16 目前只有 table model;Users::connect_social() 以 local helper 重現 legacy getRowByCode()fields=['sub'] + isset(id) 判斷。
user social fields in briefLib/Model/User.php:301-318 已包含部分 social 欄位,需實作時再確認 response path。
PHPUnittests/UsersConnectSocialTest.php:51-288 覆蓋 Google / Facebook limited / Apple success、same user、other user conflict、existing different social id、invalid token、invalid session route、blocked user。

已搬移:

  • Endpoint/V1/Users.php::connect_social()
  • Mapping.php explicit route:users/connect_social/([a-zA-Z_]+)\.json
  • PHPUnit:tests/UsersConnectSocialTest.php

Request

這支 API 走 browser FormData,等同 multipart/form-data。手寫測試時建議用 curl -F,讓 curl 自己產生 boundary;不需要手動帶 content-type: multipart/form-data; boundary=...

必要 headers:

X-PRO360-Rest-Api-Key: <api key>
X-PRO360-User-Session-Token: <session id>

browser curl 會帶 acceptoriginreferersec-*user-agent 等 headers;這些不是這支 API 的核心輸入。實測最小 request 只需要 API key、session token 與對應 form 欄位。

Google 最小 curl:

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/google.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'access_token=<google id_token>'

Google 若需要指定 app client id mapping,可多帶 platform

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/google.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'access_token=<google id_token>' \
  -F 'platform=ios'

沒有 platform 時,Google verify 會走 default google.oauth_client_id

Facebook normal login curl:

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/facebook.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'access_token=<facebook access token>'

Facebook limited login curl:

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/facebook.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'authentication_token=<facebook limited login JWT>'

這是 legacy 支援分支;目前 web-app 綁定流程看到的是 normal login 的 access_token path。

Apple web curl:

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/apple.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'platform=web' \
  -F 'code=<apple authorization code>'

Apple legacy fallback curl:

curl -sS 'https://api-staging.pro360.com.tw/users/connect_social/apple.json' \
  -X POST \
  -H 'X-PRO360-Rest-Api-Key: <api key>' \
  -H 'X-PRO360-User-Session-Token: <session id>' \
  -F 'access_token=<apple authorization code fallback>'

legacy 若沒有 code,會用 access_token$code

實際前端共用包裝:

來源內容
URL/users/connect_social/{type}.json{type}google / facebook / apple
bodyFormData
sessiongetToken() 取 session token,放在 X-PRO360-User-Session-Token
user_id 參數APIManager.linkSocialAccount(type, user_id, social_token) 有第二個參數,但目前沒有 append 到 FormData,API 不會收到。

前端實際送出的 body:

typecaller 傳入實際 body備註
googlelinkSocialAccount('google', googleid, id_token)access_token=<google id_token>googleid 只是第二個參數,沒有送到 API。
facebooklinkSocialAccount('facebook', response.id, token)access_token=<facebook access token>response.id 沒有送到 API;後端用 Graph API token verify 結果的 id
applelinkSocialAccount('apple', id_token, code)platform=webcode=<apple authorization code>id_token 是第二個參數,沒有送到 API;目前 web 只送 authorization code

支援的 platform

socialplatform用途
googleios / android / ios_consumer / android_consumer / android_firebase / android_consumer_firebase對應不同 Google OAuth client id config key。
appleweb / ios / ios_consumer對應 Apple Sign In client id / private key config。

Response

Success:

{"error":0,"message":"Success"}

Invalid request / invalid session default:

{"error":1,"message":"<err_connect_social_1 translated message>"}

Social ID 已綁到其他 user:

{"error":2,"message":"<err_connect_social_2 translated message>"}

無有效綁定資訊:

{"error":3,"message":"<err_connect_social_3 translated message>"}

Blocked user 由 session helper 直接輸出 HTTP 403:

{"error":5,"message":"Blocked User"}

DB Side Effect

成功且目前 user 對應欄位為 empty 時:

typetable欄位legacy 行為
googleusersis_google_connected設為 1
googleusersgoogle_user_id設為 Google token payload sub
googleusersgoogle_avatar_url設為 Google token payload picture
googleusersgoogle_access_token因 controller 未傳 id token,legacy helper 實際寫入空字串;new 已對齊。
facebookusersis_facebook_connected設為 1
facebookusersfacebook_user_idnormal login 設為 Graph API id;limited login 設為 JWT sub
facebookusersfacebook_user_namenormal login 設為 Graph API name;limited login 設為 JWT name
facebookusersfacebook_access_tokennormal login 設為 access_token;limited login 設為 authentication_token
appleusersis_apple_connected設為 1
appleusersapple_user_id設為 Apple verify result data.sub

2026-07-06 Google staging / legacy request 實測:

項目結果
requestPOST /users/connect_social/google.json,body 只有 access_token=<google id_token>,沒有 platform
response{"error":0,"message":"Success"}
sessionapi_sessions.id=<redacted> 對到 user_id=8617api_key=<redacted>
Google tokentoken aud 是 default Google client id;後端沒有 platform 時會用 google.oauth_client_id 驗證。
users.google_user_id更新為 token payload sub
users.is_google_connected1
users.google_access_token空字串,符合 legacy Google connect path 實際行為。
owner check同一個 google_user_id 只查到 user_id=8617
queueevent_queue / task_event_queue / task_queue 沒有 connect_social 相關 row。
request logmonolog_202607users/connect_social/google.json::POST INFO row;未在文件輸出 token context。

2026-07-08 new staging Google / Apple 實測:

scenarioresponse / DB side effect
Google success回 success,更新目前 user 的 users.google_user_id;Google connect path 的 google_access_token 維持空字串。
Google social id 已屬於其他 usererror=2,不覆蓋目前 user 的 Google social 欄位。
Apple success回 success,寫入 apple_verify_logs,並更新目前 user 的 users.apple_user_id
Apple social id 已屬於其他 usererror=2,不覆蓋目前 user 的 Apple social 欄位。
request log只保留 RouterV3 generic log;文件不記錄完整 access_token / code payload。

實測環境為 new staging,日期為 2026-07-08;實測 user / session / log row id 尚未回填,標 待確認,不可用本段替代後續 raw JSON type diff。Facebook normal / limited login 尚未完成真實 token 實測。

Error message:

table行為
multi_translation_wordsnew 目前只讀既有 lang_text,沒有資料時回 fallback;不新增 translation row。legacy controller 實際是否透過 helper 自動補 row,staging diff 時需一併確認。

Session helper side effect:

table欄位 / row行為
usersiphone_last_access, last_access_clientlegacy session helper DB load path 會更新。
task_queuereturnedQuoteServiceuser 有 active service、quote_service_count > 0iphone_last_access 超過 180 天時才可能寫入。

Apple verify side effect:

table行為
apple_verify_logslegacy controller 先讀 cached code;legacy verify_apple_code() 轉呼叫 /new/v1/apple/verify_jwt。new AppleSignInUtil::verify() 會直接寫 apple_verify_logs;2026-07-08 new staging Apple success 已確認會寫 log,legacy / new payload full diff 仍待補。

不應新增的 side effect:

  • 不建立 user
  • 不建立 login/session
  • 不寫 login_logs
  • 不寫 user_logins
  • 不寫 transaction / payment / wallet
  • 不寫 event queue / task event queue
  • 不覆蓋已存在的同類 social id

Config 對照

typelegacy key / endpointnew 對應
Google defaultgoogle.oauth_client_idPro360LoginUtil::getGoogleClientIdConfigKeyByPlatform() default 回 google.oauth_client_id
Google iOS / Androidgoogle.oauth_client_id_iosgoogle.oauth_client_id_androidnew helper mapping 已有測試覆蓋。
Google consumer / firebasegoogle.oauth_client_id_ios_consumergoogle.oauth_client_id_android_consumergoogle.oauth_client_id_android_firebasegoogle.oauth_client_id_android_consumer_firebasenew helper mapping 已有測試覆蓋。
Facebook normalhttps://graph.facebook.com/me?access_token=...Pro360LoginUtil::verify_fb_access_token() 同 endpoint。
Facebook limitedConfigure::read('site.api_url') . '/new/v1/facebook/verify_jwt'FacebookUtil::verify() 直接驗 limited JWT。此處是差異,staging 需確認 new helper 與 legacy endpoint 對 namesub、錯誤回傳的行為一致。
AppleBookingUtil->getUrl('/new/v1/apple/verify_jwt')AppleSignInUtil::verify() 直接打 Apple token endpoint 並驗 id token;Users::connect_social() 會把 new helper 回傳的 sub normalize 成 legacy data.sub shape。staging 需確認 parity。

Deployment config checklist:

  • 觸發條件:Google web request 沒有 platform 時讀 google.oauth_client_id;Apple web request 讀 apple_sign_in_proapple_sign_in_client_id_web
  • 必要 key:google.oauth_client_idapple_sign_in_pro.apple_sign_in_private_keyapple_sign_in_pro.apple_sign_in_kidapple_sign_in_pro.apple_sign_in_team_idapple_sign_in_client_id_web
  • legacy 來源與值:來源是 legacy 部署設定;實際 secret / client id 不寫入文件,正式機值是否逐項確認仍為 待確認
  • new 本地設定:2026-07-14 只檢查存在性,google.oauth_client_idMISSINGapple_sign_in_proapple_sign_in_client_id_webSET,未輸出 secret/value。
  • staging / production:2026-07-08 new staging Google / Apple success 表示當時 staging 所需設定可用;production 尚未確認。
  • 同步範圍:Lib/Common/Configuration.php.gitignore 忽略,本地 config 不會隨一般 commit / push 部署,staging / production 必須另外同步與驗證。

測試計畫

PHPUnit 至少覆蓋:

  1. valid session + Google id token + user google empty:更新 Google 欄位並回 success。
  2. valid session + Facebook access token + user facebook empty:更新 Facebook 欄位並回 success。
  3. valid session + Facebook authentication_token + user facebook empty:更新 limited login 欄位並回 success。
  4. valid session + Apple code + user apple empty:更新 Apple 欄位並回 success。
  5. social id 已屬於目前 user:回 success,不更新。
  6. social id 已屬於其他 user:回 error=2,不更新。
  7. 目前 user 已有不同同類 social id、且新 social id 未被其他 user 使用:回 success,但不覆蓋既有欄位。
  8. invalid token / missing token:回 error=3
  9. invalid session:回 legacy default error=1
  10. blocked user:回 HTTP 403 + {"error":5,"message":"Blocked User"}
  11. negative test:不建立 session / login log / user login / transaction / event queue。
  12. request log:確認不補 endpoint duplicate request log;token 類欄位是否納入 generic request log 過濾另行確認。

local container PHP 8.2:

docker exec -w /project-data cd63f9147e8d php -l Endpoint/V1/Users.php
No syntax errors detected in Endpoint/V1/Users.php
 
docker exec -w /project-data cd63f9147e8d php -l Lib/Common/RouterRule/Mapping.php
No syntax errors detected in Lib/Common/RouterRule/Mapping.php
 
docker exec -w /project-data cd63f9147e8d php -l tests/UsersConnectSocialTest.php
No syntax errors detected in tests/UsersConnectSocialTest.php
 
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersConnectSocialTest.php
OK (9 tests, 48 assertions)
 
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/UsersUpdateAutoRefillTest.php
OK (7 tests, 52 assertions)

Request Log

  • legacy RestApiHelper::getUserId() 會寫 full request info。
  • migration decision:2026-07-14 reviewer 接受 new 不補 endpoint duplicate request log,只保留 RouterV3 generic request log。
  • 保留的 generic log channel 由 RouterV3 以 request path / method / request class 組成;context shape 是 [apiKey, path, subDir, filtered post],message 是 session user id。以 staging monolog_202607 查對應 channel / user id 驗證,不在文件輸出完整 token payload。
  • 本次沒有擴大 RouterV3 sensitive key filter;目前既有 filter 主要過濾 password / passwdaccess_tokenauthentication_tokencode 是否也要納入 generic request log 過濾,需另行決定。

Staging 實測需分別驗:

  • legacy / new response JSON 型別一致。
  • users social 欄位最終值一致。
  • already-connected、conflict、invalid token 三種非 happy path。
  • Facebook normal / limited login 真實 token 的 response、DB write 與 error path。

待確認

  • Legacy AppleVerifyLog::getRowByCode() 註解寫會回 [sub, platform, email],但實際 fields 只有 sub,且用 isset($result['AppleVerifyLog']['id']) 判斷,可能永遠回 false。new 目前保留這個判斷;staging 需實測 Apple bind 是否符合。
  • Legacy Facebook limited login 透過 /new/v1/facebook/verify_jwt;new helper 直接驗 JWT。需用 staging token 確認兩者對 namesub、錯誤回傳的行為一致。
  • Legacy Apple verify 透過 /new/v1/apple/verify_jwt;new helper 直接打 Apple endpoint。2026-07-08 已確認 new staging Apple web success / conflict 行為,legacy / new verify payload full diff 仍待補。
  • connect_socialaccess_token / authentication_token / code 是否要進一步過濾 generic request log,待確認。
  • 前端主要頁面路徑需 staging 實點確認;目前只從 web-app component / APIManager 追到呼叫來源。
  • 2026-07-08 new staging 實測使用的 user / session / monolog row id 與 raw JSON type diff 待回填。
  • production 的 Google / Apple deployment config 存在性與值來源待確認。