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>
230 lines
No EOL
8.4 KiB
TypeScript
Executable file
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>
|
|
);
|
|
} |