feat: implement user deletion
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
52dfe44080
commit
1c67191c09
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
60
templates/manage/user-delete.tmpl
Normal file
60
templates/manage/user-delete.tmpl
Normal file
@ -0,0 +1,60 @@
|
||||
{{ template "head.tmpl" . }}
|
||||
<body class="min-h-screen flex flex-col justify-between bg-white dark:bg-gray-900">
|
||||
{{ template "navbar.tmpl" . }}
|
||||
<main class="grow mb-auto">
|
||||
<div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14">
|
||||
{{if and .Data .Data.user -}}
|
||||
<div class="flex justify-between place-items-center">
|
||||
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 py-2 ring-fuchsia-300">
|
||||
Deleting user "{{ .Data.user.Username }}"
|
||||
</h1>
|
||||
<a href="/manage/users" class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
|
||||
⏎ All users
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 lg:gap-0 place-items-center">
|
||||
<a href="/manage/users/{{- .Data.user.ID -}}" class="w-auto py-1 mt-0 text-center text-blue-500 dark:text-blue-400 md:mx-2 lg:mx-4 hover:underline hover:text-blue-600 dark:hover:text-blue-300">
|
||||
Back to detail
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-center place-items-center mt-8 lg:mx-auto">
|
||||
<p class="text-xl text-pink-600 dark:text-pink-400 py-2 px-10">
|
||||
<span class="italic">Are you sure</span> you want to <span class="underline non-italic">permanently</span> and irrevocably DELETE user <span class="font-mono font-bold">{{.Data.user.Username}}</span> along with all their data?
|
||||
</p>
|
||||
</div>
|
||||
<div class="container flex justify-center place-items-center mt-4 px-11 mx-auto text-xl">
|
||||
<a href="/manage/users/{{- .Data.user.ID -}}" class="w-auto mt-0 text-center text-blue-400 dark:text-blue-400 mx-2 lg:mx-4 hover:underline hover:text-blue-500 dark:hover:text-blue-300">
|
||||
Cancel
|
||||
</a>
|
||||
<form method="post" action="/manage/users/{{- .Data.user.ID -}}/delete" class="w-auto">
|
||||
<input type="hidden" name="_csrf" value="{{- .CSRF -}}">
|
||||
<button class="w-auto py-1 px-2 text-center text-pink-600 dark:text-pink-600 md:ml-6 lg:ml-4 hover:underline hover:text-pink-700 dark:hover:text-pink-400 border-2 border-pink-300 rounded-md hover:shadow-lg">
|
||||
Confirm permanent deletion
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{- else -}}
|
||||
<div class="flex justify-between place-items-center">
|
||||
<h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 py-2">
|
||||
Delete user
|
||||
</h1>
|
||||
<a href="/manage/users" class="w-auto py-1 mt-2 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
|
||||
⏎ All users
|
||||
</a>
|
||||
</div>
|
||||
<div class="items-center place-items-center">
|
||||
<p class="text-pink-400 italic">
|
||||
Encountered an issue loading the user for deletion.
|
||||
</p>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- if and .Data .Data.flash -}}
|
||||
<div class="place-items-center">
|
||||
<h1 class="text-2xl text-pink-600 dark:text-pink-500 py-2">
|
||||
{{ .Data.flash -}}
|
||||
</h1>
|
||||
</div>
|
||||
{{- end}}
|
||||
</div>
|
||||
</main>
|
||||
{{ template "footer.tmpl" . }}
|
@ -16,6 +16,9 @@
|
||||
<a href="/manage/users/{{- .Data.user.ID -}}/edit" class="w-auto py-1 mt-0 text-center text-blue-500 dark:text-blue-400 md:mx-2 lg:mx-4 hover:underline hover:text-blue-600 dark:hover:text-blue-300">
|
||||
Edit
|
||||
</a>
|
||||
<a href="/manage/users/{{- .Data.user.ID -}}/delete" class="w-auto py-1 mt-0 text-center text-pink-600 dark:text-pink-600 md:mr-6 lg:mr-4 hover:underline hover:text-pink-700 dark:hover:text-pink-400">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
<div class="p-2 mt-3 lg:mx-auto border-2 dark:border-slate-500 rounded-sm space-y-0">
|
||||
<div class="flex max-h-14 place-items-baseline justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
|
@ -25,7 +25,7 @@
|
||||
</li>
|
||||
{{ if and .User .User.IsLoggedIn }}
|
||||
<li>
|
||||
{{- 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") -}}
|
||||
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page">
|
||||
{{else}}
|
||||
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
|
||||
|
Loading…
Reference in New Issue
Block a user