feature/frontend-login #10
@@ -104,7 +104,9 @@ const defaultResponseErrorHandler =
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useResponseErrorHandler() {
|
export function useResponseErrorHandler(): {
|
||||||
|
defaultResponseErrorHandler: ReturnType<typeof defaultResponseErrorHandler>;
|
||||||
|
} {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return { defaultResponseErrorHandler: defaultResponseErrorHandler(navigate, location) };
|
return { defaultResponseErrorHandler: defaultResponseErrorHandler(navigate, location) };
|
||||||
|
|||||||
48
apps/frontend/app/hooks/ensureLoggedIn.tsx
Normal file
48
apps/frontend/app/hooks/ensureLoggedIn.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
56
apps/frontend/app/providers/ApiHealthProvider.tsx
Normal file
56
apps/frontend/app/providers/ApiHealthProvider.tsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import { createContext, use, type PropsWithChildren } from 'react';
|
|||||||
import { createTanstackApi, createApi } from '../lib/api';
|
import { createTanstackApi, createApi } from '../lib/api';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
type ApiProviderProps = PropsWithChildren<object>;
|
type ApiProviderProps = PropsWithChildren<object>;
|
||||||
type ApiContextType = {
|
type ApiContextType = {
|
||||||
|
|||||||
47
apps/frontend/app/providers/AuthProvider.tsx
Normal file
47
apps/frontend/app/providers/AuthProvider.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import AppTheme from './components/theme';
|
|||||||
import { ApiProvider } from './providers/ApiProvider';
|
import { ApiProvider } from './providers/ApiProvider';
|
||||||
import { LayoutProvider } from './providers/LayoutProvider';
|
import { LayoutProvider } from './providers/LayoutProvider';
|
||||||
import { Tooltip } from 'radix-ui';
|
import { Tooltip } from 'radix-ui';
|
||||||
|
import { AuthProvider } from './providers/AuthProvider';
|
||||||
|
import { ApiHealthProvider } from './providers/ApiHealthProvider';
|
||||||
|
|
||||||
export const links: Route.LinksFunction = () => [];
|
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 name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
|
{/* Required for react-toastify */}
|
||||||
|
<style />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
@@ -33,7 +37,11 @@ export default function App() {
|
|||||||
<ApiProvider>
|
<ApiProvider>
|
||||||
<Tooltip.Provider delayDuration={250}>
|
<Tooltip.Provider delayDuration={250}>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
|
<ApiHealthProvider>
|
||||||
|
<AuthProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</AuthProvider>
|
||||||
|
</ApiHealthProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</Tooltip.Provider>
|
</Tooltip.Provider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Box, Button, Card, Flex, Grid, Heading, Text } from '@radix-ui/themes';
|
|||||||
import type { Route } from './+types/home';
|
import type { Route } from './+types/home';
|
||||||
import TablePlaceholder from '../components/home/TablePlaceholder';
|
import TablePlaceholder from '../components/home/TablePlaceholder';
|
||||||
import { useLayout } from '../providers/LayoutProvider';
|
import { useLayout } from '../providers/LayoutProvider';
|
||||||
|
import { useEnsureLoggedIn } from '../hooks/ensureLoggedIn';
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
@@ -9,6 +10,7 @@ export function meta({}: Route.MetaArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProxyHostDemo() {
|
export default function ProxyHostDemo() {
|
||||||
|
useEnsureLoggedIn();
|
||||||
const { activeTab } = useLayout();
|
const { activeTab } = useLayout();
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user