Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 56s
289 lines
11 KiB
TypeScript
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>
|
|
);
|
|
};
|