Files
cozsweet-frontend-nextjs/src/data/repositories/auth_repository.ts
T

371 lines
11 KiB
TypeScript

"use client";
import { ExceptionHandler } from "@/core/errors";
import { ApiError, AuthApi, authApi, ErrorCode } from "@/data/services/api";
import {
AppleLoginRequest,
FacebookLoginRequest,
FbIdLoginRequest,
GoogleLoginRequest,
GuestLoginRequest,
GuestLoginResponse,
LoginRequest,
LoginResponse,
LoginStatus,
type LoginStatus as LoginStatusT,
RefreshTokenRequest,
RefreshTokenResponse,
RegisterRequest,
SendCodeRequest,
} 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;
}): 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 ?? "",
}),
),
);
await this._saveLoginData(response, LoginStatus.Email);
return response;
});
}
/** 发送邮箱验证码。 */
async sendCode(email: string): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.sendCode(SendCodeRequest.from({ email }));
});
}
/**
* 退出真实用户登录并恢复游客态。
*
* 注意:业务上不允许从其他登录态退出到未登录态,所以这里不会删除
* 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 {
await this.storage.clearBusinessAuthData();
await this.userStorage.clearUserData();
} catch (e) {
return Result.err(e);
}
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 (hasGuestToken.success && 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);
}
/**
* 游客登录:使用 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,
});
await this.storage.setGuestToken(response.token);
await this.storage.setLoginProvider(LoginStatus.Guest);
await this.storage.setDeviceId(deviceId);
if (response.userId) {
await this.userStorage.setUserId(response.userId);
}
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;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Google, () =>
this.api.googleLogin(
GoogleLoginRequest.from(
withTestAccountFlag({
idToken: input.idToken,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
}),
),
),
);
}
/** Facebook 登录(accessToken 流程)。 */
async facebookLogin(input: {
accessToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookLogin(
FacebookLoginRequest.from(
withTestAccountFlag({
accessToken: input.accessToken,
platform: getAuthPlatform(),
guestId: input.guestId ?? "",
}),
),
),
);
}
/** Facebook ID 登录(v7.0 新增,fbId 流程)。 */
async facebookIdLogin(input: {
fbId: string;
avatarUrl?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookIdLogin(
FbIdLoginRequest.from(
withTestAccountFlag({
fbId: input.fbId,
avatarUrl: input.avatarUrl ?? "",
}),
),
),
);
}
/**
* Apple 登录。
* 注:Dart 端另有一个 `uploadFacebookAvatar` 方法,但当前 TS 端 `AuthApi`
* 尚未实现该端点,故仓库暂不暴露。等 `AuthApi` 补齐后再加。
*/
async appleLogin(identityToken: string): Promise<Result<LoginResponse>> {
return this._socialLogin(LoginStatus.Apple, () =>
this.api.appleLogin(
AppleLoginRequest.from(
withTestAccountFlag({
identityToken,
platform: getAuthPlatform(),
}),
),
),
);
}
/**
* 刷新 token:先读本地的 refresh token,空则直接返回错误;
* 调用 API 成功后写回新的 login token + refresh token。
*/
async refreshToken(): Promise<Result<RefreshTokenResponse>> {
const existing = await this.storage.getRefreshToken();
if (!existing.success || !existing.data) {
return Result.err(
new ApiError(
"HTTP_UNAUTHORIZED",
"No refresh token available",
ErrorCode.httpUnauthorized,
),
);
}
const refreshToken = existing.data;
return Result.wrap(async () => {
const response = await this.api.refreshToken(
RefreshTokenRequest.from({ refreshToken }),
);
await this.storage.setLoginToken(response.token);
if (response.refreshToken) {
await this.storage.setRefreshToken(response.refreshToken);
}
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);
}
}
}
/** 全局懒单例。 */
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();
}