diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go index 1c2ccda8..d5761277 100644 --- a/backend/internal/api/handlers/user_handler.go +++ b/backend/internal/api/handlers/user_handler.go @@ -23,6 +23,7 @@ func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) { r.POST("/setup", h.Setup) r.GET("/profile", h.GetProfile) r.POST("/regenerate-api-key", h.RegenerateAPIKey) + r.PUT("/profile", h.UpdateProfile) } // GetSetupStatus checks if the application needs initial setup (i.e., no users exist). @@ -154,3 +155,45 @@ func (h *UserHandler) GetProfile(c *gin.Context) { "api_key": user.APIKey, }) } + +type UpdateProfileRequest struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` +} + +// UpdateProfile updates the authenticated user's profile. +func (h *UserHandler) UpdateProfile(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Check if email is already taken by another user + var count int64 + if err := h.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, userID).Count(&count).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check email availability"}) + return + } + + if count > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "Email already in use"}) + return + } + + if err := h.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ + "name": req.Name, + "email": req.Email, + }).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index c4c8d7b2..13875218 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -79,6 +79,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { // User Profile & API Key userHandler := handlers.NewUserHandler(db) protected.GET("/user/profile", userHandler.GetProfile) + protected.POST("/user/profile", userHandler.UpdateProfile) protected.POST("/user/api-key", userHandler.RegenerateAPIKey) // Updates diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index b27e5d75..faf1b934 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -17,3 +17,8 @@ export const regenerateApiKey = async (): Promise<{ api_key: string }> => { const response = await client.post('/user/api-key') return response.data } + +export const updateProfile = async (data: { name: string; email: string }): Promise<{ message: string }> => { + const response = await client.post('/user/profile', data) + return response.data +} diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 00551e1f..6a85ad0d 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -1,12 +1,13 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Card } from '../components/ui/Card' import { Input } from '../components/ui/Input' import { Button } from '../components/ui/Button' import { toast } from '../utils/toast' import client from '../api/client' -import { getProfile, regenerateApiKey } from '../api/user' -import { Copy, RefreshCw, Shield } from 'lucide-react' +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' export default function Security() { @@ -15,6 +16,14 @@ export default function Security() { const [confirmPassword, setConfirmPassword] = useState('') const [loading, setLoading] = useState(false) + // Profile State + const [name, setName] = useState('') + const [email, setEmail] = useState('') + + // Certificate Email State + const [certEmail, setCertEmail] = useState('') + const [useUserEmail, setUseUserEmail] = useState(true) + const queryClient = useQueryClient() const { data: profile, isLoading: isLoadingProfile } = useQuery({ @@ -22,6 +31,44 @@ export default function Security() { queryFn: getProfile, }) + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: getSettings, + }) + + // Initialize profile state + useEffect(() => { + if (profile) { + setName(profile.name) + setEmail(profile.email) + } + }, [profile]) + + // Initialize cert email state + useEffect(() => { + if (settings && profile) { + const savedEmail = settings['caddy.email'] + if (savedEmail && savedEmail !== profile.email) { + setCertEmail(savedEmail) + setUseUserEmail(false) + } else { + setCertEmail(profile.email) + setUseUserEmail(true) + } + } + }, [settings, profile]) + + const updateProfileMutation = useMutation({ + mutationFn: updateProfile, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['profile'] }) + toast.success('Profile updated successfully') + }, + onError: (error: any) => { + toast.error(`Failed to update profile: ${error.message}`) + }, + }) + const regenerateMutation = useMutation({ mutationFn: regenerateApiKey, onSuccess: () => { @@ -33,6 +80,21 @@ export default function Security() { }, }) + const saveCertEmailMutation = useMutation({ + mutationFn: async () => { + const emailToSave = useUserEmail ? profile?.email : certEmail + if (!emailToSave) return + await updateSetting('caddy.email', emailToSave, 'caddy', 'string') + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }) + toast.success('Certificate email updated') + }, + onError: (error: any) => { + toast.error(`Failed to update certificate email: ${error.message}`) + }, + }) + const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault() if (newPassword !== confirmPassword) { @@ -70,6 +132,33 @@ export default function Security() {
Passwords do not match
+ )} ++ This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. +
+ +- Email address for Let's Encrypt certificate notifications -