Files
cozsweet-frontend-nextjs/src/data/repositories/auth_repository.ts
T
admin 904cb3638a refactor(data): move Result utility to utils and simplify API
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.
2026-06-09 10:34:40 +08:00

290 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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(),
);