mirror of
https://github.com/Xevion/linkpulse.git
synced 2025-12-06 01:15:30 -06:00
Add zustand, true-myth, primitive authentication state, startup getSession()
This commit is contained in:
@@ -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",
|
||||
|
||||
36
frontend/pnpm-lock.yaml
generated
36
frontend/pnpm-lock.yaml
generated
@@ -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
37
frontend/src/lib/auth.ts
Normal 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
19
frontend/src/lib/state.ts
Normal 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 }),
|
||||
}));
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
frontend/src/routes/dashboard.tsx
Normal file
9
frontend/src/routes/dashboard.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return "Hello /dashboard!";
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
Reference in New Issue
Block a user