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,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 { ApiError, AuthApi, authApi, ErrorCode } from "@/data/services/api";
import {
FacebookIdentityRequest,
FacebookPsidLoginRequest,
FacebookLoginRequest,
FbIdLoginRequest,
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,空则直接返回错误;
* 调用 API 成功后写回新的 login token + refresh token。
@@ -325,6 +394,24 @@ export class AuthRepository implements IAuthRepository {
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 {
GuestLoginResponse,
LoginStatus,
LoginResponse,
RefreshTokenResponse,
} from "@/data/dto/auth";
@@ -66,6 +67,19 @@ export interface IAuthRepository {
psid?: string;
}): 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,空则返回错误。 */
refreshToken(): Promise<Result<RefreshTokenResponse>>;