This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
305 lines
9.6 KiB
TypeScript
305 lines
9.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { signIn } from 'next-auth/react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import Link from 'next/link';
|
|
import { ForgotPasswordDialog } from '@/components/ForgotPasswordDialog';
|
|
|
|
interface LoginDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSuccess?: () => void;
|
|
onOpenRegister?: () => void;
|
|
callbackUrl?: string;
|
|
registered?: boolean;
|
|
}
|
|
|
|
export function LoginDialog({
|
|
open,
|
|
onOpenChange,
|
|
onSuccess,
|
|
onOpenRegister,
|
|
callbackUrl: initialCallbackUrl,
|
|
registered: initialRegistered,
|
|
}: LoginDialogProps) {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const callbackUrl = initialCallbackUrl || searchParams.get('callbackUrl') || '/';
|
|
const registered = initialRegistered || searchParams.get('registered') === 'true';
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [emailNotVerified, setEmailNotVerified] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isResending, setIsResending] = useState(false);
|
|
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
// Reset all form state when dialog opens
|
|
useEffect(() => {
|
|
if (open) {
|
|
setEmail('');
|
|
setPassword('');
|
|
setError('');
|
|
setEmailNotVerified(false);
|
|
setIsLoading(false);
|
|
setIsResending(false);
|
|
setShowPassword(false);
|
|
}
|
|
}, [open]);
|
|
|
|
const handleResendConfirmation = async () => {
|
|
setIsResending(true);
|
|
try {
|
|
const response = await fetch('/api/auth/resend-confirmation', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
setError('');
|
|
setEmailNotVerified(false);
|
|
alert('Confirmation email sent! Please check your inbox.');
|
|
} else {
|
|
alert(data.error || 'Failed to resend confirmation email');
|
|
}
|
|
} catch (err) {
|
|
alert('An error occurred. Please try again.');
|
|
} finally {
|
|
setIsResending(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setEmailNotVerified(false);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// First check if email is verified
|
|
const checkResponse = await fetch('/api/auth/check-verification', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
const checkData = await checkResponse.json();
|
|
|
|
if (!checkData.exists) {
|
|
setError('Invalid email or password');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (!checkData.passwordValid) {
|
|
setError('Invalid email or password');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (!checkData.verified) {
|
|
setEmailNotVerified(true);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Email is verified, proceed with login
|
|
const result = await signIn('credentials', {
|
|
email,
|
|
password,
|
|
redirect: false,
|
|
});
|
|
|
|
if (result?.error) {
|
|
setError('Invalid email or password');
|
|
} else {
|
|
onOpenChange(false);
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
} else {
|
|
router.push(callbackUrl);
|
|
router.refresh();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError('An error occurred. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
if (!newOpen) {
|
|
// Reset form when closing
|
|
setEmail('');
|
|
setPassword('');
|
|
setError('');
|
|
setEmailNotVerified(false);
|
|
setIsResending(false);
|
|
}
|
|
onOpenChange(newOpen);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Sign in to your account</DialogTitle>
|
|
<DialogDescription>
|
|
Or{' '}
|
|
{onOpenRegister ? (
|
|
<button
|
|
type="button"
|
|
className="font-medium text-secondary hover:text-secondary/80"
|
|
onClick={() => {
|
|
handleOpenChange(false);
|
|
onOpenRegister();
|
|
}}
|
|
>
|
|
create a new account
|
|
</button>
|
|
) : (
|
|
<Link
|
|
href="/register"
|
|
className="font-medium text-secondary hover:text-secondary/80"
|
|
onClick={() => handleOpenChange(false)}
|
|
>
|
|
create a new account
|
|
</Link>
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{registered && (
|
|
<div className="rounded-md bg-green-50 p-4 dark:bg-green-900/20">
|
|
<p className="text-sm text-green-800 dark:text-green-200">
|
|
Account created successfully! Please check your email to confirm your account before signing in.
|
|
</p>
|
|
</div>
|
|
)}
|
|
{searchParams.get('verified') === 'true' && (
|
|
<div className="rounded-md bg-green-50 p-4 dark:bg-green-900/20">
|
|
<p className="text-sm text-green-800 dark:text-green-200">
|
|
Email verified successfully! You can now sign in.
|
|
</p>
|
|
</div>
|
|
)}
|
|
{emailNotVerified && (
|
|
<div className="rounded-md bg-yellow-50 p-4 dark:bg-yellow-900/20">
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 mb-2">
|
|
Please verify your email address before signing in. Check your inbox for a confirmation email.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={handleResendConfirmation}
|
|
disabled={isResending}
|
|
className="text-sm text-yellow-900 dark:text-yellow-100 underline hover:no-underline font-medium"
|
|
>
|
|
{isResending ? 'Sending...' : 'Resend confirmation email'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{error && (
|
|
<div className="rounded-md bg-red-50 p-4 dark:bg-red-900/20">
|
|
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
|
</div>
|
|
)}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Email address
|
|
</label>
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => {
|
|
setEmail(e.target.value);
|
|
// Clear email verification error when email changes
|
|
if (emailNotVerified) {
|
|
setEmailNotVerified(false);
|
|
}
|
|
}}
|
|
className="mt-1"
|
|
placeholder="you@example.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center justify-between">
|
|
<label htmlFor="password" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Password
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setForgotPasswordOpen(true);
|
|
}}
|
|
className="text-sm text-secondary hover:text-secondary/80 font-medium"
|
|
>
|
|
Forgot password?
|
|
</button>
|
|
</div>
|
|
<div className="relative mt-1">
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="pr-10"
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
<ForgotPasswordDialog
|
|
open={forgotPasswordOpen}
|
|
onOpenChange={setForgotPasswordOpen}
|
|
/>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|