Admin: Add user page

This commit is contained in:
Tom Wiesing 2023-01-04 16:10:55 +01:00
parent bc0e92bdac
commit d34e85a18f
No known key found for this signature in database
24 changed files with 456 additions and 77 deletions

View file

@ -1,7 +1,7 @@
package cli
// ===========================================================================================================
// This file was generated automatically at 04-01-2023 11:57:49 using gogenlicense.
// This file was generated automatically at 04-01-2023 14:54:26 using gogenlicense.
// Do not edit manually, as changes may be overwritten.
// ===========================================================================================================
@ -2189,7 +2189,7 @@ package cli
// # Generation
//
// This variable and the associated documentation have been automatically generated using the 'gogenlicense' tool.
// It was last updated at 04-01-2023 11:57:49.
// It was last updated at 04-01-2023 14:54:26.
var LegalNotices string
func init() {

View file

@ -32,17 +32,7 @@ func (auth *Auth) Routes() []string { return []string{"/user/"} }
func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
router := httprouter.New()
// setup the csrf handler (if needed)
auth.csrf.Get(func() func(http.Handler) http.Handler {
var opts []csrf.Option
if !auth.Config.HTTPSEnabled() {
opts = append(opts, csrf.Secure(false))
}
opts = append(opts, csrf.Path(route))
return csrf.Protect(auth.Config.CSRFSecret(), opts...)
})
router.Handler(http.MethodGet, route, auth.authHome(ctx))
router.Handler(http.MethodGet, route, auth.authUser(ctx))
{
login := auth.authLogin(ctx)
@ -78,3 +68,14 @@ func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler,
return router, nil
}
func (auth *Auth) CSRF() func(http.Handler) http.Handler {
// setup the csrf handler (if needed)
return auth.csrf.Get(func() func(http.Handler) http.Handler {
var opts []csrf.Option
if !auth.Config.HTTPSEnabled() {
opts = append(opts, csrf.Secure(false))
}
return csrf.Protect(auth.Config.CSRFSecret(), opts...)
})
}

View file

@ -74,5 +74,5 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
// Admin represents a permission that checks if a user is an administrator and has totp enabled.
var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) {
return Bool2Grant(user != nil && user.Admin && user.TOTPEnabled, "user needs to have admin permissions and TOTP enabled"), nil
return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and TOTP enabled"), nil
}

View file

@ -10,17 +10,17 @@ import (
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
//go:embed "templates/home.html"
var homeHTMLStr string
var homeTemplate = static.AssetsHome.MustParseShared(
"home.html",
homeHTMLStr,
//go:embed "templates/user.html"
var userHTMLStr string
var userTemplate = static.AssetsUser.MustParseShared(
"user.html",
userHTMLStr,
)
func (auth *Auth) authHome(ctx context.Context) http.Handler {
func (auth *Auth) authUser(ctx context.Context) http.Handler {
return auth.Protect(&httpx.HTMLHandler[*AuthUser]{
Handler: auth.UserOf,
Template: homeTemplate,
Template: userTemplate,
}, nil)
}
@ -47,7 +47,7 @@ func (auth *Auth) authPassword(ctx context.Context) http.Handler {
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
CSRF: auth.CSRF(),
RenderTemplate: passwordTemplate,
RenderTemplateContext: auth.UserFormContext,

View file

@ -49,7 +49,7 @@ func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
}
// user isn't enabled
if !user.Enabled {
if !user.IsEnabled() {
return nil, nil
}
@ -122,7 +122,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
CSRF: auth.CSRF(),
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
if context.Err != nil {

View file

@ -13,12 +13,12 @@
{{ define "content" }}
<div class="pure-u-1">
<p>
{{ if .User.Admin }}
{{ if .User.IsAdmin }}
You are an administrator.
{{ else }}
You are a regular user.
{{ end }}
{{ if .User.TOTPEnabled }}
{{ if .User.IsTOTPEnabled }}
You have TOTP enabled.
{{ else }}
You do not have TOTP enabled.
@ -26,7 +26,7 @@
</p>
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/user/password/">Change Password</a>
{{ if .User.TOTPEnabled }}
{{ if .User.IsTOTPEnabled }}
<a class="pure-button" href="/user/totp/disable/">Disable TOTP</a>
{{ else }}
<a class="pure-button" href="/user/totp/enable/">Enable TOTP</a>
@ -35,9 +35,9 @@
<hr />
</div>
{{ if .User.Admin }}
{{ if .User.IsAdmin }}
<div class="pure-u-1">
{{ if (not .User.TOTPEnabled) }}
{{ if (not .User.IsTOTPEnabled) }}
<div>
<p class="error-message">
TOTP is required to access these.

View file

@ -22,11 +22,11 @@ func (auth *Auth) authTOTPEnable(ctx context.Context) http.Handler {
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
CSRF: auth.CSRF(),
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, err := auth.UserOf(r)
return struct{}{}, err == nil && user != nil && user.TOTPEnabled
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
RenderTemplate: totpEnableTemplate,
@ -81,11 +81,11 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler {
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
CSRF: auth.CSRF(),
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, _ := auth.UserOf(r)
return struct{}{}, user != nil && user.TOTPEnabled
user, err := auth.UserOf(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
user, err := auth.UserOf(r)
@ -152,11 +152,11 @@ func (auth *Auth) authTOTPDisable(ctx context.Context) http.Handler {
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
CSRF: auth.CSRF(),
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, _ := auth.UserOf(r)
return struct{}{}, user != nil && !user.TOTPEnabled
user, err := auth.UserOf(r)
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
},
RenderTemplate: totpDisableTemplate,
RenderTemplateContext: auth.UserFormContext,

View file

@ -4,13 +4,13 @@ import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"image/png"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/pkg/errors"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
@ -90,15 +90,17 @@ func (auth *Auth) CreateUser(ctx context.Context, name string) (user *AuthUser,
user = &AuthUser{
User: models.User{
User: name,
Enabled: false,
User: name,
},
}
user.SetAdmin(false)
user.SetEnabled(false)
user.SetTOTPEnabled(false)
// do the create statement
err = table.Create(&user.User).Error
err = table.Select("*").Create(&user.User).Error
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "Create")
}
user.auth = auth
@ -116,7 +118,7 @@ func (au *AuthUser) String() string {
return "User{nil}"
}
hasPassword := len(au.PasswordHash) > 0
return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.Enabled, hasPassword, au.User.Admin)
return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.IsEnabled(), hasPassword, au.User.IsAdmin())
}
var (
@ -140,7 +142,7 @@ func (au *AuthUser) CheckTOTP(passcode string) error {
return err
}
if au.TOTPEnabled && !totp.Validate(passcode, secret.Secret()) {
if au.IsTOTPEnabled() && !totp.Validate(passcode, secret.Secret()) {
return ErrTOTPFailed
}
return nil
@ -148,7 +150,7 @@ func (au *AuthUser) CheckTOTP(passcode string) error {
// NewTOTP generates a new TOTP secret, returning a totp key.
func (au *AuthUser) NewTOTP(ctx context.Context) (*otp.Key, error) {
if au.User.TOTPEnabled {
if au.User.IsTOTPEnabled() {
return nil, ErrTOTPEnabled
}
@ -191,14 +193,14 @@ func (au *AuthUser) EnableTOTP(ctx context.Context, passcode string) error {
return ErrTOTPFailed
}
au.User.TOTPEnabled = true
au.User.SetTOTPEnabled(true)
return au.Save(ctx)
}
// DisableTOTP disables totp for the given user
func (au *AuthUser) DisableTOTP(ctx context.Context) (err error) {
au.User.TOTPEnabled = false
au.User.SetTOTPEnabled(false)
au.User.TOTPURL = ""
return au.Save(ctx)
}
@ -209,14 +211,14 @@ func (au *AuthUser) SetPassword(ctx context.Context, password []byte) (err error
if err != nil {
return err
}
au.User.Enabled = true
au.User.SetEnabled(true)
return au.Save(ctx)
}
// UnsetPassword removes the password from this user, and disables them
func (au *AuthUser) UnsetPassword(ctx context.Context) error {
au.User.PasswordHash = nil
au.User.Enabled = false
au.User.SetEnabled(false)
return au.Save(ctx)
}
@ -230,7 +232,7 @@ func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error {
if au == nil {
return ErrNoUser
}
if !au.User.Enabled {
if !au.User.IsEnabled() {
return ErrUserDisabled
}
@ -254,14 +256,14 @@ func (au *AuthUser) CheckCredentials(ctx context.Context, password []byte, passc
// MakeAdmin makes this user an admin, and saves the update in the database.
// If the user is already an admin, does not return an error.
func (au *AuthUser) MakeAdmin(ctx context.Context) error {
au.User.Admin = true
au.User.SetAdmin(true)
return au.Save(ctx)
}
// MakeRegular removes admin rights from this user.
// If this user is not an dmin, does not return an error.
func (au *AuthUser) MakeRegular(ctx context.Context) error {
au.User.Admin = true
au.User.SetAdmin(false)
return au.Save(ctx)
}
@ -271,7 +273,7 @@ func (au *AuthUser) Save(ctx context.Context) error {
if err != nil {
return err
}
return table.Save(&au.User).Error
return table.Select("*").Updates(&au.User).Error
}
// Delete deletes the user from the database

View file

@ -9,6 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
"github.com/julienschmidt/httprouter"
"github.com/rs/zerolog"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -48,6 +49,7 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
Handler: admin.serveSocket,
}
handler = admin.Dependencies.Auth.Protect(socket, auth.Admin)
handler = admin.Dependencies.Auth.CSRF()(handler)
}
// handle everything
@ -61,6 +63,26 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
Template: indexTemplate,
})
// add a handler for the user page
router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{
Handler: admin.users,
Template: userTemplate,
})
// add a user create form
{
create := admin.createUser(ctx)
router.Handler(http.MethodGet, route+"users/create", create)
router.Handler(http.MethodPost, route+"users/create", create)
}
// add all the admin actions
router.Handler(http.MethodPost, route+"users/delete", admin.usersDeleteHandler(ctx))
router.Handler(http.MethodPost, route+"users/disable", admin.usersDisableHandler(ctx))
router.Handler(http.MethodPost, route+"users/disabletotp", admin.usersDisableTOTPHandler(ctx))
router.Handler(http.MethodPost, route+"users/password", admin.usersPasswordHandler(ctx))
router.Handler(http.MethodPost, route+"users/toggleadmin", admin.usersToggleAdmin(ctx))
// add a handler for the component page
router.Handler(http.MethodGet, route+"components", httpx.HTMLHandler[componentContext]{
Handler: admin.components,
@ -79,9 +101,19 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
Template: instanceTemplate,
})
router.Handler(http.MethodPost, route+"api/login", httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
// add a router for the login page
router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx))
return
}
func (admin *Admin) loginHandler(ctx context.Context) http.Handler {
logger := zerolog.Ctx(ctx)
return httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
// parse the form
if err := r.ParseForm(); err != nil {
logger.Err(err).Msg("failed to parse admin login")
return "", 0, err
}
@ -93,10 +125,9 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
target, err := instance.Users().Login(r.Context(), nil, r.PostFormValue("user"))
if err != nil {
logger.Err(err).Msg("failed to admin login")
return "", 0, err
}
return target.String(), http.StatusSeeOther, err
}))
return
})
}

View file

@ -6,7 +6,10 @@
<a class="pure-button pure-button-primary" href="/admin/index">Admin</a>
</p>
<p>
<a class="pure-button" href="/admin/components">Components</a>
<div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button" href="/admin/users">Users</a>
<a class="pure-button pure-button-small" href="/admin/components">Components</a>
</div>
</p>
{{ end }}

View file

@ -7,7 +7,9 @@
<a class="pure-button pure-button-primary" href="/admin/instance/{{ .Info.Slug }}">Instance</a>
</p>
<p>
<a class="pure-button" href="/admin/ingredients/{{ .Info.Slug }}">Ingredients</a>
<div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button pure-button-small" href="/admin/ingredients/{{ .Info.Slug }}">Ingredients</a>
</div>
</p>
{{ end }}
@ -216,6 +218,7 @@
</thead>
<tbody>
{{ $slug := .Instance.Slug }}
{{ $csrf := .CSRF }}
{{ range $index, $user := .Info.Users }}
<tr {{ if not $user.Status }}style="color:gray"{{ end }}>
<td>
@ -247,10 +250,11 @@
<code class="date">{{ $user.Login.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<form action="/admin/api/login" method="POST" target="_blank">
<form action="/admin/login" method="POST" target="_blank">
<input type="hidden" name="slug" value="{{ $slug }}">
<input type="hidden" name="user" value="{{ $user.Name }}">
<input type="submit" class="pure-button pure-button-action" value="Login in new window">
{{ $csrf }}
</form>
</td>
</tr>

View file

@ -0,0 +1,11 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Distillery Admin - Create User{{ end }}
{{ define "form/button" }}Create{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Control</a> &gt;
<a class="pure-button" href="/admin/users">Users</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/users/create">Create</a>
</p>
{{ end }}

View file

@ -0,0 +1,95 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - Users{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Control</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/users">Users</a>
</p>
<p>
<div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button" href="/admin/users/create">Create New</a>
</div>
</p>
{{ end }}
{{ define "content" }}
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
Username
</th>
<th>
Enabled
</th>
<th>
Admin
</th>
<th>
Passcode (TOTP)
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
{{ $csrf := .CSRF }}
{{ range .Users }}
<tr {{ if not .User.IsEnabled }}style="color:gray"{{ end }}>
<td>
{{ .User.User }}
</td>
<td>
{{ .User.IsEnabled }}
</td>
<td>
{{ .User.IsAdmin }}
</td>
<td>
{{ .User.IsTOTPEnabled }}
</td>
<td>
<div class="pure-button-group" role="group">
<form action="/admin/users/toggleadmin" method="POST" class="pure-form-group">
<input type="hidden" name="user" value="{{ .User.User }}">
<input type="submit" class="pure-button" value="{{ if .User.IsAdmin }}Remove Admin{{ else }} Make Admin{{ end }}">
{{ $csrf }}
</form>
<form action="/admin/users/password" method="POST" class="pure-form pure-form-group">
<input type="hidden" name="user" value="{{ .User.User }}">
<input type="password" name="password"> &nbsp;
<input type="submit" class="pure-button" value="Update Password">
{{ $csrf }}
</form>
<form action="/admin/users/disable" method="POST" class="pure-form-group">
<input type="hidden" name="user" value="{{ .User.User }}">
<input type="submit" class="pure-button" {{ if (not .User.IsEnabled) }}disabled{{ end }} value="Disable">
{{ $csrf }}
</form>
<form action="/admin/users/disabletotp" method="POST" class="pure-form-group">
<input type="hidden" name="user" value="{{ .User.User }}">
<input type="submit" class="pure-button" {{ if (not .User.IsTOTPEnabled) }}disabled{{ end }} value="Remove Passcode">
{{ $csrf }}
</form>
<form action="/admin/users/delete" method="POST" class="pure-form-group">
<input type="hidden" name="user" value="{{ .User.User }}">
<input type="submit" class="pure-button pure-button-danger" value="Delete">
{{ $csrf }}
</form>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
{{ end }}

View file

@ -2,6 +2,7 @@ package admin
import (
_ "embed"
"html/template"
"net/http"
"time"
@ -10,6 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
@ -23,11 +25,14 @@ var instanceTemplate = static.AssetsAdmin.MustParseShared(
type instanceContext struct {
Time time.Time
CSRF template.HTML
Instance models.Instance
Info status.WissKI
}
func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) {
is.CSRF = csrf.TemplateField(r)
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["slug"])
if err == instances.ErrWissKINotFound {

View file

@ -0,0 +1,190 @@
package admin
import (
"context"
"errors"
"html/template"
"net/http"
"time"
_ "embed"
"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/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/rs/zerolog"
)
//go:embed "html/users.html"
var userTemplateString string
var userTemplate = static.AssetsAdmin.MustParseShared(
"users.html",
userTemplateString,
)
type userContext struct {
Time time.Time
CSRF template.HTML
Users []*auth.AuthUser
}
func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
uc.CSRF = csrf.TemplateField(r)
uc.Time = time.Now()
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 {
return &httpx.Form[createUserResult]{
Fields: []httpx.Field{
{Name: "username", Type: httpx.TextField, Label: "Username"},
{Name: "password", Type: httpx.PasswordField, Label: "Password"},
{Name: "admin", Type: httpx.CheckboxField, Label: "Distillery Administrator"},
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: admin.Dependencies.Auth.CSRF(),
RenderTemplate: userCreateTemplate,
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"] == "on"
if cu.User == "" {
return cu, errCreateInvalidUsername
}
if cu.Passsword == "" {
return cu, errCreateInvalidPassword
}
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")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
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
}
return user.SetPassword(r.Context(), []byte(password))
})
}

View file

@ -5,17 +5,17 @@ package static
// AssetsHome contains assets for the 'Home' entrypoint.
var AssetsHome = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4ec77c43.css"><link rel="stylesheet" href="/static/Home.2353e048.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/Home.2353e048.css">`,
}
// AssetsUser contains assets for the 'User' entrypoint.
var AssetsUser = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4ec77c43.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
}
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4ec77c43.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
Styles: `<link rel="stylesheet" href="/static/Home.9f00501f.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
}

View file

@ -21,7 +21,7 @@ footer {
}
.overflow table td,
.overflow table th{
.overflow table th {
padding: .5em .5em;
}
@ -41,14 +41,26 @@ footer {
display: block;
height: 1em;
}
.pure-form-group {
display: inline;
}
.pure-button-action {
background-color: rgb(66, 184, 221) !important;
}
.pure-button-success {
background-color: rgb(28, 184, 65) !important;
}
.pure-button-danger {
background: rgb(202, 60, 60) !important;
}
.pure-button-warning {
background: rgb(223, 117, 20) !important;
}
.pure-button-xsmall {
font-size: 70%;
}
@ -70,4 +82,4 @@ footer {
border: 1px solid red;
padding: 2px;
color: red;
}
}

View file

@ -0,0 +1,2 @@
// Package contains all database models
package models

View file

@ -10,9 +10,33 @@ type User struct {
User string `gorm:"column:user;not null;unique"` // name of the user
PasswordHash []byte `gorm:"column:password"` // password of the user, hashed
TOTPEnabled bool `gorm:"column:totpenabled"` // is totp enabled for the user
TOTPEnabled *bool `gorm:"column:totpenabled"` // is totp enabled for the user
TOTPURL string `gorm:"column:totp"` // the totp of the user
Enabled bool `gorm:"enabled;not null"`
Admin bool `gorm:"column:admin;not null"`
Enabled *bool `gorm:"enabled;not null"`
Admin *bool `gorm:"column:admin;not null"`
}
func (user *User) IsAdmin() bool {
return user.Admin != nil && *user.Admin
}
func (user *User) SetAdmin(v bool) {
user.Admin = &v
}
func (user *User) IsEnabled() bool {
return user.Enabled != nil && *user.Enabled
}
func (user *User) SetEnabled(v bool) {
user.Enabled = &v
}
func (user *User) IsTOTPEnabled() bool {
return user.TOTPEnabled != nil && *user.TOTPEnabled
}
func (user *User) SetTOTPEnabled(v bool) {
user.TOTPEnabled = &v
}

View file

@ -25,7 +25,7 @@ func (bk *Bookkeeping) Save(ctx context.Context) error {
}
// Update based on the primary key!
return sdb.Where("pk = ?", bk.Instance.Pk).Updates(&bk.Instance).Error
return sdb.Select("*").Save(&bk.Instance).Error
}
// Delete deletes this instance from the bookkeeping table