feat(auth): add facebook identity psid login
This commit is contained in:
@@ -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 { 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>>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user