wisski-cloud-distillery/internal/dis/component/control/admin/users.go
2023-01-12 13:55:07 +01:00

221 lines
6.4 KiB
Go

package admin
import (
"context"
"errors"
"net/http"
"net/url"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/rs/zerolog"
)
//go:embed "html/users.html"
var userTemplateString string
var userTemplate = static.AssetsAdmin.MustParseShared(
"users.html",
userTemplateString,
)
type userContext struct {
custom.BaseContext
Error string
Users []*auth.AuthUser
}
func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
admin.Dependencies.Custom.Update(&uc, r, custom.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users/"},
},
})
uc.Error = r.URL.Query().Get("error")
uc.Users, err = admin.Dependencies.Auth.Users(r.Context())
return
}
//go:embed "html/user_create.html"
var userCreateTemplateString string
var userCreateTemplate = static.AssetsAdmin.MustParseShared(
"user_create.html",
userCreateTemplateString,
)
var (
errCreateInvalidUsername = errors.New("invalid username")
errCreateInvalidPassword = errors.New("invalid password")
)
type createUserResult struct {
User string
Passsword string
Admin bool
}
func (admin *Admin) createUser(ctx context.Context) http.Handler {
userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate)
gaps := custom.BaseContextGaps{
Crumbs: []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users"},
{Title: "Create", Path: "/admin/users/create"},
},
Actions: []component.MenuItem{
{Title: "Create New", Path: "/admin/users/create/"},
},
}
return &httpx.Form[createUserResult]{
Fields: []field.Field{
{Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"},
{Name: "password", Type: field.Password, Autocomplete: field.NewPassword, Label: "Password"},
{Name: "admin", Type: field.Checkbox, Label: "Distillery Administrator"},
},
FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: userCreateTemplate,
RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any {
return admin.Dependencies.Custom.NewForm(ctx, r, gaps)
},
Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) {
cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked
if cu.User == "" {
return cu, errCreateInvalidUsername
}
if cu.Passsword == "" {
return cu, errCreateInvalidPassword
}
// check the password policy
err = admin.Dependencies.Auth.CheckPasswordPolicy(cu.Passsword, cu.User)
if err != nil {
return cu, err
}
return cu, nil
},
RenderSuccess: func(cu createUserResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// create the user
user, err := admin.Dependencies.Auth.CreateUser(r.Context(), cu.User)
if err != nil {
return err
}
// disable the user and setup the admin flag
user.SetAdmin(cu.Admin)
if err := user.Save(r.Context()); err != nil {
return err
}
// set the password!
err = user.SetPassword(r.Context(), []byte(cu.Passsword))
if err != nil {
return err
}
// everything went fine, redirect the user back to the user page!
http.Redirect(w, r, "/admin/users/", http.StatusSeeOther)
return nil
},
}
}
var errNotCurrentUser = httpx.Response{
Body: []byte("attempt to modify current user"),
StatusCode: http.StatusBadRequest,
}
func (admin *Admin) useraction(ctx context.Context, name string, action func(r *http.Request, user *auth.AuthUser) error) http.Handler {
logger := zerolog.Ctx(ctx)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
logger.Err(err).Str("action", name).Msg("failed to parse form")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
username := r.PostFormValue("user")
user, err := admin.Dependencies.Auth.User(r.Context(), username)
if err != nil {
logger.Err(err).Str("action", name).Msg("failed to get user")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
me, err := admin.Dependencies.Auth.UserOf(r)
if err != nil {
logger.Err(err).Str("action", name).Msg("failed to get current user")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
// don't allow the current user
if me.User.User == user.User.User {
errNotCurrentUser.ServeHTTP(w, r)
return
}
if err := action(r, user); err != nil {
logger.Err(err).Str("action", name).Msg("failed to act on user")
http.Redirect(w, r, "/admin/users/?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/users/", http.StatusSeeOther)
})
}
func (admin *Admin) usersDeleteHandler(ctx context.Context) http.Handler {
return admin.useraction(ctx, "delete user", func(r *http.Request, user *auth.AuthUser) error {
return user.Delete(r.Context())
})
}
func (admin *Admin) usersDisableHandler(ctx context.Context) http.Handler {
return admin.useraction(ctx, "disable user", func(r *http.Request, user *auth.AuthUser) error {
return user.UnsetPassword(r.Context())
})
}
func (admin *Admin) usersDisableTOTPHandler(ctx context.Context) http.Handler {
return admin.useraction(ctx, "disable user totp", func(r *http.Request, user *auth.AuthUser) error {
return user.DisableTOTP(r.Context())
})
}
func (admin *Admin) usersToggleAdmin(ctx context.Context) http.Handler {
return admin.useraction(ctx, "toggle admin", func(r *http.Request, user *auth.AuthUser) error {
if user.IsAdmin() {
return user.MakeRegular(r.Context())
}
return user.MakeAdmin(r.Context())
})
}
func (admin *Admin) usersPasswordHandler(ctx context.Context) http.Handler {
return admin.useraction(ctx, "set password", func(r *http.Request, user *auth.AuthUser) error {
password := r.PostFormValue("password")
if password == "" {
return httpx.ErrBadRequest
}
// check the password policy
err := user.CheckPasswordPolicy(password)
if err != nil {
return err
}
return user.SetPassword(r.Context(), []byte(password))
})
}