// 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 { c.Logger().Debug("this is a restricted endpoint") status := http.StatusUnauthorized 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 := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users - New User", CSRF: c.Get("csrf").(string), DevelMode: setting.IsDevel(), Current: "manage-users-new", 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, } 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 := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users", CSRF: c.Get("csrf").(string), DevelMode: setting.IsDevel(), Current: "manage-users", User: u, 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 { c.Logger().Debug("this is a restricted endpoint") status := http.StatusUnauthorized msg := http.StatusText(status) return renderErrorPage( c, status, msg+": You should not be here", "Restricted endpoint", ) } p := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users", DevelMode: setting.IsDevel(), Current: "manage-user", } 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 return c.Render( http.StatusBadRequest, "manage/user-new.tmpl", p, ) } var msg string usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin) if err == nil && usr != nil { msg = "created user '" + 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 { c.Logger().Debug("this is a restricted endpoint") 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) } p := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users - User Details", DevelMode: setting.IsDevel(), Current: "manage-users-user-details", User: u, } data := make(map[string]any) uid := new(userID) err := c.Bind(uid) if err == nil { //nolint:dupl 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 the 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("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("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 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 { c.Logger().Debug("this is a restricted endpoint") 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) } p := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users - Edit User", DevelMode: setting.IsDevel(), Current: "manage-users-edit-user", User: u, } tmpl := "manage/user-edit.tmpl" data := make(map[string]any) id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/edit"), "/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, ) } } //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 { c.Logger().Debug("this is a restricted endpoint") 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) } p := page{ AppName: setting.AppName(), AppVer: appver, Title: "Manage Users - Edit User", DevelMode: setting.IsDevel(), Current: "manage-users-edit-user", User: u, } tmpl := "manage/user-edit.tmpl" data := make(map[string]any) id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/update"), "/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) 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(), ) } 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 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 data["user"] = usr p.Data = data return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s/edit", usr.ID)) } }