// Copyright 2023 wanderer // SPDX-License-Identifier: AGPL-3.0-only package handlers import ( "context" "errors" "fmt" "net/http" "strings" "git.dotya.ml/mirre-mt/pcmt/ent" moduser "git.dotya.ml/mirre-mt/pcmt/modules/user" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" ) func ManageUsers() 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), "it appears there is no user", ) } sess, ok := c.Get("sess").(*sessions.Session) if !ok { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), "missing the session", ) } ctx, ok := c.Get("sloggerCtx").(context.Context) if !ok { ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger) } if !u.IsAdmin { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden msg := http.StatusText(status) return renderErrorPage( c, status, msg+": You should not be here", "Restricted endpoint", ) } data := make(map[string]any) flash := sess.Values["flash"] path := c.Request().URL.Path if path == "/manage/users/new" { p := newPage() p.Title = "Manage Users - New User" p.Current = "manage-users-new" p.CSRF = c.Get("csrf").(string) p.User = u if flash != nil { data["flash"] = flash.(string) delete(sess.Values, "flash") _ = sess.Save(c.Request(), c.Response()) } err := c.Render( http.StatusOK, "manage/user-new.tmpl", p, ) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } return nil } var allUsers []*moduser.User if users, err := moduser.ListAll(ctx, dbclient); err == nil && users != nil { for _, u := range users { usr := &moduser.User{ Username: u.Username, Email: u.Email, ID: u.ID, IsActive: u.IsActive, IsAdmin: u.IsAdmin, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, LastLogin: u.LastLogin, } allUsers = append(allUsers, usr) } } else { c.Logger().Error(http.StatusText(http.StatusInternalServerError) + " - " + err.Error()) return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } data["allusers"] = allUsers p := newPage() p.Title = "Manage Users" p.Current = "manage-users" p.CSRF = c.Get("csrf").(string) p.User = u p.Data = data if flash != nil { data["flash"] = flash.(string) delete(sess.Values, "flash") _ = sess.Save(c.Request(), c.Response()) } err := c.Render( http.StatusOK, "manage/user.tmpl", p, ) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } return nil } } func CreateUser() echo.HandlerFunc { //nolint:gocognit 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", ) } ctx, ok := c.Get("sloggerCtx").(context.Context) if !ok { ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger) } if !u.IsAdmin { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden msg := http.StatusText(status) return renderErrorPage( c, status, msg+": You should not be here", "Restricted endpoint", ) } p := newPage() p.Title = "Manage Users" p.Current = "manage-users" data := make(map[string]any) uc := new(userCreate) if err := c.Bind(uc); err != nil { return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest), err.Error(), ) } if uc.Username == "" || uc.Password == "" || uc.RepeatPassword == "" || uc.Password != uc.RepeatPassword { c.Logger().Error("username or password not set, returning to /manage/users/new") msg := "Error: both username and password need to be set" if uc.Password != uc.RepeatPassword { msg += "; password needs to be passed the same twice" } data["flash"] = msg data["form"] = uc p.Data = data p.User = u return c.Render( http.StatusBadRequest, "manage/user-new.tmpl", p, ) } if err := c.Validate(uc); err != nil { return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), err.Error(), ) } var msg string usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin) if err == nil && usr != nil { msg = fmt.Sprintf("Successfully created user %q!", usr.Username) if sess, ok := c.Get("sess").(*sessions.Session); ok { sess.Values["flash"] = msg _ = sess.Save(c.Request(), c.Response()) } return c.Redirect(http.StatusSeeOther, "/manage/users") } if ent.IsNotSingular(err) { c.Logger().Error("user exists") msg = "Error: user already exists: " + err.Error() } else { msg = "Error: " + err.Error() } data["flash"] = msg data["form"] = uc p.Data = data return c.Render( http.StatusInternalServerError, "manage/user-new.tmpl", p, ) } } func ViewUser() 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", ) } if !u.IsAdmin { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden msg := http.StatusText(status) return renderErrorPage( c, status, msg+": You should not be here", "Restricted endpoint", ) } sess, ok := c.Get("sess").(*sessions.Session) if !ok { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), "missing the session", ) } ctx, ok := c.Get("sloggerCtx").(context.Context) if !ok { ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger) } p := newPage() p.Title = "Manage Users - User Details" p.Current = "manage-users-user-details" p.User = u data := make(map[string]any) uid := new(userID) err := c.Bind(uid) if err == nil { //nolint:dupl if err := c.Validate(uid); err != nil { return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest)+" - make sure to pass in a proper UUID", err.Error(), ) } usr, err := getUserByID(ctx, dbclient, uid.ID) if err != nil { if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic c.Logger().Errorf("user not found by ID: '%s'", uid.ID) data["flash"] = fmt.Sprintf("No user found with UUID: %q", uid.ID) p.Data = data return c.Render( http.StatusNotFound, "manage/user-details.tmpl", p, ) } else if errors.Is(err, moduser.ErrFailedToQueryUser) { c.Logger().Errorf("failed to query user by ID: '%s'", uid.ID) data["flash"] = fmt.Sprintf("Error: failed to query user by UUID %q", uid.ID) p.Data = data return c.Render( http.StatusInternalServerError, "manage/user-details.tmpl", p, ) } else if errors.Is(err, moduser.ErrBadUUID) { c.Logger().Errorf("Invalid UUID '%s': %q", uid.ID, err) data["flash"] = fmt.Sprintf("Error: invalid UUID %q", uid.ID) p.Data = data return c.Render( http.StatusBadRequest, "manage/user-details.tmpl", p, ) } c.Logger().Errorf("UUID-related issue for UUID '%s': %q", uid.ID, err) return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } data["user"] = usr flash := sess.Values["flash"] if flash != nil { data["flash"] = flash.(string) delete(sess.Values, "flash") _ = sess.Save(c.Request(), c.Response()) } p.Data = data return c.Render( http.StatusOK, "manage/user-details.tmpl", p, ) } return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest), err.Error(), ) } } //nolint:dupl func EditUser() 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", ) } if !u.IsAdmin { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden 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) } tmpl := "manage/user-edit.tmpl" data := make(map[string]any) id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/edit"), "/manage/users/") p := newPage() p.Title = "Manage Users - Edit User" p.Current = "manage-users-edit-user" p.User = u 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, ) } } //nolint:dupl func UpdateUser() echo.HandlerFunc { //nolint:gocognit 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", ) } if !u.IsAdmin { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden 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) } tmpl := "manage/user-edit.tmpl" data := make(map[string]any) id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/update"), "/manage/users/") p := newPage() p.Title = "Manage Users - Edit User" p.Current = "manage-users-edit-user" p.User = u 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) 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(), ) } uu := new(userCreate) if err := c.Bind(uu); err != nil { return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest), err.Error(), ) } f, err := moduser.UsrFinishedSetup(ctx, dbclient, usr.ID) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } if f { if uu.Username == "" { c.Logger().Error("username not set, returning to /manage/users/edit") msg := "Error: the username needs to be set" data["flash"] = msg data["form"] = uu p.Data = data return c.Render( http.StatusBadRequest, "manage/user-edit.tmpl", p, ) } } else { if uu.Username == "" || uu.Password == "" || uu.RepeatPassword == "" || uu.Password != uu.RepeatPassword { c.Logger().Error("username or password not set, returning to /manage/users/edit") msg := "Error: both username and password need to be set" if uu.Password != uu.RepeatPassword { msg += "; the same password needs to be passed in twice" } data["flash"] = msg data["form"] = uu p.Data = data return c.Render( http.StatusBadRequest, "manage/user-edit.tmpl", p, ) } } if err := c.Validate(uu); err != nil { return renderErrorPage( c, http.StatusBadRequest, http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(), err.Error(), ) } if usr.Username != uu.Username { exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } if exists { msg := fmt.Sprintf("Error: username %q is already taken", uu.Username) c.Logger().Warn(msg) data["flash"] = msg p.Data = data return c.Render( http.StatusBadRequest, "manage/user-edit.tmpl", p, ) } } if usr.Email != uu.Email { exists, err := moduser.EmailExists(ctx, dbclient, uu.Email) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } if exists { msg := fmt.Sprintf("Error: email %q is already taken", uu.Email) c.Logger().Error(msg) return renderErrorPage( c, http.StatusBadRequest, msg, msg, ) } } // now update err = moduser.UpdateUserByAdmin( ctx, dbclient, usr.ID, uu.Email, uu.Username, uu.Password, uu.IsAdmin, uu.IsActive, ) if err != nil { return renderErrorPage( c, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), err.Error(), ) } data["user"] = usr p.Data = data if sess, ok := c.Get("sess").(*sessions.Session); ok { sess.Values["flash"] = fmt.Sprintf("Successfully updated user %q", uu.Username) _ = sess.Save(c.Request(), c.Response()) } return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s", 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 { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden 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) tmpl := "manage/user-delete.tmpl" data := make(map[string]any) id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/delete"), "/manage/users/") p := newPage() p.Title = "Manage Users - Delete User" p.Current = "manage-users-delete-user" p.CSRF = csrf p.User = u 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 { log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin) status := http.StatusForbidden 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") } }