Admin: Add user page
This commit is contained in:
parent
bc0e92bdac
commit
d34e85a18f
24 changed files with 456 additions and 77 deletions
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
11
internal/dis/component/control/admin/html/user_create.html
Normal file
11
internal/dis/component/control/admin/html/user_create.html
Normal 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> >
|
||||
<a class="pure-button" href="/admin/users">Users</a> >
|
||||
<a class="pure-button pure-button-primary" href="/admin/users/create">Create</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
95
internal/dis/component/control/admin/html/users.html
Normal file
95
internal/dis/component/control/admin/html/users.html
Normal 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> >
|
||||
<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">
|
||||
<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 }}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
190
internal/dis/component/control/admin/users.go
Normal file
190
internal/dis/component/control/admin/users.go
Normal 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))
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue