Add zustand, true-myth, primitive authentication state, startup getSession()

This commit is contained in:
2024-11-10 20:41:58 -06:00
parent af5e42abe7
commit f18adf0f41
8 changed files with 159 additions and 19 deletions

View File

@@ -24,7 +24,9 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"true-myth": "^8.0.1",
"zustand": "^5.0.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",

View File

@@ -38,6 +38,12 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.14)
true-myth:
specifier: ^8.0.1
version: 8.0.1
zustand:
specifier: ^5.0.1
version: 5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1))
devDependencies:
'@eslint/js':
specifier: ^9.13.0
@@ -1746,6 +1752,10 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
true-myth@8.0.1:
resolution: {integrity: sha512-/6Tga43I+LfMJmg7hiCGI8KqjjlDEa1XJnWTZOKmmyQ9s7Kv4WPmpoQT/LST0Q98GAnsB+UPtGFwVHi8h7+qFw==}
engines: {node: 18.* || >= 20.*}
ts-api-utils@1.4.0:
resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
engines: {node: '>=16'}
@@ -1908,6 +1918,24 @@ packages:
zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
zustand@5.0.1:
resolution: {integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -3433,6 +3461,8 @@ snapshots:
dependencies:
is-number: 7.0.0
true-myth@8.0.1: {}
ts-api-utils@1.4.0(typescript@5.6.3):
dependencies:
typescript: 5.6.3
@@ -3579,3 +3609,9 @@ snapshots:
yocto-queue@0.1.0: {}
zod@3.23.8: {}
zustand@5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)):
optionalDependencies:
'@types/react': 18.3.12
react: 18.3.1
use-sync-external-store: 1.2.2(react@18.3.1)

37
frontend/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,37 @@
import Result, { ok, err } from "true-myth/result";
import { useUserStore } from "./state";
// Authentication on the frontend is managed by Zustand's state.
// Upon application load, a single request is made to acquire session state.
// Any route that requires authentication will redirect if a valid session is not acquired.
// The session state is stored in the user store.
// If any protected API call suddenly fails with a 401 status code, the user store is reset, a logout message is displayed, and the user is redirected to the login page.
// All redirects to the login page will carry a masked URL parameter that can be used to redirect the user back to the page they were on after logging in.
const TARGET = import.meta.env.DEV
? `http://${import.meta.env.VITE_BACKEND_TARGET}`
: "";
console.log({ env: import.meta.env, target: TARGET });
type ErrorResponse = {
detail: string;
};
type SessionResponse = {
user: {};
};
export const getSession = async (): Promise<
Result<SessionResponse, ErrorResponse>
> => {
const response = await fetch(TARGET + "/api/session");
if (response.ok) {
const user = await response.json();
useUserStore.getState().setUser(user);
return ok({ user });
} else {
useUserStore.getState().logout();
const error = await response.json();
return err({ detail: error.detail });
}
};

19
frontend/src/lib/state.ts Normal file
View File

@@ -0,0 +1,19 @@
import { create } from "zustand";
type UserState = {
user: {
// TODO: This will eventually carry more user information (name, avatar, etc.)
email: string;
} | null;
};
type UserActions = {
setUser: (user: UserState["user"]) => void;
logout: () => void;
};
export const useUserStore = create<UserState & UserActions>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));

View File

@@ -1,5 +1,6 @@
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react";
import { getSession } from "@/lib/auth";
import ReactDOM from "react-dom/client";
import "./index.css";
@@ -9,6 +10,8 @@ import { routeTree } from "./routeTree.gen";
// Create a new router instance
const router = createRouter({ routeTree });
getSession();
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {

View File

@@ -13,19 +13,26 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as LoginImport } from './routes/login'
import { Route as DashboardImport } from './routes/dashboard'
// Create Virtual Routes
const LoginLazyImport = createFileRoute('/login')()
const IndexLazyImport = createFileRoute('/')()
// Create/Update Routes
const LoginLazyRoute = LoginLazyImport.update({
const LoginRoute = LoginImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))
} as any)
const DashboardRoute = DashboardImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRoute,
} as any)
const IndexLazyRoute = IndexLazyImport.update({
id: '/',
@@ -44,11 +51,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexLazyImport
parentRoute: typeof rootRoute
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginLazyImport
preLoaderRoute: typeof LoginImport
parentRoute: typeof rootRoute
}
}
@@ -58,37 +72,42 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexLazyRoute
'/login': typeof LoginLazyRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRoutesByTo {
'/': typeof IndexLazyRoute
'/login': typeof LoginLazyRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexLazyRoute
'/login': typeof LoginLazyRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login'
fullPaths: '/' | '/dashboard' | '/login'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/login'
id: '__root__' | '/' | '/login'
to: '/' | '/dashboard' | '/login'
id: '__root__' | '/' | '/dashboard' | '/login'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexLazyRoute: typeof IndexLazyRoute
LoginLazyRoute: typeof LoginLazyRoute
DashboardRoute: typeof DashboardRoute
LoginRoute: typeof LoginRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexLazyRoute: IndexLazyRoute,
LoginLazyRoute: LoginLazyRoute,
DashboardRoute: DashboardRoute,
LoginRoute: LoginRoute,
}
export const routeTree = rootRoute
@@ -102,14 +121,18 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/dashboard",
"/login"
]
},
"/": {
"filePath": "index.lazy.tsx"
},
"/dashboard": {
"filePath": "dashboard.tsx"
},
"/login": {
"filePath": "login.lazy.tsx"
"filePath": "login.tsx"
}
}
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
});
function RouteComponent() {
return "Hello /dashboard!";
}

View File

@@ -1,14 +1,25 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/login")({
component: Login,
});
import { createFileRoute, redirect } from "@tanstack/react-router";
import { useUserStore } from "@/lib/state";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { UserAuthForm } from "@/components/auth/UserAuthForm";
import { Icons } from "@/components/icons";
export const Route = createFileRoute("/login")({
beforeLoad: async ({ location }) => {
const isLoggedIn = useUserStore.getState().user !== null;
if (isLoggedIn) {
return redirect({
to: "/dashboard",
search: { redirect: location.href },
});
}
},
component: Login,
});
function Login() {
return (
<>