From 1c67191c09253b3b4a8ab89727226e776b67b5b9 Mon Sep 17 00:00:00 2001 From: surtur Date: Mon, 7 Aug 2023 21:29:30 +0200 Subject: [PATCH] feat: implement user deletion --- app/routes.go | 2 + handlers/manage-user.go | 195 +++++++++++++++++++++++++++++ modules/user/user.go | 30 +++++ templates/manage/user-delete.tmpl | 60 +++++++++ templates/manage/user-details.tmpl | 3 + templates/navbar.tmpl | 2 +- 6 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 templates/manage/user-delete.tmpl diff --git a/app/routes.go b/app/routes.go index 5bef3ea..8108c12 100644 --- a/app/routes.go +++ b/app/routes.go @@ -62,7 +62,9 @@ func (a *App) SetupRoutes() error { e.POST("/manage/users/create", handlers.CreateUser(), handlers.MiddlewareSession) e.GET("/manage/users/:id", handlers.ViewUser(), handlers.MiddlewareSession) e.GET("/manage/users/:id/edit", handlers.EditUser(), handlers.MiddlewareSession, handlers.MiddlewareCache, compress) + e.GET("/manage/users/:id/delete", handlers.DeleteUserConfirmation(), handlers.MiddlewareSession) e.POST("/manage/users/:id/update", handlers.UpdateUser(), handlers.MiddlewareSession) + e.POST("/manage/users/:id/delete", handlers.DeleteUser(), handlers.MiddlewareSession) e.GET("/logout", handlers.Logout(), compress) e.POST("/logout", handlers.Logout(), handlers.MiddlewareSession) diff --git a/handlers/manage-user.go b/handlers/manage-user.go index c23eaa9..ca7df25 100644 --- a/handlers/manage-user.go +++ b/handlers/manage-user.go @@ -663,3 +663,198 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s/edit", usr.ID)) } } + +// DeleteUserConfirmation displays user deletion confirmation confirmation page. +func DeleteUserConfirmation() echo.HandlerFunc { + return func(c echo.Context) error { + addHeaders(c) + + u, ok := c.Get("sessUsr").(moduser.User) + if !ok { + return renderErrorPage( + c, + http.StatusUnauthorized, + http.StatusText(http.StatusUnauthorized), + "username was nil", + ) + } else if !u.IsAdmin { + status := http.StatusUnauthorized + msg := http.StatusText(status) + + return renderErrorPage( + c, status, msg+": You should not be here", "Restricted endpoint", + ) + } + + ctx, ok := c.Get("sloggerCtx").(context.Context) + if !ok { + ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger) + } + + csrf := c.Get("csrf").(string) + p := page{ + AppName: setting.AppName(), + AppVer: appver, + Title: "Manage Users - Delete User", + CSRF: csrf, + DevelMode: setting.IsDevel(), + Current: "manage-users-delete-user", + User: u, + } + tmpl := "manage/user-delete.tmpl" + data := make(map[string]any) + id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/delete"), "/manage/users/") + + usr, err := getUserByID(ctx, dbclient, id) + if err != nil { // nolint:dupl + switch { + case errors.Is(err, moduser.ErrUserNotFound): + c.Logger().Errorf("user not found by ID: '%s'", id) + + data["flash"] = fmt.Sprintf("No user found with the UUID: %q", id) + p.Data = data + + return c.Render( + http.StatusNotFound, + tmpl, + p, + ) + + case errors.Is(err, moduser.ErrFailedToQueryUser): + c.Logger().Errorf("failed to query user by ID: '%s'", id) + + data["flash"] = fmt.Sprintf("failed to query user by UUID %q", id) + p.Data = data + + return c.Render( + http.StatusInternalServerError, + tmpl, + p, + ) + + case errors.Is(err, moduser.ErrBadUUID): + c.Logger().Errorf("Invalid UUID '%s': %q", id, err) + + data["flash"] = fmt.Sprintf("invalid UUID %q", id) + p.Data = data + + return c.Render( + http.StatusBadRequest, + "manage/user-details.tmpl", + p, + ) + } + + c.Logger().Errorf("UUID-related issue for UUID '%s': %q", id, err) + + return renderErrorPage( + c, + http.StatusInternalServerError, + http.StatusText(http.StatusInternalServerError), + err.Error(), + ) + } + + data["user"] = usr + p.Data = data + + return c.Render( + http.StatusOK, + tmpl, + p, + ) + } +} + +// DeleteUser handles user deletion POST requests. +func DeleteUser() echo.HandlerFunc { + return func(c echo.Context) error { + addHeaders(c) + + u, ok := c.Get("sessUsr").(moduser.User) + if !ok { + return renderErrorPage( + c, + http.StatusUnauthorized, + http.StatusText(http.StatusUnauthorized), + "username was nil", + ) + } else if !u.IsAdmin { + status := http.StatusUnauthorized + msg := http.StatusText(status) + + return renderErrorPage( + c, status, msg+": You should not be here", "Restricted endpoint", + ) + } + + ctx, ok := c.Get("sloggerCtx").(context.Context) + if !ok { + ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger) + } + + id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/delete"), "/manage/users/") + + usr, err := getUserByID(ctx, dbclient, id) + if err != nil { + switch { + case errors.Is(err, moduser.ErrUserNotFound): + c.Logger().Errorf("user not found by ID: '%s'", id) + + if sess, ok := c.Get("sess").(*sessions.Session); ok { + sess.Values["flash"] = fmt.Sprintf("Error: Could not delete user - no user found with UUID %q", id) + _ = sess.Save(c.Request(), c.Response()) + } + + return c.Redirect(http.StatusSeeOther, "/manage/users") + + case errors.Is(err, moduser.ErrFailedToQueryUser): + c.Logger().Errorf("failed to query user by ID: '%s'", id) + + if sess, ok := c.Get("sess").(*sessions.Session); ok { + sess.Values["flash"] = fmt.Sprintf("failed to query user by UUID %q", id) + _ = sess.Save(c.Request(), c.Response()) + } + + return c.Redirect(http.StatusSeeOther, "/manage/users") + + case errors.Is(err, moduser.ErrBadUUID): + c.Logger().Errorf("Invalid UUID '%s': %q", id, err) + + if sess, ok := c.Get("sess").(*sessions.Session); ok { + sess.Values["flash"] = fmt.Sprintf("invalid UUID %q", id) + _ = sess.Save(c.Request(), c.Response()) + } + + return c.Redirect(http.StatusSeeOther, c.Request().URL.String()) + } + + c.Logger().Errorf("Encountered a UUID-related issue for UUID '%s': %q", id, err) + + return renderErrorPage( + c, + http.StatusInternalServerError, + http.StatusText(http.StatusInternalServerError), + err.Error(), + ) + } + + if err = moduser.DeleteUserByID(ctx, dbclient, usr.ID.String()); err != nil { + c.Logger().Errorf("Could not delete user (id: %q), error: %q", id, err) + + return renderErrorPage( + c, + http.StatusInternalServerError, + http.StatusText(http.StatusInternalServerError), + err.Error(), + ) + } + + if sess, ok := c.Get("sess").(*sessions.Session); ok { + sess.Values["flash"] = fmt.Sprintf("Successfully deleted user %q!", usr.Username) + _ = sess.Save(c.Request(), c.Response()) + } + + return c.Redirect(http.StatusSeeOther, "/manage/users") + } +} diff --git a/modules/user/user.go b/modules/user/user.go index d53a6f1..1f44b64 100644 --- a/modules/user/user.go +++ b/modules/user/user.go @@ -139,6 +139,36 @@ func QueryUserByID(ctx context.Context, client *ent.Client, strID string) (*ent. return u, nil } +// DeleteUserByID returns nil on successful deletion, err otherwise. +func DeleteUserByID(ctx context.Context, client *ent.Client, strID string) error { + slogger := ctx.Value(CtxKey{}).(*slogging.Slogger) + log := *slogger + + log.Logger = log.Logger.With( + slog.Group("pcmt extra", slog.String("module", "modules/user")), + ) + + id, err := uuid.Parse(strID) + if err != nil { + return ErrBadUUID + } + + err = client.User. + DeleteOneID(id).Exec(ctx) + + switch { + case ent.IsNotFound(err): + log.Warnf("user not found by ID: %q", err) + return ErrUserNotFound + + case err != nil: + log.Warn("failed to query user by ID", "error", err, "uuid requested", id) + return ErrFailedToQueryUser + } + + return nil +} + // Exists determines whether the username OR email in question was previously // used to register a user. func Exists(ctx context.Context, client *ent.Client, username, email string) (bool, error) { diff --git a/templates/manage/user-delete.tmpl b/templates/manage/user-delete.tmpl new file mode 100644 index 0000000..3abdf90 --- /dev/null +++ b/templates/manage/user-delete.tmpl @@ -0,0 +1,60 @@ +{{ template "head.tmpl" . }} + + {{ template "navbar.tmpl" . }} +
+
+ {{if and .Data .Data.user -}} +
+

+ Deleting user "{{ .Data.user.Username }}" +

+ + ⏎ All users + +
+ +
+

+ Are you sure you want to permanently and irrevocably DELETE user {{.Data.user.Username}} along with all their data? +

+
+
+ + Cancel + +
+ + +
+
+ {{- else -}} +
+

+ Delete user +

+ + ⏎ All users + +
+
+

+ Encountered an issue loading the user for deletion. +

+
+ {{- end -}} + {{- if and .Data .Data.flash -}} +
+

+ {{ .Data.flash -}} +

+
+ {{- end}} +
+
+{{ template "footer.tmpl" . }} diff --git a/templates/manage/user-details.tmpl b/templates/manage/user-details.tmpl index e14c5bc..4d8253d 100644 --- a/templates/manage/user-details.tmpl +++ b/templates/manage/user-details.tmpl @@ -16,6 +16,9 @@ Edit + + Delete +
diff --git a/templates/navbar.tmpl b/templates/navbar.tmpl index 250e154..47863d2 100644 --- a/templates/navbar.tmpl +++ b/templates/navbar.tmpl @@ -25,7 +25,7 @@ {{ if and .User .User.IsLoggedIn }}
  • - {{- if or (pageIs .Current "manage-users") (pageIs .Current "manage-users-new") (pageIs .Current "manage-users-user-details") (pageIs .Current "manage-users-edit-user") -}} + {{- if or (pageIs .Current "manage-users") (pageIs .Current "manage-users-new") (pageIs .Current "manage-users-user-details") (pageIs .Current "manage-users-edit-user") (pageIs .Current "manage-users-delete-user") -}} {{else}}