-
CPM+
+
+ {!isCollapsed &&
CPM+
}
@@ -65,20 +75,32 @@ export default function Layout({ children }: LayoutProps) {
setSidebarOpen(false)}
+ onClick={() => setMobileSidebarOpen(false)}
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
isActive
? 'bg-blue-100 text-blue-700 dark:bg-blue-active dark:text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'
- }`}
+ } ${isCollapsed ? 'justify-center' : ''}`}
+ title={isCollapsed ? item.name : ''}
>
{item.icon}
- {item.name}
+ {!isCollapsed && item.name}
)
})}
-
+
+ {/* Collapse Toggle */}
+
+
+
+
+
Version {health?.version || 'dev'}
{health?.git_commit && health.git_commit !== 'unknown' && (
@@ -89,7 +111,7 @@ export default function Layout({ children }: LayoutProps) {
+
+ {/* Collapsed Logout */}
+ {isCollapsed && (
+
+
+
+ )}
+
{/* Overlay for mobile */}
- {sidebarOpen && (
+ {/* Mobile Overlay */}
+ {mobileSidebarOpen && (
setSidebarOpen(false)}
+ className="fixed inset-0 bg-gray-900/50 z-20 lg:hidden"
+ onClick={() => setMobileSidebarOpen(false)}
/>
)}
diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx
index 6a85ad0d..8447ebca 100644
--- a/frontend/src/pages/Security.tsx
+++ b/frontend/src/pages/Security.tsx
@@ -9,6 +9,7 @@ import { getProfile, regenerateApiKey, updateProfile } from '../api/user'
import { getSettings, updateSetting } from '../api/settings'
import { Copy, RefreshCw, Shield, Mail, User } from 'lucide-react'
import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter'
+import { isValidEmail } from '../utils/validation'
export default function Security() {
const [oldPassword, setOldPassword] = useState('')
@@ -19,9 +20,11 @@ export default function Security() {
// Profile State
const [name, setName] = useState('')
const [email, setEmail] = useState('')
+ const [emailValid, setEmailValid] = useState
(null)
// Certificate Email State
const [certEmail, setCertEmail] = useState('')
+ const [certEmailValid, setCertEmailValid] = useState(null)
const [useUserEmail, setUseUserEmail] = useState(true)
const queryClient = useQueryClient()
@@ -44,6 +47,15 @@ export default function Security() {
}
}, [profile])
+ // Validate profile email
+ useEffect(() => {
+ if (email) {
+ setEmailValid(isValidEmail(email))
+ } else {
+ setEmailValid(null)
+ }
+ }, [email])
+
// Initialize cert email state
useEffect(() => {
if (settings && profile) {
@@ -58,6 +70,15 @@ export default function Security() {
}
}, [settings, profile])
+ // Validate cert email
+ useEffect(() => {
+ if (certEmail && !useUserEmail) {
+ setCertEmailValid(isValidEmail(certEmail))
+ } else {
+ setCertEmailValid(null)
+ }
+ }, [certEmail, useUserEmail])
+
const updateProfileMutation = useMutation({
mutationFn: updateProfile,
onSuccess: () => {
@@ -144,12 +165,18 @@ export default function Security() {
value={name}
onChange={(e) => setName(e.target.value)}
/>
- setEmail(e.target.value)}
- />
+
+
setEmail(e.target.value)}
+ className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''}
+ />
+ {emailValid === false && (
+
Please enter a valid email address
+ )}
+
+ {/* Certificate Email Configuration */}
+
+
+
+
Certificate Email
+
+
+ This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications.
+
+
+
+
+ setUseUserEmail(e.target.checked)}
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+
+
+
+ {!useUserEmail && (
+
+
setCertEmail(e.target.value)}
+ placeholder="certs@example.com"
+ className={certEmailValid === false ? 'border-red-500 focus:ring-red-500' : certEmailValid === true ? 'border-green-500 focus:ring-green-500' : ''}
+ />
+ {certEmailValid === false && (
+
Please enter a valid email address
+ )}
+
+ )}
+
+
+
+
+
{/* Change Password */}
Change Password
@@ -199,49 +275,6 @@ export default function Security() {
- {/* Certificate Email Configuration */}
-
-
-
-
Certificate Email
-
-
- This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications.
-
-
-
-
-
{/* API Key */}
API Key
diff --git a/frontend/src/pages/SettingsLayout.tsx b/frontend/src/pages/SettingsLayout.tsx
index 929b3948..5d78eb94 100644
--- a/frontend/src/pages/SettingsLayout.tsx
+++ b/frontend/src/pages/SettingsLayout.tsx
@@ -4,7 +4,7 @@ import { useState } from 'react'
export default function SettingsLayout() {
const location = useLocation()
- const [tasksOpen, setTasksOpen] = useState(true)
+ const [tasksOpen, setTasksOpen] = useState(false)
const isActive = (path: string) => location.pathname === path
diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx
index 36542a9c..f5d51cc8 100644
--- a/frontend/src/pages/Setup.tsx
+++ b/frontend/src/pages/Setup.tsx
@@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth';
import { Input } from '../components/ui/Input';
import { Button } from '../components/ui/Button';
import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter';
+import { isValidEmail } from '../utils/validation';
const Setup: React.FC = () => {
const navigate = useNavigate();
@@ -18,6 +19,7 @@ const Setup: React.FC = () => {
password: '',
});
const [error, setError] = useState(null);
+ const [emailValid, setEmailValid] = useState(null);
const { data: status, isLoading: statusLoading } = useQuery({
queryKey: ['setupStatus'],
@@ -25,6 +27,14 @@ const Setup: React.FC = () => {
retry: false,
});
+ useEffect(() => {
+ if (formData.email) {
+ setEmailValid(isValidEmail(formData.email));
+ } else {
+ setEmailValid(null);
+ }
+ }, [formData.email]);
+
useEffect(() => {
if (isAuthenticated) {
navigate('/');
@@ -94,18 +104,23 @@ const Setup: React.FC = () => {
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
- setFormData({ ...formData, email: e.target.value })}
- helperText="This email will be used for Let's Encrypt certificate notifications and recovery."
- />
-
+
+
setFormData({ ...formData, email: e.target.value })}
+ className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''}
+ />
+ {emailValid === false && (
+
Please enter a valid email address
+ )}
+
+
{
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}