From 9d25ca20dfb517860854044b73f2ae7a49a0f155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Thu, 2 Jan 2025 21:18:56 +0100 Subject: [PATCH] Gestion des utilisateurs --- controllers/lists.go | 16 +-- controllers/mfa.go | 12 +- controllers/roles.go | 6 +- controllers/sections.go | 6 +- controllers/users.go | 281 ++++++++++++++++++++++++++++++++++++++++ controllers/welcome.go | 2 +- main.go | 9 ++ middlewares/mfa.go | 4 +- middlewares/welcome.go | 2 +- models/users.go | 2 +- static/main.css | 2 +- views/user.html | 75 +++++++++++ views/user_form.html | 121 +++++++++++++++++ views/users.html | 64 +++++++++ 14 files changed, 576 insertions(+), 26 deletions(-) create mode 100644 controllers/users.go create mode 100644 views/user.html create mode 100644 views/user_form.html create mode 100644 views/users.html diff --git a/controllers/lists.go b/controllers/lists.go index 3ce6c3e..0d2d650 100644 --- a/controllers/lists.go +++ b/controllers/lists.go @@ -45,7 +45,7 @@ func ListShow(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -122,7 +122,7 @@ func ListEdit(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -191,7 +191,7 @@ func ListDelete(c *fiber.Ctx) error { result := db.Delete(&models.List{}, id) if result.Error != nil { - return err + return result.Error } return c.Redirect("/admin/lists") @@ -213,7 +213,7 @@ func ListItemAdd(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -285,7 +285,7 @@ func ListItemEdit(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } var listItem models.ListItem @@ -296,7 +296,7 @@ func ListItemEdit(c *fiber.Ctx) error { } if result2.Error != nil { - return err + return result2.Error } title := fmt.Sprintf( @@ -364,12 +364,12 @@ func ListItemDelete(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } result2 := db.Delete(&models.ListItem{}, itemid) if result2.Error != nil { - return err + return result2.Error } return c.Redirect(fmt.Sprintf( diff --git a/controllers/mfa.go b/controllers/mfa.go index a5c0124..b40acec 100644 --- a/controllers/mfa.go +++ b/controllers/mfa.go @@ -28,10 +28,10 @@ func TotpEnrollPage(c *fiber.Ctx) error { result := db.First(&user, "id = ?", userid) if result.Error != nil { - return err + return result.Error } - if user.TotpSercet.Valid { + if user.TotpSecret.Valid { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } @@ -79,7 +79,7 @@ func TotpEnrollPage(c *fiber.Ctx) error { if c.Method() == "POST" { otp := c.FormValue("otp") if totp.Validate(otp, key.Secret()) { - err = user.TotpSercet.Scan(key.Secret()) + err = user.TotpSecret.Scan(key.Secret()) if err != nil { return err } @@ -152,17 +152,17 @@ func TotpVerifyPage(c *fiber.Ctx) error { result := db.First(&user, "id = ?", userid) if result.Error != nil { - return err + return result.Error } - if !user.TotpSercet.Valid { + if !user.TotpSecret.Valid { return fiber.NewError(fiber.StatusForbidden, "Forbidden") } var mfaError string if c.Method() == "POST" { otp := c.FormValue("otp") - if totp.Validate(otp, user.TotpSercet.String) { + if totp.Validate(otp, user.TotpSecret.String) { redirectId := c.Query("redirect") redirectUrl := "/" diff --git a/controllers/roles.go b/controllers/roles.go index bc2cc23..fd2780a 100644 --- a/controllers/roles.go +++ b/controllers/roles.go @@ -45,7 +45,7 @@ func RoleShow(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -134,7 +134,7 @@ func RoleEdit(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -203,7 +203,7 @@ func RoleDelete(c *fiber.Ctx) error { result := db.Delete(&models.Role{}, id) if result.Error != nil { - return err + return result.Error } return c.Redirect("/admin/roles") diff --git a/controllers/sections.go b/controllers/sections.go index cebebbb..b37174e 100644 --- a/controllers/sections.go +++ b/controllers/sections.go @@ -46,7 +46,7 @@ func SectionShow(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -143,7 +143,7 @@ func SectionEdit(c *fiber.Ctx) error { } if result.Error != nil { - return err + return result.Error } title := fmt.Sprintf( @@ -220,7 +220,7 @@ func SectionDelete(c *fiber.Ctx) error { result := db.Delete(&models.Section{}, id) if result.Error != nil { - return err + return result.Error } return c.Redirect("/admin/sections") diff --git a/controllers/users.go b/controllers/users.go new file mode 100644 index 0000000..e71f2ac --- /dev/null +++ b/controllers/users.go @@ -0,0 +1,281 @@ +package controllers + +import ( + "errors" + "fmt" + "strconv" + + "git.readonly.ch/bouzoure/popvaud-people/helpers" + "git.readonly.ch/bouzoure/popvaud-people/models" + "github.com/go-playground/validator" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type UserValidation struct { + Email string `validate:"required,min=6,max=100,email"` + Name string `validate:"required,min=2,max=100"` + Password string `validate:"required,min=10,max=100"` +} + +func Users(c *fiber.Ctx) error { + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var users []models.User + result := db.Order("name collate nocase asc").Find(&users) + + if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return err + } + + return c.Render("users", fiber.Map{ + "PageTitle": "Utilisateurs", + "Users": users, + }) +} + +func UserShow(c *fiber.Ctx) error { + id := c.Params("id") + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var user models.User + result := db.Find(&user, "id = ?", id) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Not found") + } + + if result.Error != nil { + return result.Error + } + + title := fmt.Sprintf( + "%s | Utilisateurs", + user.Name, + ) + + return c.Render("user", fiber.Map{ + "PageTitle": title, + "User": user, + }) +} + +func UserAdd(c *fiber.Ctx) error { + var user models.User + var errors []string + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + if c.Method() == "POST" { + data := UserValidation{ + Email: c.FormValue("email"), + Name: c.FormValue("name"), + Password: c.FormValue("password"), + } + + validate := validator.New() + validErrs := validate.Struct(data) + + if validErrs != nil { + for _, validErr := range validErrs.(validator.ValidationErrors) { + switch validErr.Field() { + case "Email": + errors = append(errors, "L'adresse email doit être valide") + case "Name": + errors = append(errors, "Le nom doit contenir entre 2 et 100 caractères") + case "Password": + errors = append(errors, "Le mot de passe doit contenir entre 10 et 100 caractères") + } + } + } + + user.Name = data.Name + user.Email = data.Email + + passwordHash, err := helpers.HashPassword(data.Password) + if err != nil { + return err + } + + user.Password = passwordHash + user.SkipWelcome = false + + user.IsAdmin = (c.FormValue("is_admin") == "on") + + if len(errors) == 0 { + result := db.Create(&user) + if result.Error != nil { + return result.Error + } else { + c.Redirect(fmt.Sprintf( + "/admin/users/%d", + user.ID, + )) + } + } + } + + return c.Render("user_form", fiber.Map{ + "PageTitle": "Ajouter un utilisateur", + "User": user, + "Errors": errors, + }) +} + +func UserEdit(c *fiber.Ctx) error { + id := c.Params("id") + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var user models.User + result := db.Find(&user, "id = ?", id) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Not found") + } + + if result.Error != nil { + return result.Error + } + + title := fmt.Sprintf( + "%s | Modifier utilisateur", + user.Name, + ) + + var errors []string + if c.Method() == "POST" { + data := UserValidation{ + Email: c.FormValue("email"), + Name: c.FormValue("name"), + Password: c.FormValue("password"), + } + + validate := validator.New() + validErrs := validate.Struct(data) + + if validErrs != nil { + for _, validErr := range validErrs.(validator.ValidationErrors) { + switch validErr.Field() { + case "Email": + errors = append(errors, "L'adresse email doit être valide") + case "Name": + errors = append(errors, "Le nom doit contenir entre 2 et 100 caractères") + case "Password": + if len(data.Password) > 0 { + errors = append(errors, "Le mot de passe doit contenir entre 10 et 100 caractères") + } + } + } + } + + user.Name = data.Name + user.Email = data.Email + + if len(data.Password) > 0 { + passwordHash, err := helpers.HashPassword(data.Password) + if err != nil { + return err + } + + user.Password = passwordHash + user.SkipWelcome = false + } + + if c.FormValue("reset_totp") == "on" { + user.TotpSecret.Valid = false + } + + user.IsAdmin = (c.FormValue("is_admin") == "on") + + var users []models.User + result := db.Find(&users, "is_admin = ?", true) + if result.Error != nil { + return result.Error + } + + if !user.IsAdmin && result.RowsAffected < 2 { + errors = append(errors, "Il doit y avoir au moins un administrateur") + } + + if len(errors) == 0 { + result2 := db.Save(&user) + if result2.Error != nil { + return result2.Error + } else { + c.Redirect(fmt.Sprintf( + "/admin/users/%d", + user.ID, + )) + } + } + } + + return c.Render("user_form", fiber.Map{ + "PageTitle": title, + "User": user, + "Errors": errors, + }) +} + +func UserDelete(c *fiber.Ctx) error { + id := c.Params("id") + + sess, err := helpers.GetSessionStore(c) + if err != nil { + return err + } + + deleteUser, err := strconv.ParseUint(id, 10, 0) + if err != nil { + return err + } + + currentUser := sess.Get("userid") + if deleteUser == uint64(currentUser.(uint)) { + return fiber.NewError(fiber.StatusForbidden, "Forbidden") + } + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var user models.User + result := db.Find(&user, "id = ?", id) + if result.Error != nil { + return result.Error + } + + user.Name = "Disabled Account" + user.Email = "disabled-account@invlalid.tld" + user.Password = "" + user.TotpSecret.Valid = false + user.IsAdmin = false + user.SkipWelcome = false + + result2 := db.Save(&user) + if result2.Error != nil { + return result2.Error + } + + result3 := db.Delete(&models.User{}, id) + if result3.Error != nil { + return result3.Error + } + + return c.Redirect("/admin/users") +} diff --git a/controllers/welcome.go b/controllers/welcome.go index 0f03455..b72184d 100644 --- a/controllers/welcome.go +++ b/controllers/welcome.go @@ -31,7 +31,7 @@ func WelcomePage(c *fiber.Ctx) error { result := db.First(&user, "id = ?", userid) if result.Error != nil { - return err + return result.Error } if user.SkipWelcome { diff --git a/main.go b/main.go index 80e773e..2603c2c 100644 --- a/main.go +++ b/main.go @@ -135,6 +135,15 @@ func main() { app.Post("/admin/lists/:id/items/:itemid", controllers.ListItemEdit) app.Post("/admin/lists/:id/items/:itemid/delete", controllers.ListItemDelete) + // Admin: Users + app.Get("/admin/users", controllers.Users) + app.Get("/admin/users/:id", controllers.UserShow) + app.Get("/admin/users/add", controllers.UserAdd) + app.Post("/admin/users/add", controllers.UserAdd) + app.Get("/admin/users/:id/edit", controllers.UserEdit) + app.Post("/admin/users/:id/edit", controllers.UserEdit) + app.Post("/admin/users/:id/delete", controllers.UserDelete) + // Admin: Roles app.Get("/admin/roles", controllers.Roles) app.Get("/admin/roles/:id", controllers.RoleShow) diff --git a/middlewares/mfa.go b/middlewares/mfa.go index 94c00f4..0e062a7 100644 --- a/middlewares/mfa.go +++ b/middlewares/mfa.go @@ -29,10 +29,10 @@ func MfaEnrollMiddleware(c *fiber.Ctx) error { result := db.First(&user, "id = ?", userid) if result.Error != nil { - return err + return result.Error } - if user.TotpSercet.Valid { + if user.TotpSecret.Valid { return c.Next() } diff --git a/middlewares/welcome.go b/middlewares/welcome.go index 085d3c1..d92f16d 100644 --- a/middlewares/welcome.go +++ b/middlewares/welcome.go @@ -28,7 +28,7 @@ func WelcomeMiddleware(c *fiber.Ctx) error { result := db.First(&user, "id = ?", userid) if result.Error != nil { - return err + return result.Error } if user.SkipWelcome { diff --git a/models/users.go b/models/users.go index 9bde084..501709e 100644 --- a/models/users.go +++ b/models/users.go @@ -11,7 +11,7 @@ type User struct { Name string Email string Password string - TotpSercet sql.NullString + TotpSecret sql.NullString IsAdmin bool SkipWelcome bool } diff --git a/static/main.css b/static/main.css index 57eea62..5d98f83 100644 --- a/static/main.css +++ b/static/main.css @@ -23,7 +23,7 @@ img#header-logo { } .user-photo { - background-color: purple; + background-color: #555; color: #fff; display: inline-block; width: 25px; diff --git a/views/user.html b/views/user.html new file mode 100644 index 0000000..0769c43 --- /dev/null +++ b/views/user.html @@ -0,0 +1,75 @@ +{% extends "layouts/main.html" %} + +{% block main %} +
+
+ +
+
+ +
+ Nom complet
+ {{ User.Name }} +
+ +
+ Email
+ {{ User.Email }} +
+ +
+ Administrateur
+ {% if User.IsAdmin %} + Oui + {% else %} + Non + {% endif %} +
+ +
+ Ecran de bienvenue à la prochaine connexion
+ {% if User.SkipWelcome %} + Non + {% else %} + Oui + {% endif %} +
+ +
+ Double facteur (TOTP)
+ {% if User.TotpSecret.Valid %} + Enrollé + {% else %} + Enrollement lors de la prochaine connexion + {% endif %} +
+ +
+ + + Modifier + + + {% if User.ID != Globals.UserID %} +
+ +
+ {% endif %} +
+ +
+{% endblock %} diff --git a/views/user_form.html b/views/user_form.html new file mode 100644 index 0000000..7de0169 --- /dev/null +++ b/views/user_form.html @@ -0,0 +1,121 @@ +{% extends "layouts/main.html" %} + +{% block main %} +
+
+ +
+
+ + {% if Errors %} +
+
    + {% for Error in Errors %} +
  • {{ Error }}
  • + {% endfor %} +
+
+ {% endif %} + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + {% if User.ID %} +
+ Laisser vide pour ne pas changer +
+ {% endif %} +
+ + {% if User.ID %} +
+ + +
+ Si la case est cochée, l'utilisateur devra effectuer + un enrollement TOTP à la prochaine connexion. +
+
+ {% endif %} + +
+ +
+
+ +
+{% endblock %} diff --git a/views/users.html b/views/users.html new file mode 100644 index 0000000..965fda8 --- /dev/null +++ b/views/users.html @@ -0,0 +1,64 @@ +{% extends "layouts/main.html" %} + +{% block main %} +
+
+ +
+
+ + {% if Users %} +
+ + + + + + + + + + {% for User in Users %} + + + + + + {% endfor %} + +
Nom completEmailAdministrateur
+ {{ User.Name|first }} + + {{ User.Name }} + + + {{ User.Email }} + + {% if User.IsAdmin %} + Oui + {% else %} + Non + {% endif %} +
+
+ {% else %} +
+ Pas d'utilisateurs pour le moment +
+ {% endif %} + + + +
+{% endblock %}