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.
282 lines
8.7 KiB
TypeScript
282 lines
8.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from '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 { isValidEmail } from '@/lib/utils';
|
|
|
|
interface RegisterDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSuccess?: () => void;
|
|
onOpenLogin?: () => void;
|
|
callbackUrl?: string;
|
|
}
|
|
|
|
export function RegisterDialog({
|
|
open,
|
|
onOpenChange,
|
|
onSuccess,
|
|
onOpenLogin,
|
|
callbackUrl: initialCallbackUrl,
|
|
}: RegisterDialogProps) {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const callbackUrl = initialCallbackUrl || searchParams.get('callbackUrl') || '/';
|
|
|
|
const [name, setName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
// Clear form when dialog opens
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName('');
|
|
setEmail('');
|
|
setPassword('');
|
|
setConfirmPassword('');
|
|
setError('');
|
|
setShowPassword(false);
|
|
setShowConfirmPassword(false);
|
|
}
|
|
}, [open]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
setError('Name is required');
|
|
return;
|
|
}
|
|
|
|
if (!email || !isValidEmail(email)) {
|
|
setError('Please enter a valid email address');
|
|
return;
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
setError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
setError('Password must be at least 6 characters');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/register', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email, password, name }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
setError(data.error || 'Failed to create account');
|
|
return;
|
|
}
|
|
|
|
// Registration successful - clear form and show success message
|
|
setName('');
|
|
setEmail('');
|
|
setPassword('');
|
|
setConfirmPassword('');
|
|
setError('');
|
|
|
|
// Show success state
|
|
alert('Account created successfully! Please check your email to confirm your account before signing in.');
|
|
|
|
onOpenChange(false);
|
|
if (onOpenLogin) {
|
|
// Open login dialog with registered flag
|
|
onOpenLogin();
|
|
} else if (onSuccess) {
|
|
onSuccess();
|
|
} else {
|
|
// Redirect to login with registered flag
|
|
router.push(`/login?registered=true&callbackUrl=${encodeURIComponent(callbackUrl)}`);
|
|
}
|
|
} catch (err) {
|
|
setError('An error occurred. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
if (!newOpen) {
|
|
// Reset form when closing
|
|
setName('');
|
|
setEmail('');
|
|
setPassword('');
|
|
setConfirmPassword('');
|
|
setError('');
|
|
setShowPassword(false);
|
|
setShowConfirmPassword(false);
|
|
}
|
|
onOpenChange(newOpen);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Create your account</DialogTitle>
|
|
<DialogDescription>
|
|
Or{' '}
|
|
<button
|
|
type="button"
|
|
className="font-medium text-secondary hover:text-secondary/80"
|
|
onClick={() => {
|
|
handleOpenChange(false);
|
|
if (onOpenLogin) {
|
|
onOpenLogin();
|
|
}
|
|
}}
|
|
>
|
|
sign in to your existing account
|
|
</button>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{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="name" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Name <span className="text-red-500">*</span>
|
|
</label>
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
type="text"
|
|
autoComplete="off"
|
|
required
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="mt-1"
|
|
placeholder="Your full name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Email address <span className="text-red-500">*</span>
|
|
</label>
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="off"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="mt-1"
|
|
placeholder="you@example.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Password
|
|
</label>
|
|
<div className="relative mt-1">
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
autoComplete="new-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>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Must be at least 6 characters
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-secondary dark:text-gray-300">
|
|
Confirm Password
|
|
</label>
|
|
<div className="relative mt-1">
|
|
<Input
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
autoComplete="new-password"
|
|
required
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className="pr-10"
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-secondary hover:text-secondary/80 focus:outline-none"
|
|
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<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 ? 'Creating account...' : 'Create account'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|