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:
webdevcody
2026-01-07 10:18:06 -05:00
parent 11accac5ae
commit 0d206fe75f
2 changed files with 64 additions and 22 deletions

View File

@@ -3,17 +3,26 @@
*
* Prompts user to enter the API key shown in server console.
* 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 { login } from '@/lib/http-api-client';
import { login, verifySession } from '@/lib/http-api-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { KeyRound, AlertCircle, Loader2 } from 'lucide-react';
import { useAuthStore } from '@/store/auth-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() {
const navigate = useNavigate();
const setAuthState = useAuthStore((s) => s.setAuthState);
@@ -21,6 +30,45 @@ export function LoginView() {
const [apiKey, setApiKey] = useState('');
const [error, setError] = useState<string | null>(null);
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) => {
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 (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md space-y-8">

View File

@@ -1,7 +1,7 @@
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
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 { buildProviderConfigs } from '@/config/api-providers';
import { SecurityNotice } from './security-notice';
@@ -10,13 +10,11 @@ import { cn } from '@/lib/utils';
import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import { useNavigate } from '@tanstack/react-router';
export function ApiKeysSection() {
const { apiKeys, setApiKeys } = useAppStore();
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
const { claudeAuthStatus, setClaudeAuthStatus } = useSetupStore();
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const navigate = useNavigate();
const { providerConfigParams, handleSave, saved } = useApiKeyManagement();
@@ -51,12 +49,6 @@ export function ApiKeysSection() {
}
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
// Open setup wizard
const openSetupWizard = useCallback(() => {
setSetupComplete(false);
navigate({ to: '/setup' });
}, [setSetupComplete, navigate]);
return (
<div
className={cn(
@@ -111,16 +103,6 @@ export function ApiKeysSection() {
)}
</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 && (
<Button
onClick={deleteAnthropicKey}