129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
/**
|
|
* Auth Context Provider
|
|
* Provides convenient access to auth state throughout the app
|
|
*
|
|
* Security Note:
|
|
* - Context is for UI display only (showing/hiding buttons, user email, etc.)
|
|
* - Server endpoints ALWAYS validate independently from cookies
|
|
* - Never trust client-side state for authorization decisions
|
|
*/
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
createSignal,
|
|
onMount,
|
|
onCleanup,
|
|
createEffect,
|
|
Accessor,
|
|
ParentComponent
|
|
} from "solid-js";
|
|
import { createAsync, revalidate } from "@solidjs/router";
|
|
import { getUserState, type UserState } from "~/lib/auth-query";
|
|
|
|
interface AuthContextType {
|
|
/** Current user state (for UI display) */
|
|
userState: Accessor<UserState | undefined>;
|
|
|
|
/** Is user authenticated (convenience) */
|
|
isAuthenticated: Accessor<boolean>;
|
|
|
|
/** User email (null if not authenticated) */
|
|
email: Accessor<string | null>;
|
|
|
|
/** User display name (null if not set) */
|
|
displayName: Accessor<string | null>;
|
|
|
|
/** User ID (null if not authenticated) */
|
|
userId: Accessor<string | null>;
|
|
|
|
/** Is user admin (for UI display only - server still validates) */
|
|
isAdmin: Accessor<boolean>;
|
|
|
|
/** Is email verified */
|
|
isEmailVerified: Accessor<boolean>;
|
|
|
|
/** Refresh auth state from server */
|
|
refreshAuth: () => void;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType>();
|
|
|
|
export const AuthProvider: ParentComponent = (props) => {
|
|
// Get server state using createAsync which works with cache()
|
|
const serverAuth = createAsync(() => getUserState(), { deferStream: true });
|
|
|
|
// Refresh callback that forces re-fetch
|
|
const refreshAuth = () => {
|
|
// Manually trigger a re-fetch by calling the revalidate function
|
|
revalidate(["user-auth-state"]);
|
|
};
|
|
|
|
// Convenience accessors with safe defaults - MUST BE DEFINED BEFORE onMount
|
|
const isAuthenticated = () => serverAuth()?.isAuthenticated ?? false;
|
|
const email = () => serverAuth()?.email ?? null;
|
|
const displayName = () => serverAuth()?.displayName ?? null;
|
|
const userId = () => serverAuth()?.userId ?? null;
|
|
const isAdmin = () => serverAuth()?.isAdmin ?? false;
|
|
const isEmailVerified = () => serverAuth()?.emailVerified ?? false;
|
|
|
|
// Server handles all token refresh logic
|
|
// Client just displays the current auth state from server
|
|
|
|
// Listen for auth refresh events from external sources (token refresh, etc.)
|
|
onMount(() => {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const handleAuthRefresh = () => {
|
|
console.log("[AuthContext] Received auth refresh event");
|
|
refreshAuth();
|
|
};
|
|
|
|
window.addEventListener("auth-state-changed", handleAuthRefresh);
|
|
|
|
onCleanup(() => {
|
|
window.removeEventListener("auth-state-changed", handleAuthRefresh);
|
|
});
|
|
});
|
|
|
|
const value: AuthContextType = {
|
|
userState: serverAuth,
|
|
isAuthenticated,
|
|
email,
|
|
displayName,
|
|
userId,
|
|
isAdmin,
|
|
isEmailVerified,
|
|
refreshAuth
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Hook to access auth state anywhere in the app
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* function MyComponent() {
|
|
* const { isAuthenticated, email, refreshAuth } = useAuth();
|
|
*
|
|
* return (
|
|
* <Show when={isAuthenticated()}>
|
|
* <p>Welcome, {email()}!</p>
|
|
* <button onClick={() => refreshAuth()}>Refresh</button>
|
|
* </Show>
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error("useAuth must be used within AuthProvider");
|
|
}
|
|
return context;
|
|
}
|