feat(auth): persist login provider for restored sessions

This commit is contained in:
2026-06-30 12:50:18 +08:00
parent 768b24774d
commit a99a6e056d
5 changed files with 67 additions and 12 deletions
+18 -7
View File
@@ -10,6 +10,8 @@ import {
GuestLoginResponse,
LoginRequest,
LoginResponse,
LoginStatus,
type LoginStatus as LoginStatusT,
RefreshTokenRequest,
RefreshTokenResponse,
RegisterRequest,
@@ -79,7 +81,7 @@ export class AuthRepository implements IAuthRepository {
}),
),
);
await this._saveLoginData(response);
await this._saveLoginData(response, LoginStatus.Email);
return response;
});
}
@@ -131,6 +133,7 @@ export class AuthRepository implements IAuthRepository {
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);
@@ -150,7 +153,7 @@ export class AuthRepository implements IAuthRepository {
idToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
return this._socialLogin(LoginStatus.Google, () =>
this.api.googleLogin(
GoogleLoginRequest.from(
withTestAccountFlag({
@@ -168,7 +171,7 @@ export class AuthRepository implements IAuthRepository {
accessToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookLogin(
FacebookLoginRequest.from(
withTestAccountFlag({
@@ -186,7 +189,7 @@ export class AuthRepository implements IAuthRepository {
fbId: string;
avatarUrl?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
return this._socialLogin(LoginStatus.Facebook, () =>
this.api.facebookIdLogin(
FbIdLoginRequest.from(
withTestAccountFlag({
@@ -204,7 +207,7 @@ export class AuthRepository implements IAuthRepository {
* 尚未实现该端点,故仓库暂不暴露。等 `AuthApi` 补齐后再加。
*/
async appleLogin(identityToken: string): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
return this._socialLogin(LoginStatus.Apple, () =>
this.api.appleLogin(
AppleLoginRequest.from(
withTestAccountFlag({
@@ -263,11 +266,12 @@ export class AuthRepository implements IAuthRepository {
* `_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);
await this._saveLoginData(response, provider);
return response;
});
}
@@ -277,11 +281,18 @@ export class AuthRepository implements IAuthRepository {
* 全部 best-effort,错误用 `Logger.warn` 记录但不让登录「失败」——
* 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。
*/
private async _saveLoginData(data: LoginResponse): Promise<void> {
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) {
+26 -3
View File
@@ -11,9 +11,10 @@
* - 浏览器 → `localStorage` driverSSR → memory driverunstorage 自动)
* - 单例挂在 class 静态字段,规避 HMR 复用污染
* - `hasXxx` = `token != null && token.length > 0`,对齐 Dart 端 `isNotEmpty` 语义
* - `clearAuthData` 顺序清理:loginToken → guestToken → refreshToken,任一失败立即返回
* - `clearAuthData` 顺序清理:loginToken → loginProvider → guestToken → refreshToken,任一失败立即返回
*/
import { LoginStatus, type LoginStatus as LoginStatusT } from "@/data/dto/auth";
import { Result, type Result as ResultT, SpAsyncUtil } from "@/utils";
import { StorageKeys } from "../storage_keys";
import type { IAuthStorage } from "./iauth_storage";
@@ -48,6 +49,22 @@ export class AuthStorage implements IAuthStorage {
return SpAsyncUtil.remove(StorageKeys.loginToken);
}
// ---- login provider ----
async getLoginProvider(): Promise<ResultT<LoginStatusT | null>> {
const r = await SpAsyncUtil.getString(StorageKeys.loginProvider);
if (!r.success) return Result.err(r.error);
if (r.data === null || r.data.length === 0) return Result.ok(null);
if (isLoginStatus(r.data)) return Result.ok(r.data);
return Result.ok(null);
}
setLoginProvider(provider: LoginStatusT): Promise<ResultT<void>> {
return SpAsyncUtil.setString(StorageKeys.loginProvider, provider);
}
clearLoginProvider(): Promise<ResultT<void>> {
return SpAsyncUtil.remove(StorageKeys.loginProvider);
}
// ---- guest token ----
getGuestToken(): Promise<ResultT<string | null>> {
@@ -103,10 +120,16 @@ export class AuthStorage implements IAuthStorage {
async clearAuthData(): Promise<ResultT<void>> {
const r1 = await this.clearLoginToken();
if (!r1.success) return r1;
const r2 = await this.clearGuestToken();
const r2 = await this.clearLoginProvider();
if (!r2.success) return r2;
const r3 = await this.clearRefreshToken();
const r3 = await this.clearGuestToken();
if (!r3.success) return r3;
const r4 = await this.clearRefreshToken();
if (!r4.success) return r4;
return this.clearFacebookId();
}
}
function isLoginStatus(value: string): value is LoginStatusT {
return Object.values(LoginStatus).includes(value as LoginStatusT);
}
+6 -1
View File
@@ -4,12 +4,13 @@
* IAuthStorage 接口
*
* 对齐 Dart 端 `IAuthStorage`lib/data/services/storage/iauth_storage.dart):
* - loginToken / guestToken / deviceId / refreshToken / facebookId 五个字段的 CRUD
* - loginToken / loginProvider / guestToken / deviceId / refreshToken / facebookId 字段的 CRUD
* - `clearAuthData()` 批量清空登录态
* - 所有方法返回 `Promise<Result<T>>`,与 Dart `Future<Result<T>>` 对齐
*/
import type { Result } from "@/utils";
import type { LoginStatus } from "@/data/dto/auth";
export interface IAuthStorage {
getLoginToken(): Promise<Result<string | null>>;
@@ -17,6 +18,10 @@ export interface IAuthStorage {
hasLoginToken(): Promise<Result<boolean>>;
clearLoginToken(): Promise<Result<void>>;
getLoginProvider(): Promise<Result<LoginStatus | null>>;
setLoginProvider(provider: LoginStatus): Promise<Result<void>>;
clearLoginProvider(): Promise<Result<void>>;
getGuestToken(): Promise<Result<string | null>>;
setGuestToken(token: string): Promise<Result<void>>;
hasGuestToken(): Promise<Result<boolean>>;
+1
View File
@@ -9,6 +9,7 @@
export const StorageKeys = {
// auth
loginToken: "login_token",
loginProvider: "login_provider",
guestToken: "guest_token",
deviceId: "device_id",
refreshToken: "refresh_token",
+16 -1
View File
@@ -235,7 +235,7 @@ export const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
// 1. deviceIdentifier.getDeviceId() —— 无则生成 + 落盘
// (生产环境使用 fingerprintjs;非生产环境使用本地 UUID)
// (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客)
// 2. AuthStorage.getLoginToken() —— 有 → "email"
// 2. AuthStorage.getLoginToken() —— 有 → 读取 loginProvider,缺省回退 "email"
// 3. AuthStorage.getFacebookId() —— 有 → facebookIdLogin → "facebook"
// 4. AuthStorage.getGuestToken() —— 有 → "guest"
// 5. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口)
@@ -248,6 +248,10 @@ export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
// 2. 查 loginToken
const loginTokenR = await storage.getLoginToken();
if (loginTokenR.success && loginTokenR.data) {
const providerR = await storage.getLoginProvider();
if (providerR.success && isBusinessLoginProvider(providerR.data)) {
return providerR.data;
}
return "email" as LoginStatus;
}
@@ -281,3 +285,14 @@ export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
// 5. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口)
return "notLoggedIn" as LoginStatus;
});
function isBusinessLoginProvider(
provider: LoginStatus | null,
): provider is LoginStatus {
return (
provider === "email" ||
provider === "facebook" ||
provider === "google" ||
provider === "apple"
);
}