Files
flyer-crawler.projectium.com/src/pages/admin/components/AuthView.tsx

289 lines
11 KiB
TypeScript

// src/pages/admin/components/AuthView.tsx
import React, { useState } from 'react';
import type { UserProfile } from '../../../types';
import { useApi } from '../../../hooks/useApi';
import * as apiClient from '../../../services/apiClient';
import { notifySuccess } from '../../../services/notificationService';
import { LoadingSpinner } from '../../../components/LoadingSpinner';
import { GoogleIcon } from '../../../components/icons/GoogleIcon';
import { GithubIcon } from '../../../components/icons/GithubIcon';
import { PasswordInput } from '../../../components/PasswordInput';
interface AuthResponse {
userprofile: UserProfile;
token: string;
}
interface AuthViewProps {
onLoginSuccess: (user: UserProfile, token: string, rememberMe: boolean) => void;
onClose: () => void;
}
export const AuthView: React.FC<AuthViewProps> = ({ onLoginSuccess, onClose }) => {
const [isRegistering, setIsRegistering] = useState(false);
const [authEmail, setAuthEmail] = useState('');
const [authPassword, setAuthPassword] = useState('');
const [authFullName, setAuthFullName] = useState('');
const [isForgotPassword, setIsForgotPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const { execute: executeLogin, loading: loginLoading } = useApi<
AuthResponse,
[string, string, boolean]
>(apiClient.loginUser);
const { execute: executeRegister, loading: registerLoading } = useApi<
AuthResponse,
[string, string, string, string]
>(apiClient.registerUser);
const { execute: executePasswordReset, loading: passwordResetLoading } = useApi<
{ message: string },
[string]
>(apiClient.requestPasswordReset);
const handleAuthSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const authResult = isRegistering
? await executeRegister(authEmail, authPassword, authFullName, '')
: await executeLogin(authEmail, authPassword, rememberMe);
if (authResult) {
onLoginSuccess(authResult.userprofile, authResult.token, rememberMe);
onClose();
}
};
const handlePasswordResetRequest = async (e: React.FormEvent) => {
e.preventDefault();
const result = await executePasswordReset(authEmail);
if (result) {
notifySuccess(result.message);
}
};
const handleOAuthSignIn = (provider: 'google' | 'github') => {
window.location.href = '/api/auth/' + provider;
};
if (isForgotPassword) {
return (
<div className="p-8">
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-1">Reset Password</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Enter your email to receive a password reset link.
</p>
<form
data-testid="reset-password-form"
onSubmit={handlePasswordResetRequest}
className="space-y-4"
>
<div>
<label
htmlFor="resetEmail"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email Address
</label>
<input
id="resetEmail"
type="email"
value={authEmail}
onChange={(e) => setAuthEmail(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm"
disabled={passwordResetLoading}
/>
</div>
<div className="pt-2">
<button
type="submit"
disabled={passwordResetLoading}
className="w-full bg-brand-primary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center shadow-sm transition-colors"
>
{passwordResetLoading ? (
<div className="w-5 h-5">
<LoadingSpinner />
</div>
) : (
'Send Reset Link'
)}
</button>
</div>
</form>
<div className="text-center mt-4">
<button
onClick={() => {
setIsForgotPassword(false);
}}
className="text-sm font-medium text-brand-primary hover:text-brand-dark dark:hover:text-brand-light underline"
>
Back to Sign In
</button>
</div>
</div>
);
}
return (
<div className="p-8">
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-1">
{isRegistering ? 'Create an Account' : 'Sign In'}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
{isRegistering ? 'to get started.' : 'to access your account.'}
</p>
{isRegistering && (
<div className="space-y-4 mb-4">
<div>
<label
htmlFor="authFullName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Full Name
</label>
<input
id="authFullName"
type="text"
value={authFullName}
onChange={(e) => setAuthFullName(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm"
placeholder="Optional"
/>
</div>
</div>
)}
<form data-testid="auth-form" onSubmit={handleAuthSubmit} className="space-y-4">
<div>
<label
htmlFor="authEmail"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email Address
</label>
<input
id="authEmail"
type="email"
value={authEmail}
onChange={(e) => setAuthEmail(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary sm:text-sm"
disabled={loginLoading || registerLoading}
/>
</div>
<div>
<label
htmlFor="authPassword"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Password
</label>
<PasswordInput
id="authPassword"
value={authPassword}
onChange={(e) => setAuthPassword(e.target.value)}
required
className="mt-1"
disabled={loginLoading || registerLoading}
showStrength={isRegistering}
/>
</div>
{!isRegistering && (
<div className="flex items-center justify-between text-sm">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 text-brand-primary border-gray-300 rounded focus:ring-brand-secondary"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-900 dark:text-gray-300"
>
Remember me
</label>
</div>
<button
type="button"
onClick={() => setIsForgotPassword(true)}
className="font-medium text-brand-primary hover:text-brand-dark dark:hover:text-brand-light underline"
>
Forgot password?
</button>
</div>
)}
<div className="pt-2">
<button
type="submit"
disabled={loginLoading || registerLoading}
className="w-full bg-brand-primary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center shadow-sm transition-colors"
>
{loginLoading || registerLoading ? (
<div className="w-5 h-5">
<LoadingSpinner />
</div>
) : isRegistering ? (
'Register'
) : (
'Sign In'
)}
</button>
</div>
</form>
<div className="mt-4">
<button
onClick={() => {
setIsRegistering(!isRegistering);
}}
className="w-full text-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"}
</button>
</div>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200 dark:border-gray-700" />
</div>
<div className="relative flex justify-center">
<span className="bg-white dark:bg-gray-800 px-3 text-sm text-gray-500 dark:text-gray-400">
Or continue with
</span>
</div>
</div>
<div className="space-y-3">
<button
type="button"
onClick={() => handleOAuthSignIn('google')}
disabled={loginLoading || registerLoading}
className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loginLoading || registerLoading ? (
<div className="w-5 h-5 mr-3">
<LoadingSpinner />
</div>
) : (
<GoogleIcon className="w-5 h-5 mr-3" />
)}
Sign In with Google
</button>
<button
type="button"
onClick={() => handleOAuthSignIn('github')}
disabled={loginLoading || registerLoading}
className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loginLoading || registerLoading ? (
<div className="w-5 h-5 mr-3">
<LoadingSpinner />
</div>
) : (
<GithubIcon className="w-5 h-5 mr-3" />
)}
Sign In with GitHub
</button>
</div>
</div>
);
};