Files
cozsweet-frontend-nextjs/src/data/repositories/auth_repository.ts
T
admin 225d232763 fix(auth): handle logout and refresh results
Propagate storage Result failures through logout, guest restoration, token refresh, and user state actors. Clear invalid sessions after failed automatic refreshes and cover both business and guest flows.
2026-07-13 17:23:54 +08:00

454 lines
14 KiB
TypeScript

"use client";
import { ExceptionHandler } from "@/core/errors";
import { ApiError, AuthApi, authApi, ErrorCode } from "@/data/services/api";
import {
FacebookIdentityRequest,
FacebookPsidLoginRequest,
FacebookLoginRequest,
FbIdLoginRequest,
GoogleLoginRequest,
GuestLoginRequest,
GuestLoginResponse,
LoginRequest,
LoginResponse,
LoginStatus,
type LoginStatus as LoginStatusT,
RefreshTokenRequest,
RefreshTokenResponse,
RegisterRequest,
} from "@/data/dto/auth";
import { User } from "@/data/dto/user";
import {
AppEnvUtil,
PlatformDetector,
Result,
Logger,
deviceIdentifier,
} from "@/utils";
import type { IAuthRepository } from "@/data/repositories/interfaces";
import { AuthStorage, type IAuthStorage } from "@/data/storage/auth";
import { UserStorage, type IUserStorage } from "@/data/storage/user";
import { createLazySingleton } from "./lazy_singleton";
const log = new Logger("DataRepositoriesAuthRepository");
const TEST_ACCOUNT_FLAG = "isTestAccount";
export class AuthRepository implements IAuthRepository {
constructor(
private readonly api: AuthApi,
private readonly storage: IAuthStorage,
private readonly userStorage: IUserStorage,
) {}
// ============ 公共方法 ============
/**
* 用户注册。
* 注册成功不会自动登录(与 Dart 行为一致),调用方需自行调 `emailLogin`。
*/
async register(input: {
username: string;
email: string;
password: string;
guestId?: string;
}): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.register(
RegisterRequest.from(
withTestAccountFlag({
username: input.username,
email: input.email,
password: input.password,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
}),
),
);
});
}
/** 邮箱/用户名 + 密码登录。 */
async emailLogin(input: {
email?: string;
username?: string;
password: string;
guestId?: string;
psid?: string;
}): Promise<Result<LoginResponse>> {
return Result.wrap(async () => {
const email = input.email?.trim() ?? "";
const response = await this.api.emailLogin(
LoginRequest.from(
withTestAccountFlag({
email,
username: input.username,
password: input.password,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
psid: input.psid ?? "",
}),
),
);
await this._saveLoginData(response, LoginStatus.Email);
return response;
});
}
/**
* 退出真实用户登录并恢复游客态。
*
* 注意:业务上不允许从其他登录态退出到未登录态,所以这里不会删除
* guestToken / deviceId。退出后会尝试重新跑游客登录,以便恢复游客 userId;
* 如果游客登录接口失败但本地仍有 guestToken,则降级回本地游客态,避免
* 用户从聊天页被打回 splash。
*/
async logout(): Promise<Result<void>> {
try {
await this.api.logout();
} catch (e) {
log.warn("[AuthRepository] logout API failed, restoring guest anyway", e);
}
try {
const authClearResult = await this.storage.clearBusinessAuthData();
const userClearResult = await this.userStorage.clearUserData();
if (Result.isErr(authClearResult)) return authClearResult;
if (Result.isErr(userClearResult)) return userClearResult;
} catch (e) {
return Result.err(e);
}
try {
const deviceId = await deviceIdentifier.getDeviceId();
const guestResult = await this.guestLogin(deviceId);
if (Result.isOk(guestResult)) return Result.ok(undefined);
const hasGuestToken = await this.storage.hasGuestToken();
if (Result.isErr(hasGuestToken)) return hasGuestToken;
if (hasGuestToken.data) {
const providerResult = await this.storage.setLoginProvider(
LoginStatus.Guest,
);
if (Result.isErr(providerResult)) return providerResult;
log.warn(
"[AuthRepository] guest login failed after logout, falling back to local guest token",
guestResult.error,
);
return Result.ok(undefined);
}
return Result.err(guestResult.error);
} catch (e) {
return Result.err(e);
}
}
/**
* 游客登录:使用 deviceId 换取 guest token。
* 成功后写 guest token + deviceId + userId。
*/
async guestLogin(deviceId: string): Promise<Result<GuestLoginResponse>> {
log.debug("[AuthRepository.guestLogin] API call START", {
deviceIdLength: deviceId.length,
deviceIdPrefix: deviceId.slice(0, 8),
});
return Result.wrap(async () => {
const response = await this.api.guestLogin(
GuestLoginRequest.from(withTestAccountFlag({ deviceId })),
);
log.debug("[AuthRepository.guestLogin] API call SUCCESS (Zod parsed)", {
hasToken: !!response.token,
hasUser: !!response.user,
userLastMessageAt: response.user?.lastMessageAt,
});
const tokenResult = await this.storage.setGuestToken(response.token);
if (Result.isErr(tokenResult)) throw tokenResult.error;
const providerResult = await this.storage.setLoginProvider(
LoginStatus.Guest,
);
if (Result.isErr(providerResult)) throw providerResult.error;
const deviceResult = await this.storage.setDeviceId(deviceId);
if (Result.isErr(deviceResult)) throw deviceResult.error;
if (response.userId) {
const userIdResult = await this.userStorage.setUserId(response.userId);
if (Result.isErr(userIdResult)) throw userIdResult.error;
}
return response;
}).catch((e) => {
const errorResult = ExceptionHandler.handle(e);
log.error("[AuthRepository.guestLogin] API call FAILED", {
errorCode: errorResult.code,
errorMessage: errorResult.message,
});
return Result.err(e);
});
}
/** Google 登录。 */
async googleLogin(input: {
idToken: string;
guestId?: string;
psid?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Google, () =>
this.api.googleLogin(
GoogleLoginRequest.from(
withTestAccountFlag({
idToken: input.idToken,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
psid: input.psid ?? "",
}),
),
),
);
}
/** Facebook 登录(accessToken 流程)。 */
async facebookLogin(input: {
accessToken: string;
guestId?: string;
psid?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookLogin(
FacebookLoginRequest.from(
withTestAccountFlag({
accessToken: input.accessToken,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
psid: input.psid ?? "",
}),
),
),
);
}
/** 通过 ASID 登录。 */
async facebookAsidLogin(input: {
asid: string;
avatarUrl?: string;
psid?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookIdLogin(
FbIdLoginRequest.from(
withTestAccountFlag({
fbId: input.asid,
avatarUrl: input.avatarUrl ?? "",
psid: input.psid ?? "",
}),
),
),
);
}
/** 绑定 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。
*/
async refreshToken(): Promise<Result<RefreshTokenResponse>> {
return Result.wrap(async () => {
const existing = await this.storage.getRefreshToken();
if (Result.isErr(existing)) throw existing.error;
if (!existing.data) {
throw new ApiError(
"HTTP_UNAUTHORIZED",
"No refresh token available",
ErrorCode.httpUnauthorized,
);
}
const response = await this.api.refreshToken(
RefreshTokenRequest.from({ refreshToken: existing.data }),
);
const tokenResult = await this.storage.setLoginToken(response.token);
if (Result.isErr(tokenResult)) throw tokenResult.error;
if (response.refreshToken) {
const refreshTokenResult = await this.storage.setRefreshToken(
response.refreshToken,
);
if (Result.isErr(refreshTokenResult)) throw refreshTokenResult.error;
}
return response;
});
}
/**
* 获取当前登录用户。成功后尽力写本地 User 缓存(offline hydrate),
* 缓存失败用 `Logger.warn` 记录,不影响主流程。
*/
async getCurrentUser(): Promise<Result<User>> {
return Result.wrap(async () => {
const user = await this.api.getCurrentUser();
const writeResult = await this.userStorage.setUser(user.toJson());
if (!writeResult.success) {
log.warn(
"[AuthRepository] failed to cache current user",
writeResult.error,
);
}
return user;
});
}
// ============ 私有助手 ============
/**
* 社交登录的公共包装:调 API → 调 `_saveLoginData` 持久化。
* `_saveLoginData` 内部全部 best-effort,存储写失败不抛错。
*/
private async _socialLogin(
provider: LoginStatusT,
call: () => Promise<LoginResponse>,
): Promise<Result<LoginResponse>> {
return Result.wrap(async () => {
const response = await call();
await this._saveLoginData(response, provider);
return response;
});
}
/**
* 持久化登录态:login token → refresh token → User 对象 → userId。
* 全部 best-effort,错误用 `Logger.warn` 记录但不让登录「失败」——
* 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。
*/
private async _saveLoginData(
data: LoginResponse,
provider: LoginStatusT,
): Promise<void> {
const r1 = await this.storage.setLoginToken(data.token);
if (!r1.success) {
log.warn("[AuthRepository] setLoginToken failed", r1.error);
}
const providerResult = await this.storage.setLoginProvider(provider);
if (!providerResult.success) {
log.warn("[AuthRepository] setLoginProvider failed", providerResult.error);
}
if (data.refreshToken) {
const r2 = await this.storage.setRefreshToken(data.refreshToken);
if (!r2.success) {
log.warn("[AuthRepository] setRefreshToken failed", r2.error);
}
}
const r3 = await this.userStorage.setUser(data.user.toJson());
if (!r3.success) {
log.warn("[AuthRepository] setUser failed", r3.error);
}
const r4 = await this.userStorage.setUserId(data.user.id);
if (!r4.success) {
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);
}
}
}
}
/** 全局懒单例。 */
export const getAuthRepository = createLazySingleton<IAuthRepository>(
() =>
new AuthRepository(
authApi,
AuthStorage.getInstance(),
UserStorage.getInstance(),
),
);
function withTestAccountFlag<T extends Record<string, unknown>>(
payload: T,
): T & { isTestAccount: boolean } {
return {
...payload,
[TEST_ACCOUNT_FLAG]: !AppEnvUtil.isProduction(),
};
}
function getAuthPlatform(): string {
return PlatformDetector.getPlatform();
}