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'} `}>
- -

- Tasks -

-
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 +

+ +
+
+ + {/* Content Area */} +
+ +
+
+ ) +}