feat: implement authentication and health check providers with hooks for user management

This commit is contained in:
GW_MC
2025-12-20 12:27:42 +08:00
parent e59e7ca4c8
commit 7d99a4852b
7 changed files with 165 additions and 3 deletions

View File

@@ -104,7 +104,9 @@ const defaultResponseErrorHandler =
return false;
};
export function useResponseErrorHandler() {
export function useResponseErrorHandler(): {
defaultResponseErrorHandler: ReturnType<typeof defaultResponseErrorHandler>;
} {
const navigate = useNavigate();
const location = useLocation();
return { defaultResponseErrorHandler: defaultResponseErrorHandler(navigate, location) };

View File

@@ -0,0 +1,48 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useAuth } from '../providers/AuthProvider';
import { useApi } from '../providers/ApiProvider';
import { useQuery } from '@tanstack/react-query';
import { useResponseErrorHandler } from './ResponseHelper';
export type EnsureLoggedInResult = {
checking: boolean;
loggedIn: boolean;
};
export function useEnsureLoggedIn(): EnsureLoggedInResult {
const { user, setUser } = useAuth();
const navigate = useNavigate();
const { tanstackApiClient } = useApi();
const { defaultResponseErrorHandler } = useResponseErrorHandler();
const { queryOptions: currentUserQuery } = tanstackApiClient.get('/api/user/me');
const { isFetched, isPending } = useQuery({
...currentUserQuery,
queryFn: async (...args) => {
try {
const data = await currentUserQuery.queryFn!(...args);
setUser({
id: data.id,
name: data.username,
});
return data;
} catch (error) {
if (defaultResponseErrorHandler(error)) return {} as never;
throw error;
}
},
});
useEffect(() => {
if (user) {
navigate('/', { replace: true });
return;
}
}, [user, setUser, navigate]);
return {
checking: isPending,
loggedIn: isFetched && !!user,
};
}

View File

@@ -0,0 +1,56 @@
import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { createContext, use, type PropsWithChildren } from 'react';
import { useApi } from './ApiProvider';
import { useResponseErrorHandler } from '../hooks/ResponseHelper';
import type { Schemas } from '../generated/api-client/api-client';
export type HealthStatus = Schemas.HealthInfo;
export type ApiHealthProviderProps = PropsWithChildren<object>;
export type ApiHealthContextType = {
healthStatus: HealthStatus | undefined;
};
const ApiHealthContext = createContext<ApiHealthContextType | null>(null);
export const ApiHealthProvider: React.FC<ApiHealthProviderProps> = ({ children }) => {
const navigate = useNavigate();
const { tanstackApiClient } = useApi();
const { defaultResponseErrorHandler } = useResponseErrorHandler();
const { queryOptions: healthInfoQuery } = tanstackApiClient.get('/api/health/info');
const { data } = useQuery({
...healthInfoQuery,
queryFn: async (...args) => {
try {
const data = await healthInfoQuery.queryFn!(...args);
if (!data.is_initialized) {
navigate('/init');
}
return data;
} catch (error) {
if (defaultResponseErrorHandler(error)) return {} as never;
throw error;
}
},
});
return (
<ApiHealthContext
value={{
healthStatus: data,
}}
>
{children}
</ApiHealthContext>
);
};
export const useApiHealth = (): ApiHealthContextType => {
const context = use(ApiHealthContext);
if (!context) {
throw new Error('useApiHealth must be used within an ApiHealthProvider');
}
return context;
};

View File

@@ -2,7 +2,6 @@ import { createContext, use, type PropsWithChildren } from 'react';
import { createTanstackApi, createApi } from '../lib/api';
import axios from 'axios';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useNavigate } from 'react-router';
type ApiProviderProps = PropsWithChildren<object>;
type ApiContextType = {

View File

@@ -0,0 +1,47 @@
import { createContext, use, useCallback, useState, type PropsWithChildren } from 'react';
export type User = {
id: string;
name: string;
};
export type AuthProviderProps = PropsWithChildren<object>;
export type AuthContextType = {
setUser: (user: User) => void;
logOut: () => void;
user: User | null;
};
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUserState] = useState<User | null>(null);
const setUser = useCallback((user: User) => {
setUserState(user);
}, []);
const logout = useCallback(() => {
setUserState(null);
}, []);
return (
<AuthContext
value={{
user: user,
logOut: logout,
setUser: setUser,
}}
>
{children}
</AuthContext>
);
};
export function useAuth() {
const context = use(AuthContext);
if (!context) {
throw new Error('useAuth must be used within a AuthProvider');
}
return context;
}

View File

@@ -6,6 +6,8 @@ import AppTheme from './components/theme';
import { ApiProvider } from './providers/ApiProvider';
import { LayoutProvider } from './providers/LayoutProvider';
import { Tooltip } from 'radix-ui';
import { AuthProvider } from './providers/AuthProvider';
import { ApiHealthProvider } from './providers/ApiHealthProvider';
export const links: Route.LinksFunction = () => [];
@@ -17,6 +19,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
{/* Required for react-toastify */}
<style />
</head>
<body>
{children}
@@ -33,7 +37,11 @@ export default function App() {
<ApiProvider>
<Tooltip.Provider delayDuration={250}>
<LayoutProvider>
<Outlet />
<ApiHealthProvider>
<AuthProvider>
<Outlet />
</AuthProvider>
</ApiHealthProvider>
</LayoutProvider>
</Tooltip.Provider>
</ApiProvider>

View File

@@ -2,6 +2,7 @@ import { Box, Button, Card, Flex, Grid, Heading, Text } from '@radix-ui/themes';
import type { Route } from './+types/home';
import TablePlaceholder from '../components/home/TablePlaceholder';
import { useLayout } from '../providers/LayoutProvider';
import { useEnsureLoggedIn } from '../hooks/ensureLoggedIn';
// eslint-disable-next-line no-empty-pattern
export function meta({}: Route.MetaArgs) {
@@ -9,6 +10,7 @@ export function meta({}: Route.MetaArgs) {
}
export default function ProxyHostDemo() {
useEnsureLoggedIn();
const { activeTab } = useLayout();
return (
<Box>