+
-
Welcome to Your Blank App
-
Start building your amazing project here!
+
+
Loading...
);
};
-export default Index;
+export default Index;
\ No newline at end of file
diff --git a/application/src/pages/Login.tsx b/application/src/pages/Login.tsx
index 089773b..d4a7797 100644
--- a/application/src/pages/Login.tsx
+++ b/application/src/pages/Login.tsx
@@ -1,11 +1,10 @@
-
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } 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 '@/components/ui/use-toast';
-import { Eye, EyeOff, Mail, LogIn, Settings } from "lucide-react";
+import { toast } from '@/hooks/use-toast';
+import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react";
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase';
@@ -13,51 +12,183 @@ 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, password });
+ await authService.login({ email: email.toLowerCase().trim(), password });
+
+ updateLoginAttempts(false);
+
toast({
title: t("loginSuccessful"),
description: t("loginSuccessMessage"),
});
+
navigate('/dashboard');
} catch (error) {
- console.error("Login error details:", 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 });
+
toast({
variant: "destructive",
title: t("loginFailed"),
- description: error instanceof Error
- ? error.message
- : `${t("authenticationFailed")}. Server: ${currentEndpoint}`,
+ description: errorMessage,
});
} finally {
setLoading(false);
@@ -73,96 +204,175 @@ 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 (
-
-
-
- {/* //this allow test api connection in login page
-
-
API Endpoint Settings
-
-
-
-
-
-
-
-
+ {/* API Endpoint Settings - Only show in development */}
+ {/*isDevelopment && (
+
+
+
+
+
+
+
+
API Endpoint Settings
+
+
+
+
+
+
+
+
+
+
+
+ Current: {currentEndpoint}
-
-
- Current endpoint: {currentEndpoint}
-
- */}
-
-
-
{t("signInToYourAccount")}
+
+
+
+ )*/}
-
+ {/* Logo */}
+
{
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
-
-
-
-
-
{t("orContinueWith")}
+
+ {t("signInToYourAccount")}
+
+
+ {isLocked && (
+
+
+
+
+ {t("accountLocked")} - {formatLockoutTime(lockoutTimeRemaining)}
+
+
-
+ )}
diff --git a/application/src/pages/OperationalPage.tsx b/application/src/pages/OperationalPage.tsx
index dc44a43..ccfd3ed 100644
--- a/application/src/pages/OperationalPage.tsx
+++ b/application/src/pages/OperationalPage.tsx
@@ -1,15 +1,15 @@
-import React, { useState } from "react";
+import React from "react";
import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { OperationalPageContent } from '@/components/operational-page/OperationalPageContent';
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
+import { useSidebar } from "@/contexts/SidebarContext";
const OperationalPage = () => {
- // State for sidebar collapse functionality
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
- const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+ // Use shared sidebar state
+ const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
diff --git a/application/src/pages/Profile.tsx b/application/src/pages/Profile.tsx
index 0ba6aed..7126fb2 100644
--- a/application/src/pages/Profile.tsx
+++ b/application/src/pages/Profile.tsx
@@ -9,11 +9,11 @@ import { ProfileContent } from "@/components/profile/ProfileContent";
import { User } from "@/services/userService";
import { useToast } from "@/hooks/use-toast";
import { Loader2 } from "lucide-react";
+import { useSidebar } from "@/contexts/SidebarContext";
const Profile = () => {
- // State for sidebar collapse functionality
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
- const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+ // Use shared sidebar state
+ const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
diff --git a/application/src/pages/ScheduleIncident.tsx b/application/src/pages/ScheduleIncident.tsx
index 6adef1f..c58cb62 100644
--- a/application/src/pages/ScheduleIncident.tsx
+++ b/application/src/pages/ScheduleIncident.tsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from 'react';
+import React, { useEffect } from 'react';
import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { useTheme } from "@/contexts/ThemeContext";
@@ -8,11 +8,11 @@ import { ScheduleIncidentContent } from "@/components/schedule-incident/Schedule
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
import { initMaintenanceNotifications, stopMaintenanceNotifications } from "@/services/maintenance/maintenanceNotificationService";
+import { useSidebar } from "@/contexts/SidebarContext";
const ScheduleIncident = () => {
- // State for sidebar collapse functionality
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
- const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+ // Use shared sidebar state
+ const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current theme and language
const { theme } = useTheme();
@@ -24,12 +24,10 @@ const ScheduleIncident = () => {
// Initialize maintenance notifications
useEffect(() => {
- console.log("Initializing maintenance notifications");
initMaintenanceNotifications();
// Clean up on unmount
return () => {
- console.log("Stopping maintenance notifications");
stopMaintenanceNotifications();
};
}, []);
diff --git a/application/src/pages/Settings.tsx b/application/src/pages/Settings.tsx
index 6dd3516..45c2018 100644
--- a/application/src/pages/Settings.tsx
+++ b/application/src/pages/Settings.tsx
@@ -10,11 +10,11 @@ import { NotificationSettings } from "@/components/settings/notification-setting
import { AlertsTemplates } from "@/components/settings/alerts-templates";
import { AboutSystem } from "@/components/settings/about-system";
import DataRetentionSettings from "@/components/settings/data-retention/DataRetentionSettings";
+import { useSidebar } from "@/contexts/SidebarContext";
const Settings = () => {
- // State for sidebar collapse functionality
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
- const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+ // Use shared sidebar state
+ const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
diff --git a/application/src/pages/SslDomain.tsx b/application/src/pages/SslDomain.tsx
index cf20c3d..a7de6ee 100644
--- a/application/src/pages/SslDomain.tsx
+++ b/application/src/pages/SslDomain.tsx
@@ -1,3 +1,4 @@
+
import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { authService } from "@/services/authService";
@@ -8,14 +9,14 @@ import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
import { useLanguage } from "@/contexts/LanguageContext";
+import { useSidebar } from "@/contexts/SidebarContext";
const SslDomain = () => {
// Get language context for translations
const { t } = useLanguage();
- // State for sidebar collapse functionality
- const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
- const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+ // Use shared sidebar state
+ const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();