diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 06915346..9075151c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,3 +37,9 @@ repos:
language: system
files: '^frontend/.*\.(ts|tsx)$'
pass_filenames: false
+ - id: frontend-lint
+ name: Frontend Lint (Fix)
+ entry: bash -c 'cd frontend && npm run lint -- --fix'
+ language: system
+ files: '^frontend/.*\.(ts|tsx|js|jsx)$'
+ pass_filenames: false
diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go
index fa2d0f68..c214bd42 100644
--- a/backend/internal/api/handlers/user_handler.go
+++ b/backend/internal/api/handlers/user_handler.go
@@ -74,6 +74,7 @@ func (h *UserHandler) Setup(c *gin.Context) {
Email: strings.ToLower(req.Email),
Role: "admin",
Enabled: true,
+ APIKey: uuid.New().String(),
}
if err := user.SetPassword(req.Password); err != nil {
@@ -158,8 +159,9 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
}
type UpdateProfileRequest struct {
- Name string `json:"name" binding:"required"`
- Email string `json:"email" binding:"required,email"`
+ Name string `json:"name" binding:"required"`
+ Email string `json:"email" binding:"required,email"`
+ CurrentPassword string `json:"current_password"`
}
// UpdateProfile updates the authenticated user's profile.
@@ -176,6 +178,13 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
return
}
+ // Get current user
+ var user models.User
+ if err := h.DB.First(&user, userID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
+ return
+ }
+
// Check if email is already taken by another user
req.Email = strings.ToLower(req.Email)
var count int64
@@ -189,6 +198,18 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
return
}
+ // If email is changing, verify password
+ if req.Email != user.Email {
+ if req.CurrentPassword == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Current password is required to change email"})
+ return
+ }
+ if !user.CheckPassword(req.CurrentPassword) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
+ return
+ }
+ }
+
if err := h.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"name": req.Name,
"email": req.Email,
diff --git a/backend/internal/api/handlers/user_integration_test.go b/backend/internal/api/handlers/user_integration_test.go
index 745ab30f..64bc17a9 100644
--- a/backend/internal/api/handlers/user_integration_test.go
+++ b/backend/internal/api/handlers/user_integration_test.go
@@ -69,8 +69,9 @@ func TestUserLoginAfterEmailChange(t *testing.T) {
// 3. Update Profile (Change Email)
newEmail := "updated@example.com"
updateBody := map[string]string{
- "name": "Test User Updated",
- "email": newEmail,
+ "name": "Test User Updated",
+ "email": newEmail,
+ "current_password": password,
}
jsonUpdate, _ := json.Marshal(updateBody)
req, _ = http.NewRequest("POST", "/user/profile", bytes.NewBuffer(jsonUpdate))
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c595e9a0..d129c24a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,6 +10,7 @@ import RemoteServers from './pages/RemoteServers'
import ImportCaddy from './pages/ImportCaddy'
import Certificates from './pages/Certificates'
import SettingsLayout from './pages/SettingsLayout'
+import TasksLayout from './pages/TasksLayout'
import SystemSettings from './pages/SystemSettings'
import Account from './pages/Account'
import Backups from './pages/Backups'
@@ -44,10 +45,13 @@ export default function App() {
} />
} />
} />
-
- } />
- } />
-
+
+
+ {/* Tasks Routes */}
+ }>
+ } />
+ } />
+ } />
diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts
index faf1b934..4cfe4cfe 100644
--- a/frontend/src/api/user.ts
+++ b/frontend/src/api/user.ts
@@ -18,7 +18,7 @@ export const regenerateApiKey = async (): Promise<{ api_key: string }> => {
return response.data
}
-export const updateProfile = async (data: { name: string; email: string }): Promise<{ message: string }> => {
+export const updateProfile = async (data: { name: string; email: string; current_password?: string }): Promise<{ message: string }> => {
const response = await client.post('/user/profile', data)
return response.data
}
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index c79ccfd2..760a87a7 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -39,6 +39,7 @@ export default function Layout({ children }: LayoutProps) {
{ name: 'Certificates', path: '/certificates', icon: '🔒' },
{ name: 'Import Caddyfile', path: '/import', icon: '📥' },
{ name: 'Settings', path: '/settings/system', icon: '⚙️' },
+ { name: 'Tasks', path: '/tasks/backups', icon: '📋' },
]
return (
@@ -63,14 +64,16 @@ export default function Layout({ children }: LayoutProps) {
${isCollapsed ? 'w-20' : 'w-64'}
`}>
- {!isCollapsed &&
CPM+
}
- {isCollapsed && C+
}
+ {/* Logo moved to header */}
diff --git a/frontend/src/pages/TasksLayout.tsx b/frontend/src/pages/TasksLayout.tsx
new file mode 100644
index 00000000..50fbf84c
--- /dev/null
+++ b/frontend/src/pages/TasksLayout.tsx
@@ -0,0 +1,50 @@
+import { Outlet, Link, useLocation } from 'react-router-dom'
+import { Archive, FileText } from 'lucide-react'
+
+export default function TasksLayout() {
+ const location = useLocation()
+
+ const isActive = (path: string) => location.pathname === path
+
+ return (
+
+ {/* Tasks Sidebar */}
+
+
+
+ Tasks
+
+
+
+
+ Backups
+
+
+
+ Logs
+
+
+
+
+
+ {/* Content Area */}
+
+
+
+
+ )
+}