mirror of
https://github.com/Xevion/linkpulse.git
synced 2025-12-06 05:15:35 -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": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.0",
|
||||||
|
|||||||
36
frontend/pnpm-lock.yaml
generated
36
frontend/pnpm-lock.yaml
generated
@@ -38,6 +38,12 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.14)
|
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:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.13.0
|
specifier: ^9.13.0
|
||||||
@@ -1746,6 +1752,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
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:
|
ts-api-utils@1.4.0:
|
||||||
resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
|
resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -1908,6 +1918,24 @@ packages:
|
|||||||
zod@3.23.8:
|
zod@3.23.8:
|
||||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
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:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -3433,6 +3461,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
true-myth@8.0.1: {}
|
||||||
|
|
||||||
ts-api-utils@1.4.0(typescript@5.6.3):
|
ts-api-utils@1.4.0(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
@@ -3579,3 +3609,9 @@ snapshots:
|
|||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.23.8: {}
|
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 { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
|
import { getSession } from "@/lib/auth";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ import { routeTree } from "./routeTree.gen";
|
|||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = createRouter({ routeTree });
|
const router = createRouter({ routeTree });
|
||||||
|
|
||||||
|
getSession();
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
|
|||||||
@@ -13,19 +13,26 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
import { Route as LoginImport } from './routes/login'
|
||||||
|
import { Route as DashboardImport } from './routes/dashboard'
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
const LoginLazyImport = createFileRoute('/login')()
|
|
||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const LoginLazyRoute = LoginLazyImport.update({
|
const LoginRoute = LoginImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRoute,
|
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({
|
const IndexLazyRoute = IndexLazyImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
@@ -44,11 +51,18 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexLazyImport
|
preLoaderRoute: typeof IndexLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/dashboard': {
|
||||||
|
id: '/dashboard'
|
||||||
|
path: '/dashboard'
|
||||||
|
fullPath: '/dashboard'
|
||||||
|
preLoaderRoute: typeof DashboardImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/login': {
|
'/login': {
|
||||||
id: '/login'
|
id: '/login'
|
||||||
path: '/login'
|
path: '/login'
|
||||||
fullPath: '/login'
|
fullPath: '/login'
|
||||||
preLoaderRoute: typeof LoginLazyImport
|
preLoaderRoute: typeof LoginImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,37 +72,42 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/login': typeof LoginLazyRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/login': typeof LoginLazyRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/login': typeof LoginLazyRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/login'
|
fullPaths: '/' | '/dashboard' | '/login'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/login'
|
to: '/' | '/dashboard' | '/login'
|
||||||
id: '__root__' | '/' | '/login'
|
id: '__root__' | '/' | '/dashboard' | '/login'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
LoginLazyRoute: typeof LoginLazyRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
|
LoginRoute: typeof LoginRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
LoginLazyRoute: LoginLazyRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
|
LoginRoute: LoginRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -102,14 +121,18 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/dashboard",
|
||||||
"/login"
|
"/login"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.lazy.tsx"
|
"filePath": "index.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/dashboard": {
|
||||||
|
"filePath": "dashboard.tsx"
|
||||||
|
},
|
||||||
"/login": {
|
"/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";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
import { useUserStore } from "@/lib/state";
|
||||||
export const Route = createLazyFileRoute("/login")({
|
|
||||||
component: Login,
|
|
||||||
});
|
|
||||||
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { UserAuthForm } from "@/components/auth/UserAuthForm";
|
import { UserAuthForm } from "@/components/auth/UserAuthForm";
|
||||||
import { Icons } from "@/components/icons";
|
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() {
|
function Login() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
Reference in New Issue
Block a user