Tanya de2144be2a feat: Add new scripts and update project structure for database management and user authentication
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.
2026-01-06 13:53:24 -05:00

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>
);
}