feat: add native password and sms login
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { create } from 'zustand';
|
||||
import { create, type StoreApi } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import {
|
||||
@@ -64,6 +64,16 @@ type RefreshSessionOptions = {
|
||||
forceRefresh?: boolean;
|
||||
};
|
||||
|
||||
type PasswordLoginInput = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
type MobileLoginInput = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type AuthState = {
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
@@ -80,7 +90,8 @@ type AuthState = {
|
||||
user: AuthUser | null;
|
||||
moduleAccess: ModuleAccess;
|
||||
init: () => Promise<void>;
|
||||
loginWithBrowser: () => Promise<void>;
|
||||
loginWithPassword: (input: PasswordLoginInput) => Promise<void>;
|
||||
loginWithMobile: (input: MobileLoginInput) => Promise<void>;
|
||||
refreshSession: (options?: RefreshSessionOptions) => Promise<string | null>;
|
||||
getValidAccessToken: () => Promise<string | null>;
|
||||
markActivity: () => Promise<void>;
|
||||
@@ -242,6 +253,50 @@ async function readCurrentModuleAccess(fallback: ModuleAccess): Promise<ModuleAc
|
||||
}
|
||||
}
|
||||
|
||||
async function loginViaHost(
|
||||
path: '/api/auth/login' | '/api/auth/mobile-login',
|
||||
input: PasswordLoginInput | MobileLoginInput,
|
||||
set: StoreApi<AuthState>['setState'],
|
||||
): Promise<void> {
|
||||
advanceAuthSessionEpoch();
|
||||
const operationEpoch = authSessionEpoch;
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await hostApiFetch<AuthTokenResponse>(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const session = parseMainSession(response.session);
|
||||
if (!response.success || !response.token || !session) {
|
||||
throw new Error(response.error || 'Login failed');
|
||||
}
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
|
||||
const moduleAccess = await readCurrentModuleAccess({ ...DEFAULT_MODULE_ACCESS });
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
authBase: trimTrailingSlash(DEFAULT_AUTH_BASE),
|
||||
clientId: DEFAULT_CLIENT_ID,
|
||||
...sessionFieldsFromMain(session),
|
||||
user: createUserFromToken(response.token),
|
||||
moduleAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
const terminal = isTerminalAuthError(error);
|
||||
const message = terminal
|
||||
? '登录已过期,请重新授权。'
|
||||
: (error instanceof Error ? error.message : String(error));
|
||||
advanceAuthSessionEpoch();
|
||||
set({ loading: false, error: message, ...getClearedSession() });
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -371,44 +426,9 @@ export const useAuthStore = create<AuthState>()(
|
||||
set({ initialized: true, loading: false, error: null, moduleAccess });
|
||||
},
|
||||
|
||||
loginWithBrowser: async () => {
|
||||
advanceAuthSessionEpoch();
|
||||
const operationEpoch = authSessionEpoch;
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await hostApiFetch<AuthTokenResponse>('/api/auth/browser/start', {
|
||||
method: 'POST',
|
||||
});
|
||||
const session = parseMainSession(response.session);
|
||||
if (!response.success || !response.token || !session) {
|
||||
throw new Error(response.error || 'Browser authorization failed');
|
||||
}
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
loginWithPassword: (input) => loginViaHost('/api/auth/login', input, set),
|
||||
|
||||
const moduleAccess = await readCurrentModuleAccess({ ...DEFAULT_MODULE_ACCESS });
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
|
||||
set({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
authBase: trimTrailingSlash(DEFAULT_AUTH_BASE),
|
||||
clientId: DEFAULT_CLIENT_ID,
|
||||
...sessionFieldsFromMain(session),
|
||||
user: createUserFromToken(response.token),
|
||||
moduleAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
|
||||
const terminal = isTerminalAuthError(error);
|
||||
const message = terminal
|
||||
? '登录已过期,请重新授权。'
|
||||
: (error instanceof Error ? error.message : String(error));
|
||||
advanceAuthSessionEpoch();
|
||||
set({ loading: false, error: message, ...getClearedSession() });
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
},
|
||||
loginWithMobile: (input) => loginViaHost('/api/auth/mobile-login', input, set),
|
||||
|
||||
refreshSession: async (options = {}) => {
|
||||
const state = get();
|
||||
|
||||
Reference in New Issue
Block a user