feat(auth): sync OAuth provider tokens to backend via NextAuth session
Wire the Google/Facebook OAuth callback flow end-to-end so the provider's id_token (Google) and access_token (Facebook) captured by NextAuth are forwarded to the backend in exchange for business tokens: - Extend the NextAuth config with jwt/session callbacks that surface `account.id_token` / `account.access_token` to the client during first sign-in - Add an `OAuthSessionSync` bridge mounted inside `RootProviders` that listens to `useSession()` and dispatches new `AuthGoogleSyncSubmitted` / `AuthFacebookSyncSubmitted` events - Add corresponding actors in the auth XState machine that call `authRepository.googleLogin` / `facebookLogin`, persisting the backend's `LoginResponse` through the existing repository path This keeps all authentication orchestrated by the auth state machine while preserving NextAuth's OAuth redirect UX.
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
*
|
||||
* 本轮迁移:所有认证登录调用(含 OAuth 社交登录)统一收口到状态机内。
|
||||
* 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等),
|
||||
* 状态机通过 actors 调 `authRepository`(邮箱)或 `next-auth/react.signIn`(OAuth)。
|
||||
* 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或
|
||||
* `next-auth/react.signIn`(OAuth 跳转)。
|
||||
*/
|
||||
import { setup, fromPromise, assign } from "xstate";
|
||||
import { signIn } from "next-auth/react";
|
||||
@@ -77,6 +78,36 @@ const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
|
||||
await signIn(input);
|
||||
});
|
||||
|
||||
// ========== 新增:OAuth token → 后端业务 token sync actors ==========
|
||||
// 由 <OAuthSessionSync /> 在 NextAuth 回调后派发(监听 useSession())。
|
||||
// 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。
|
||||
// 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。
|
||||
|
||||
const syncGoogleBackendActor = fromPromise<LoginStatus, { idToken: string }>(
|
||||
async ({ input }) => {
|
||||
const guestId = await readGuestId();
|
||||
const result = await authRepository.googleLogin({
|
||||
idToken: input.idToken,
|
||||
guestId,
|
||||
});
|
||||
if (Result.isErr(result)) throw result.error;
|
||||
return "google" as LoginStatus;
|
||||
},
|
||||
);
|
||||
|
||||
const syncFacebookBackendActor = fromPromise<
|
||||
LoginStatus,
|
||||
{ accessToken: string }
|
||||
>(async ({ input }) => {
|
||||
const guestId = await readGuestId();
|
||||
const result = await authRepository.facebookLogin({
|
||||
accessToken: input.accessToken,
|
||||
guestId,
|
||||
});
|
||||
if (Result.isErr(result)) throw result.error;
|
||||
return "facebook" as LoginStatus;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Machine
|
||||
// ============================================================
|
||||
@@ -89,6 +120,8 @@ export const authMachine = setup({
|
||||
emailLogin: emailLoginActor,
|
||||
emailRegisterThenLogin: emailRegisterThenLoginActor,
|
||||
oauthSignIn: oauthSignInActor,
|
||||
syncGoogleBackend: syncGoogleBackendActor,
|
||||
syncFacebookBackend: syncFacebookBackendActor,
|
||||
},
|
||||
}).createMachine({
|
||||
id: "auth",
|
||||
@@ -125,6 +158,9 @@ export const authMachine = setup({
|
||||
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
||||
AuthGoogleLoginSubmitted: "loadingOAuth",
|
||||
AuthFacebookLoginSubmitted: "loadingOAuth",
|
||||
// OAuth 回调后 sync 入口 —— 由 <OAuthSessionSync /> 派发
|
||||
AuthGoogleSyncSubmitted: "syncingGoogleBackend",
|
||||
AuthFacebookSyncSubmitted: "syncingFacebookBackend",
|
||||
AuthAppleLoginSubmitted: {
|
||||
actions: assign({
|
||||
errorMessage: "Apple login not implemented",
|
||||
@@ -213,6 +249,58 @@ export const authMachine = setup({
|
||||
},
|
||||
},
|
||||
|
||||
// ========== 新增:OAuth token → 后端 sync 状态 ==========
|
||||
syncingGoogleBackend: {
|
||||
entry: assign({ errorMessage: null }),
|
||||
invoke: {
|
||||
src: "syncGoogleBackend",
|
||||
input: ({ event }) => {
|
||||
// event 来自 idle 跳转(AuthGoogleSyncSubmitted)
|
||||
if (event.type !== "AuthGoogleSyncSubmitted") return { idToken: "" };
|
||||
return { idToken: event.idToken };
|
||||
},
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginStatus: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
syncingFacebookBackend: {
|
||||
entry: assign({ errorMessage: null }),
|
||||
invoke: {
|
||||
src: "syncFacebookBackend",
|
||||
input: ({ event }) => {
|
||||
if (event.type !== "AuthFacebookSyncSubmitted") return { accessToken: "" };
|
||||
return { accessToken: event.accessToken };
|
||||
},
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginStatus: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
success: {
|
||||
// 终态:业务层通过 state.matches("success") 判断
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user