From 1e77f2747ef1debf43f95f99dd293ba32e043d00 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 7 Jun 2025 23:10:43 +0800 Subject: [PATCH 1/3] Added CheckCle Logo into the login page --- application/public/checkcle_logo.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 application/public/checkcle_logo.svg diff --git a/application/public/checkcle_logo.svg b/application/public/checkcle_logo.svg new file mode 100644 index 0000000..d112473 --- /dev/null +++ b/application/public/checkcle_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From da0808cefef647cdc373b0bc302800494d9d0a53 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sat, 7 Jun 2025 23:20:04 +0800 Subject: [PATCH 2/3] Updated: Security Improvements Improved and Secure Login. - Input Validation: Added comprehensive client-side validation with proper error messages - Rate Limiting: Added attempt tracking to prevent brute force attacks - Secure Error Handling: Removed detailed error logging and implemented generic error messages - Production Security: Conditional API endpoint switching (disabled in production) - Enhanced UX: Better loading states, form validation, and accessibility - Password Security: Improved password visibility toggle with proper ARIA labels - Form Security: Added autocomplete attributes and proper form submission handling --- application/src/pages/Login.tsx | 356 ++++++++++++++++++++++++++------ 1 file changed, 290 insertions(+), 66 deletions(-) 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 */} + Checkcle Logo { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> -
-
-
-
-
-
- {t("orContinueWith")} +

+ {t("signInToYourAccount")} +

+ + {isLocked && ( +
+
+ + + {t("accountLocked")} - {formatLockoutTime(lockoutTimeRemaining)} + +
-
+ )}
+ {/* General Error */} + {errors.general && ( +
+
+ + {errors.general} +
+
+ )} +
- +
setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + if (errors.email) { + setErrors(prev => ({ ...prev, email: undefined })); + } + }} required - className="pl-10 text-sm sm:text-base h-9 sm:h-10" + 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} />
+ {errors.email && ( +

+ {errors.email} +

+ )}
- +
- - {t("forgot")} + + e.preventDefault()} + > + {t("forgot")} +
- +
setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + if (errors.password) { + setErrors(prev => ({ ...prev, password: undefined })); + } + }} required - className="pl-10 text-sm sm:text-base h-9 sm:h-10" + 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} />
+ {errors.password && ( +

+ {errors.password} +

+ )}
- -

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

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

From f6592d331f6301bf2cd675c408196c3367ed26b5 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Sun, 8 Jun 2025 21:56:09 +0800 Subject: [PATCH 3/3] Refactor: Split Sidebar.tsx into smaller components Refactored the Sidebar component into smaller, more manageable files to improve code organization and maintainability. Fix sidebar auto-expansion issue Ensured the sidebar's collapsed state is maintained when navigating between pages. --- application/src/App.tsx | 230 ++++++++---------- application/src/components/ErrorBoundary.tsx | 49 ++++ .../src/components/dashboard/Sidebar.tsx | 146 ++--------- .../dashboard/sidebar/MainNavigation.tsx | 27 ++ .../components/dashboard/sidebar/MenuItem.tsx | 54 ++++ .../dashboard/sidebar/SettingsPanel.tsx | 100 ++++++++ .../dashboard/sidebar/SidebarHeader.tsx | 20 ++ .../src/components/dashboard/sidebar/index.ts | 6 + .../dashboard/sidebar/navigationData.ts | 94 +++++++ application/src/contexts/SidebarContext.tsx | 38 +++ application/src/pages/Dashboard.tsx | 10 +- application/src/pages/Index.tsx | 25 +- application/src/pages/OperationalPage.tsx | 8 +- application/src/pages/Profile.tsx | 6 +- application/src/pages/ScheduleIncident.tsx | 10 +- application/src/pages/Settings.tsx | 6 +- application/src/pages/SslDomain.tsx | 7 +- 17 files changed, 549 insertions(+), 287 deletions(-) create mode 100644 application/src/components/ErrorBoundary.tsx create mode 100644 application/src/components/dashboard/sidebar/MainNavigation.tsx create mode 100644 application/src/components/dashboard/sidebar/MenuItem.tsx create mode 100644 application/src/components/dashboard/sidebar/SettingsPanel.tsx create mode 100644 application/src/components/dashboard/sidebar/SidebarHeader.tsx create mode 100644 application/src/components/dashboard/sidebar/index.ts create mode 100644 application/src/components/dashboard/sidebar/navigationData.ts create mode 100644 application/src/contexts/SidebarContext.tsx diff --git a/application/src/App.tsx b/application/src/App.tsx index 46c461c..e97d3bb 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -1,137 +1,115 @@ -import { Toaster } from "@/components/ui/toaster"; -import { Toaster as Sonner } from "@/components/ui/sonner"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { authService } from "@/services/authService"; -import { ThemeProvider } from "@/contexts/ThemeContext"; -import { LanguageProvider } from "@/contexts/LanguageContext"; +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { LanguageProvider } from './contexts/LanguageContext'; +import { SidebarProvider } from './contexts/SidebarContext'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { Toaster } from './components/ui/sonner'; +import { authService } from './services/authService'; -import Login from "./pages/Login"; -import Dashboard from "./pages/Dashboard"; -import ServiceDetail from "./pages/ServiceDetail"; -import Settings from "./pages/Settings"; -import Profile from "./pages/Profile"; -import NotFound from "./pages/NotFound"; -import SslDomain from "./pages/SslDomain"; -import ScheduleIncident from "./pages/ScheduleIncident"; -import OperationalPage from "./pages/OperationalPage"; -import PublicStatusPage from "./pages/PublicStatusPage"; +// Pages +import Index from './pages/Index'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import ServiceDetail from './pages/ServiceDetail'; +import Settings from './pages/Settings'; +import Profile from './pages/Profile'; +import SslDomain from './pages/SslDomain'; +import ScheduleIncident from './pages/ScheduleIncident'; +import OperationalPage from './pages/OperationalPage'; +import PublicStatusPage from './pages/PublicStatusPage'; +import NotFound from './pages/NotFound'; -// Create a Protected route component +const queryClient = new QueryClient(); + +// Protected Route Component const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const isAuthenticated = authService.isAuthenticated(); - - if (!isAuthenticated) { - return ; - } - - return <>{children}; + return isAuthenticated ? <>{children} : ; }; -const queryClient = new QueryClient(); - -const App = () => { - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // Check authentication status when the app loads - const checkAuth = async () => { - try { - // Just check the auth state - authService.isAuthenticated(); - } finally { - setIsLoading(false); - } - }; - - checkAuth(); - }, []); - - if (isLoading) { - return
Loading...
; - } - +function App() { return ( - - - - - - - - - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - {/* Public status page route */} - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - - - - + + + + + + +
+ + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + + +
+
+
+
+
+
+
); -}; +} export default App; \ No newline at end of file diff --git a/application/src/components/ErrorBoundary.tsx b/application/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6a8ced8 --- /dev/null +++ b/application/src/components/ErrorBoundary.tsx @@ -0,0 +1,49 @@ + +import React from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

+ {this.state.error?.message || 'An unexpected error occurred'} +

+ +
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index f543936..4d15f9d 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -1,140 +1,22 @@ -import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, UserCog, Bell, FileClock, Database, RefreshCw, Info, ChevronDown, BookOpen } from "lucide-react"; + +import React from "react"; import { useTheme } from "@/contexts/ThemeContext"; -import { Link, useLocation } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useLanguage } from "@/contexts/LanguageContext"; +import { SidebarHeader } from "./sidebar/SidebarHeader"; +import { MainNavigation } from "./sidebar/MainNavigation"; +import { SettingsPanel } from "./sidebar/SettingsPanel"; interface SidebarProps { collapsed: boolean; } -export const Sidebar = ({ - collapsed -}: SidebarProps) => { - const { - theme - } = useTheme(); - const { - t - } = useLanguage(); - const location = useLocation(); - const [activeSettingsItem, setActiveSettingsItem] = useState("general"); - const [settingsPanelOpen, setSettingsPanelOpen] = useState(false); - - // Update active settings item based on URL - useEffect(() => { - if (location.pathname === '/settings') { - const params = new URLSearchParams(location.search); - const panel = params.get('panel'); - if (panel) { - setActiveSettingsItem(panel); - } - } - }, [location]); - - const handleSettingsItemClick = (item: string) => { - setActiveSettingsItem(item); - }; - - const getMenuItemClasses = (isActive: boolean) => { - return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`; - }; - - // New larger icon size for the main menu - const mainIconSize = "h-6 w-6"; +export const Sidebar = ({ collapsed }: SidebarProps) => { + const { theme } = useTheme(); - return
-
-
- C -
- {!collapsed &&

CheckCle App

} -
- - - - {!collapsed &&
- - -
- {t("settingPanel")} -
-
- - -
-
- - -
- -
- handleSettingsItemClick('general')}> - - {t("generalSettings")} - - handleSettingsItemClick('users')}> - - {t("userManagement")} - - handleSettingsItemClick('notifications')}> - - {t("notificationSettings")} - - handleSettingsItemClick('templates')}> - - {t("alertsTemplates")} - - handleSettingsItemClick('data-retention')}> - - {t("dataRetention")} - - handleSettingsItemClick('about')}> - - {t("aboutSystem")} - -
-
-
-
-
-
} - - {collapsed &&
- - - -
} -
; + return ( +
+ + + +
+ ); }; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/MainNavigation.tsx b/application/src/components/dashboard/sidebar/MainNavigation.tsx new file mode 100644 index 0000000..9b4e103 --- /dev/null +++ b/application/src/components/dashboard/sidebar/MainNavigation.tsx @@ -0,0 +1,27 @@ + +import React from "react"; +import { MenuItem } from "./MenuItem"; +import { mainMenuItems } from "./navigationData"; + +interface MainNavigationProps { + collapsed: boolean; +} + +export const MainNavigation: React.FC = ({ collapsed }) => { + return ( + + ); +}; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/MenuItem.tsx b/application/src/components/dashboard/sidebar/MenuItem.tsx new file mode 100644 index 0000000..019d5ad --- /dev/null +++ b/application/src/components/dashboard/sidebar/MenuItem.tsx @@ -0,0 +1,54 @@ + +import React from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { LucideIcon } from "lucide-react"; + +interface MenuItemProps { + id: string; + path: string | null; + icon: LucideIcon; + translationKey: string; + color: string; + hasNavigation: boolean; + collapsed: boolean; +} + +export const MenuItem: React.FC = ({ + id, + path, + icon: Icon, + translationKey, + color, + hasNavigation, + collapsed +}) => { + const { theme } = useTheme(); + const { t } = useLanguage(); + const location = useLocation(); + const navigate = useNavigate(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (hasNavigation && path) { + // Use navigate instead of window.location to prevent full page reload + navigate(path, { replace: false }); + } + }; + + const isActive = path && location.pathname === path; + const mainIconSize = "h-6 w-6"; + + return ( +
+ + {!collapsed && {t(translationKey)}} +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/SettingsPanel.tsx b/application/src/components/dashboard/sidebar/SettingsPanel.tsx new file mode 100644 index 0000000..2e311c3 --- /dev/null +++ b/application/src/components/dashboard/sidebar/SettingsPanel.tsx @@ -0,0 +1,100 @@ + +import React, { useState, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useLanguage } from "@/contexts/LanguageContext"; +import { Settings, ChevronDown } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { settingsMenuItems } from "./navigationData"; + +interface SettingsPanelProps { + collapsed: boolean; +} + +export const SettingsPanel: React.FC = ({ collapsed }) => { + const { theme } = useTheme(); + const { t } = useLanguage(); + const location = useLocation(); + const navigate = useNavigate(); + const [activeSettingsItem, setActiveSettingsItem] = useState("general"); + const [settingsPanelOpen, setSettingsPanelOpen] = useState(false); + + // Update active settings item based on URL + useEffect(() => { + if (location.pathname === '/settings') { + const params = new URLSearchParams(location.search); + const panel = params.get('panel'); + if (panel) { + setActiveSettingsItem(panel); + } + } + }, [location]); + + const handleSettingsItemClick = (item: string) => { + setActiveSettingsItem(item); + }; + + const handleMenuItemClick = (path: string, event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + // Use navigate instead of window.location to prevent full page reload + navigate(path, { replace: false }); + }; + + const getMenuItemClasses = (isActive: boolean) => { + return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`; + }; + + if (collapsed) { + const mainIconSize = "h-6 w-6"; + return ( +
+
handleMenuItemClick('/settings', e)} + className="cursor-pointer" + > + +
+
+ ); + } + + return ( +
+ + +
+ {t("settingPanel")} +
+
+ + +
+
+ + +
+ +
+ {settingsMenuItems.map((item) => ( +
{ + handleMenuItemClick(`/settings?panel=${item.id}`, e); + handleSettingsItemClick(item.id); + }} + > + + {t(item.translationKey)} +
+ ))} +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/SidebarHeader.tsx b/application/src/components/dashboard/sidebar/SidebarHeader.tsx new file mode 100644 index 0000000..4c0564b --- /dev/null +++ b/application/src/components/dashboard/sidebar/SidebarHeader.tsx @@ -0,0 +1,20 @@ + +import React from "react"; +import { useTheme } from "@/contexts/ThemeContext"; + +interface SidebarHeaderProps { + collapsed: boolean; +} + +export const SidebarHeader: React.FC = ({ collapsed }) => { + const { theme } = useTheme(); + + return ( +
+
+ C +
+ {!collapsed &&

CheckCle App

} +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/index.ts b/application/src/components/dashboard/sidebar/index.ts new file mode 100644 index 0000000..ec3edb4 --- /dev/null +++ b/application/src/components/dashboard/sidebar/index.ts @@ -0,0 +1,6 @@ + +export { SidebarHeader } from './SidebarHeader'; +export { MainNavigation } from './MainNavigation'; +export { MenuItem } from './MenuItem'; +export { SettingsPanel } from './SettingsPanel'; +export { mainMenuItems, settingsMenuItems } from './navigationData'; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/navigationData.ts b/application/src/components/dashboard/sidebar/navigationData.ts new file mode 100644 index 0000000..486e395 --- /dev/null +++ b/application/src/components/dashboard/sidebar/navigationData.ts @@ -0,0 +1,94 @@ + +import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react"; + +export const mainMenuItems = [ + { + id: 'uptime-monitoring', + path: '/dashboard', + icon: Globe, + translationKey: 'uptimeMonitoring', + color: 'text-purple-400', + hasNavigation: true + }, + { + id: 'instance-monitoring', + path: null, + icon: Boxes, + translationKey: 'instanceMonitoring', + color: 'text-blue-400', + hasNavigation: false + }, + { + id: 'ssl-domain', + path: '/ssl-domain', + icon: Radar, + translationKey: 'sslDomain', + color: 'text-cyan-400', + hasNavigation: true + }, + { + id: 'schedule-incident', + path: '/schedule-incident', + icon: Calendar, + translationKey: 'scheduleIncident', + color: 'text-emerald-400', + hasNavigation: true + }, + { + id: 'operational-page', + path: '/operational-page', + icon: BarChart2, + translationKey: 'operationalPage', + color: 'text-amber-400', + hasNavigation: true + }, + { + id: 'reports', + path: null, + icon: LineChart, + translationKey: 'reports', + color: 'text-rose-400', + hasNavigation: false + }, + { + id: 'api-documentation', + path: null, + icon: FileText, + translationKey: 'apiDocumentation', + color: 'text-indigo-400', + hasNavigation: false + } +]; + +export const settingsMenuItems = [ + { + id: 'general', + icon: Settings, + translationKey: 'generalSettings' + }, + { + id: 'users', + icon: User, + translationKey: 'userManagement' + }, + { + id: 'notifications', + icon: Bell, + translationKey: 'notificationSettings' + }, + { + id: 'templates', + icon: BookOpen, + translationKey: 'alertsTemplates' + }, + { + id: 'data-retention', + icon: Database, + translationKey: 'dataRetention' + }, + { + id: 'about', + icon: Info, + translationKey: 'aboutSystem' + } +]; \ No newline at end of file diff --git a/application/src/contexts/SidebarContext.tsx b/application/src/contexts/SidebarContext.tsx new file mode 100644 index 0000000..cfab360 --- /dev/null +++ b/application/src/contexts/SidebarContext.tsx @@ -0,0 +1,38 @@ + +import React, { createContext, useContext, useState } from 'react'; + +interface SidebarContextType { + sidebarCollapsed: boolean; + setSidebarCollapsed: (collapsed: boolean) => void; + toggleSidebar: () => void; +} + +const SidebarContext = createContext(undefined); + +export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + + const toggleSidebar = () => { + setSidebarCollapsed(prev => !prev); + }; + + const value = { + sidebarCollapsed, + setSidebarCollapsed, + toggleSidebar + }; + + return ( + + {children} + + ); +}; + +export const useSidebar = () => { + const context = useContext(SidebarContext); + if (context === undefined) { + throw new Error('useSidebar must be used within a SidebarProvider'); + } + return context; +}; \ No newline at end of file diff --git a/application/src/pages/Dashboard.tsx b/application/src/pages/Dashboard.tsx index 81f0c04..34437ce 100644 --- a/application/src/pages/Dashboard.tsx +++ b/application/src/pages/Dashboard.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { Header } from "@/components/dashboard/Header"; import { Sidebar } from "@/components/dashboard/Sidebar"; @@ -8,11 +8,11 @@ import { serviceService } from "@/services/serviceService"; import { authService } from "@/services/authService"; import { useNavigate } from "react-router-dom"; import { LoadingState } from "@/components/services/LoadingState"; +import { useSidebar } from "@/contexts/SidebarContext"; const Dashboard = () => { - // 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(); @@ -71,4 +71,4 @@ const Dashboard = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/application/src/pages/Index.tsx b/application/src/pages/Index.tsx index 52ea22c..0f01835 100644 --- a/application/src/pages/Index.tsx +++ b/application/src/pages/Index.tsx @@ -1,14 +1,29 @@ -// Update this page (the content is just a fallback if you fail to update the page) + +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { authService } from '@/services/authService'; const Index = () => { + const navigate = useNavigate(); + + useEffect(() => { + // Check if user is authenticated and redirect accordingly + if (authService.isAuthenticated()) { + navigate('/dashboard', { replace: true }); + } else { + navigate('/login', { replace: true }); + } + }, [navigate]); + + // Show a loading state while redirecting return ( -
+
-

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/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();