diff --git a/backend/internal/api/handlers/auth_handler.go b/backend/internal/api/handlers/auth_handler.go index 12ef55d3..5bb42718 100644 --- a/backend/internal/api/handlers/auth_handler.go +++ b/backend/internal/api/handlers/auth_handler.go @@ -71,3 +71,29 @@ func (h *AuthHandler) Me(c *gin.Context) { role, _ := c.Get("role") c.JSON(http.StatusOK, gin.H{"user_id": userID, "role": role}) } + +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=8"` +} + +func (h *AuthHandler) ChangePassword(c *gin.Context) { + var req ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + if err := h.authService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 5c134ea6..a059e9ee 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -47,6 +47,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { { protected.POST("/auth/logout", authHandler.Logout) protected.GET("/auth/me", authHandler.Me) + protected.POST("/auth/change-password", authHandler.ChangePassword) } proxyHostHandler := handlers.NewProxyHostHandler(db) diff --git a/backend/internal/services/auth_service.go b/backend/internal/services/auth_service.go index e21901f9..3d1033b6 100644 --- a/backend/internal/services/auth_service.go +++ b/backend/internal/services/auth_service.go @@ -104,6 +104,23 @@ func (s *AuthService) GenerateToken(user *models.User) (string, error) { return token.SignedString([]byte(s.config.JWTSecret)) } +func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error { + var user models.User + if err := s.db.First(&user, userID).Error; err != nil { + return errors.New("user not found") + } + + if !user.CheckPassword(oldPassword) { + return errors.New("invalid current password") + } + + if err := user.SetPassword(newPassword); err != nil { + return err + } + + return s.db.Save(&user).Error +} + func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index b71208ab..57cd15fe 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,11 +1,74 @@ +import { useState } from 'react' +import { Card } from '../components/ui/Card' +import { Input } from '../components/ui/Input' +import { Button } from '../components/ui/Button' +import { toast } from '../components/Toast' +import client from '../api/client' + export default function Settings() { + const [oldPassword, setOldPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [loading, setLoading] = useState(false) + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault() + if (newPassword !== confirmPassword) { + toast.error('New passwords do not match') + return + } + + setLoading(true) + try { + await client.post('/auth/change-password', { + old_password: oldPassword, + new_password: newPassword, + }) + toast.success('Password updated successfully') + setOldPassword('') + setNewPassword('') + setConfirmPassword('') + } catch (err: any) { + toast.error(err.response?.data?.error || 'Failed to update password') + } finally { + setLoading(false) + } + } + return (