feat(auth): add facebook identity psid login
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FacebookIdentityResponse,
|
||||||
|
FacebookPsidLoginResponse,
|
||||||
|
LoginRequest,
|
||||||
|
} from "@/data/dto/auth";
|
||||||
|
import { UserSchema } from "@/data/schemas/user/user";
|
||||||
|
|
||||||
|
describe("facebook identity DTO/schema", () => {
|
||||||
|
it("parses facebook identity binding responses", () => {
|
||||||
|
const response = FacebookIdentityResponse.fromJson({
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
facebookBinding: {
|
||||||
|
conflicts: [],
|
||||||
|
bound: [{ field: "psid" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.toJson()).toMatchObject({
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
facebookBinding: {
|
||||||
|
conflicts: [],
|
||||||
|
bound: [{ field: "psid" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses facebook psid login responses for real users and guests", () => {
|
||||||
|
expect(
|
||||||
|
FacebookPsidLoginResponse.fromJson({
|
||||||
|
token: "login-token",
|
||||||
|
refreshToken: "refresh-token",
|
||||||
|
matchedBy: "psid",
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
hasCompleteFacebookIdentity: true,
|
||||||
|
isGuest: false,
|
||||||
|
user: { id: "user-1", username: "Elio" },
|
||||||
|
}).toJson(),
|
||||||
|
).toMatchObject({
|
||||||
|
isGuest: false,
|
||||||
|
hasCompleteFacebookIdentity: true,
|
||||||
|
user: { id: "user-1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
FacebookPsidLoginResponse.fromJson({
|
||||||
|
token: "guest-token",
|
||||||
|
matchedBy: "guest",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
hasCompleteFacebookIdentity: false,
|
||||||
|
isGuest: true,
|
||||||
|
userId: "guest-1",
|
||||||
|
}).toJson(),
|
||||||
|
).toMatchObject({
|
||||||
|
refreshToken: "",
|
||||||
|
isGuest: true,
|
||||||
|
userId: "guest-1",
|
||||||
|
user: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps psid in request serialization without requiring DTO field declarations", () => {
|
||||||
|
const request = LoginRequest.from({
|
||||||
|
email: "user@example.com",
|
||||||
|
password: "password",
|
||||||
|
platform: "web",
|
||||||
|
guestId: "",
|
||||||
|
psid: "psid-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.toJson()).toMatchObject({ psid: "psid-1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts facebook identity fields in user schema", () => {
|
||||||
|
expect(
|
||||||
|
UserSchema.parse({
|
||||||
|
id: "user-1",
|
||||||
|
username: "Elio",
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Facebook identity 绑定请求 DTO
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FacebookIdentityRequestSchema,
|
||||||
|
type FacebookIdentityRequestInput,
|
||||||
|
type FacebookIdentityRequestData,
|
||||||
|
} from "@/data/schemas/auth/facebook_identity_request";
|
||||||
|
|
||||||
|
export class FacebookIdentityRequest {
|
||||||
|
private constructor(input: FacebookIdentityRequestInput) {
|
||||||
|
const data = FacebookIdentityRequestSchema.parse(input);
|
||||||
|
Object.assign(this, data);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(input: FacebookIdentityRequestInput): FacebookIdentityRequest {
|
||||||
|
return new FacebookIdentityRequest(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): FacebookIdentityRequest {
|
||||||
|
return FacebookIdentityRequest.from(json as FacebookIdentityRequestInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): FacebookIdentityRequestData {
|
||||||
|
return FacebookIdentityRequestSchema.parse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ export class FacebookLoginRequest {
|
|||||||
declare readonly accessToken: string;
|
declare readonly accessToken: string;
|
||||||
declare readonly platform: string;
|
declare readonly platform: string;
|
||||||
declare readonly guestId: string;
|
declare readonly guestId: string;
|
||||||
declare readonly psid: string;
|
|
||||||
declare readonly isTestAccount: boolean;
|
declare readonly isTestAccount: boolean;
|
||||||
|
|
||||||
private constructor(input: FacebookLoginRequestInput) {
|
private constructor(input: FacebookLoginRequestInput) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Facebook PSID 登录请求 DTO
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FacebookPsidLoginRequestSchema,
|
||||||
|
type FacebookPsidLoginRequestInput,
|
||||||
|
type FacebookPsidLoginRequestData,
|
||||||
|
} from "@/data/schemas/auth/facebook_psid_login_request";
|
||||||
|
|
||||||
|
export class FacebookPsidLoginRequest {
|
||||||
|
private constructor(input: FacebookPsidLoginRequestInput) {
|
||||||
|
const data = FacebookPsidLoginRequestSchema.parse(input);
|
||||||
|
Object.assign(this, data);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(input: FacebookPsidLoginRequestInput): FacebookPsidLoginRequest {
|
||||||
|
return new FacebookPsidLoginRequest(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): FacebookPsidLoginRequest {
|
||||||
|
return FacebookPsidLoginRequest.from(
|
||||||
|
json as FacebookPsidLoginRequestInput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): FacebookPsidLoginRequestData {
|
||||||
|
return FacebookPsidLoginRequestSchema.parse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
export class FbIdLoginRequest {
|
export class FbIdLoginRequest {
|
||||||
declare readonly fbId: string;
|
declare readonly fbId: string;
|
||||||
declare readonly avatarUrl: string;
|
declare readonly avatarUrl: string;
|
||||||
declare readonly psid: string;
|
|
||||||
declare readonly isTestAccount: boolean;
|
declare readonly isTestAccount: boolean;
|
||||||
|
|
||||||
private constructor(input: FbIdLoginRequestInput) {
|
private constructor(input: FbIdLoginRequestInput) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export class GoogleLoginRequest {
|
|||||||
declare readonly idToken: string;
|
declare readonly idToken: string;
|
||||||
declare readonly platform: string;
|
declare readonly platform: string;
|
||||||
declare readonly guestId: string;
|
declare readonly guestId: string;
|
||||||
declare readonly psid: string;
|
|
||||||
declare readonly isTestAccount: boolean;
|
declare readonly isTestAccount: boolean;
|
||||||
|
|
||||||
private constructor(input: GoogleLoginRequestInput) {
|
private constructor(input: GoogleLoginRequestInput) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
export * from "./facebook_identity_request";
|
||||||
export * from "./facebook_login_request";
|
export * from "./facebook_login_request";
|
||||||
|
export * from "./facebook_psid_login_request";
|
||||||
export * from "./fb_id_login_request";
|
export * from "./fb_id_login_request";
|
||||||
export * from "./google_login_request";
|
export * from "./google_login_request";
|
||||||
export * from "./guest_login_request";
|
export * from "./guest_login_request";
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export class LoginRequest {
|
|||||||
declare readonly password: string;
|
declare readonly password: string;
|
||||||
declare readonly platform: string;
|
declare readonly platform: string;
|
||||||
declare readonly guestId: string;
|
declare readonly guestId: string;
|
||||||
declare readonly psid: string;
|
|
||||||
declare readonly isTestAccount: boolean;
|
declare readonly isTestAccount: boolean;
|
||||||
|
|
||||||
private constructor(input: LoginRequestInput) {
|
private constructor(input: LoginRequestInput) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Facebook identity 绑定响应 DTO
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FacebookIdentityResponseSchema,
|
||||||
|
type FacebookIdentityResponseInput,
|
||||||
|
type FacebookIdentityResponseData,
|
||||||
|
} from "@/data/schemas/auth/facebook_identity_response";
|
||||||
|
|
||||||
|
export class FacebookIdentityResponse {
|
||||||
|
private constructor(input: FacebookIdentityResponseInput) {
|
||||||
|
const data = FacebookIdentityResponseSchema.parse(input);
|
||||||
|
Object.assign(this, data);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(input: FacebookIdentityResponseInput): FacebookIdentityResponse {
|
||||||
|
return new FacebookIdentityResponse(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): FacebookIdentityResponse {
|
||||||
|
return FacebookIdentityResponse.from(
|
||||||
|
json as FacebookIdentityResponseInput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): FacebookIdentityResponseData {
|
||||||
|
return FacebookIdentityResponseSchema.parse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Facebook PSID 登录响应 DTO
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FacebookPsidLoginResponseSchema,
|
||||||
|
type FacebookPsidLoginResponseInput,
|
||||||
|
type FacebookPsidLoginResponseData,
|
||||||
|
} from "@/data/schemas/auth/facebook_psid_login_response";
|
||||||
|
|
||||||
|
export class FacebookPsidLoginResponse {
|
||||||
|
private constructor(input: FacebookPsidLoginResponseInput) {
|
||||||
|
const data = FacebookPsidLoginResponseSchema.parse(input);
|
||||||
|
Object.assign(this, data);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(
|
||||||
|
input: FacebookPsidLoginResponseInput,
|
||||||
|
): FacebookPsidLoginResponse {
|
||||||
|
return new FacebookPsidLoginResponse(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): FacebookPsidLoginResponse {
|
||||||
|
return FacebookPsidLoginResponse.from(
|
||||||
|
json as FacebookPsidLoginResponseInput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): FacebookPsidLoginResponseData {
|
||||||
|
return FacebookPsidLoginResponseSchema.parse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from "./facebook_identity_response";
|
||||||
|
export * from "./facebook_psid_login_response";
|
||||||
export * from "./guest_login_response";
|
export * from "./guest_login_response";
|
||||||
export * from "./login_response";
|
export * from "./login_response";
|
||||||
export * from "./logout_response";
|
export * from "./logout_response";
|
||||||
|
|||||||
@@ -54,10 +54,9 @@ export class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson(): UserData {
|
toJson(): UserData {
|
||||||
const { personalityTraits, ...rest } = this;
|
return UserSchema.parse({
|
||||||
return {
|
...this,
|
||||||
...rest,
|
personalityTraits: this.personalityTraits.toJson(),
|
||||||
personalityTraits: personalityTraits.toJson(),
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FacebookIdentityResponse,
|
||||||
|
FacebookPsidLoginResponse,
|
||||||
|
LoginStatus,
|
||||||
|
} from "@/data/dto/auth";
|
||||||
|
import { AuthRepository } from "@/data/repositories/auth_repository";
|
||||||
|
import type { AuthApi } from "@/data/services/api";
|
||||||
|
import type { IAuthStorage } from "@/data/storage/auth";
|
||||||
|
import type { IUserStorage } from "@/data/storage/user";
|
||||||
|
import { Result } from "@/utils";
|
||||||
|
|
||||||
|
function createRepository(overrides: Partial<AuthApi>) {
|
||||||
|
const storage = {
|
||||||
|
setAsid: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setPsid: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setLoginToken: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setRefreshToken: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setLoginProvider: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setGuestToken: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setDeviceId: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
} as unknown as IAuthStorage;
|
||||||
|
const userStorage = {
|
||||||
|
setUser: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
setUserId: vi.fn(async () => Result.ok(undefined)),
|
||||||
|
} as unknown as IUserStorage;
|
||||||
|
const repository = new AuthRepository(
|
||||||
|
overrides as AuthApi,
|
||||||
|
storage,
|
||||||
|
userStorage,
|
||||||
|
);
|
||||||
|
return { repository, storage, userStorage };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AuthRepository facebook identity", () => {
|
||||||
|
it("binds facebook identity and stores returned ids", async () => {
|
||||||
|
const bindFacebookIdentity = vi.fn(async () =>
|
||||||
|
FacebookIdentityResponse.fromJson({
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
facebookBinding: { conflicts: [], bound: [] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { repository, storage } = createRepository({ bindFacebookIdentity });
|
||||||
|
|
||||||
|
const result = await repository.bindFacebookIdentity({
|
||||||
|
asid: "asid-1",
|
||||||
|
psid: "psid-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(bindFacebookIdentity).toHaveBeenCalledOnce();
|
||||||
|
expect(storage.setAsid).toHaveBeenCalledWith("asid-1");
|
||||||
|
expect(storage.setPsid).toHaveBeenCalledWith("psid-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves real login data from complete psid login responses", async () => {
|
||||||
|
const facebookPsidLogin = vi.fn(async () =>
|
||||||
|
FacebookPsidLoginResponse.fromJson({
|
||||||
|
token: "login-token",
|
||||||
|
refreshToken: "refresh-token",
|
||||||
|
matchedBy: "psid",
|
||||||
|
fbAsid: "asid-1",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
hasCompleteFacebookIdentity: true,
|
||||||
|
isGuest: false,
|
||||||
|
user: { id: "user-1", username: "Elio" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { repository, storage, userStorage } = createRepository({
|
||||||
|
facebookPsidLogin,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await repository.facebookPsidLogin({
|
||||||
|
psid: "psid-1",
|
||||||
|
deviceId: "device-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(Result.ok(LoginStatus.Facebook));
|
||||||
|
expect(storage.setLoginToken).toHaveBeenCalledWith("login-token");
|
||||||
|
expect(storage.setRefreshToken).toHaveBeenCalledWith("refresh-token");
|
||||||
|
expect(storage.setLoginProvider).toHaveBeenCalledWith(LoginStatus.Facebook);
|
||||||
|
expect(userStorage.setUserId).toHaveBeenCalledWith("user-1");
|
||||||
|
expect(storage.setAsid).toHaveBeenCalledWith("asid-1");
|
||||||
|
expect(storage.setPsid).toHaveBeenCalledWith("psid-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves guest data from incomplete psid login responses", async () => {
|
||||||
|
const facebookPsidLogin = vi.fn(async () =>
|
||||||
|
FacebookPsidLoginResponse.fromJson({
|
||||||
|
token: "guest-token",
|
||||||
|
matchedBy: "psid_incomplete",
|
||||||
|
fbPsid: "psid-1",
|
||||||
|
hasCompleteFacebookIdentity: false,
|
||||||
|
isGuest: true,
|
||||||
|
userId: "guest-1",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { repository, storage, userStorage } = createRepository({
|
||||||
|
facebookPsidLogin,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await repository.facebookPsidLogin({
|
||||||
|
psid: "psid-1",
|
||||||
|
deviceId: "device-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(Result.ok(LoginStatus.Guest));
|
||||||
|
expect(storage.setGuestToken).toHaveBeenCalledWith("guest-token");
|
||||||
|
expect(storage.setLoginProvider).toHaveBeenCalledWith(LoginStatus.Guest);
|
||||||
|
expect(storage.setDeviceId).toHaveBeenCalledWith("device-1");
|
||||||
|
expect(userStorage.setUserId).toHaveBeenCalledWith("guest-1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import { ExceptionHandler } from "@/core/errors";
|
import { ExceptionHandler } from "@/core/errors";
|
||||||
import { ApiError, AuthApi, authApi, ErrorCode } from "@/data/services/api";
|
import { ApiError, AuthApi, authApi, ErrorCode } from "@/data/services/api";
|
||||||
import {
|
import {
|
||||||
|
FacebookIdentityRequest,
|
||||||
|
FacebookPsidLoginRequest,
|
||||||
FacebookLoginRequest,
|
FacebookLoginRequest,
|
||||||
FbIdLoginRequest,
|
FbIdLoginRequest,
|
||||||
GoogleLoginRequest,
|
GoogleLoginRequest,
|
||||||
@@ -230,6 +232,73 @@ export class AuthRepository implements IAuthRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 绑定 Facebook ASID / PSID 到当前登录用户。 */
|
||||||
|
async bindFacebookIdentity(input: {
|
||||||
|
asid?: string;
|
||||||
|
psid?: string;
|
||||||
|
}): Promise<Result<void>> {
|
||||||
|
if (!input.asid && !input.psid) return Result.ok(undefined);
|
||||||
|
|
||||||
|
return Result.wrap(async () => {
|
||||||
|
const response = await this.api.bindFacebookIdentity(
|
||||||
|
FacebookIdentityRequest.from({
|
||||||
|
asid: input.asid ?? "",
|
||||||
|
psid: input.psid ?? "",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const data = response.toJson();
|
||||||
|
await this._saveFacebookIdentity({
|
||||||
|
asid: data.fbAsid,
|
||||||
|
psid: data.fbPsid,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过 Facebook PSID 直接登录或返回游客态。 */
|
||||||
|
async facebookPsidLogin(input: {
|
||||||
|
psid: string;
|
||||||
|
deviceId?: string;
|
||||||
|
bindToGuest?: boolean;
|
||||||
|
}): Promise<Result<LoginStatusT>> {
|
||||||
|
return Result.wrap(async () => {
|
||||||
|
const response = await this.api.facebookPsidLogin(
|
||||||
|
FacebookPsidLoginRequest.from({
|
||||||
|
psid: input.psid,
|
||||||
|
deviceId: input.deviceId ?? "",
|
||||||
|
bindToGuest: input.bindToGuest ?? true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const data = response.toJson();
|
||||||
|
await this._saveFacebookIdentity({
|
||||||
|
asid: data.fbAsid,
|
||||||
|
psid: data.fbPsid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.isGuest || !data.hasCompleteFacebookIdentity) {
|
||||||
|
await this.storage.setGuestToken(data.token);
|
||||||
|
await this.storage.setLoginProvider(LoginStatus.Guest);
|
||||||
|
if (input.deviceId) {
|
||||||
|
await this.storage.setDeviceId(input.deviceId);
|
||||||
|
}
|
||||||
|
const userId = data.userId || data.user?.id || "";
|
||||||
|
if (userId) {
|
||||||
|
await this.userStorage.setUserId(userId);
|
||||||
|
}
|
||||||
|
return LoginStatus.Guest;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._saveLoginData(
|
||||||
|
LoginResponse.from({
|
||||||
|
token: data.token,
|
||||||
|
refreshToken: data.refreshToken,
|
||||||
|
user: data.user ?? { id: data.userId },
|
||||||
|
}),
|
||||||
|
LoginStatus.Facebook,
|
||||||
|
);
|
||||||
|
return LoginStatus.Facebook;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新 token:先读本地的 refresh token,空则直接返回错误;
|
* 刷新 token:先读本地的 refresh token,空则直接返回错误;
|
||||||
* 调用 API 成功后写回新的 login token + refresh token。
|
* 调用 API 成功后写回新的 login token + refresh token。
|
||||||
@@ -325,6 +394,24 @@ export class AuthRepository implements IAuthRepository {
|
|||||||
log.warn("[AuthRepository] setUserId failed", r4.error);
|
log.warn("[AuthRepository] setUserId failed", r4.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _saveFacebookIdentity(input: {
|
||||||
|
asid?: string;
|
||||||
|
psid?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (input.asid) {
|
||||||
|
const r = await this.storage.setAsid(input.asid);
|
||||||
|
if (!r.success) {
|
||||||
|
log.warn("[AuthRepository] setAsid failed", r.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input.psid) {
|
||||||
|
const r = await this.storage.setPsid(input.psid);
|
||||||
|
if (!r.success) {
|
||||||
|
log.warn("[AuthRepository] setPsid failed", r.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全局懒单例。 */
|
/** 全局懒单例。 */
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import type { Result } from "@/utils";
|
import type { Result } from "@/utils";
|
||||||
import type {
|
import type {
|
||||||
GuestLoginResponse,
|
GuestLoginResponse,
|
||||||
|
LoginStatus,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
RefreshTokenResponse,
|
RefreshTokenResponse,
|
||||||
} from "@/data/dto/auth";
|
} from "@/data/dto/auth";
|
||||||
@@ -66,6 +67,19 @@ export interface IAuthRepository {
|
|||||||
psid?: string;
|
psid?: string;
|
||||||
}): Promise<Result<LoginResponse>>;
|
}): Promise<Result<LoginResponse>>;
|
||||||
|
|
||||||
|
/** 绑定 Facebook ASID / PSID 到当前登录用户。 */
|
||||||
|
bindFacebookIdentity(input: {
|
||||||
|
asid?: string;
|
||||||
|
psid?: string;
|
||||||
|
}): Promise<Result<void>>;
|
||||||
|
|
||||||
|
/** 通过 Facebook PSID 直接登录或返回游客态。 */
|
||||||
|
facebookPsidLogin(input: {
|
||||||
|
psid: string;
|
||||||
|
deviceId?: string;
|
||||||
|
bindToGuest?: boolean;
|
||||||
|
}): Promise<Result<LoginStatus>>;
|
||||||
|
|
||||||
/** 刷新 token:先读本地的 refresh token,空则返回错误。 */
|
/** 刷新 token:先读本地的 refresh token,空则返回错误。 */
|
||||||
refreshToken(): Promise<Result<RefreshTokenResponse>>;
|
refreshToken(): Promise<Result<RefreshTokenResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Facebook identity 绑定请求
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { stringOrEmpty } from "../nullable-defaults";
|
||||||
|
|
||||||
|
export const FacebookIdentityRequestSchema = z.object({
|
||||||
|
asid: stringOrEmpty,
|
||||||
|
psid: stringOrEmpty,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FacebookIdentityRequestInput = z.input<
|
||||||
|
typeof FacebookIdentityRequestSchema
|
||||||
|
>;
|
||||||
|
export type FacebookIdentityRequestData = z.output<
|
||||||
|
typeof FacebookIdentityRequestSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Facebook identity 绑定响应
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { arrayOrEmpty, stringOrEmpty } from "../nullable-defaults";
|
||||||
|
|
||||||
|
export const FacebookIdentityBindingSchema = z.object({
|
||||||
|
conflicts: arrayOrEmpty(z.unknown()),
|
||||||
|
bound: arrayOrEmpty(z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FacebookIdentityResponseSchema = z.object({
|
||||||
|
fbAsid: stringOrEmpty,
|
||||||
|
fbPsid: stringOrEmpty,
|
||||||
|
facebookBinding: FacebookIdentityBindingSchema.default({
|
||||||
|
conflicts: [],
|
||||||
|
bound: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FacebookIdentityResponseInput = z.input<
|
||||||
|
typeof FacebookIdentityResponseSchema
|
||||||
|
>;
|
||||||
|
export type FacebookIdentityResponseData = z.output<
|
||||||
|
typeof FacebookIdentityResponseSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Facebook PSID 登录请求
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { booleanOrTrue, stringOrEmpty } from "../nullable-defaults";
|
||||||
|
|
||||||
|
export const FacebookPsidLoginRequestSchema = z.object({
|
||||||
|
psid: z.string(),
|
||||||
|
deviceId: stringOrEmpty,
|
||||||
|
bindToGuest: booleanOrTrue,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FacebookPsidLoginRequestInput = z.input<
|
||||||
|
typeof FacebookPsidLoginRequestSchema
|
||||||
|
>;
|
||||||
|
export type FacebookPsidLoginRequestData = z.output<
|
||||||
|
typeof FacebookPsidLoginRequestSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Facebook PSID 登录响应
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
booleanOrFalse,
|
||||||
|
schemaOrNull,
|
||||||
|
stringOrEmpty,
|
||||||
|
} from "../nullable-defaults";
|
||||||
|
import { UserSchema } from "../user/user";
|
||||||
|
|
||||||
|
export const FacebookPsidLoginResponseSchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
refreshToken: stringOrEmpty,
|
||||||
|
matchedBy: stringOrEmpty,
|
||||||
|
fbAsid: stringOrEmpty,
|
||||||
|
fbPsid: stringOrEmpty,
|
||||||
|
hasCompleteFacebookIdentity: booleanOrFalse,
|
||||||
|
isGuest: booleanOrFalse,
|
||||||
|
user: schemaOrNull(UserSchema),
|
||||||
|
userId: stringOrEmpty,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FacebookPsidLoginResponseInput = z.input<
|
||||||
|
typeof FacebookPsidLoginResponseSchema
|
||||||
|
>;
|
||||||
|
export type FacebookPsidLoginResponseData = z.output<
|
||||||
|
typeof FacebookPsidLoginResponseSchema
|
||||||
|
>;
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
* @file Automatically generated by barrelsby.
|
* @file Automatically generated by barrelsby.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./facebook_identity_request";
|
||||||
|
export * from "./facebook_identity_response";
|
||||||
export * from "./facebook_login_request";
|
export * from "./facebook_login_request";
|
||||||
|
export * from "./facebook_psid_login_request";
|
||||||
|
export * from "./facebook_psid_login_response";
|
||||||
export * from "./facebook_user_data";
|
export * from "./facebook_user_data";
|
||||||
export * from "./fb_id_login_request";
|
export * from "./fb_id_login_request";
|
||||||
export * from "./google_login_request";
|
export * from "./google_login_request";
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const UserSchema = z.object({
|
|||||||
platform: stringOrEmpty,
|
platform: stringOrEmpty,
|
||||||
country: stringOrEmpty,
|
country: stringOrEmpty,
|
||||||
countryCode: stringOrEmpty,
|
countryCode: stringOrEmpty,
|
||||||
|
fbAsid: stringOrEmpty,
|
||||||
|
fbPsid: stringOrEmpty,
|
||||||
intimacy: numberOrZero,
|
intimacy: numberOrZero,
|
||||||
dolBalance: numberOrZero,
|
dolBalance: numberOrZero,
|
||||||
creditBalance: numberOrZero,
|
creditBalance: numberOrZero,
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export class ApiPath {
|
|||||||
/** Facebook ID 登录(v7.0 新增) */
|
/** Facebook ID 登录(v7.0 新增) */
|
||||||
static readonly facebookIdLogin = `${ApiPath._auth}/login/facebook/by-id`;
|
static readonly facebookIdLogin = `${ApiPath._auth}/login/facebook/by-id`;
|
||||||
|
|
||||||
|
/** Facebook PSID 登录 */
|
||||||
|
static readonly facebookPsidLogin = `${ApiPath._auth}/login/facebook/psid`;
|
||||||
|
|
||||||
/** 刷新 Token */
|
/** 刷新 Token */
|
||||||
static readonly refresh = `${ApiPath._auth}/refresh`;
|
static readonly refresh = `${ApiPath._auth}/refresh`;
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ export class ApiPath {
|
|||||||
/** 获取当前用户权益快照 */
|
/** 获取当前用户权益快照 */
|
||||||
static readonly userEntitlements = `${ApiPath._user}/entitlements`;
|
static readonly userEntitlements = `${ApiPath._user}/entitlements`;
|
||||||
|
|
||||||
|
/** 绑定 Facebook ASID / PSID */
|
||||||
|
static readonly userFacebookIdentity = `${ApiPath._user}/facebook/identity`;
|
||||||
|
|
||||||
// ============ 支付相关 ============
|
// ============ 支付相关 ============
|
||||||
/** 创建充值订单 */
|
/** 创建充值订单 */
|
||||||
static readonly paymentCreateOrder = `${ApiPath._payment}/create-order`;
|
static readonly paymentCreateOrder = `${ApiPath._payment}/create-order`;
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { httpClient } from "./http_client";
|
|||||||
import { ApiPath } from "./api_path";
|
import { ApiPath } from "./api_path";
|
||||||
import { ApiEnvelope, unwrap } from "./response_helper";
|
import { ApiEnvelope, unwrap } from "./response_helper";
|
||||||
import {
|
import {
|
||||||
|
FacebookIdentityRequest,
|
||||||
|
FacebookIdentityResponse,
|
||||||
FacebookLoginRequest,
|
FacebookLoginRequest,
|
||||||
|
FacebookPsidLoginRequest,
|
||||||
|
FacebookPsidLoginResponse,
|
||||||
FbIdLoginRequest,
|
FbIdLoginRequest,
|
||||||
GoogleLoginRequest,
|
GoogleLoginRequest,
|
||||||
GuestLoginRequest,
|
GuestLoginRequest,
|
||||||
@@ -82,6 +86,36 @@ export class AuthApi {
|
|||||||
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
|
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facebook PSID 登录
|
||||||
|
*/
|
||||||
|
async facebookPsidLogin(
|
||||||
|
body: FacebookPsidLoginRequest,
|
||||||
|
): Promise<FacebookPsidLoginResponse> {
|
||||||
|
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||||
|
ApiPath.facebookPsidLogin,
|
||||||
|
{ method: "POST", body: body.toJson() },
|
||||||
|
);
|
||||||
|
return FacebookPsidLoginResponse.fromJson(
|
||||||
|
unwrap(env) as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 Facebook ASID / PSID 到当前用户
|
||||||
|
*/
|
||||||
|
async bindFacebookIdentity(
|
||||||
|
body: FacebookIdentityRequest,
|
||||||
|
): Promise<FacebookIdentityResponse> {
|
||||||
|
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||||
|
ApiPath.userFacebookIdentity,
|
||||||
|
{ method: "POST", body: body.toJson() },
|
||||||
|
);
|
||||||
|
return FacebookIdentityResponse.fromJson(
|
||||||
|
unwrap(env) as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录(fire-and-forget)
|
* 退出登录(fire-and-forget)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -17,13 +17,17 @@ export * from "../../core/net/interceptor/logging_interceptor";
|
|||||||
export * from "../../core/net/interceptor/token_interceptor";
|
export * from "../../core/net/interceptor/token_interceptor";
|
||||||
// 注:原 ./auth/* 平台适配层已删除(社交登录改由 NextAuth 处理)
|
// 注:原 ./auth/* 平台适配层已删除(社交登录改由 NextAuth 处理)
|
||||||
export * from "../dto/auth/facebook_user_data";
|
export * from "../dto/auth/facebook_user_data";
|
||||||
|
export * from "../dto/auth/request/facebook_identity_request";
|
||||||
export * from "../dto/auth/request/facebook_login_request";
|
export * from "../dto/auth/request/facebook_login_request";
|
||||||
|
export * from "../dto/auth/request/facebook_psid_login_request";
|
||||||
export * from "../dto/auth/request/fb_id_login_request";
|
export * from "../dto/auth/request/fb_id_login_request";
|
||||||
export * from "../dto/auth/request/google_login_request";
|
export * from "../dto/auth/request/google_login_request";
|
||||||
export * from "../dto/auth/request/guest_login_request";
|
export * from "../dto/auth/request/guest_login_request";
|
||||||
export * from "../dto/auth/request/login_request";
|
export * from "../dto/auth/request/login_request";
|
||||||
export * from "../dto/auth/request/refresh_token_request";
|
export * from "../dto/auth/request/refresh_token_request";
|
||||||
export * from "../dto/auth/request/register_request";
|
export * from "../dto/auth/request/register_request";
|
||||||
|
export * from "../dto/auth/response/facebook_identity_response";
|
||||||
|
export * from "../dto/auth/response/facebook_psid_login_response";
|
||||||
export * from "../dto/auth/response/guest_login_response";
|
export * from "../dto/auth/response/guest_login_response";
|
||||||
export * from "../dto/auth/response/login_response";
|
export * from "../dto/auth/response/login_response";
|
||||||
export * from "../dto/auth/response/logout_response";
|
export * from "../dto/auth/response/logout_response";
|
||||||
@@ -39,6 +43,10 @@ export * from "../dto/user/personality_traits";
|
|||||||
export * from "../dto/user/recent_memory";
|
export * from "../dto/user/recent_memory";
|
||||||
export * from "../dto/user/user";
|
export * from "../dto/user/user";
|
||||||
export * from "../schemas/auth/facebook_login_request";
|
export * from "../schemas/auth/facebook_login_request";
|
||||||
|
export * from "../schemas/auth/facebook_identity_request";
|
||||||
|
export * from "../schemas/auth/facebook_identity_response";
|
||||||
|
export * from "../schemas/auth/facebook_psid_login_request";
|
||||||
|
export * from "../schemas/auth/facebook_psid_login_response";
|
||||||
export * from "../schemas/auth/facebook_user_data";
|
export * from "../schemas/auth/facebook_user_data";
|
||||||
export * from "../schemas/auth/fb_id_login_request";
|
export * from "../schemas/auth/fb_id_login_request";
|
||||||
export * from "../schemas/auth/google_login_request";
|
export * from "../schemas/auth/google_login_request";
|
||||||
|
|||||||
@@ -83,16 +83,10 @@ function resolveTargetAlias(
|
|||||||
const target = value.trim().toLowerCase();
|
const target = value.trim().toLowerCase();
|
||||||
|
|
||||||
if (target === "chat") return ROUTES.chat;
|
if (target === "chat") return ROUTES.chat;
|
||||||
if (target === "tip" || target === "tips" || target === "coffee") {
|
if (target === "tip" || target === "tips") {
|
||||||
return ROUTES.tip;
|
return ROUTES.tip;
|
||||||
}
|
}
|
||||||
if (
|
if (target === "private-room") {
|
||||||
target === "private-room" ||
|
|
||||||
target === "private_room" ||
|
|
||||||
target === "privateroom" ||
|
|
||||||
target === "private" ||
|
|
||||||
target === "room"
|
|
||||||
) {
|
|
||||||
return ROUTES.privateRoom;
|
return ROUTES.privateRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,29 +242,60 @@ export const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
// ========== 启动 / 恢复时检查登录态 actor(只查不创) ==========
|
// ========== 启动 / 恢复时检查登录态 actor(只查不创) ==========
|
||||||
|
|
||||||
export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
||||||
// 1. 拿 deviceId(不用于 auto-guest-login,仅供后续登录用)
|
// 1. 拿启动期可能参与身份同步的本地标识。
|
||||||
await deviceIdentifier.getDeviceId();
|
const deviceId = await deviceIdentifier.getDeviceId();
|
||||||
const storage = AuthStorage.getInstance();
|
const storage = AuthStorage.getInstance();
|
||||||
|
const asidR = await storage.getAsid();
|
||||||
|
const psidR = await storage.getPsid();
|
||||||
|
const asid = asidR.success ? asidR.data : null;
|
||||||
|
const psid = psidR.success ? psidR.data : null;
|
||||||
|
const authRepo = getAuthRepository();
|
||||||
|
|
||||||
// 2. 查业务登录 token:loginToken 和 refreshToken 必须同时存在。
|
// 2. 查业务登录 token:loginToken 和 refreshToken 必须同时存在。
|
||||||
if (await hasCompleteBusinessAuthSession(storage)) {
|
if (await hasCompleteBusinessAuthSession(storage)) {
|
||||||
const providerR = await storage.getLoginProvider();
|
const providerR = await storage.getLoginProvider();
|
||||||
if (providerR.success && isBusinessLoginProvider(providerR.data)) {
|
const provider =
|
||||||
return providerR.data;
|
providerR.success && isBusinessLoginProvider(providerR.data)
|
||||||
|
? providerR.data
|
||||||
|
: ("email" as LoginStatus);
|
||||||
|
|
||||||
|
const bindResult = await authRepo.bindFacebookIdentity({
|
||||||
|
asid: asid ?? undefined,
|
||||||
|
psid: psid ?? undefined,
|
||||||
|
});
|
||||||
|
if (Result.isErr(bindResult)) {
|
||||||
|
log.warn(
|
||||||
|
{ err: bindResult.error.message },
|
||||||
|
"[auth-machine] checkAuthStatusActor: bindFacebookIdentity failed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return "email" as LoginStatus;
|
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 外部浏览器深链同步:仅 asid 落地时,用它换业务 token。
|
// 3. 有 PSID 时优先让后端用本地绑定关系直接登录;未匹配则返回游客态。
|
||||||
const asidR = await storage.getAsid();
|
if (psid) {
|
||||||
if (asidR.success && asidR.data) {
|
const psidLoginResult = await authRepo.facebookPsidLogin({
|
||||||
const authRepo = getAuthRepository();
|
|
||||||
const avatarUrlR = await UserStorage.getInstance().getAvatarUrl();
|
|
||||||
const psid = await readPsid();
|
|
||||||
const result = await authRepo.facebookAsidLogin({
|
|
||||||
asid: asidR.data,
|
|
||||||
avatarUrl: avatarUrlR.success ? avatarUrlR.data ?? undefined : undefined,
|
|
||||||
psid,
|
psid,
|
||||||
|
deviceId,
|
||||||
|
bindToGuest: true,
|
||||||
|
});
|
||||||
|
if (Result.isOk(psidLoginResult)) {
|
||||||
|
return psidLoginResult.data;
|
||||||
|
}
|
||||||
|
log.warn(
|
||||||
|
{ err: psidLoginResult.error.message },
|
||||||
|
"[auth-machine] checkAuthStatusActor: facebookPsidLogin failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 无 PSID 或 PSID 直登失败时,继续用 ASID fallback 换业务 token。
|
||||||
|
if (asid) {
|
||||||
|
const avatarUrlR = await UserStorage.getInstance().getAvatarUrl();
|
||||||
|
const result = await authRepo.facebookAsidLogin({
|
||||||
|
asid,
|
||||||
|
avatarUrl: avatarUrlR.success ? avatarUrlR.data ?? undefined : undefined,
|
||||||
|
psid: psid ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Result.isErr(result)) {
|
if (Result.isErr(result)) {
|
||||||
@@ -278,13 +309,13 @@ export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
return "facebook" as LoginStatus;
|
return "facebook" as LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 查 guestToken
|
// 5. 查 guestToken
|
||||||
const guestTokenR = await storage.getGuestToken();
|
const guestTokenR = await storage.getGuestToken();
|
||||||
if (guestTokenR.success && guestTokenR.data) {
|
if (guestTokenR.success && guestTokenR.data) {
|
||||||
return "guest" as LoginStatus;
|
return "guest" as LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口)
|
// 6. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口)
|
||||||
return "notLoggedIn" as LoginStatus;
|
return "notLoggedIn" as LoginStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const toAuthErrorMessage = (error: unknown): string =>
|
|||||||
//
|
//
|
||||||
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
||||||
//
|
//
|
||||||
// 初始化只恢复已有登录态;游客登录必须由用户明确操作触发。
|
// 初始化会恢复已有登录态,并按后端 Facebook identity 规则处理绑定 / PSID 直登。
|
||||||
|
// 普通游客登录仍必须由用户明确操作触发;PSID 未匹配时后端可返回游客态。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* AuthInit 派发器 —— App 启动时读 storage 同步 loginStatus 到状态机
|
* AuthInit 派发器 —— App 启动时恢复登录态并同步 Facebook identity
|
||||||
*
|
*
|
||||||
* 派发时机:仅 mount 时一次(`useEffect` deps = `[dispatch]`,永远不重跑)。
|
* 派发时机:仅 mount 时一次(`useEffect` deps = `[dispatch]`,永远不重跑)。
|
||||||
*
|
*
|
||||||
* 与 <OAuthSessionSync /> 平级:
|
* 与 <OAuthSessionSync /> 平级:
|
||||||
* - AuthStatusChecker: 启动时一次性 init("自己启动时同步")
|
* - AuthStatusChecker: 启动时一次性 init,恢复登录态、绑定 ASID/PSID、尝试 PSID 直登
|
||||||
* - OAuthSessionSync: 持续监听 NextAuth session("别人在写")
|
* - OAuthSessionSync: 持续监听 NextAuth session("别人在写")
|
||||||
*/
|
*/
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|||||||
Reference in New Issue
Block a user