feat(auth): add Messenger topup handoff entry
Docker Image / Build and Push Docker Image (push) Successful in 2m9s
Docker Image / Build and Push Docker Image (push) Successful in 2m9s
This commit is contained in:
@@ -3,8 +3,6 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { AuthRepository } from "@/data/repositories/auth_repository";
|
||||
import {
|
||||
FacebookIdentityResponseSchema,
|
||||
FacebookPsidLoginResponseSchema,
|
||||
LoginStatus,
|
||||
} from "@/data/schemas/auth";
|
||||
import type { AuthApi } from "@/data/services/api";
|
||||
import type { IAuthStorage } from "@/data/storage/auth";
|
||||
@@ -54,85 +52,4 @@ describe("AuthRepository facebook identity", () => {
|
||||
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 () =>
|
||||
FacebookPsidLoginResponseSchema.parse({
|
||||
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 () =>
|
||||
FacebookPsidLoginResponseSchema.parse({
|
||||
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");
|
||||
});
|
||||
|
||||
it("does not report guest psid login success when session persistence fails", async () => {
|
||||
const storageError = new Error("guest token write failed");
|
||||
const facebookPsidLogin = vi.fn(async () =>
|
||||
FacebookPsidLoginResponseSchema.parse({
|
||||
token: "guest-token",
|
||||
matchedBy: "psid_incomplete",
|
||||
fbPsid: "psid-1",
|
||||
hasCompleteFacebookIdentity: false,
|
||||
isGuest: true,
|
||||
userId: "guest-1",
|
||||
}),
|
||||
);
|
||||
const { repository, storage } = createRepository({ facebookPsidLogin });
|
||||
vi.mocked(storage.setGuestToken).mockResolvedValueOnce(
|
||||
Result.err(storageError),
|
||||
);
|
||||
|
||||
const result = await repository.facebookPsidLogin({ psid: "psid-1" });
|
||||
|
||||
expect(Result.isErr(result)).toBe(true);
|
||||
expect(storage.setLoginProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,4 +239,27 @@ describe("AuthRepository session Result handling", () => {
|
||||
|
||||
expect(result).toEqual(Result.ok(loginResponse.user));
|
||||
});
|
||||
|
||||
it("persists a top-up handoff as a formal Messenger session", async () => {
|
||||
const handoffResponse = {
|
||||
...loginResponse,
|
||||
loginStatus: LoginStatus.FacebookMessenger,
|
||||
merged: false,
|
||||
} as const;
|
||||
const { repository, storage, userStorage } = createRepository({
|
||||
api: {
|
||||
consumeTopUpHandoff: vi.fn(async () => handoffResponse),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await repository.consumeTopUpHandoff("x".repeat(32));
|
||||
|
||||
expect(result).toEqual(Result.ok(LoginStatus.FacebookMessenger));
|
||||
expect(storage.setLoginToken).toHaveBeenCalledWith("login-token");
|
||||
expect(storage.setRefreshToken).toHaveBeenCalledWith("refresh-token");
|
||||
expect(userStorage.setUserId).toHaveBeenCalledWith("user-1");
|
||||
expect(storage.setLoginProvider).toHaveBeenCalledWith(
|
||||
LoginStatus.FacebookMessenger,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,18 +3,17 @@ import type { IAuthRepository } from "@/data/repositories/interfaces";
|
||||
import {
|
||||
FacebookIdentityRequestSchema,
|
||||
FacebookLoginRequestSchema,
|
||||
FacebookPsidLoginRequestSchema,
|
||||
FbIdLoginRequestSchema,
|
||||
GoogleLoginRequestSchema,
|
||||
GuestLoginRequestSchema,
|
||||
GuestLoginResponse,
|
||||
LoginRequestSchema,
|
||||
LoginResponse,
|
||||
LoginResponseSchema,
|
||||
LoginStatus,
|
||||
RefreshTokenRequestSchema,
|
||||
RefreshTokenResponse,
|
||||
RegisterRequestSchema,
|
||||
TopUpHandoffRequestSchema,
|
||||
type LoginStatus as LoginStatusT,
|
||||
} from "@/data/schemas/auth";
|
||||
import { User } from "@/data/schemas/user";
|
||||
@@ -264,54 +263,16 @@ export class AuthRepository implements IAuthRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/** 通过 Facebook PSID 直接登录或返回游客态。 */
|
||||
async facebookPsidLogin(input: {
|
||||
psid: string;
|
||||
deviceId?: string;
|
||||
bindToGuest?: boolean;
|
||||
}): Promise<Result<LoginStatusT>> {
|
||||
/** 消费一次性充值凭证并按后端返回的 canonical 账号建立正式会话。 */
|
||||
async consumeTopUpHandoff(
|
||||
handoffToken: string,
|
||||
): Promise<Result<LoginStatusT>> {
|
||||
return Result.wrap(async () => {
|
||||
const response = await this.api.facebookPsidLogin(
|
||||
FacebookPsidLoginRequestSchema.parse({
|
||||
psid: input.psid,
|
||||
deviceId: input.deviceId ?? "",
|
||||
bindToGuest: input.bindToGuest ?? true,
|
||||
}),
|
||||
const response = await this.api.consumeTopUpHandoff(
|
||||
TopUpHandoffRequestSchema.parse({ handoffToken }),
|
||||
);
|
||||
const data = response;
|
||||
await this._saveFacebookIdentity({
|
||||
asid: data.fbAsid,
|
||||
psid: data.fbPsid,
|
||||
});
|
||||
|
||||
if (data.isGuest || !data.hasCompleteFacebookIdentity) {
|
||||
const tokenResult = await this.storage.setGuestToken(data.token);
|
||||
if (Result.isErr(tokenResult)) throw tokenResult.error;
|
||||
if (input.deviceId) {
|
||||
const deviceResult = await this.storage.setDeviceId(input.deviceId);
|
||||
if (Result.isErr(deviceResult)) throw deviceResult.error;
|
||||
}
|
||||
const userId = data.userId || data.user?.id || "";
|
||||
if (userId) {
|
||||
const userIdResult = await this.userStorage.setUserId(userId);
|
||||
if (Result.isErr(userIdResult)) throw userIdResult.error;
|
||||
}
|
||||
const providerResult = await this.storage.setLoginProvider(
|
||||
LoginStatus.Guest,
|
||||
);
|
||||
if (Result.isErr(providerResult)) throw providerResult.error;
|
||||
return LoginStatus.Guest;
|
||||
}
|
||||
|
||||
await this._saveLoginData(
|
||||
LoginResponseSchema.parse({
|
||||
token: data.token,
|
||||
refreshToken: data.refreshToken,
|
||||
user: data.user ?? { id: data.userId },
|
||||
}),
|
||||
LoginStatus.Facebook,
|
||||
);
|
||||
return LoginStatus.Facebook;
|
||||
await this._saveLoginData(response, response.loginStatus);
|
||||
return response.loginStatus;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,12 +71,8 @@ export interface IAuthRepository {
|
||||
psid?: string;
|
||||
}): Promise<Result<void>>;
|
||||
|
||||
/** 通过 Facebook PSID 直接登录或返回游客态。 */
|
||||
facebookPsidLogin(input: {
|
||||
psid: string;
|
||||
deviceId?: string;
|
||||
bindToGuest?: boolean;
|
||||
}): Promise<Result<LoginStatus>>;
|
||||
/** 消费一次性充值登录凭证并建立正式会话。 */
|
||||
consumeTopUpHandoff(handoffToken: string): Promise<Result<LoginStatus>>;
|
||||
|
||||
/** 刷新 token:先读本地的 refresh token,空则返回错误。 */
|
||||
refreshToken(): Promise<Result<RefreshTokenResponse>>;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
FacebookIdentityResponseSchema,
|
||||
FacebookPsidLoginResponseSchema,
|
||||
LoginRequestSchema,
|
||||
TopUpHandoffResponseSchema,
|
||||
} from "@/data/schemas/auth";
|
||||
import { UserSchema } from "@/data/schemas/user/user";
|
||||
|
||||
@@ -28,41 +28,6 @@ describe("facebook identity schema models", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses facebook psid login responses for real users and guests", () => {
|
||||
expect(
|
||||
FacebookPsidLoginResponseSchema.parse({
|
||||
token: "login-token",
|
||||
refreshToken: "refresh-token",
|
||||
matchedBy: "psid",
|
||||
fbAsid: "asid-1",
|
||||
fbPsid: "psid-1",
|
||||
hasCompleteFacebookIdentity: true,
|
||||
isGuest: false,
|
||||
user: { id: "user-1", username: "Elio" },
|
||||
}),
|
||||
).toMatchObject({
|
||||
isGuest: false,
|
||||
hasCompleteFacebookIdentity: true,
|
||||
user: { id: "user-1" },
|
||||
});
|
||||
|
||||
expect(
|
||||
FacebookPsidLoginResponseSchema.parse({
|
||||
token: "guest-token",
|
||||
matchedBy: "guest",
|
||||
fbPsid: "psid-1",
|
||||
hasCompleteFacebookIdentity: false,
|
||||
isGuest: true,
|
||||
userId: "guest-1",
|
||||
}),
|
||||
).toMatchObject({
|
||||
refreshToken: "",
|
||||
isGuest: true,
|
||||
userId: "guest-1",
|
||||
user: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps psid in request serialization without a public field declaration", () => {
|
||||
const request = LoginRequestSchema.parse({
|
||||
email: "user@example.com",
|
||||
@@ -75,6 +40,21 @@ describe("facebook identity schema models", () => {
|
||||
expect(request).toMatchObject({ psid: "psid-1" });
|
||||
});
|
||||
|
||||
it("parses a formal Messenger top-up handoff session", () => {
|
||||
expect(
|
||||
TopUpHandoffResponseSchema.parse({
|
||||
token: "login-token",
|
||||
refreshToken: "refresh-token",
|
||||
loginStatus: "facebookMessenger",
|
||||
merged: false,
|
||||
user: { id: "user-1", username: "Messenger User" },
|
||||
}),
|
||||
).toMatchObject({
|
||||
loginStatus: "facebookMessenger",
|
||||
user: { id: "user-1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts facebook identity fields in user schema", () => {
|
||||
expect(
|
||||
UserSchema.parse({
|
||||
|
||||
@@ -6,16 +6,16 @@ export * from "./facebook_user_data";
|
||||
export * from "./login_status";
|
||||
export * from "./request/facebook_identity_request";
|
||||
export * from "./request/facebook_login_request";
|
||||
export * from "./request/facebook_psid_login_request";
|
||||
export * from "./request/fb_id_login_request";
|
||||
export * from "./request/google_login_request";
|
||||
export * from "./request/guest_login_request";
|
||||
export * from "./request/login_request";
|
||||
export * from "./request/refresh_token_request";
|
||||
export * from "./request/register_request";
|
||||
export * from "./request/top_up_handoff_request";
|
||||
export * from "./response/facebook_identity_response";
|
||||
export * from "./response/facebook_psid_login_response";
|
||||
export * from "./response/guest_login_response";
|
||||
export * from "./response/login_response";
|
||||
export * from "./response/logout_response";
|
||||
export * from "./response/refresh_token_response";
|
||||
export * from "./response/top_up_handoff_response";
|
||||
|
||||
@@ -3,6 +3,7 @@ export const LoginStatus = {
|
||||
NotLoggedIn: "notLoggedIn",
|
||||
Guest: "guest",
|
||||
Facebook: "facebook",
|
||||
FacebookMessenger: "facebookMessenger",
|
||||
Google: "google",
|
||||
Email: "email",
|
||||
} as const;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Facebook PSID 登录请求
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
import { booleanOrTrue, stringOrEmpty } from "../../nullable-defaults";
|
||||
|
||||
export const FacebookPsidLoginRequestSchema = z
|
||||
.object({
|
||||
psid: z.string(),
|
||||
deviceId: stringOrEmpty,
|
||||
bindToGuest: booleanOrTrue,
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export type FacebookPsidLoginRequestInput = z.input<
|
||||
typeof FacebookPsidLoginRequestSchema
|
||||
>;
|
||||
export type FacebookPsidLoginRequestData = z.output<
|
||||
typeof FacebookPsidLoginRequestSchema
|
||||
>;
|
||||
export type FacebookPsidLoginRequest = FacebookPsidLoginRequestData;
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
export * from "./facebook_identity_request";
|
||||
export * from "./facebook_login_request";
|
||||
export * from "./facebook_psid_login_request";
|
||||
export * from "./fb_id_login_request";
|
||||
export * from "./google_login_request";
|
||||
export * from "./guest_login_request";
|
||||
export * from "./login_request";
|
||||
export * from "./refresh_token_request";
|
||||
export * from "./register_request";
|
||||
export * from "./top_up_handoff_request";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TopUpHandoffRequestSchema = z
|
||||
.object({
|
||||
handoffToken: z.string().min(32).max(512),
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export type TopUpHandoffRequest = z.output<
|
||||
typeof TopUpHandoffRequestSchema
|
||||
>;
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export type FacebookPsidLoginResponseInput = z.input<
|
||||
typeof FacebookPsidLoginResponseSchema
|
||||
>;
|
||||
export type FacebookPsidLoginResponseData = z.output<
|
||||
typeof FacebookPsidLoginResponseSchema
|
||||
>;
|
||||
|
||||
export type FacebookPsidLoginResponse = FacebookPsidLoginResponseData;
|
||||
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
|
||||
export * from "./facebook_identity_response";
|
||||
export * from "./facebook_psid_login_response";
|
||||
export * from "./guest_login_response";
|
||||
export * from "./login_response";
|
||||
export * from "./logout_response";
|
||||
export * from "./refresh_token_response";
|
||||
export * from "./top_up_handoff_response";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { stringOrEmpty } from "../../nullable-defaults";
|
||||
import { UserSchema } from "../../user/user";
|
||||
|
||||
export const TopUpHandoffResponseSchema = z
|
||||
.object({
|
||||
user: UserSchema,
|
||||
token: z.string(),
|
||||
refreshToken: stringOrEmpty,
|
||||
loginStatus: z.enum([
|
||||
"email",
|
||||
"google",
|
||||
"facebook",
|
||||
"facebookMessenger",
|
||||
]),
|
||||
merged: z.boolean().default(false),
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export type TopUpHandoffResponse = z.output<
|
||||
typeof TopUpHandoffResponseSchema
|
||||
>;
|
||||
@@ -5,7 +5,7 @@
|
||||
"googleLogin": { "method": "post", "path": "/api/auth/login/google" },
|
||||
"facebookLogin": { "method": "post", "path": "/api/auth/login/facebook" },
|
||||
"facebookIdLogin": { "method": "post", "path": "/api/auth/login/facebook/by-id" },
|
||||
"facebookPsidLogin": { "method": "post", "path": "/api/auth/login/facebook/psid" },
|
||||
"topUpHandoffConsume": { "method": "post", "path": "/api/auth/handoff/topup/consume" },
|
||||
"refresh": { "method": "post", "path": "/api/auth/refresh" },
|
||||
"logout": { "method": "post", "path": "/api/auth/logout" },
|
||||
"getCurrentUser": { "method": "get", "path": "/api/auth/me" },
|
||||
|
||||
@@ -27,8 +27,8 @@ export class ApiPath {
|
||||
/** Facebook ID 登录(v7.0 新增) */
|
||||
static readonly facebookIdLogin = apiContract.facebookIdLogin.path;
|
||||
|
||||
/** Facebook PSID 登录 */
|
||||
static readonly facebookPsidLogin = apiContract.facebookPsidLogin.path;
|
||||
/** 消费一次性充值登录凭证 */
|
||||
static readonly topUpHandoffConsume = apiContract.topUpHandoffConsume.path;
|
||||
|
||||
/** 刷新 Token */
|
||||
static readonly refresh = apiContract.refresh.path;
|
||||
|
||||
@@ -11,9 +11,6 @@ import {
|
||||
FacebookIdentityResponse,
|
||||
FacebookIdentityResponseSchema,
|
||||
FacebookLoginRequest,
|
||||
FacebookPsidLoginRequest,
|
||||
FacebookPsidLoginResponse,
|
||||
FacebookPsidLoginResponseSchema,
|
||||
FbIdLoginRequest,
|
||||
GoogleLoginRequest,
|
||||
GuestLoginRequest,
|
||||
@@ -26,6 +23,9 @@ import {
|
||||
RefreshTokenResponse,
|
||||
RefreshTokenResponseSchema,
|
||||
RegisterRequest,
|
||||
TopUpHandoffRequest,
|
||||
TopUpHandoffResponse,
|
||||
TopUpHandoffResponseSchema,
|
||||
} from "@/data/schemas/auth";
|
||||
import { User, UserSchema } from "@/data/schemas/user";
|
||||
import type { UserData } from "@/data/schemas/user/user";
|
||||
@@ -91,17 +91,15 @@ export class AuthApi {
|
||||
return LoginResponseSchema.parse(unwrap(env) as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook PSID 登录
|
||||
*/
|
||||
async facebookPsidLogin(
|
||||
body: FacebookPsidLoginRequest,
|
||||
): Promise<FacebookPsidLoginResponse> {
|
||||
/** 消费一次性充值登录凭证。当前正式登录 token 会由拦截器自动携带。 */
|
||||
async consumeTopUpHandoff(
|
||||
body: TopUpHandoffRequest,
|
||||
): Promise<TopUpHandoffResponse> {
|
||||
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||
ApiPath.facebookPsidLogin,
|
||||
ApiPath.topUpHandoffConsume,
|
||||
{ method: "POST", body },
|
||||
);
|
||||
return FacebookPsidLoginResponseSchema.parse(
|
||||
return TopUpHandoffResponseSchema.parse(
|
||||
unwrap(env) as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ export * from "../../core/net/interceptor/token_interceptor";
|
||||
export * from "../schemas/auth/request/facebook_login_request";
|
||||
export * from "../schemas/auth/request/facebook_identity_request";
|
||||
export * from "../schemas/auth/response/facebook_identity_response";
|
||||
export * from "../schemas/auth/request/facebook_psid_login_request";
|
||||
export * from "../schemas/auth/response/facebook_psid_login_response";
|
||||
export * from "../schemas/auth/facebook_user_data";
|
||||
export * from "../schemas/auth/request/fb_id_login_request";
|
||||
export * from "../schemas/auth/request/google_login_request";
|
||||
@@ -31,6 +29,8 @@ export * from "../schemas/auth/response/logout_response";
|
||||
export * from "../schemas/auth/request/refresh_token_request";
|
||||
export * from "../schemas/auth/response/refresh_token_response";
|
||||
export * from "../schemas/auth/request/register_request";
|
||||
export * from "../schemas/auth/request/top_up_handoff_request";
|
||||
export * from "../schemas/auth/response/top_up_handoff_response";
|
||||
export * from "../schemas/chat/response/chat_history_response";
|
||||
export * from "../schemas/chat/chat_message";
|
||||
export * from "../schemas/chat/response/chat_send_response";
|
||||
|
||||
Reference in New Issue
Block a user