From 6d63d9f48516a3ce25b6a11c98b8fd69bc1aad2d Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Wed, 11 Jun 2025 20:23:45 +0800 Subject: [PATCH] feat: Add logo and remove elements in login page --- application/src/lib/pocketbase.ts | 1 + application/src/pages/Login.tsx | 369 ++++++------------------------ 2 files changed, 77 insertions(+), 293 deletions(-) diff --git a/application/src/lib/pocketbase.ts b/application/src/lib/pocketbase.ts index ce192e9..4ad1dfb 100644 --- a/application/src/lib/pocketbase.ts +++ b/application/src/lib/pocketbase.ts @@ -51,6 +51,7 @@ if (typeof window !== 'undefined') { localStorage.removeItem('pocketbase_auth'); } } + // Subscribe to authStore changes to persist authentication pb.authStore.onChange(() => { if (pb.authStore.isValid) { diff --git a/application/src/pages/Login.tsx b/application/src/pages/Login.tsx index d4a7797..efaa951 100644 --- a/application/src/pages/Login.tsx +++ b/application/src/pages/Login.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect, useCallback } from 'react'; + +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { authService } from '@/services/authService'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { toast } from '@/hooks/use-toast'; -import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react"; +import { toast } from '@/components/ui/use-toast'; +import { Eye, EyeOff, Mail, LogIn, Settings } from "lucide-react"; import { useLanguage } from '@/contexts/LanguageContext'; import { useTheme } from '@/contexts/ThemeContext'; import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase'; @@ -12,183 +13,51 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; -const MAX_LOGIN_ATTEMPTS = 5; -const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes - -interface LoginAttempts { - count: number; - lastAttempt: number; - lockedUntil?: number; -} - const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); const [currentEndpoint, setCurrentEndpoint] = useState(getCurrentEndpoint()); - const [errors, setErrors] = useState<{ email?: string; password?: string; general?: string }>({}); - const [loginAttempts, setLoginAttempts] = useState({ count: 0, lastAttempt: 0 }); - const [isLocked, setIsLocked] = useState(false); - const [lockoutTimeRemaining, setLockoutTimeRemaining] = useState(0); - const navigate = useNavigate(); const { t } = useLanguage(); const { theme } = useTheme(); + + // Add responsiveness check const [isMobile, setIsMobile] = useState(false); - - // Check if we're in development mode - const isDevelopment = import.meta.env.DEV; - + useEffect(() => { const checkScreenSize = () => { setIsMobile(window.innerWidth < 640); }; - + checkScreenSize(); window.addEventListener('resize', checkScreenSize); + return () => { window.removeEventListener('resize', checkScreenSize); }; }, []); - // Load login attempts from localStorage - useEffect(() => { - const stored = localStorage.getItem('login_attempts'); - if (stored) { - try { - const attempts: LoginAttempts = JSON.parse(stored); - setLoginAttempts(attempts); - - if (attempts.lockedUntil && Date.now() < attempts.lockedUntil) { - setIsLocked(true); - setLockoutTimeRemaining(Math.ceil((attempts.lockedUntil - Date.now()) / 1000)); - } - } catch { - localStorage.removeItem('login_attempts'); - } - } - }, []); - - // Update lockout timer - useEffect(() => { - if (isLocked && lockoutTimeRemaining > 0) { - const timer = setInterval(() => { - setLockoutTimeRemaining(prev => { - if (prev <= 1) { - setIsLocked(false); - setLoginAttempts({ count: 0, lastAttempt: 0 }); - localStorage.removeItem('login_attempts'); - return 0; - } - return prev - 1; - }); - }, 1000); - - return () => clearInterval(timer); - } - }, [isLocked, lockoutTimeRemaining]); - - const validateEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - }; - - const validatePassword = (password: string): boolean => { - return password.length >= 6; // Minimum 6 characters - }; - - const validateForm = (): boolean => { - const newErrors: { email?: string; password?: string } = {}; - - if (!email.trim()) { - newErrors.email = t("emailRequired"); - } else if (!validateEmail(email)) { - newErrors.email = t("emailInvalid"); - } - - if (!password) { - newErrors.password = t("passwordRequired"); - } else if (!validatePassword(password)) { - newErrors.password = t("passwordTooShort"); - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const updateLoginAttempts = useCallback((failed: boolean) => { - const now = Date.now(); - - if (failed) { - const newCount = loginAttempts.count + 1; - let newAttempts: LoginAttempts = { - count: newCount, - lastAttempt: now - }; - - if (newCount >= MAX_LOGIN_ATTEMPTS) { - newAttempts.lockedUntil = now + LOCKOUT_DURATION; - setIsLocked(true); - setLockoutTimeRemaining(LOCKOUT_DURATION / 1000); - } - - setLoginAttempts(newAttempts); - localStorage.setItem('login_attempts', JSON.stringify(newAttempts)); - } else { - // Reset on successful login - setLoginAttempts({ count: 0, lastAttempt: 0 }); - localStorage.removeItem('login_attempts'); - } - }, [loginAttempts.count]); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - - if (isLocked) { - toast({ - variant: "destructive", - title: t("accountLocked"), - description: t("tooManyAttempts"), - }); - return; - } - - if (!validateForm()) { - return; - } - setLoading(true); - setErrors({}); try { - await authService.login({ email: email.toLowerCase().trim(), password }); - - updateLoginAttempts(false); - + await authService.login({ email, password }); toast({ title: t("loginSuccessful"), description: t("loginSuccessMessage"), }); - navigate('/dashboard'); } catch (error) { - updateLoginAttempts(true); - - // Generic error message for security - const remainingAttempts = MAX_LOGIN_ATTEMPTS - (loginAttempts.count + 1); - let errorMessage = t("invalidCredentials"); - - if (remainingAttempts > 0 && remainingAttempts <= 2) { - errorMessage += ` ${t("attemptsRemaining")}: ${remainingAttempts}`; - } - - setErrors({ general: errorMessage }); - + console.error("Login error details:", error); toast({ variant: "destructive", title: t("loginFailed"), - description: errorMessage, + description: error instanceof Error + ? error.message + : `${t("authenticationFailed")}. Server: ${currentEndpoint}`, }); } finally { setLoading(false); @@ -204,175 +73,103 @@ const Login = () => { setShowPassword(!showPassword); }; - const formatLockoutTime = (seconds: number): string => { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; - }; - return (
- {/* API Endpoint Settings - Only show in development */} - {/*isDevelopment && ( -
- - - - - -
-

API Endpoint Settings

- -
- - -
-
- - -
-
-
- Current: {currentEndpoint} + {/* Commented out API Endpoint Settings button +
+ + + + + +
+

API Endpoint Settings

+ +
+ +
+
+ + +
+
+
+ Current endpoint: {currentEndpoint}
- - -
- )*/} - - {/* Logo */} - Checkcle Logo { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - -

- {t("signInToYourAccount")} -

+
+ + +
+ */} - {isLocked && ( -
-
- - - {t("accountLocked")} - {formatLockoutTime(lockoutTimeRemaining)} - -
-
- )} + {/* Logo */} +
+ Checkcle Logo +
+ +

{t("signInToYourAccount")}

+ {/* Removed "Don't have an account? Create one" text */}
-
- {/* General Error */} - {errors.general && ( -
-
- - {errors.general} -
-
- )} + {/* Removed Google Sign in button section */} +
- +
{ - setEmail(e.target.value); - if (errors.email) { - setErrors(prev => ({ ...prev, email: undefined })); - } - }} + onChange={(e) => setEmail(e.target.value)} required - autoComplete="email" - disabled={loading || isLocked} - className={`pl-10 text-sm sm:text-base h-9 sm:h-10 ${ - errors.email ? 'border-destructive focus:border-destructive' : '' - }`} - aria-describedby={errors.email ? 'email-error' : undefined} + className="pl-10 text-sm sm:text-base h-9 sm:h-10" />
- {errors.email && ( -

- {errors.email} -

- )}
- +
- - e.preventDefault()} - > - {t("forgot")} - + + {t("forgot")}
- +
{ - setPassword(e.target.value); - if (errors.password) { - setErrors(prev => ({ ...prev, password: undefined })); - } - }} + onChange={(e) => setPassword(e.target.value)} required - autoComplete="current-password" - disabled={loading || isLocked} - className={`pl-10 pr-10 text-sm sm:text-base h-9 sm:h-10 ${ - errors.password ? 'border-destructive focus:border-destructive' : '' - }`} - aria-describedby={errors.password ? 'password-error' : undefined} + className="pl-10 text-sm sm:text-base h-9 sm:h-10" />
- {errors.password && ( -

- {errors.password} -

- )}
- -

- {t("bySigningIn")} {t("termsAndConditions")} {t("and")} {t("privacyPolicy")}. +

+ {t("bySigningIn")} {t("termsAndConditions")} {t("and")} {t("privacyPolicy")}.

@@ -416,4 +199,4 @@ const Login = () => { ); }; -export default Login; +export default Login; \ No newline at end of file