feat(auth,ui): support github sign-in in the UI (#2545)

# What does this PR do?
Uses NextAuth to add github sign in support.

## Test Plan
Start server with auth configured as in
https://github.com/meta-llama/llama-stack/pull/2509


https://github.com/user-attachments/assets/61ff7442-f601-4b39-8686-5d0afb3b45ac
This commit is contained in:
ehhuang 2025-07-08 11:02:57 -07:00 committed by GitHub
parent c8bac888af
commit daf660c4ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 577 additions and 81 deletions

View file

@ -0,0 +1,24 @@
import { useSession } from "next-auth/react";
import { useMemo } from "react";
import LlamaStackClient from "llama-stack-client";
export function useAuthClient() {
const { data: session } = useSession();
const client = useMemo(() => {
const clientHostname =
typeof window !== "undefined" ? window.location.origin : "";
const options: any = {
baseURL: `${clientHostname}/api`,
};
if (session?.accessToken) {
options.apiKey = session.accessToken;
}
return new LlamaStackClient(options);
}, [session?.accessToken]);
return client;
}

View file

@ -2,6 +2,9 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { PaginationStatus, UsePaginationOptions } from "@/lib/types";
import { useSession } from "next-auth/react";
import { useAuthClient } from "@/hooks/use-auth-client";
import { useRouter } from "next/navigation";
interface PaginationState<T> {
data: T[];
@ -28,13 +31,18 @@ export interface PaginationReturn<T> {
}
interface UsePaginationParams<T> extends UsePaginationOptions {
fetchFunction: (params: {
after?: string;
limit: number;
model?: string;
order?: string;
}) => Promise<PaginationResponse<T>>;
fetchFunction: (
client: ReturnType<typeof useAuthClient>,
params: {
after?: string;
limit: number;
model?: string;
order?: string;
},
) => Promise<PaginationResponse<T>>;
errorMessagePrefix: string;
enabled?: boolean;
useAuth?: boolean;
}
export function usePagination<T>({
@ -43,7 +51,12 @@ export function usePagination<T>({
order = "desc",
fetchFunction,
errorMessagePrefix,
enabled = true,
useAuth = true,
}: UsePaginationParams<T>): PaginationReturn<T> {
const { status: sessionStatus } = useSession();
const client = useAuthClient();
const router = useRouter();
const [state, setState] = useState<PaginationState<T>>({
data: [],
status: "loading",
@ -74,7 +87,7 @@ export function usePagination<T>({
error: null,
}));
const response = await fetchFunction({
const response = await fetchFunction(client, {
after: after || undefined,
limit: fetchLimit,
...(model && { model }),
@ -91,6 +104,17 @@ export function usePagination<T>({
status: "idle",
}));
} catch (err) {
// Check if it's a 401 unauthorized error
if (
err &&
typeof err === "object" &&
"status" in err &&
err.status === 401
) {
router.push("/auth/signin");
return;
}
const errorMessage = isInitialLoad
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
: `Failed to load more ${errorMessagePrefix}. Please try again.`;
@ -107,7 +131,7 @@ export function usePagination<T>({
}));
}
},
[limit, model, order, fetchFunction, errorMessagePrefix],
[limit, model, order, fetchFunction, errorMessagePrefix, client, router],
);
/**
@ -120,17 +144,28 @@ export function usePagination<T>({
}
}, [fetchData]);
// Auto-load initial data on mount
// Auto-load initial data on mount when enabled
useEffect(() => {
if (!hasFetchedInitialData.current) {
// If using auth, wait for session to load
const isAuthReady = !useAuth || sessionStatus !== "loading";
const shouldFetch = enabled && isAuthReady;
if (shouldFetch && !hasFetchedInitialData.current) {
hasFetchedInitialData.current = true;
fetchData();
} else if (!shouldFetch) {
// Reset the flag when disabled so it can fetch when re-enabled
hasFetchedInitialData.current = false;
}
}, [fetchData]);
}, [fetchData, enabled, useAuth, sessionStatus]);
// Override status if we're waiting for auth
const effectiveStatus =
useAuth && sessionStatus === "loading" ? "loading" : state.status;
return {
data: state.data,
status: state.status,
status: effectiveStatus,
hasMore: state.hasMore,
error: state.error,
loadMore,