migrated
This commit is contained in:
@@ -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"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user