904cb3638a
Relocate the Result type from `@/data/result` to `@/utils/result` and replace the discriminated-union API (`kind`/`value`) with a simpler boolean-based API (`success`/`data`) across all repositories and storage classes for improved readability and consistency.
290 lines
9.3 KiB
TypeScript
290 lines
9.3 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* AuthRepository
|
||
*
|
||
* 认证仓库:编排远程 API + 本地 AuthStorage + UserStorage。
|
||
* 行为对齐原 Dart `lib/data/repositories/auth_repository_impl.dart`:
|
||
* - 登录成功后调 `_saveLoginData` 持久化 token + User
|
||
* - 登出先调 API,再**强制**清本地态(无论 API 成败)
|
||
* - `getCurrentUser` 成功后**尽力**写本地 User 缓存(不因缓存失败影响主流程)
|
||
* - 写 storage 失败用 `console.warn` 记录,不让本地缓存失败影响主流程
|
||
*
|
||
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
|
||
*/
|
||
import { AuthApi, authApi } from "@/data/api";
|
||
import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request";
|
||
import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request";
|
||
import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request";
|
||
import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request";
|
||
import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
|
||
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
|
||
import { LoginRequest } from "@/data/dto/auth/login_request";
|
||
import { LoginResponse } from "@/data/dto/auth/login_response";
|
||
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
|
||
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
||
import { RegisterRequest } from "@/data/dto/auth/register_request";
|
||
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
|
||
import { User } from "@/data/dto/user/user";
|
||
import { Result } from "@/utils/result";
|
||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
|
||
import { UserStorage } from "@/data/storage/user/user_storage";
|
||
import type { IUserStorage } from "@/data/storage/user/iuser_storage";
|
||
|
||
/** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */
|
||
const WEB_PLATFORM = "web";
|
||
|
||
export class AuthRepository {
|
||
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({
|
||
username: input.username,
|
||
email: input.email,
|
||
password: input.password,
|
||
platform: WEB_PLATFORM,
|
||
guestId: input.guestId ?? "",
|
||
}),
|
||
);
|
||
});
|
||
}
|
||
|
||
/** 邮箱/用户名 + 密码登录。 */
|
||
async emailLogin(input: {
|
||
email?: string;
|
||
username?: string;
|
||
password: string;
|
||
guestId?: string;
|
||
}): Promise<Result<LoginResponse>> {
|
||
return Result.wrap(async () => {
|
||
const response = await this.api.emailLogin(
|
||
LoginRequest.from({
|
||
email: input.email ?? "",
|
||
username: input.username ?? "",
|
||
password: input.password,
|
||
platform: WEB_PLATFORM,
|
||
guestId: input.guestId ?? "",
|
||
}),
|
||
);
|
||
await this._saveLoginData(response);
|
||
return response;
|
||
});
|
||
}
|
||
|
||
/** 发送邮箱验证码。 */
|
||
async sendCode(email: string): Promise<Result<void>> {
|
||
return Result.wrap(async () => {
|
||
await this.api.sendCode(SendCodeRequest.from({ email }));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。
|
||
* 强制清本地的逻辑:用户即使在 API 失败时也想从设备上登出。
|
||
*/
|
||
async logout(): Promise<Result<void>> {
|
||
try {
|
||
await this.api.logout();
|
||
} catch (e) {
|
||
// API 失败也要继续清本地态
|
||
console.warn("[AuthRepository] logout API failed, clearing local anyway", e);
|
||
}
|
||
try {
|
||
await this.storage.clearAuthData();
|
||
await this.userStorage.clearUserData();
|
||
return Result.ok(undefined);
|
||
} catch (e) {
|
||
// 清本地是 best-effort;理论上 LS.remove 不应失败
|
||
return Result.err(e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 游客登录:使用 deviceId 换取 guest token。
|
||
* 成功后写 guest token + deviceId + userId。
|
||
*/
|
||
async guestLogin(deviceId: string): Promise<Result<GuestLoginResponse>> {
|
||
return Result.wrap(async () => {
|
||
const response = await this.api.guestLogin(
|
||
GuestLoginRequest.from({ deviceId }),
|
||
);
|
||
await this.storage.setGuestToken(response.token);
|
||
await this.storage.setDeviceId(deviceId);
|
||
if (response.userId) {
|
||
await this.userStorage.setUserId(response.userId);
|
||
}
|
||
return response;
|
||
});
|
||
}
|
||
|
||
/** Google 登录。 */
|
||
async googleLogin(input: {
|
||
idToken: string;
|
||
guestId?: string;
|
||
}): Promise<Result<LoginResponse>> {
|
||
return this._socialLogin(() =>
|
||
this.api.googleLogin(
|
||
GoogleLoginRequest.from({
|
||
idToken: input.idToken,
|
||
platform: WEB_PLATFORM,
|
||
guestId: input.guestId ?? "",
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
/** Facebook 登录(accessToken 流程)。 */
|
||
async facebookLogin(input: {
|
||
accessToken: string;
|
||
guestId?: string;
|
||
}): Promise<Result<LoginResponse>> {
|
||
return this._socialLogin(() =>
|
||
this.api.facebookLogin(
|
||
FacebookLoginRequest.from({
|
||
accessToken: input.accessToken,
|
||
platform: WEB_PLATFORM,
|
||
guestId: input.guestId ?? "",
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
/** Facebook ID 登录(v7.0 新增,fbId 流程)。 */
|
||
async facebookIdLogin(input: {
|
||
fbId: string;
|
||
avatarUrl?: string;
|
||
}): Promise<Result<LoginResponse>> {
|
||
return this._socialLogin(() =>
|
||
this.api.facebookIdLogin(
|
||
FbIdLoginRequest.from({
|
||
fbId: input.fbId,
|
||
avatarUrl: input.avatarUrl ?? "",
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Apple 登录。
|
||
* 注:Dart 端另有一个 `uploadFacebookAvatar` 方法,但当前 TS 端 `AuthApi`
|
||
* 尚未实现该端点,故仓库暂不暴露。等 `AuthApi` 补齐后再加。
|
||
*/
|
||
async appleLogin(identityToken: string): Promise<Result<LoginResponse>> {
|
||
return this._socialLogin(() =>
|
||
this.api.appleLogin(
|
||
AppleLoginRequest.from({
|
||
identityToken,
|
||
platform: WEB_PLATFORM,
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 刷新 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 Error("No refresh token available"));
|
||
}
|
||
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),
|
||
* 缓存失败用 `console.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) {
|
||
console.warn(
|
||
"[AuthRepository] failed to cache current user",
|
||
writeResult.error,
|
||
);
|
||
}
|
||
return user;
|
||
});
|
||
}
|
||
|
||
// ============ 私有助手 ============
|
||
|
||
/**
|
||
* 社交登录的公共包装:调 API → 调 `_saveLoginData` 持久化。
|
||
* `_saveLoginData` 内部全部 best-effort,存储写失败不抛错。
|
||
*/
|
||
private async _socialLogin(
|
||
call: () => Promise<LoginResponse>,
|
||
): Promise<Result<LoginResponse>> {
|
||
return Result.wrap(async () => {
|
||
const response = await call();
|
||
await this._saveLoginData(response);
|
||
return response;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 持久化登录态:login token → refresh token → User 对象 → userId。
|
||
* 全部 best-effort,错误用 `console.warn` 记录但不让登录「失败」——
|
||
* 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。
|
||
*/
|
||
private async _saveLoginData(data: LoginResponse): Promise<void> {
|
||
const r1 = await this.storage.setLoginToken(data.token);
|
||
if (!r1.success) {
|
||
console.warn("[AuthRepository] setLoginToken failed", r1.error);
|
||
}
|
||
if (data.refreshToken) {
|
||
const r2 = await this.storage.setRefreshToken(data.refreshToken);
|
||
if (!r2.success) {
|
||
console.warn("[AuthRepository] setRefreshToken failed", r2.error);
|
||
}
|
||
}
|
||
const r3 = await this.userStorage.setUser(data.user.toJson());
|
||
if (!r3.success) {
|
||
console.warn("[AuthRepository] setUser failed", r3.error);
|
||
}
|
||
const r4 = await this.userStorage.setUserId(data.user.id);
|
||
if (!r4.success) {
|
||
console.warn("[AuthRepository] setUserId failed", r4.error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 全局单例。 */
|
||
export const authRepository = new AuthRepository(
|
||
authApi,
|
||
AuthStorage.getInstance(),
|
||
UserStorage.getInstance(),
|
||
);
|