Implement authentication flow with API key support and loading states

This commit is contained in:
2025-07-31 18:59:12 +02:00
parent 89f90697ef
commit c038cabaf6
7 changed files with 414 additions and 17 deletions

View File

@@ -0,0 +1,162 @@
import { type ReactNode, createContext, useContext, useState, useEffect, useCallback } from 'react'
interface AuthContextState {
isAuthenticated: boolean
isLoading: boolean
apiKey: string | null
error: string | null
}
interface AuthContextActions {
login: (apiKey: string) => Promise<void>
logout: () => void
clearError: () => void
validateAuth: () => Promise<boolean>
}
type AuthContextType = AuthContextState & AuthContextActions
const AuthContext = createContext<AuthContextType | undefined>(undefined)
interface AuthProviderProps {
children: ReactNode
}
const AUTH_STORAGE_KEY = 'llamactl_management_key'
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [apiKey, setApiKey] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
// Load auth state from sessionStorage on mount
useEffect(() => {
const loadStoredAuth = async () => {
try {
const storedKey = sessionStorage.getItem(AUTH_STORAGE_KEY)
if (storedKey) {
setApiKey(storedKey)
// Validate the stored key
const isValid = await validateApiKey(storedKey)
if (isValid) {
setIsAuthenticated(true)
} else {
// Invalid key, remove it
sessionStorage.removeItem(AUTH_STORAGE_KEY)
setApiKey(null)
}
}
} catch (err) {
console.error('Error loading stored auth:', err)
// Clear potentially corrupted storage
sessionStorage.removeItem(AUTH_STORAGE_KEY)
} finally {
setIsLoading(false)
}
}
void loadStoredAuth()
}, [])
// Validate API key by making a test request
const validateApiKey = async (key: string): Promise<boolean> => {
try {
const response = await fetch('/api/v1/instances', {
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json'
}
})
return response.ok
} catch (err) {
console.error('Auth validation error:', err)
return false
}
}
const login = useCallback(async (key: string) => {
setIsLoading(true)
setError(null)
try {
// Validate the provided API key
const isValid = await validateApiKey(key)
if (!isValid) {
throw new Error('Invalid API key')
}
// Store the key and update state
sessionStorage.setItem(AUTH_STORAGE_KEY, key)
setApiKey(key)
setIsAuthenticated(true)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Authentication failed'
setError(errorMessage)
throw new Error(errorMessage)
} finally {
setIsLoading(false)
}
}, [])
const logout = useCallback(() => {
sessionStorage.removeItem(AUTH_STORAGE_KEY)
setApiKey(null)
setIsAuthenticated(false)
setError(null)
}, [])
const clearError = useCallback(() => {
setError(null)
}, [])
const validateAuth = useCallback(async (): Promise<boolean> => {
if (!apiKey) return false
const isValid = await validateApiKey(apiKey)
if (!isValid) {
logout()
}
return isValid
}, [apiKey, logout])
const value: AuthContextType = {
isAuthenticated,
isLoading,
apiKey,
error,
login,
logout,
clearError,
validateAuth,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
// Helper hook for getting auth headers
export const useAuthHeaders = (): HeadersInit => {
const { apiKey, isAuthenticated } = useAuth()
if (!isAuthenticated || !apiKey) {
return {}
}
return {
'Authorization': `Bearer ${apiKey}`
}
}

View File

@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { type ReactNode, createContext, useContext, useState, useEffect, useCallback } from 'react'
import type { CreateInstanceOptions, Instance } from '@/types/instance'
import { instancesApi } from '@/lib/api'
import { useAuth } from '@/contexts/AuthContext'
interface InstancesContextState {
instances: Instance[]
@@ -29,6 +29,7 @@ interface InstancesProviderProps {
}
export const InstancesProvider = ({ children }: InstancesProviderProps) => {
const { isAuthenticated, isLoading: authLoading } = useAuth()
const [instancesMap, setInstancesMap] = useState<Map<string, Instance>>(new Map())
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -41,6 +42,11 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
}, [])
const fetchInstances = useCallback(async () => {
if (!isAuthenticated) {
setLoading(false)
return
}
try {
setLoading(true)
setError(null)
@@ -57,7 +63,7 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
} finally {
setLoading(false)
}
}, [])
}, [isAuthenticated])
const updateInstanceInMap = useCallback((name: string, updates: Partial<Instance>) => {
setInstancesMap(prev => {
@@ -154,9 +160,19 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
}
}, [])
// Only fetch instances when auth is ready and user is authenticated
useEffect(() => {
fetchInstances()
}, [fetchInstances])
if (!authLoading) {
if (isAuthenticated) {
void fetchInstances()
} else {
// Clear instances when not authenticated
setInstancesMap(new Map())
setLoading(false)
setError(null)
}
}
}, [authLoading, isAuthenticated, fetchInstances])
const value: InstancesContextType = {
instances,