This commit is contained in:
cangui 2025-05-09 17:28:15 +02:00
parent c23cc3824a
commit 97900074f9
9 changed files with 214 additions and 16 deletions

View File

@ -534,5 +534,25 @@ func HandleTemplateTest(db *gorm.DB) http.HandlerFunc {
json.NewEncoder(w).Encode(respBody)
}
}
func AdminUserDelete(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var user models.User
if err := db.First(&user, id).Error; err != nil {
http.Error(w, "Utilisateur introuvable", http.StatusNotFound)
return
}
if err := db.Delete(&user).Error; err != nil {
http.Error(w, "Erreur suppression", http.StatusInternalServerError)
return
}
// On retourne du HTML vide car on swap "outerHTML"
w.WriteHeader(http.StatusOK)
w.Write([]byte(""))
}
}

View File

@ -6,8 +6,9 @@ import (
"strconv"
"text/template"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@ -55,14 +56,107 @@ func AdminUserEdit(db *gorm.DB) http.HandlerFunc {
renderPartial(w, "admin_user_edit", data)
}
}
func AdminUserCreate() http.HandlerFunc {
func AdminUserCreate(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"User": models.User{},
if err := r.ParseForm(); err != nil {
http.Error(w, "Formulaire invalide", http.StatusBadRequest)
return
}
email := r.FormValue("email")
password := r.FormValue("password")
role := models.UserRole(r.FormValue("role"))
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Erreur hash", http.StatusInternalServerError)
return
}
ssoid := "sso_" + uuid.New().String()
user := models.User{
Email: email,
Password: string(hashed),
Role: role,
IsActive: true,
SSOID: ssoid,
}
if err := db.Create(&user).Error; err != nil {
http.Error(w, "Erreur enregistrement", http.StatusInternalServerError)
return
}
w.Header().Set("HX-Trigger", `{"userCreated":"Utilisateur créé avec succès"}`)
w.Header().Set("HX-Remove", "true") // cache le form
renderPartial(w, "admin_user_row", map[string]interface{}{"User": user})
}
}
func AdminUserCreateForm() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
renderPartial(w, "admin_user_create", nil)
}
}
func AdminUserEditForm(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var user models.User
if err := db.First(&user, id).Error; err != nil {
http.Error(w, "Utilisateur introuvable", http.StatusNotFound)
return
}
data := map[string]interface{}{
"User": user,
}
renderPartial(w, "admin_user_edit", data)
}
}
func AdminUserUpdate(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "ID invalide", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Formulaire invalide", http.StatusBadRequest)
return
}
var user models.User
if err := db.First(&user, id).Error; err != nil {
http.Error(w, "Utilisateur introuvable", http.StatusNotFound)
return
}
// Mise à jour des champs
user.Email = r.FormValue("email")
user.Role = models.UserRole(r.FormValue("role"))
user.IsActive = r.FormValue("is_active") == "on"
if err := db.Save(&user).Error; err != nil {
http.Error(w, "Erreur mise à jour", http.StatusInternalServerError)
return
}
// Réafficher la ligne utilisateur mise à jour
data := map[string]interface{}{
"User": user,
}
renderPartial(w, "admin_user_row", data)
}
}
func AdminConversationPage(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]

View File

@ -35,8 +35,11 @@ func RoutesProtected(r *mux.Router, db *gorm.DB) {
r.HandleFunc("/admin/user/{id}/conversations", renders.AdminConversationPage(db))
r.HandleFunc("/api/user/{id}/conversations", renders.AdminConversationRows(db))
r.HandleFunc("/admin/user/{id}/edit", renders.AdminUserEdit(db)).Methods("GET")
r.HandleFunc("/admin/user/new", renders.AdminUserCreate()).Methods("GET")
r.HandleFunc("/admin/user", renders.AdminUserList(db))
r.HandleFunc("/api/user/delete/{id}", handlers.AdminUserDelete(db)).Methods("DELETE")
r.HandleFunc("/api/user/update/{id}", renders.AdminUserUpdate(db)).Methods("POST")
r.HandleFunc("/api/user/create", renders.AdminUserCreate(db)).Methods("POST")
r.HandleFunc("/admin/user/create-form", renders.AdminUserCreateForm()).Methods("GET")
r.HandleFunc("/dashboard", renders.Dashboard(db))
r.HandleFunc("/test/send", renders.TestMessagesPages)

View File

@ -0,0 +1,40 @@
{{ define "admin_user_create.pages.tmpl" }}
<form hx-post="/api/user/create" hx-target="#userList" hx-swap="beforeend" class="box">
<div class="field">
<label class="label">Email</label>
<div class="control">
<input class="input" type="email" name="email" required placeholder="ex: admin@example.com">
</div>
</div>
<div class="field">
<label class="label">Mot de passe</label>
<div class="control">
<input class="input" type="password" name="password" required placeholder="Mot de passe">
</div>
</div>
<div class="field">
<label class="label">Rôle</label>
<div class="control">
<div class="select">
<select name="role">
<option value="CLIENT">Client</option>
<option value="ADMIN">Admin</option>
</select>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-success" type="submit">✅ Créer</button>
</div>
<div class="control">
<button type="button" class="button is-light" hx-get="javascript:void(0)" onclick="document.getElementById('createForm').innerHTML = ''">❌ Annuler</button>
</div>
</div>
</form>
{{ end }}

View File

@ -46,7 +46,7 @@
<div class="field">
<div class="control">
<button class="button is-success" type="submit">💾 Enregistrer</button>
<button class="button is-success" type="submit" hx-ext="json-enc">💾 Enregistrer</button>
</div>
</div>
</form>

View File

@ -1,13 +1,25 @@
{{ define "admin_users.pages.tmpl" }}
<h1 class="title">Gestion des utilisateurs</h1>
{{ template "head" . }}
<button class="button is-primary"
hx-get="/admin/user/new"
hx-target="#editForm"
hx-swap="innerHTML">
Créer un utilisateur
</button>
<h1 class="title">Gestion des utilisateurs</h1>
<!-- Flash message -->
<div id="userFlash"></div>
<!-- Bouton création -->
<div class="mb-4">
<button class="button is-primary"
hx-get="/admin/user/create-form"
hx-target="#createForm"
hx-swap="innerHTML">
Ajouter un utilisateur
</button>
</div>
<!-- Formulaire création -->
<div id="createForm" class="mb-4"></div>
<!-- Tableau -->
<table class="table is-fullwidth is-striped">
<thead>
<tr>
@ -26,14 +38,40 @@
<td>{{ .Role }}</td>
<td>{{ if .IsActive }}✅{{ else }}❌{{ end }}</td>
<td>
<button class="button is-small is-info" hx-get="/admin/user/{{ .ID }}/edit" hx-target="#editForm" hx-swap="innerHTML">✏️ Modifier</button>
<button class="button is-small is-danger" hx-delete="/api/user/delete/{{ .ID }}" hx-confirm="Confirmer suppression ?" hx-target="#userList" hx-swap="outerHTML">🗑️ Supprimer</button>
<button class="button is-small is-info"
hx-get="/admin/user/{{ .ID }}/edit"
hx-target="#editForm"
hx-swap="innerHTML">
✏️ Modifier
</button>
<button class="button is-small is-danger"
hx-delete="/api/user/delete/{{ .ID }}"
hx-confirm="Confirmer suppression ?"
hx-target="#userList"
hx-swap="outerHTML">
🗑️ Supprimer
</button>
</td>
</tr>
{{ end }}
</tbody>
</table>
<!-- Formulaire modification -->
<hr>
<div id="editForm"></div>
<!-- Script HTMX flash -->
<script>
document.body.addEventListener('htmx:afterOnLoad', (e) => {
if (e.detail && e.detail.xhr) {
const json = JSON.parse(e.detail.xhr.getResponseHeader("HX-Trigger") || "{}")
if (json.userCreated) {
const flash = document.getElementById("userFlash")
flash.innerHTML = `<div class="notification is-success">${json.userCreated}</div>`
setTimeout(() => flash.innerHTML = "", 3000)
}
}
})
</script>
{{ end }}

View File

@ -5,7 +5,7 @@
<li><a href="/dashboard">🏠 Dashboard</a></li>
{{ if eq .User.Role "ADMIN" }}
<li><a href="/admin/user/new">👤 Utilisateurs</a></li>
<li><a href="/admin/user">👤 Utilisateurs</a></li>
<li><a href="/test/send">📤 Test envoi</a></li>
<li><a href="/test/send2">📤 Test envoi template</a></li>

1
go.mod
View File

@ -19,6 +19,7 @@ require (
require (
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/stretchr/testify v1.10.0

2
go.sum
View File

@ -6,6 +6,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=