mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-06 09:04:27 +00:00
Implement authentication flow with API key support and loading states
This commit is contained in:
@@ -2,11 +2,14 @@ import { useState } from "react";
|
|||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import InstanceList from "@/components/InstanceList";
|
import InstanceList from "@/components/InstanceList";
|
||||||
import InstanceModal from "@/components/InstanceModal";
|
import InstanceModal from "@/components/InstanceModal";
|
||||||
|
import LoginDialog from "@/components/LoginDialog";
|
||||||
|
import SystemInfoModal from "./components/SystemInfoModal";
|
||||||
import { type CreateInstanceOptions, type Instance } from "@/types/instance";
|
import { type CreateInstanceOptions, type Instance } from "@/types/instance";
|
||||||
import { useInstances } from "@/contexts/InstancesContext";
|
import { useInstances } from "@/contexts/InstancesContext";
|
||||||
import SystemInfoModal from "./components/SystemInfoModal";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { isAuthenticated, isLoading: authLoading } = useAuth();
|
||||||
const [isInstanceModalOpen, setIsInstanceModalOpen] = useState(false);
|
const [isInstanceModalOpen, setIsInstanceModalOpen] = useState(false);
|
||||||
const [isSystemInfoModalOpen, setIsSystemInfoModalOpen] = useState(false);
|
const [isSystemInfoModalOpen, setIsSystemInfoModalOpen] = useState(false);
|
||||||
const [editingInstance, setEditingInstance] = useState<Instance | undefined>(
|
const [editingInstance, setEditingInstance] = useState<Instance | undefined>(
|
||||||
@@ -36,6 +39,28 @@ function App() {
|
|||||||
setIsSystemInfoModalOpen(true);
|
setIsSystemInfoModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading spinner while checking auth
|
||||||
|
if (authLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show login dialog if not authenticated
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<LoginDialog open={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show main app if authenticated
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<Header onCreateInstance={handleCreateInstance} onShowSystemInfo={handleShowSystemInfo} />
|
<Header onCreateInstance={handleCreateInstance} onShowSystemInfo={handleShowSystemInfo} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { HelpCircle } from "lucide-react";
|
import { HelpCircle, LogOut } from "lucide-react";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onCreateInstance: () => void;
|
onCreateInstance: () => void;
|
||||||
@@ -7,6 +8,14 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Header({ onCreateInstance, onShowSystemInfo }: HeaderProps) {
|
function Header({ onCreateInstance, onShowSystemInfo }: HeaderProps) {
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (confirm("Are you sure you want to logout?")) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="container mx-auto max-w-4xl px-4 py-4">
|
<div className="container mx-auto max-w-4xl px-4 py-4">
|
||||||
@@ -16,7 +25,9 @@ function Header({ onCreateInstance, onShowSystemInfo }: HeaderProps) {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button onClick={onCreateInstance} data-testid="create-instance-button">Create Instance</Button>
|
<Button onClick={onCreateInstance} data-testid="create-instance-button">
|
||||||
|
Create Instance
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -27,6 +38,16 @@ function Header({ onCreateInstance, onShowSystemInfo }: HeaderProps) {
|
|||||||
>
|
>
|
||||||
<HelpCircle className="h-4 w-4" />
|
<HelpCircle className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleLogout}
|
||||||
|
data-testid="logout-button"
|
||||||
|
title="Logout"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
157
webui/src/components/LoginDialog.tsx
Normal file
157
webui/src/components/LoginDialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { AlertCircle, Key, Loader2 } from 'lucide-react'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
|
||||||
|
interface LoginDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}) => {
|
||||||
|
const { login, isLoading, error, clearError } = useAuth()
|
||||||
|
const [apiKey, setApiKey] = useState('')
|
||||||
|
const [localLoading, setLocalLoading] = useState(false)
|
||||||
|
|
||||||
|
// Clear form and errors when dialog opens/closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setApiKey('')
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
}, [open, clearError])
|
||||||
|
|
||||||
|
// Clear error when user starts typing
|
||||||
|
useEffect(() => {
|
||||||
|
if (error && apiKey) {
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
}, [apiKey, error, clearError])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!apiKey.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(apiKey.trim())
|
||||||
|
// Login successful - dialog will close automatically when auth state changes
|
||||||
|
setApiKey('')
|
||||||
|
} catch (err) {
|
||||||
|
// Error is handled by the AuthContext
|
||||||
|
console.error('Login failed:', err)
|
||||||
|
} finally {
|
||||||
|
setLocalLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setApiKey('')
|
||||||
|
clearError()
|
||||||
|
onOpenChange?.(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter' && !isSubmitDisabled) {
|
||||||
|
// Create a synthetic FormEvent to satisfy handleSubmit's type
|
||||||
|
const syntheticEvent = {
|
||||||
|
preventDefault: () => {},
|
||||||
|
} as React.FormEvent<HTMLFormElement>;
|
||||||
|
void handleSubmit(syntheticEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSubmitDisabled = !apiKey.trim() || isLoading || localLoading
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-md"
|
||||||
|
showCloseButton={false} // Prevent closing without auth
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
Authentication Required
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Please enter your management API key to access the Llamactl dashboard.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => { void handleSubmit(e) }}>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||||
|
<AlertCircle className="h-4 w-4 text-destructive flex-shrink-0" />
|
||||||
|
<span className="text-sm text-destructive">{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Key Input */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="apiKey">
|
||||||
|
Management API Key <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="apiKey"
|
||||||
|
type="password"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="sk-management-..."
|
||||||
|
disabled={isLoading || localLoading}
|
||||||
|
className={error ? "border-red-500" : ""}
|
||||||
|
autoFocus
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Your management API key is required to access instance management features.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
data-testid="login-submit-button"
|
||||||
|
>
|
||||||
|
{(isLoading || localLoading) ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Authenticating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Key className="h-4 w-4" />
|
||||||
|
Login
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginDialog
|
||||||
162
webui/src/contexts/AuthContext.tsx
Normal file
162
webui/src/contexts/AuthContext.tsx
Normal 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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from 'react';
|
import { type ReactNode, createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
|
||||||
import type { CreateInstanceOptions, Instance } from '@/types/instance'
|
import type { CreateInstanceOptions, Instance } from '@/types/instance'
|
||||||
import { instancesApi } from '@/lib/api'
|
import { instancesApi } from '@/lib/api'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
|
||||||
interface InstancesContextState {
|
interface InstancesContextState {
|
||||||
instances: Instance[]
|
instances: Instance[]
|
||||||
@@ -29,6 +29,7 @@ interface InstancesProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const InstancesProvider = ({ children }: InstancesProviderProps) => {
|
export const InstancesProvider = ({ children }: InstancesProviderProps) => {
|
||||||
|
const { isAuthenticated, isLoading: authLoading } = useAuth()
|
||||||
const [instancesMap, setInstancesMap] = useState<Map<string, Instance>>(new Map())
|
const [instancesMap, setInstancesMap] = useState<Map<string, Instance>>(new Map())
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -41,6 +42,11 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchInstances = useCallback(async () => {
|
const fetchInstances = useCallback(async () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -57,7 +63,7 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [isAuthenticated])
|
||||||
|
|
||||||
const updateInstanceInMap = useCallback((name: string, updates: Partial<Instance>) => {
|
const updateInstanceInMap = useCallback((name: string, updates: Partial<Instance>) => {
|
||||||
setInstancesMap(prev => {
|
setInstancesMap(prev => {
|
||||||
@@ -154,9 +160,19 @@ export const InstancesProvider = ({ children }: InstancesProviderProps) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Only fetch instances when auth is ready and user is authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInstances()
|
if (!authLoading) {
|
||||||
}, [fetchInstances])
|
if (isAuthenticated) {
|
||||||
|
void fetchInstances()
|
||||||
|
} else {
|
||||||
|
// Clear instances when not authenticated
|
||||||
|
setInstancesMap(new Map())
|
||||||
|
setLoading(false)
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [authLoading, isAuthenticated, fetchInstances])
|
||||||
|
|
||||||
const value: InstancesContextType = {
|
const value: InstancesContextType = {
|
||||||
instances,
|
instances,
|
||||||
|
|||||||
@@ -10,18 +10,31 @@ async function apiCall<T>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = `${API_BASE}${endpoint}`;
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
|
||||||
// Prepare headers
|
// Get auth token from sessionStorage (same as AuthContext)
|
||||||
const headers: HeadersInit = {
|
const storedKey = sessionStorage.getItem('llamactl_management_key');
|
||||||
|
|
||||||
|
// Prepare headers with auth
|
||||||
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...(options.headers as Record<string, string>),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add auth header if available
|
||||||
|
if (storedKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${storedKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle authentication errors
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error('Authentication required');
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Try to get error message from response
|
// Try to get error message from response
|
||||||
let errorMessage = `HTTP ${response.status}`;
|
let errorMessage = `HTTP ${response.status}`;
|
||||||
@@ -47,7 +60,7 @@ async function apiCall<T>(
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
return text as T;
|
return text as T;
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json() as T;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import { InstancesProvider } from './contexts/InstancesContext'
|
import { InstancesProvider } from './contexts/InstancesContext'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import { AuthProvider } from './contexts/AuthContext'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AuthProvider>
|
||||||
<InstancesProvider>
|
<InstancesProvider>
|
||||||
<App />
|
<App />
|
||||||
</InstancesProvider>
|
</InstancesProvider>
|
||||||
|
</AuthProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user