This commit is contained in:
2026-01-07 16:22:31 -05:00
parent 041b2f8dc2
commit 0a0c0e313e
15 changed files with 809 additions and 2251 deletions

View File

@@ -1,13 +1,3 @@
/**
* 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";
@@ -21,70 +11,31 @@ export interface UserState {
}
/**
* Global auth state query - single source of truth
* Called on server during SSR, cached by SolidStart router
* Get current user state from server
* Uses cache() to ensure single execution per request and proper SSR hydration
*/
export const getUserState = query(async (): Promise<UserState> => {
"use server";
const { getPrivilegeLevel, getUserID } = await import("~/server/auth");
const { checkAuthStatus } = await import("~/server/auth");
const { ConnectionFactory } = await import("~/server/utils");
const { getCookie, setCookie } = await import("vinxi/http");
const event = getRequestEvent()!;
let privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
let userId = await getUserID(event.nativeEvent);
const event = getRequestEvent();
// If no userId but refresh token exists, attempt server-side token refresh
// Use a flag cookie to prevent infinite loops (only try once per request)
if (!userId) {
const refreshToken = getCookie(event.nativeEvent, "refreshToken");
const refreshAttempted = getCookie(event.nativeEvent, "_refresh_attempted");
if (refreshToken && !refreshAttempted) {
console.log(
"[Auth-Query] Access token expired but refresh token exists, attempting server-side refresh"
);
// Set flag to prevent retry loops (expires immediately, just for this request)
setCookie(event.nativeEvent, "_refresh_attempted", "1", {
maxAge: 1,
path: "/",
httpOnly: true
});
try {
// Import token rotation function
const { attemptTokenRefresh } =
await import("~/server/api/routers/auth");
// Attempt to refresh tokens server-side
const refreshed = await attemptTokenRefresh(
event.nativeEvent,
refreshToken
);
if (refreshed) {
console.log("[Auth-Query] Server-side token refresh successful");
// Re-check auth state with new tokens
privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
userId = await getUserID(event.nativeEvent);
} else {
console.log("[Auth-Query] Server-side token refresh failed");
}
} catch (error) {
console.error(
"[Auth-Query] Error during server-side token refresh:",
error
);
}
} else if (refreshAttempted) {
console.log(
"[Auth-Query] Refresh already attempted this request, skipping"
);
}
// Safety check: if no event, we're not in a request context
if (!event || !event.nativeEvent) {
return {
isAuthenticated: false,
userId: null,
email: null,
displayName: null,
emailVerified: false,
privilegeLevel: "anonymous"
};
}
if (!userId) {
const auth = await checkAuthStatus(event.nativeEvent);
if (!auth.isAuthenticated || !auth.userId) {
return {
isAuthenticated: false,
userId: null,
@@ -98,7 +49,7 @@ export const getUserState = query(async (): Promise<UserState> => {
const conn = ConnectionFactory();
const res = await conn.execute({
sql: "SELECT email, display_name, email_verified FROM User WHERE id = ?",
args: [userId]
args: [auth.userId]
});
if (res.rows.length === 0) {
@@ -116,22 +67,23 @@ export const getUserState = query(async (): Promise<UserState> => {
return {
isAuthenticated: true,
userId,
userId: auth.userId,
email: user.email ?? null,
displayName: user.display_name ?? null,
emailVerified: user.email_verified === 1,
privilegeLevel
privilegeLevel: auth.isAdmin ? "admin" : "user"
};
}, "global-auth-state");
}, "user-auth-state");
/**
* Revalidate auth state globally
* Call this after login, logout, token refresh, email verification
*/
export function revalidateAuth() {
revalidateKey(getUserState.key);
// Revalidate the cache
revalidateKey("user-auth-state");
// Dispatch browser event to trigger UI updates (client-side only)
// Dispatch event to trigger UI updates (client-side only)
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth-state-changed"));