371 lines
11 KiB
TypeScript
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();
|
|
}
|