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 / path | status | count |
|---|---|---|
POST /users/connect_social/apple.json | 200 | 495 |
POST /users/connect_social/google.json | 200 | 277 |
OPTIONS /users/connect_social/google.json | 200 | 166 |
POST /users/connect_social/facebook.json | 200 | 36 |
OPTIONS /users/connect_social/facebook.json | 200 | 17 |
POST /users/connect_social/google.json | 403 | 14 |
OPTIONS /users/connect_social/apple.json | 200 | 9 |
POST /users/connect_social/apple.json | 403 | 3 |
POST /users/connect_social/facebook.json | 403 | 1 |
/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-3283 | linkSocialAccount(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-52 | Google bind 成功後用 profile.token 當 access_token 呼叫。 |
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:64-80 | Apple bind 成功後用 authorization code 呼叫,id_token 只當第二個參數傳入但 APIManager 未送出。 |
/Users/mattsu/Documents/Site/web-app/modules/components/dashboard/account/SocialAccountSetting.js:128-139 | Facebook 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 流程:
- 只處理 JSON request。
- 預設 response 是
error=1+err_connect_social_1翻譯文字。 - 必須通過
RestApiHelper::hasValidApiSession($this)。 - 從 session 取得
user_id。 - 依
{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_tokenlimited login verify,成功時取sub。apple:先查AppleVerifyLog::getRowByCode($code);找不到才呼叫 Apple verify。
- 若
social_id已屬於目前 user,回 success,不更新。 - 若
social_id已屬於其他 user,回error=2。 - 若 social token 有效但該
social_id未被使用,讀目前 user:- 只有目前 user 對應 social 欄位為 empty 時才更新。
- 不論是否真的更新欄位,最後都回 success。
- 若無法取得有效 social 資訊,回
error=3。
重要 parity:
- legacy
getBySocialId()只用User.<type>_user_id = social_id查詢,沒有is_active = 1guard。 - 若目前 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 只保留RouterV3generic request log,不補 endpoint duplicate full request log。
New 對照
| 項目 | new |
|---|---|
| endpoint | Endpoint/V1/Users.php:950-1081 已補 connect_social()。 |
| helper | Endpoint/V1/Users.php:1397-1575 已補 input helper、social owner lookup、Apple cached lookup、verify wrapper、DB update 與 legacy error message helper。 |
| routing | Lib/Common/RouterRule/Mapping.php:104-107 已補 users/connect_social/([a-zA-Z_]+)\.json explicit mapping。 |
| Google / Facebook / Apple verify helper | Lib/Util/Pro360LoginUtil.php:49-140,由 Users::connect_social() 的 protected wrapper 呼叫,方便測試替換外部 token verify。 |
| Google client id mapping test | tests/Pro360LoginUtilTest.php:22-54 已覆蓋與 legacy mapping 一致。 |
| Facebook limited login verify | Lib/Util/FacebookUtil.php:21-68。 |
| Apple verify | Lib/Util/AppleSignInUtil.php:36-197,成功時會寫 apple_verify_logs。 |
| AppleVerifyLog model | Lib/Model/AppleVerifyLog.php:14-16 目前只有 table model;Users::connect_social() 以 local helper 重現 legacy getRowByCode() 的 fields=['sub'] + isset(id) 判斷。 |
| user social fields in brief | Lib/Model/User.php:301-318 已包含部分 social 欄位,需實作時再確認 response path。 |
| PHPUnit | tests/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.phpexplicit 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 會帶 accept、origin、referer、sec-*、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。 |
| body | FormData。 |
| session | 從 getToken() 取 session token,放在 X-PRO360-User-Session-Token。 |
user_id 參數 | APIManager.linkSocialAccount(type, user_id, social_token) 有第二個參數,但目前沒有 append 到 FormData,API 不會收到。 |
前端實際送出的 body:
| type | caller 傳入 | 實際 body | 備註 |
|---|---|---|---|
google | linkSocialAccount('google', googleid, id_token) | access_token=<google id_token> | googleid 只是第二個參數,沒有送到 API。 |
facebook | linkSocialAccount('facebook', response.id, token) | access_token=<facebook access token> | response.id 沒有送到 API;後端用 Graph API token verify 結果的 id。 |
apple | linkSocialAccount('apple', id_token, code) | platform=web、code=<apple authorization code> | id_token 是第二個參數,沒有送到 API;目前 web 只送 authorization code。 |
支援的 platform:
| social | platform | 用途 |
|---|---|---|
google | ios / android / ios_consumer / android_consumer / android_firebase / android_consumer_firebase | 對應不同 Google OAuth client id config key。 |
apple | web / 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 時:
| type | table | 欄位 | legacy 行為 |
|---|---|---|---|
google | users | is_google_connected | 設為 1。 |
google | users | google_user_id | 設為 Google token payload sub。 |
google | users | google_avatar_url | 設為 Google token payload picture。 |
google | users | google_access_token | 因 controller 未傳 id token,legacy helper 實際寫入空字串;new 已對齊。 |
facebook | users | is_facebook_connected | 設為 1。 |
facebook | users | facebook_user_id | normal login 設為 Graph API id;limited login 設為 JWT sub。 |
facebook | users | facebook_user_name | normal login 設為 Graph API name;limited login 設為 JWT name。 |
facebook | users | facebook_access_token | normal login 設為 access_token;limited login 設為 authentication_token。 |
apple | users | is_apple_connected | 設為 1。 |
apple | users | apple_user_id | 設為 Apple verify result data.sub。 |
2026-07-06 Google staging / legacy request 實測:
| 項目 | 結果 |
|---|---|
| request | POST /users/connect_social/google.json,body 只有 access_token=<google id_token>,沒有 platform。 |
| response | {"error":0,"message":"Success"}。 |
| session | api_sessions.id=<redacted> 對到 user_id=8617,api_key=<redacted>。 |
| Google token | token aud 是 default Google client id;後端沒有 platform 時會用 google.oauth_client_id 驗證。 |
users.google_user_id | 更新為 token payload sub。 |
users.is_google_connected | 1。 |
users.google_access_token | 空字串,符合 legacy Google connect path 實際行為。 |
| owner check | 同一個 google_user_id 只查到 user_id=8617。 |
| queue | event_queue / task_event_queue / task_queue 沒有 connect_social 相關 row。 |
| request log | monolog_202607 有 users/connect_social/google.json::POST INFO row;未在文件輸出 token context。 |
2026-07-08 new staging Google / Apple 實測:
| scenario | response / DB side effect |
|---|---|
| Google success | 回 success,更新目前 user 的 users.google_user_id;Google connect path 的 google_access_token 維持空字串。 |
| Google social id 已屬於其他 user | 回 error=2,不覆蓋目前 user 的 Google social 欄位。 |
| Apple success | 回 success,寫入 apple_verify_logs,並更新目前 user 的 users.apple_user_id。 |
| Apple social id 已屬於其他 user | 回 error=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_words | new 目前只讀既有 lang_text,沒有資料時回 fallback;不新增 translation row。legacy controller 實際是否透過 helper 自動補 row,staging diff 時需一併確認。 |
Session helper side effect:
| table | 欄位 / row | 行為 |
|---|---|---|
users | iphone_last_access, last_access_client | legacy session helper DB load path 會更新。 |
task_queue | returnedQuoteService | user 有 active service、quote_service_count > 0 且 iphone_last_access 超過 180 天時才可能寫入。 |
Apple verify side effect:
| table | 行為 |
|---|---|
apple_verify_logs | legacy 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 對照
| type | legacy key / endpoint | new 對應 |
|---|---|---|
| Google default | google.oauth_client_id | Pro360LoginUtil::getGoogleClientIdConfigKeyByPlatform() default 回 google.oauth_client_id。 |
| Google iOS / Android | google.oauth_client_id_ios、google.oauth_client_id_android | new helper mapping 已有測試覆蓋。 |
| Google consumer / firebase | google.oauth_client_id_ios_consumer、google.oauth_client_id_android_consumer、google.oauth_client_id_android_firebase、google.oauth_client_id_android_consumer_firebase | new helper mapping 已有測試覆蓋。 |
| Facebook normal | https://graph.facebook.com/me?access_token=... | Pro360LoginUtil::verify_fb_access_token() 同 endpoint。 |
| Facebook limited | Configure::read('site.api_url') . '/new/v1/facebook/verify_jwt' | FacebookUtil::verify() 直接驗 limited JWT。此處是差異,staging 需確認 new helper 與 legacy endpoint 對 name、sub、錯誤回傳的行為一致。 |
| Apple | BookingUtil->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_pro與apple_sign_in_client_id_web。 - 必要 key:
google.oauth_client_id、apple_sign_in_pro.apple_sign_in_private_key、apple_sign_in_pro.apple_sign_in_kid、apple_sign_in_pro.apple_sign_in_team_id、apple_sign_in_client_id_web。 - legacy 來源與值:來源是 legacy 部署設定;實際 secret / client id 不寫入文件,正式機值是否逐項確認仍為
待確認。 - new 本地設定:2026-07-14 只檢查存在性,
google.oauth_client_id為MISSING;apple_sign_in_pro、apple_sign_in_client_id_web為SET,未輸出 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 至少覆蓋:
- valid session + Google id token + user google empty:更新 Google 欄位並回 success。
- valid session + Facebook access token + user facebook empty:更新 Facebook 欄位並回 success。
- valid session + Facebook
authentication_token+ user facebook empty:更新 limited login 欄位並回 success。 - valid session + Apple code + user apple empty:更新 Apple 欄位並回 success。
- social id 已屬於目前 user:回 success,不更新。
- social id 已屬於其他 user:回
error=2,不更新。 - 目前 user 已有不同同類 social id、且新 social id 未被其他 user 使用:回 success,但不覆蓋既有欄位。
- invalid token / missing token:回
error=3。 - invalid session:回 legacy default
error=1。 - blocked user:回 HTTP 403 +
{"error":5,"message":"Blocked User"}。 - negative test:不建立 session / login log / user login / transaction / event queue。
- 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,只保留
RouterV3generic request log。 - 保留的 generic log channel 由 RouterV3 以 request path / method / request class 組成;context shape 是
[apiKey, path, subDir, filtered post],message 是 session user id。以 stagingmonolog_202607查對應 channel / user id 驗證,不在文件輸出完整 token payload。 - 本次沒有擴大
RouterV3sensitive key filter;目前既有 filter 主要過濾password/passwd。access_token、authentication_token、code是否也要納入 generic request log 過濾,需另行決定。
Staging 實測需分別驗:
- legacy / new response JSON 型別一致。
userssocial 欄位最終值一致。- 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 確認兩者對name、sub、錯誤回傳的行為一致。 - 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_social的access_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 存在性與值來源待確認。