diff --git a/control-plane/api/internal/profile/GormStore.go b/control-plane/api/internal/profile/GormStore.go index 1f5b53c..8c9cb68 100644 --- a/control-plane/api/internal/profile/GormStore.go +++ b/control-plane/api/internal/profile/GormStore.go @@ -16,6 +16,7 @@ func NewGormStore(db *gorm.DB) *GormStore { type Store interface { UpdateUsername(id, newUsername string) (auth.User, error) + UpdatePassword(id, passwordHahs string) (auth.User, error) } func (s *GormStore) UpdateUsername(id, newUsername string) (auth.User, error) { @@ -34,3 +35,15 @@ func (s *GormStore) UpdateUsername(id, newUsername string) (auth.User, error) { return user, nil } + +func (s *GormStore) UpdatePassword(id, passwordHash string) (auth.User, error) { + result := s.db.Model(&auth.User{}).Where("id = ?", id).Update("password", passwordHash) + if result.Error != nil { + return auth.User{}, result.Error + } + var user auth.User + if err := s.db.First(&user, "id = ?", id).Error; err != nil { + return auth.User{}, err + } + return user, nil +} diff --git a/control-plane/api/internal/profile/handler.go b/control-plane/api/internal/profile/handler.go index dbe2de1..7b88118 100644 --- a/control-plane/api/internal/profile/handler.go +++ b/control-plane/api/internal/profile/handler.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/omnex/control-plane/api/internal/auth" ) type Handler struct { @@ -18,6 +19,10 @@ type newUsername struct { Username string `json:"username" binding:"required,min=2,max=120"` } +type newPassword struct { + Password string `json:"password" binding:"required,min=8"` +} + func (h *Handler) UpdateUsernameById(c *gin.Context) { var u newUsername @@ -40,3 +45,31 @@ func (h *Handler) UpdateUsernameById(c *gin.Context) { c.JSON(http.StatusOK, user) } + +func (h *Handler) UpdatePasswordById(c *gin.Context) { + var u newPassword + if err := c.ShouldBindJSON(&u); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"}) + return + } + + id, ok := c.MustGet("id").(string) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "id invalide"}) + return + } + + hash, err := auth.HashPassword(u.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) + return + } + + user, err := h.store.UpdatePassword(id, hash) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"}) + return + } + + c.JSON(http.StatusOK, user) +} diff --git a/control-plane/api/internal/router/router.go b/control-plane/api/internal/router/router.go index 4a5d49d..e3d697f 100644 --- a/control-plane/api/internal/router/router.go +++ b/control-plane/api/internal/router/router.go @@ -64,6 +64,7 @@ func New(d Deps) *gin.Engine { client.PATCH("/leads/:id/status", d.LeadsH.SetStatus) client.POST("/subscription", d.SubH.AddCode) client.POST("/profile/username", d.ProfileH.UpdateUsernameById) + client.POST("/profile/password", d.ProfileH.UpdatePasswordById) } // Espace admin : provisioning des démos (admin uniquement). @@ -73,6 +74,8 @@ func New(d Deps) *gin.Engine { admin.GET("/codes", d.SubH.ListCodes) admin.POST("/codes", d.SubH.CreateCodeForBuy) admin.GET("/messages", d.ContactH.GetMessage) + admin.POST("/profile/username", d.ProfileH.UpdateUsernameById) + admin.POST("/profile/password", d.ProfileH.UpdatePasswordById) if d.DemosH != nil { admin.POST("/demos", d.DemosH.Create) admin.GET("/demos", d.DemosH.List) diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index beceb3c..8913a62 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -104,7 +104,9 @@ export interface UpdateUsername { username: string } - +export interface UpdatePassword { + password: string +} // --- Endpoints --- export const api = { @@ -155,4 +157,6 @@ export const api = { request('POST', '/demos/details', { username }), updateUsername: (username: string) => request('POST', '/profile/username', { username }), + updatePassword: (password: string) => + request('POST', '/profile/password', { password }) } diff --git a/web/src/pages/backoffice/Profile.tsx b/web/src/pages/backoffice/Profile.tsx index 46dd678..c5976db 100644 --- a/web/src/pages/backoffice/Profile.tsx +++ b/web/src/pages/backoffice/Profile.tsx @@ -99,7 +99,8 @@ export function Profile() { const [loading, setLoading] = useState(true) const [loggingOut, setLoggingOut] = useState(false) const [updatingUsername, setUpdatingUsername] = useState(false) - + const [updatingPassword, setUpdatingPassword] = useState(false) + const [passwordVersion, setPasswordVersion] = useState(0) useEffect(() => { let cancelled = false ;(async () => { @@ -145,6 +146,36 @@ export function Profile() { } } + const handleUpdatePassword = async (newPassword: string) => { + const trimmed = newPassword.trim() + if (!me || trimmed.length < 8) { + if (trimmed.length > 0) { + toast({ status: 'warning', title: 'Le mot de passe doit contenir au moins 8 caractères' }) + } + setPasswordVersion(v => v + 1) // reset le champ même en cas d'annulation + return + } + + setUpdatingPassword(true) + try { + await api.updatePassword(trimmed) + toast({ status: 'success', title: 'Mot de passe mis à jour' }) + } catch (err) { + if (err instanceof ApiError && err.status === 401) { + navigate('/login') + return + } + toast({ + status: 'error', + title: 'Impossible de mettre à jour le mot de passe', + description: err instanceof ApiError ? err.message : undefined, + }) + } finally { + setUpdatingPassword(false) + setPasswordVersion(v => v + 1) // vide le champ après tentative + } + } + const handleLogout = async () => { setLoggingOut(true) try { @@ -236,6 +267,25 @@ export function Profile() { Type d'abonnement {me.type_abonnement || '—'} + + + Mot de passe + + + + + + + + + Expire le {formatDate(me.expired_at)}