semblance-dev/src/pages/Login.tsx
Vadym Samoilenko 2c6505f472 Switch to loginRedirect — popup blocked by server COOP header
Server-level COOP prevents parent from monitoring popup.location,
so loginPopup never resolves in parent. Redirect flow is the correct
solution: page navigates to Microsoft, returns with #code, and
handleRedirectPromise exchanges the idToken with backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 14:44:19 +00:00

230 lines
No EOL
8.4 KiB
TypeScript
Executable file

import { useState, useEffect } from 'react';
import { useNavigate, Link, useLocation } from 'react-router-dom';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toastService } from '@/lib/toast';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { authApi } from '@/lib/api';
import { useAuth } from '@/contexts/AuthContext';
import { Loader2 } from 'lucide-react';
const loginSchema = z.object({
username: z.string().min(3, "Username must be at least 3 characters"),
password: z.string().min(4, "Password must be at least 4 characters"),
});
type LoginFormValues = z.infer<typeof loginSchema>;
export default function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login, loginWithMicrosoft, isAuthenticated, isMsalLoading } = useAuth();
const [isLoading, setIsLoading] = useState(false);
// Check if local login is enabled (defaults to true for backwards compatibility)
const enableLocalLogin = import.meta.env.VITE_ENABLE_LOCAL_LOGIN !== 'false';
// Get the intended destination from state, or default to home page
const from = location.state?.from || '/';
// Debug the location state
console.log('Login page - destination path:', from);
// Redirect if already logged in
useEffect(() => {
if (isAuthenticated) {
console.log('User already authenticated, redirecting from login page');
navigate('/', { replace: true });
}
}, [isAuthenticated, navigate]);
const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
defaultValues: {
username: '',
password: '',
},
});
async function onSubmit(values: LoginFormValues) {
setIsLoading(true);
try {
// Use the login function from auth context
const token = await login(values.username, values.password);
if (token) {
console.log('Login successful, received token, navigating to:', from);
// Use React Router navigation to preserve state
navigate(from, { replace: true });
} else {
console.error('Login succeeded but no token received');
setIsLoading(false);
}
} catch (error: unknown) {
// Error handling is done in login function already
console.error('Login error in form handler:', error);
setIsLoading(false);
}
}
async function handleMicrosoftLogin() {
try {
await loginWithMicrosoft();
// loginRedirect navigates the page away — no navigate() call needed
} catch (error: unknown) {
console.error('Microsoft login error in form handler:', error);
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-900 dark:to-gray-800 px-4">
<Card className="w-full max-w-md">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-center">Sign In</CardTitle>
<CardDescription className="text-center">
Enter your credentials to access your account
</CardDescription>
</CardHeader>
<CardContent>
{/* Microsoft Sign-In Button */}
<div className="mb-6">
<Button
type="button"
variant="outline"
className="w-full bg-[#0078d4] hover:bg-[#106ebe] text-white border-[#0078d4] hover:border-[#106ebe]"
onClick={handleMicrosoftLogin}
disabled={isLoading || isMsalLoading}
>
{isMsalLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in with Microsoft...
</>
) : (
<>
<svg className="mr-2 h-4 w-4" viewBox="0 0 21 21" fill="currentColor">
<path d="M10 0H0v10h10V0z" />
<path d="M21 0H11v10h10V0z" />
<path d="M10 11H0v10h10V11z" />
<path d="M21 11H11v10h10V11z" />
</svg>
Sign in with Microsoft
</>
)}
</Button>
</div>
{enableLocalLogin && (
<>
{/* Divider */}
<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500 dark:bg-gray-800 dark:text-gray-400">
Or continue with username
</span>
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Enter your username"
{...field}
disabled={isLoading}
autoComplete="username"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Enter your password"
type="password"
{...field}
disabled={isLoading}
autoComplete="current-password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isLoading || isMsalLoading}>
{isLoading ? "Signing in..." : "Sign In"}
</Button>
</form>
</Form>
</>
)}
</CardContent>
<CardFooter className="flex flex-col space-y-2">
{enableLocalLogin && (
<div className="text-sm text-center text-gray-500 mb-2">
Default account: user / pass
</div>
)}
{!isLoading && !isMsalLoading && (
<div className="flex flex-col items-center justify-center gap-2">
<Button
variant="outline"
onClick={() => navigate('/', { replace: true })}
className="mt-2"
>
Return to Home
</Button>
<Button
variant="link"
onClick={() => {
// Set offline mode flag
localStorage.setItem('offline_mode', 'true');
// Create a mock user and token
const mockUser = { username: 'guest', email: 'guest@example.com', role: 'user' };
const mockToken = 'offline-mode-token';
localStorage.setItem('auth_token', mockToken);
localStorage.setItem('user', JSON.stringify(mockUser));
toastService.success('Offline mode activated', {
description: 'Using demo account with limited functionality'
});
// Navigate to home
navigate('/', { replace: true });
}}
className="text-sm text-gray-500"
>
Use offline mode
</Button>
</div>
)}
</CardFooter>
</Card>
</div>
);
}