mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
feat: enhance login view with session verification and loading state
- Implemented session verification on component mount using exponential backoff to handle server live reload scenarios. - Added loading state to the login view while checking for an existing session, improving user experience. - Removed unused setup wizard navigation from the API keys section for cleaner code.
This commit is contained in:
@@ -3,17 +3,26 @@
|
|||||||
*
|
*
|
||||||
* Prompts user to enter the API key shown in server console.
|
* Prompts user to enter the API key shown in server console.
|
||||||
* On successful login, sets an HTTP-only session cookie.
|
* On successful login, sets an HTTP-only session cookie.
|
||||||
|
*
|
||||||
|
* On mount, verifies if an existing session is valid using exponential backoff.
|
||||||
|
* This handles cases where server live reloads kick users back to login
|
||||||
|
* even though their session is still valid.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { login } from '@/lib/http-api-client';
|
import { login, verifySession } from '@/lib/http-api-client';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { KeyRound, AlertCircle, Loader2 } from 'lucide-react';
|
import { KeyRound, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
import { useAuthStore } from '@/store/auth-store';
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delay helper for exponential backoff
|
||||||
|
*/
|
||||||
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export function LoginView() {
|
export function LoginView() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setAuthState = useAuthStore((s) => s.setAuthState);
|
const setAuthState = useAuthStore((s) => s.setAuthState);
|
||||||
@@ -21,6 +30,45 @@ export function LoginView() {
|
|||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isCheckingSession, setIsCheckingSession] = useState(true);
|
||||||
|
const sessionCheckRef = useRef(false);
|
||||||
|
|
||||||
|
// Check for existing valid session on mount with exponential backoff
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent duplicate checks in strict mode
|
||||||
|
if (sessionCheckRef.current) return;
|
||||||
|
sessionCheckRef.current = true;
|
||||||
|
|
||||||
|
const checkExistingSession = async () => {
|
||||||
|
const maxRetries = 5;
|
||||||
|
const baseDelay = 500; // Start with 500ms
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const isValid = await verifySession();
|
||||||
|
if (isValid) {
|
||||||
|
// Session is valid, redirect to the main app
|
||||||
|
setAuthState({ isAuthenticated: true, authChecked: true });
|
||||||
|
navigate({ to: setupComplete ? '/' : '/setup' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Session is invalid, no need to retry - show login form
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
// Network error or server not ready, retry with exponential backoff
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
const waitTime = baseDelay * Math.pow(2, attempt); // 500, 1000, 2000, 4000, 8000ms
|
||||||
|
await delay(waitTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session check complete (either invalid or all retries exhausted)
|
||||||
|
setIsCheckingSession(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkExistingSession();
|
||||||
|
}, [navigate, setAuthState, setupComplete]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -45,6 +93,18 @@ export function LoginView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading state while checking existing session
|
||||||
|
if (isCheckingSession) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
||||||
|
<p className="text-sm text-muted-foreground">Checking session...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from 'lucide-react';
|
import { Key, CheckCircle2, Trash2, Loader2 } from 'lucide-react';
|
||||||
import { ApiKeyField } from './api-key-field';
|
import { ApiKeyField } from './api-key-field';
|
||||||
import { buildProviderConfigs } from '@/config/api-providers';
|
import { buildProviderConfigs } from '@/config/api-providers';
|
||||||
import { SecurityNotice } from './security-notice';
|
import { SecurityNotice } from './security-notice';
|
||||||
@@ -10,13 +10,11 @@ import { cn } from '@/lib/utils';
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
export function ApiKeysSection() {
|
export function ApiKeysSection() {
|
||||||
const { apiKeys, setApiKeys } = useAppStore();
|
const { apiKeys, setApiKeys } = useAppStore();
|
||||||
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
|
const { claudeAuthStatus, setClaudeAuthStatus } = useSetupStore();
|
||||||
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
|
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { providerConfigParams, handleSave, saved } = useApiKeyManagement();
|
const { providerConfigParams, handleSave, saved } = useApiKeyManagement();
|
||||||
|
|
||||||
@@ -51,12 +49,6 @@ export function ApiKeysSection() {
|
|||||||
}
|
}
|
||||||
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
|
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
|
||||||
|
|
||||||
// Open setup wizard
|
|
||||||
const openSetupWizard = useCallback(() => {
|
|
||||||
setSetupComplete(false);
|
|
||||||
navigate({ to: '/setup' });
|
|
||||||
}, [setSetupComplete, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -111,16 +103,6 @@ export function ApiKeysSection() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={openSetupWizard}
|
|
||||||
variant="outline"
|
|
||||||
className="h-10 border-border"
|
|
||||||
data-testid="run-setup-wizard"
|
|
||||||
>
|
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
|
||||||
Run Setup Wizard
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{apiKeys.anthropic && (
|
{apiKeys.anthropic && (
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteAnthropicKey}
|
onClick={deleteAnthropicKey}
|
||||||
|
|||||||
Reference in New Issue
Block a user