feat(auth): add facebook identity psid login

This commit is contained in:
2026-07-10 17:07:22 +08:00
parent 2e60bf64e6
commit 53801490e5
28 changed files with 637 additions and 37 deletions
@@ -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) {
+2
View File
@@ -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);
}
}
+2
View File
@@ -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";
+4 -5
View File
@@ -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");
});
});
+87
View File
@@ -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
>;
+4
View File
@@ -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";
+2
View File
@@ -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,
+6
View File
@@ -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`;
+34
View File
@@ -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
* *
+8
View File
@@ -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";
+2 -8
View File
@@ -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;
} }
+48 -17
View File
@@ -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. 查业务登录 tokenloginToken 和 refreshToken 必须同时存在。 // 2. 查业务登录 tokenloginToken 和 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
return "email" as LoginStatus; : ("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",
);
} }
// 3. 外部浏览器深链同步:仅 asid 落地时,用它换业务 token。 return provider;
const asidR = await storage.getAsid(); }
if (asidR.success && asidR.data) {
const authRepo = getAuthRepository(); // 3. 有 PSID 时优先让后端用本地绑定关系直接登录;未匹配则返回游客态。
const avatarUrlR = await UserStorage.getInstance().getAvatarUrl(); if (psid) {
const psid = await readPsid(); const psidLoginResult = await authRepo.facebookPsidLogin({
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;
}); });
+2 -1
View File
@@ -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: {
+2 -2
View File
@@ -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";