first commit

This commit is contained in:
Awuqing
2026-03-17 13:29:09 +08:00
commit eadd3f8961
219 changed files with 22394 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { authApi } from '../services/auth';
import { useAuthStore } from './auth';
vi.mock('../services/auth', () => ({
authApi: {
login: vi.fn(),
fetchProfile: vi.fn(),
},
}));
describe('useAuthStore', () => {
beforeEach(() => {
window.localStorage.clear();
useAuthStore.setState({
token: null,
user: null,
hydrated: true,
status: 'idle',
});
vi.clearAllMocks();
});
it('stores token and user after login', async () => {
vi.mocked(authApi.login).mockResolvedValue({
token: 'jwt-token',
user: {
id: 1,
username: 'admin',
displayName: '管理员',
role: 'admin',
},
});
await useAuthStore.getState().login({ username: 'admin', password: 'secret' });
expect(useAuthStore.getState().token).toBe('jwt-token');
expect(useAuthStore.getState().status).toBe('authenticated');
expect(window.localStorage.getItem('backupx-auth-token')).toBe('jwt-token');
});
it('clears state when bootstrap profile request fails', async () => {
useAuthStore.setState({ token: 'expired-token', status: 'idle' });
vi.mocked(authApi.fetchProfile).mockRejectedValue(new Error('unauthorized'));
await useAuthStore.getState().bootstrap();
expect(useAuthStore.getState().token).toBeNull();
expect(useAuthStore.getState().status).toBe('anonymous');
});
});

74
web/src/stores/auth.ts Normal file
View File

@@ -0,0 +1,74 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { fetchProfile, login, setup, type LoginPayload, type SetupPayload, type UserInfo } from '../services/auth'
import { setAccessToken, setUnauthorizedHandler } from '../services/http'
type AuthStatus = 'unknown' | 'loading' | 'anonymous' | 'authenticated'
interface AuthState {
token: string
user: UserInfo | null
status: AuthStatus
bootstrapped: boolean
bootstrap: () => Promise<void>
login: (payload: LoginPayload) => Promise<void>
setup: (payload: SetupPayload) => Promise<void>
logout: () => void
applyAuth: (token: string, user: UserInfo) => void
}
function clearAuthState(set: (partial: Partial<AuthState>) => void) {
setAccessToken('')
set({ token: '', user: null, status: 'anonymous', bootstrapped: true })
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
token: '',
user: null,
status: 'unknown',
bootstrapped: false,
bootstrap: async () => {
const token = get().token
setUnauthorizedHandler(() => {
clearAuthState(set)
})
if (!token) {
setAccessToken('')
set({ status: 'anonymous', bootstrapped: true })
return
}
setAccessToken(token)
set({ status: 'loading' })
try {
const user = await fetchProfile()
set({ user, status: 'authenticated', bootstrapped: true })
} catch {
clearAuthState(set)
}
},
login: async (payload) => {
const result = await login(payload)
get().applyAuth(result.token, result.user)
},
setup: async (payload) => {
const result = await setup(payload)
get().applyAuth(result.token, result.user)
},
logout: () => {
clearAuthState(set)
},
applyAuth: (token, user) => {
setAccessToken(token)
set({ token, user, status: 'authenticated', bootstrapped: true })
},
}),
{
name: 'backupx-auth',
partialize: (state) => ({ token: state.token }),
},
),
)