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