mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-27 15:37:03 +00:00
feat: add garage.toml support (#30)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export function TokenLoginForm() {
|
||||
const [token, setToken] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { loginToken } = useAuthStore();
|
||||
|
||||
const returnUrl = searchParams.get('returnUrl') || '/';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await loginToken(token);
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="space-y-1">
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<img
|
||||
src="/garage.png"
|
||||
alt="Garage Logo"
|
||||
className="h-16 w-16 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-center">
|
||||
Welcome to Garage UI
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="admin-token" className="text-sm font-medium">Admin Token</label>
|
||||
<Input
|
||||
id="admin-token"
|
||||
type="password"
|
||||
placeholder="Enter your Garage admin token"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading || !token}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -133,6 +133,7 @@ export const authApi = {
|
||||
const response = await authApiClient.get<{
|
||||
admin: { enabled: boolean };
|
||||
oidc: { enabled: boolean; provider?: string };
|
||||
token: { enabled: boolean };
|
||||
}>('/config');
|
||||
return response;
|
||||
},
|
||||
@@ -145,6 +146,13 @@ export const authApi = {
|
||||
return response;
|
||||
},
|
||||
|
||||
loginToken: async (token: string) => {
|
||||
const response = await authApiClient.post<{ success: boolean; token: string; user: AuthUser }>('/login-token', {
|
||||
token,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
|
||||
me: async () => {
|
||||
const response = await authApiClient.get<{ success: boolean; user: AuthUser }>('/me');
|
||||
return response;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
import { BasicLoginForm } from '@/components/auth/BasicLoginForm';
|
||||
import { OIDCLoginView } from '@/components/auth/OIDCLoginView';
|
||||
import { TokenLoginForm } from '@/components/auth/TokenLoginForm';
|
||||
import { LoadingSpinner } from '@/components/auth/LoadingSpinner';
|
||||
|
||||
export function Login() {
|
||||
@@ -14,9 +15,7 @@ export function Login() {
|
||||
const returnUrl = searchParams.get('returnUrl') || '/';
|
||||
|
||||
useEffect(() => {
|
||||
// Handle OIDC callback
|
||||
if (loginSuccess === 'success') {
|
||||
// OIDC login successful, re-initialize auth to fetch user
|
||||
initialize().then(() => {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
});
|
||||
@@ -24,7 +23,6 @@ export function Login() {
|
||||
}, [loginSuccess, initialize, navigate, returnUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// If already authenticated, redirect to return URL
|
||||
if (isAuthenticated && !loginSuccess) {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
}
|
||||
@@ -35,16 +33,27 @@ export function Login() {
|
||||
}
|
||||
|
||||
// No auth enabled, redirect to dashboard immediately
|
||||
if (config && !config.admin.enabled && !config.oidc.enabled) {
|
||||
if (config && !config.admin.enabled && !config.oidc.enabled && !config.token.enabled) {
|
||||
navigate('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show login options based on what's enabled
|
||||
const showAdmin = config?.admin.enabled || false;
|
||||
const showOIDC = config?.oidc.enabled || false;
|
||||
const showToken = config?.token.enabled || false;
|
||||
|
||||
// If both are enabled, show both options in single modal
|
||||
// Token-only auth (zero-config fallback)
|
||||
if (showToken && !showAdmin && !showOIDC) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<TokenLoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Both admin and OIDC enabled
|
||||
if (showAdmin && showOIDC) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
@@ -55,12 +64,12 @@ export function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Show only OIDC if enabled
|
||||
// Only OIDC
|
||||
if (showOIDC) {
|
||||
return <OIDCLoginView />;
|
||||
}
|
||||
|
||||
// Show only admin if enabled
|
||||
// Only admin
|
||||
if (showAdmin) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
@@ -71,6 +80,5 @@ export function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Still loading config
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface AuthStore extends AuthState {
|
||||
// Async actions
|
||||
initialize: () => Promise<void>;
|
||||
loginAdmin: (username: string, password: string) => Promise<void>;
|
||||
loginToken: (token: string) => Promise<void>;
|
||||
loginOIDC: () => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
@@ -45,7 +46,7 @@ export const useAuthStore = create<AuthStore>()(
|
||||
set({ config });
|
||||
|
||||
// If no auth is enabled, mark as authenticated immediately
|
||||
if (!config.admin.enabled && !config.oidc.enabled) {
|
||||
if (!config.admin.enabled && !config.oidc.enabled && !config.token.enabled) {
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
@@ -113,6 +114,36 @@ export const useAuthStore = create<AuthStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
loginToken: async (token) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await authApi.loginToken(token);
|
||||
const { token: sessionToken, user } = response.data;
|
||||
|
||||
localStorage.setItem('auth-token', sessionToken);
|
||||
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
(error as { response?: { data?: { error?: { message?: string } } } })
|
||||
.response?.data?.error?.message ||
|
||||
(error instanceof Error ? error.message : 'Login failed');
|
||||
set({
|
||||
error: errorMessage,
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
loginOIDC: () => {
|
||||
// Redirect to OIDC login endpoint
|
||||
window.location.href = '/auth/oidc/login';
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface AuthConfig {
|
||||
enabled: boolean;
|
||||
provider?: string;
|
||||
};
|
||||
token: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
|
||||
Reference in New Issue
Block a user