From 445ab6d7de379b9bcfd3b9840eaa95a2c4d153c6 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 6 Jan 2026 23:26:51 -0500 Subject: [PATCH] auth querying consolidation --- src/app.tsx | 9 ++- src/components/Bars.tsx | 65 ++++------------------ src/context/auth.tsx | 94 ++++++++++++++++++++++++++++++++ src/lib/auth-query.ts | 81 +++++++++++++++++++++++++++ src/lib/token-refresh.ts | 2 + src/routes/analytics.tsx | 9 +-- src/routes/blog/create/index.tsx | 20 +++---- src/routes/blog/edit/[id].tsx | 18 +++--- src/routes/login/index.tsx | 4 +- src/routes/test.tsx | 13 ++--- 10 files changed, 224 insertions(+), 91 deletions(-) create mode 100644 src/context/auth.tsx create mode 100644 src/lib/auth-query.ts diff --git a/src/app.tsx b/src/app.tsx index f8b92c2..634f189 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -14,6 +14,7 @@ import { MetaProvider } from "@solidjs/meta"; import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback"; import { BarsProvider, useBars } from "./context/bars"; import { DarkModeProvider } from "./context/darkMode"; +import { AuthProvider } from "./context/auth"; import { createWindowWidth, isMobile } from "~/lib/resize-utils"; import { MOBILE_CONFIG } from "./config"; import CustomScrollbar from "./components/CustomScrollbar"; @@ -210,7 +211,13 @@ export default function App() { > - {props.children}}> + ( + + {props.children} + + )} + > diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 0880a7a..94234b7 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -1,5 +1,7 @@ import { Typewriter } from "./Typewriter"; import { useBars } from "~/context/bars"; +import { useAuth } from "~/context/auth"; +import { revalidateAuth } from "~/lib/auth-query"; import { onMount, createSignal, Show, For, onCleanup } from "solid-js"; import { api } from "~/lib/api"; import { insertSoftHyphens, glitchText } from "~/lib/client-utils"; @@ -10,54 +12,8 @@ import { ActivityHeatmap } from "./ActivityHeatmap"; import { DarkModeToggle } from "./DarkModeToggle"; import { SkeletonBox, SkeletonText } from "./SkeletonLoader"; import { env } from "~/env/client"; -import { - A, - useNavigate, - useLocation, - query, - createAsync, - revalidate -} from "@solidjs/router"; +import { A, useNavigate, useLocation } from "@solidjs/router"; import { BREAKPOINTS } from "~/config"; -import { getRequestEvent } from "solid-js/web"; - -const getUserState = query(async () => { - "use server"; - const { getPrivilegeLevel, getUserID } = await import("~/server/utils"); - const { ConnectionFactory } = await import("~/server/utils"); - const event = getRequestEvent()!; - const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); - const userId = await getUserID(event.nativeEvent); - - if (!userId) { - return { - isAuthenticated: false, - email: null, - privilegeLevel: "anonymous" as const - }; - } - - const conn = ConnectionFactory(); - const res = await conn.execute({ - sql: "SELECT email FROM User WHERE id = ?", - args: [userId] - }); - - const email = res.rows[0] ? (res.rows[0].email as string | null) : null; - - return { - isAuthenticated: true, - email, - privilegeLevel - }; -}, "bars-user-state"); - -/** - * Call this function after login/logout to refresh the user state in the sidebar - */ -export function revalidateUserState() { - revalidate(getUserState.key); -} function formatDomainName(url: string): string { const domain = url.split("://")[1]?.split(":")[0] ?? url; @@ -247,7 +203,7 @@ export function RightBarContent() { export function LeftBar() { const { leftBarVisible, setLeftBarVisible } = useBars(); const location = useLocation(); - const userState = createAsync(() => getUserState()); + const { isAuthenticated, email, isAdmin } = useAuth(); let ref: HTMLDivElement | undefined; const [recentPosts, setRecentPosts] = createSignal( @@ -277,6 +233,7 @@ export function LeftBar() { setSignOutLoading(true); try { await api.auth.signOut.mutate(); + revalidateAuth(); // Clear auth state immediately window.location.href = "/"; } catch (error) { console.error("Sign out failed:", error); @@ -540,9 +497,7 @@ export function LeftBar() { Blog - +
  • Analytics @@ -557,7 +512,7 @@ export function LeftBar() { }} > Login @@ -566,16 +521,16 @@ export function LeftBar() { > Account - + {" "} - ({userState()!.email}) + ({email()})
  • - +
  • + * + * ); + * } + * ``` + */ +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within AuthProvider"); + } + return context; +} diff --git a/src/lib/auth-query.ts b/src/lib/auth-query.ts new file mode 100644 index 0000000..f24220d --- /dev/null +++ b/src/lib/auth-query.ts @@ -0,0 +1,81 @@ +/** + * Shared Auth Query + * Single source of truth for authentication state across the app + * + * Security Model: + * - Server query reads from httpOnly cookies (secure) + * - Client context syncs from this query (UI convenience) + * - Server endpoints always validate independently (never trust client) + */ + +import { query, revalidate as revalidateKey } from "@solidjs/router"; +import { getRequestEvent } from "solid-js/web"; + +export interface UserState { + isAuthenticated: boolean; + userId: string | null; + email: string | null; + displayName: string | null; + emailVerified: boolean; + privilegeLevel: "admin" | "user" | "anonymous"; +} + +/** + * Global auth state query - single source of truth + * Called on server during SSR, cached by SolidStart router + */ +export const getUserState = query(async (): Promise => { + "use server"; + const { getPrivilegeLevel, getUserID, ConnectionFactory } = + await import("~/server/utils"); + const event = getRequestEvent()!; + const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); + const userId = await getUserID(event.nativeEvent); + + if (!userId) { + return { + isAuthenticated: false, + userId: null, + email: null, + displayName: null, + emailVerified: false, + privilegeLevel: "anonymous" + }; + } + + const conn = ConnectionFactory(); + const res = await conn.execute({ + sql: "SELECT email, display_name, email_verified FROM User WHERE id = ?", + args: [userId] + }); + + if (res.rows.length === 0) { + return { + isAuthenticated: false, + userId: null, + email: null, + displayName: null, + emailVerified: false, + privilegeLevel: "anonymous" + }; + } + + const user = res.rows[0] as any; + + return { + isAuthenticated: true, + userId, + email: user.email ?? null, + displayName: user.display_name ?? null, + emailVerified: user.email_verified === 1, + privilegeLevel + }; +}, "global-auth-state"); + +/** + * Revalidate auth state globally + * Call this after login, logout, token refresh, email verification + */ +export function revalidateAuth() { + revalidateKey(getUserState.key); +} diff --git a/src/lib/token-refresh.ts b/src/lib/token-refresh.ts index aa23c1c..bdb1850 100644 --- a/src/lib/token-refresh.ts +++ b/src/lib/token-refresh.ts @@ -6,6 +6,7 @@ import { api } from "~/lib/api"; import { getClientCookie } from "~/lib/cookies.client"; import { getTimeUntilExpiry } from "~/lib/client-utils"; +import { revalidateAuth } from "~/lib/auth-query"; class TokenRefreshManager { private refreshTimer: ReturnType | null = null; @@ -108,6 +109,7 @@ class TokenRefreshManager { if (result.success) { console.log("[Token Refresh] Token refreshed successfully"); + revalidateAuth(); // Refresh auth state after token refresh this.scheduleNextRefresh(); // Schedule next refresh return true; } else { diff --git a/src/routes/analytics.tsx b/src/routes/analytics.tsx index 84b1f7f..03d4725 100644 --- a/src/routes/analytics.tsx +++ b/src/routes/analytics.tsx @@ -1,17 +1,14 @@ import { createSignal, Show, For, createEffect, ErrorBoundary } from "solid-js"; import { PageHead } from "~/components/PageHead"; import { redirect, query, createAsync, useNavigate } from "@solidjs/router"; -import { getEvent } from "vinxi/http"; import { api } from "~/lib/api"; const checkAdmin = query(async (): Promise => { "use server"; - const { getUserID } = await import("~/server/auth"); - const { env } = await import("~/env/server"); - const event = getEvent()!; - const userId = await getUserID(event); + const { getUserState } = await import("~/lib/auth-query"); + const userState = await getUserState(); - if (!userId || userId !== env.ADMIN_ID) { + if (userState.privilegeLevel !== "admin") { throw redirect("/"); } diff --git a/src/routes/blog/create/index.tsx b/src/routes/blog/create/index.tsx index 90cdff2..f0cfaa3 100644 --- a/src/routes/blog/create/index.tsx +++ b/src/routes/blog/create/index.tsx @@ -2,32 +2,30 @@ import { Show, lazy } from "solid-js"; import { query, redirect } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; import { createAsync } from "@solidjs/router"; -import { getEvent } from "vinxi/http"; +import { getUserState } from "~/lib/auth-query"; import { Spinner } from "~/components/Spinner"; import "../post.css"; const PostForm = lazy(() => import("~/components/blog/PostForm")); -const getAuthState = query(async () => { +const checkAdminAccess = query(async () => { "use server"; - const { getPrivilegeLevel, getUserID } = await import("~/server/utils"); - const event = getEvent()!; - const privilegeLevel = await getPrivilegeLevel(event); - const userID = await getUserID(event); + // Reuse shared auth query for consistency + const userState = await getUserState(); - if (privilegeLevel !== "admin") { + if (userState.privilegeLevel !== "admin") { throw redirect("/401"); } - return { privilegeLevel, userID }; -}, "create-post-auth"); + return { userID: userState.userId! }; +}, "create-post-admin-check"); export const route = { - load: () => getAuthState() + load: () => checkAdminAccess() }; export default function CreatePost() { - const authState = createAsync(() => getAuthState()); + const authState = createAsync(() => checkAdminAccess()); return ( <> diff --git a/src/routes/blog/edit/[id].tsx b/src/routes/blog/edit/[id].tsx index 13e9311..f1b3e1d 100644 --- a/src/routes/blog/edit/[id].tsx +++ b/src/routes/blog/edit/[id].tsx @@ -2,20 +2,17 @@ import { Show, lazy } from "solid-js"; import { useParams, query, redirect } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; import { createAsync } from "@solidjs/router"; -import { getEvent } from "vinxi/http"; import "../post.css"; const PostForm = lazy(() => import("~/components/blog/PostForm")); const getPostForEdit = query(async (id: string) => { "use server"; - const { getPrivilegeLevel, getUserID, ConnectionFactory } = - await import("~/server/utils"); - const event = getEvent()!; - const privilegeLevel = await getPrivilegeLevel(event); - const userID = await getUserID(event); + const { getUserState } = await import("~/lib/auth-query"); + const { ConnectionFactory } = await import("~/server/utils"); + const userState = await getUserState(); - if (privilegeLevel !== "admin") { + if (userState.privilegeLevel !== "admin") { throw redirect("/401"); } @@ -35,7 +32,12 @@ const getPostForEdit = query(async (id: string) => { const post = results.rows[0]; const tags = tagRes.rows; - return { post, tags, privilegeLevel, userID }; + return { + post, + tags, + privilegeLevel: userState.privilegeLevel, + userID: userState.userId + }; }, "post-for-edit"); export const route = { diff --git a/src/routes/login/index.tsx b/src/routes/login/index.tsx index 0c0aabe..5ef167c 100644 --- a/src/routes/login/index.tsx +++ b/src/routes/login/index.tsx @@ -7,7 +7,7 @@ import { query } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; -import { revalidateUserState } from "~/components/Bars"; +import { revalidateAuth } from "~/lib/auth-query"; import { getEvent } from "vinxi/http"; import GoogleLogo from "~/components/icons/GoogleLogo"; import GitHub from "~/components/icons/GitHub"; @@ -206,7 +206,7 @@ export default function LoginPage() { if (response.ok && result.result?.data?.success) { setShowPasswordSuccess(true); - revalidateUserState(); // Refresh user state in sidebar + revalidateAuth(); // Refresh auth state globally setTimeout(() => { navigate("/account", { replace: true }); }, 500); diff --git a/src/routes/test.tsx b/src/routes/test.tsx index 5edb758..ce2b567 100644 --- a/src/routes/test.tsx +++ b/src/routes/test.tsx @@ -1,16 +1,13 @@ import { createSignal, For, Show } from "solid-js"; import { query, createAsync } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; -import { getRequestEvent } from "solid-js/web"; import { api } from "~/lib/api"; +import { getUserState } from "~/lib/auth-query"; -const getAuthState = query(async () => { +const checkAdminAccess = query(async () => { "use server"; - const { getPrivilegeLevel } = await import("~/server/utils"); - const event = getRequestEvent()!; - const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); - - return { privilegeLevel }; + const userState = await getUserState(); + return { privilegeLevel: userState.privilegeLevel }; }, "test-auth-state"); type EndpointTest = { @@ -840,7 +837,7 @@ const routerSections: RouterSection[] = [ ]; export default function TestPage() { - const authState = createAsync(() => getAuthState()); + const authState = createAsync(() => checkAdminAccess()); const [expandedSections, setExpandedSections] = createSignal>( new Set()