Refactor server and templates package
This commit is contained in:
parent
b6bf0a8900
commit
6ede99d7c6
105 changed files with 341 additions and 339 deletions
163
internal/dis/component/server/admin/admin.go
Normal file
163
internal/dis/component/server/admin/admin.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"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/auth/policy"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"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"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
Fetchers []component.DistilleryFetcher
|
||||
|
||||
Exporter *exporter.Exporter
|
||||
Instances *instances.Instances
|
||||
SnapshotsLog *logger.Logger
|
||||
|
||||
Auth *auth.Auth
|
||||
|
||||
Policy *policy.Policy
|
||||
|
||||
Templating *templates.Templating
|
||||
|
||||
Purger *purger.Purger
|
||||
}
|
||||
|
||||
Analytics *lazy.PoolAnalytics
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.DistilleryFetcher = (*Admin)(nil)
|
||||
_ component.Routeable = (*Admin)(nil)
|
||||
_ component.Menuable = (*Admin)(nil)
|
||||
)
|
||||
|
||||
func (admin *Admin) Routes() component.Routes {
|
||||
return component.Routes{
|
||||
Prefix: "/admin/",
|
||||
CSRF: true,
|
||||
Decorator: admin.Dependencies.Auth.Require(auth.Admin),
|
||||
}
|
||||
}
|
||||
|
||||
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
|
||||
if !admin.Dependencies.Auth.Has(auth.Admin, r) {
|
||||
return nil
|
||||
}
|
||||
return []component.MenuItem{
|
||||
{
|
||||
Title: "Admin",
|
||||
Path: "/admin/",
|
||||
Priority: component.MenuAdmin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
|
||||
|
||||
router := httprouter.New()
|
||||
|
||||
{
|
||||
handler = &httpx.WebSocket{
|
||||
Context: ctx,
|
||||
Fallback: router,
|
||||
Handler: admin.serveSocket,
|
||||
}
|
||||
}
|
||||
|
||||
// add a handler for the index page
|
||||
{
|
||||
index := admin.index(ctx)
|
||||
router.Handler(http.MethodGet, route, index)
|
||||
}
|
||||
|
||||
// add a handler for the user page
|
||||
{
|
||||
users := admin.users(ctx)
|
||||
router.Handler(http.MethodGet, route+"users", users)
|
||||
}
|
||||
|
||||
// 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))
|
||||
router.Handler(http.MethodPost, route+"users/impersonate", admin.usersImpersonateHandler(ctx))
|
||||
router.Handler(http.MethodPost, route+"users/unsetpassword", admin.usersUnsetPasswordHandler(ctx))
|
||||
|
||||
// add a handler for the component page
|
||||
{
|
||||
components := admin.components(ctx)
|
||||
router.Handler(http.MethodGet, route+"components", components)
|
||||
}
|
||||
|
||||
// add a handler for the ingredients page
|
||||
{
|
||||
ingredients := admin.ingredients(ctx)
|
||||
router.Handler(http.MethodGet, route+"ingredients/:slug", ingredients)
|
||||
}
|
||||
|
||||
// add a handler for the instance page
|
||||
{
|
||||
instance := admin.instance(ctx)
|
||||
router.Handler(http.MethodGet, route+"instance/:slug", instance)
|
||||
}
|
||||
|
||||
{
|
||||
grants := admin.grants(ctx)
|
||||
router.Handler(http.MethodGet, route+"grants/:slug", grants)
|
||||
router.Handler(http.MethodPost, route+"grants/", grants) // NOTE(twiesing): This path is intentionally different!
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// get the instance
|
||||
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), r.PostFormValue("slug"))
|
||||
if err != nil {
|
||||
return "", 0, httpx.ErrNotFound
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
85
internal/dis/component/server/admin/components.go
Normal file
85
internal/dis/component/server/admin/components.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
//go:embed "html/components.html"
|
||||
var componentsHTML []byte
|
||||
var componentsTemplate = templates.Parse[componentContext]("components.html", componentsHTML, assets.AssetsAdmin)
|
||||
|
||||
type componentContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
Analytics lazy.PoolAnalytics
|
||||
}
|
||||
|
||||
func (admin *Admin) components(ctx context.Context) http.Handler {
|
||||
tpl := componentsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Components", Path: "/admin/components/"},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandler(func(r *http.Request) (cp componentContext, err error) {
|
||||
cp.Analytics = *admin.Analytics
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
//go:embed "html/ingredients.html"
|
||||
var ingredientsHTML []byte
|
||||
var ingredientsTemplate = templates.Parse[ingredientsContext]("ingredients.html", ingredientsHTML, assets.AssetsAdmin)
|
||||
|
||||
type ingredientsContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
Instance models.Instance
|
||||
Analytics *lazy.PoolAnalytics
|
||||
}
|
||||
|
||||
func (admin *Admin) ingredients(ctx context.Context) http.Handler {
|
||||
tpl := ingredientsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: "* to be updated *"},
|
||||
{Title: "Ingredients", Path: "* to be updated *"},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic ingredientsContext, err error) {
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
|
||||
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
|
||||
gaps.Crumbs[2] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")}
|
||||
|
||||
// find the instance itself!
|
||||
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return ic, httpx.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return ic, err
|
||||
}
|
||||
ic.Instance = instance.Instance
|
||||
|
||||
// and get the components
|
||||
ic.Analytics = instance.Info().Analytics
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
173
internal/dis/component/server/admin/grants.go
Normal file
173
internal/dis/component/server/admin/grants.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
//go:embed "html/grants.html"
|
||||
var grantsHTML []byte
|
||||
var grantsTemplate = templates.Parse[grantsContext]("grants.html", grantsHTML, assets.AssetsAdmin)
|
||||
|
||||
type grantsContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
Error string
|
||||
|
||||
instance *wisski.WissKI
|
||||
Instance models.Instance // current instance
|
||||
|
||||
Grants []models.Grant // grants that exist for the user
|
||||
Usernames []string // unuused distillery usernames
|
||||
Drupals []string // unusued drupal usernames
|
||||
}
|
||||
|
||||
func (admin *Admin) grants(ctx context.Context) http.Handler {
|
||||
tpl := grantsTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: "*to be updated*"},
|
||||
{Title: "Grants", Path: "*to be updated*"},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (grantsContext, error) {
|
||||
if r.Method == http.MethodGet {
|
||||
return admin.getGrants(r, gaps)
|
||||
} else {
|
||||
return admin.postGrants(r, gaps)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (admin *Admin) getGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) {
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
if err := gc.use(r, gaps, slug, admin); err != nil {
|
||||
return gc, err
|
||||
}
|
||||
|
||||
if err := gc.useGrants(r, admin); err != nil {
|
||||
return gc, err
|
||||
}
|
||||
|
||||
return gc, nil
|
||||
}
|
||||
|
||||
func (admin *Admin) postGrants(r *http.Request, gaps *templates.BaseContextGaps) (gc grantsContext, err error) {
|
||||
// parse the form
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return gc, err
|
||||
}
|
||||
|
||||
// read out the form values
|
||||
var (
|
||||
slug = r.PostFormValue("slug")
|
||||
delete = r.PostFormValue("action") == "delete"
|
||||
distilleryUser = r.PostFormValue("distillery-user")
|
||||
drupalUser = r.PostFormValue("drupal-user")
|
||||
adminRole = r.PostFormValue("admin") == field.CheckboxChecked
|
||||
)
|
||||
|
||||
// set the common fields
|
||||
if err := gc.use(r, gaps, slug, admin); err != nil {
|
||||
return gc, err
|
||||
}
|
||||
|
||||
if delete {
|
||||
// delete the user grant
|
||||
err := admin.Dependencies.Policy.Remove(r.Context(), distilleryUser, slug)
|
||||
if err != nil {
|
||||
return gc, err
|
||||
}
|
||||
} else {
|
||||
// update the grant
|
||||
err := admin.Dependencies.Policy.Set(r.Context(), models.Grant{
|
||||
User: distilleryUser,
|
||||
Slug: slug,
|
||||
|
||||
DrupalUsername: drupalUser,
|
||||
DrupalAdminRole: adminRole,
|
||||
})
|
||||
if err != nil {
|
||||
gc.Error = fmt.Sprintf("Unable to update grant for user %s: %s", distilleryUser, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the grants for the instance
|
||||
if err := gc.useGrants(r, admin); err != nil {
|
||||
return gc, err
|
||||
}
|
||||
return gc, nil
|
||||
}
|
||||
|
||||
func (gc *grantsContext) use(r *http.Request, gaps *templates.BaseContextGaps, slug string, admin *Admin) (err error) {
|
||||
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
|
||||
gaps.Crumbs[2] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}
|
||||
|
||||
// find the instance itself
|
||||
gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return httpx.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gc.Instance = gc.instance.Instance
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *grantsContext) useGrants(r *http.Request, admin *Admin) (err error) {
|
||||
gc.Grants, err = admin.Dependencies.Policy.Instance(r.Context(), gc.Instance.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users, err := admin.Dependencies.Auth.Users(r.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create a namemap of users, but not those already taken
|
||||
userNameMap := make(map[string]struct{}, len(users))
|
||||
for _, user := range users {
|
||||
userNameMap[user.User.User] = struct{}{}
|
||||
}
|
||||
for _, grant := range gc.Grants {
|
||||
delete(userNameMap, grant.User)
|
||||
}
|
||||
|
||||
// setup the usernames
|
||||
gc.Usernames = maps.Keys(userNameMap)
|
||||
slices.Sort(gc.Usernames)
|
||||
|
||||
// get the drupal usernames
|
||||
drupals, err := gc.instance.Users().All(r.Context(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and convert them to strings only
|
||||
gc.Drupals = make([]string, len(drupals))
|
||||
for i, drupal := range drupals {
|
||||
gc.Drupals[i] = string(drupal.Name)
|
||||
}
|
||||
slices.Sort(gc.Drupals)
|
||||
|
||||
return nil
|
||||
}
|
||||
6
internal/dis/component/server/admin/html/components.html
Normal file
6
internal/dis/component/server/admin/html/components.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Components{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ template "_anal.html" .Analytics }}
|
||||
{{ end }}
|
||||
127
internal/dis/component/server/admin/html/grants.html
Normal file
127
internal/dis/component/server/admin/html/grants.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}{{ .Instance.Slug }} - Grants{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ $csrf := .CSRF }}
|
||||
{{ $slug := .Instance.Slug }}
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Grants</h2>
|
||||
|
||||
<p>
|
||||
A grant provides access for a specific distillery user to a specific WissKI instance.
|
||||
Only <em>Distillery Administrators</em> can manage grants.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<em>Distillery Users</em> must exist in order to grant them access to a specific instance
|
||||
</li>
|
||||
<li>
|
||||
<em>Drupal Users</em> will be automatically created if they do not exist.
|
||||
</li>
|
||||
<li>
|
||||
If <em>Admin</em> is checked and a user logs in, they will automatically be given the admin role.
|
||||
For security reasons, an admin role is never automatically removed.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ block "form/message" . }}
|
||||
{{ $E := .Error }}
|
||||
{{ if not (eq $E "") }}
|
||||
<div class="pure-form-group">
|
||||
<p class="error-message">
|
||||
{{ $E }}
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered pure-form">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Distillery Username
|
||||
</th>
|
||||
<th>
|
||||
Drupal Username
|
||||
</th>
|
||||
<th>
|
||||
Roles
|
||||
</th>
|
||||
<th>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $id, $grant := .Grants }}
|
||||
<tr>
|
||||
<td>
|
||||
{{ $grant.User }}
|
||||
<input type="hidden" name="distillery-user" value="{{ $grant.User }}" form="update-{{ $id }}">
|
||||
<input type="hidden" name="distillery-user" value="{{ $grant.User }}" form="delete-{{ $id }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="drupal-user" list="drupal-users" value="{{ $grant.DrupalUsername }}" form="update-{{ $id }}">
|
||||
</td>
|
||||
<td>
|
||||
<label for="update={{ $id }}-admin">Admin</label>
|
||||
<input type="checkbox" name="admin" id="update-{{ $id }}-admin" {{ if $grant.DrupalAdminRole }}checked{{end}} form="update-{{ $id }}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="pure-button-group" role="group">
|
||||
<form id="update-{{ $id }}" method="POST" action="/admin/grants/" class="pure-form-group" autocomplete="off">
|
||||
{{ $csrf }}
|
||||
<input type="hidden" name="slug" value="{{ $slug }}">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="submit" class="pure-button" value="Update">
|
||||
</form>
|
||||
<form id="delete-{{ $id }}" method="POST" action="/admin/grants/" class="pure-form-group" autocomplete="off">
|
||||
{{ $csrf }}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="slug" value="{{ $slug }}">
|
||||
<input type="submit" class="pure-button pure-button-danger" value="Delete">
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" name="distillery-user" list="distillery-users" placeholder="Distillery User" form="add-grant">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="drupal-user" list="drupal-users" placeholder="Drupal User" form="add-grant">
|
||||
</td>
|
||||
<td>
|
||||
<label form="add-grant-admin">Admin</label>
|
||||
<input type="checkbox" name="admin" id="add-grant-admin" form="add-grant">
|
||||
</td>
|
||||
<td>
|
||||
<form id="add-grant" method="POST" action="/admin/grants/" class="pure-form-group">
|
||||
{{ $csrf }}
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="slug" value="{{ $slug }}">
|
||||
<input type="submit" class="pure-button" value="Add New">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<datalist id="distillery-users">
|
||||
{{ range $unused, $user := .Usernames }}
|
||||
<option value="{{ $user }}">
|
||||
{{ end }}
|
||||
</datalist>
|
||||
<datalist id="drupal-users">
|
||||
{{ range $unused, $drupal := .Drupals }}
|
||||
<option value="{{ $drupal }}">
|
||||
{{ end }}
|
||||
</datalist>
|
||||
{{ end }}
|
||||
260
internal/dis/component/server/admin/html/index.html
Normal file
260
internal/dis/component/server/admin/html/index.html
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Admin{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Distillery Configuration</h2>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-1-3">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Domains
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Primary
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.DefaultDomain}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Extra
|
||||
</td>
|
||||
<td>
|
||||
{{ range .Config.SelfExtraDomains }}
|
||||
<code>{{.}}</code><br />
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Email <small>(HTTPS)</small>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.CertbotEmail}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-1-3">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Database Settings
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
MySQL User Prefix
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.MysqlUserPrefix}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
MySQL Database Prefix
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.MysqlDatabasePrefix}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
GraphDB User Prefix
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.GraphDBUserPrefix}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
GraphDB Database Prefix
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.GraphDBRepoPrefix}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Bookkeeping Database
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.DistilleryDatabase}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-1-3">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Directory Settings
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>root</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.DeployRoot}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>config</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.ConfigPath}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Misc Settings
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Homepage
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{.Config.SelfRedirect}}" target="_blank" rel="noopener noreferrer">{{.Config.SelfRedirect}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Docker Network Name
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.DockerNetworkName}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Backup Age
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Config.MaxBackupAge}}</code> Day(s)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="backups">Backups</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<table class="pure-table pure-table-bordered padding">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Created</th>
|
||||
<th>Packed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Backups }}
|
||||
<tr>
|
||||
<td>
|
||||
<code class="path">{{ .Path }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{{ .Packed }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<h2 id="instances">Instances</h2>
|
||||
|
||||
<table class="pure-table pure-table-bordered padding">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<th>Running</th>
|
||||
<th>Stopped</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ .TotalCount }}
|
||||
</td>
|
||||
<td>
|
||||
{{ .RunningCount }}
|
||||
</td>
|
||||
<td>
|
||||
{{ .StoppedCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span class="hspace"></span>
|
||||
</div>
|
||||
|
||||
{{range .Instances}}
|
||||
<div class="pure-u-1 pure-u-xl-1-3">
|
||||
<div class="wisski {{ if .Running }}running{{ else }}stopped{{ end }}">
|
||||
<h3>
|
||||
{{.Slug}}
|
||||
{{ if not .Running }} <small>not running</small>{{ end }}
|
||||
</h3>
|
||||
<p>
|
||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a><br>
|
||||
|
||||
<a class="pure-button" href="/admin/instance/{{.Slug}}">Details</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ end }}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}{{ .Instance.Slug }} - Ingredients{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
{{ template "_anal.html" .Analytics }}
|
||||
{{ end }}
|
||||
488
internal/dis/component/server/admin/html/instance.html
Normal file
488
internal/dis/component/server/admin/html/instance.html
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}{{ .Instance.Slug }}{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Info & Status</h2>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Overview
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Slug
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Info.Slug }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
URL
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ .Info.URL }}" target="_blank" rel="noopener noreferrer">{{ .Info.URL }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Running
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Info.Running }}</code>
|
||||
<div class="pure-button-group" role="group">
|
||||
<button class="remote-action pure-button pure-button-action" data-action="start" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
|
||||
(Re)Start
|
||||
</button>
|
||||
<button class="remote-action pure-button pure-button-danger" data-action="stop" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Locked
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Info.Locked }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Component Settings
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Directory
|
||||
</td>
|
||||
<td>
|
||||
<code style="overflow: auto;">{{ .Instance.FilesystemBase }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
SQL DB
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Instance.SqlDatabase }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
SQL User
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Instance.SqlUsername }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
TS Repo
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Instance.GraphDBRepository }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
TS User
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Instance.GraphDBUsername }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Build Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Created
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Instance.Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Last Rebuild <br>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Rebuild</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Last Cron<br>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="cron" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Cron</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Last Update <br>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="update" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Update</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code><br>
|
||||
(Automatic: <code>{{ .Instance.AutoBlindUpdateEnabled }}</code>)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="wisski">Users (Drupal)</h2>
|
||||
<a class="pure-button" href="/admin/grants/{{ .Info.Slug }}">Manage Grants</a>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Active
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Roles
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Last Login
|
||||
</th>
|
||||
<th>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ $slug := .Instance.Slug }}
|
||||
{{ $csrf := .CSRF }}
|
||||
{{ range $index, $user := .Info.Users }}
|
||||
<tr {{ if not $user.Status }}style="color:gray"{{ end }}>
|
||||
<td>
|
||||
<code>{{ $user.UID }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ $user.Status }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ $user.Name }}</code>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ if $user.Mail }}
|
||||
<a href="mailto:{{ $user.Mail }}">{{ $user.Mail }}</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ range $role, $unuused := $user.Roles }}
|
||||
<code>
|
||||
{{ $role }}
|
||||
</code>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ $user.Created.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ $user.Login.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="wisski">WissKI Data</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-1-2">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Pathbuilders
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{ range $name, $xml := .Info.Pathbuilders }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ $name }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="pathbuilder" data-name="{{ $name }}">{{ $xml }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-xl-1-2">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
URI Prefixes
|
||||
|
||||
{{ if .Info.NoPrefixes }}
|
||||
(excluded from resolver)
|
||||
{{ end }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $index, $prefix := .Info.Prefixes }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ $prefix }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="stats">Statistics</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-1-2">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="5">
|
||||
Bundles
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Label
|
||||
</th>
|
||||
<th>
|
||||
Machine Name
|
||||
</th>
|
||||
<th>
|
||||
Count
|
||||
</th>
|
||||
<th>
|
||||
LastEdit
|
||||
</th>
|
||||
<th>
|
||||
MainBundle
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Info.Statistics.Bundles.Bundles }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ .Label }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .MachineName }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Count }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .MainBundle }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-1-2">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
Triplestore
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
URI
|
||||
</th>
|
||||
<th>
|
||||
Count
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Info.Statistics.Triplestore.Graphs }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ .URI }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ .Count }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="ssh">SSH Keys</h2>
|
||||
<table class="pure-table pure-table-bordered padding">
|
||||
<tbody>
|
||||
{{ range .Info.SSHKeys }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ . }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="snapshots">Snapshots</h2>
|
||||
<p>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Take a snapshot</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<table class="pure-table pure-table-bordered padding">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Created</th>
|
||||
<th>Packed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Info.Snapshots }}
|
||||
<tr>
|
||||
<td>
|
||||
<code class="path">{{ .Path }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{{ .Packed }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Dangerous Actions</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<p>
|
||||
Purging this instance completely removes it from the distillery.
|
||||
Backups containing the instance will remain, but it will not be possible to restore it directly.
|
||||
You must enter the slug <code>{{ .Instance.Slug }}</code> to confirm purging.
|
||||
</p>
|
||||
<form class="pure-form">
|
||||
<fieldset>
|
||||
<input type="text" id="purge-confirm-slug" placeholder="{{ .Instance.Slug }}" />
|
||||
<button class="remote-action pure-button pure-button-danger" data-action="purge" data-param="{{ .Instance.Slug }}" data-confirm-param="#purge-confirm-slug" data-buffer="1000" data-force-reload="/admin/">Purge Instance</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{{ template "_form.html" . }}
|
||||
{{ define "form/title" }}Create User{{ end }}
|
||||
{{ define "form/button" }}Create{{ end }}
|
||||
|
||||
109
internal/dis/component/server/admin/html/users.html
Normal file
109
internal/dis/component/server/admin/html/users.html
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{{ template "_base.html" . }}
|
||||
{{ define "title" }}Users{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
|
||||
{{ $E := .Error }}
|
||||
{{ if not (eq $E "") }}
|
||||
<div class="pure-form-group">
|
||||
<p class="error-message">
|
||||
{{ $E }}
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Username
|
||||
</th>
|
||||
<th>
|
||||
Enabled
|
||||
</th>
|
||||
<th>
|
||||
Has Password
|
||||
</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.HasPassword }}
|
||||
</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" autocomplete="new-password">
|
||||
<input type="submit" class="pure-button" value="Update Password">
|
||||
{{ $csrf }}
|
||||
</form>
|
||||
<form action="/admin/users/unsetpassword" method="POST" class="pure-form-group">
|
||||
<input type="hidden" name="user" value="{{ .User.User }}">
|
||||
<input type="submit" class="pure-button" value="Unset 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>
|
||||
<form action="/admin/users/impersonate" 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="Impersonate">
|
||||
{{ $csrf }}
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
107
internal/dis/component/server/admin/index.go
Normal file
107
internal/dis/component/server/admin/index.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Status produces a new observation of the distillery, and a new information of all instances
|
||||
// The information on all instances is passed the given quick flag.
|
||||
func (admin *Admin) Status(ctx context.Context, QuickInformation bool) (target status.Distillery, information []status.WissKI, err error) {
|
||||
var group errgroup.Group
|
||||
|
||||
group.Go(func() error {
|
||||
// list all the instances
|
||||
all, err := admin.Dependencies.Instances.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get all of their info!
|
||||
information = make([]status.WissKI, len(all))
|
||||
for i, instance := range all {
|
||||
{
|
||||
i := i
|
||||
instance := instance
|
||||
|
||||
// store the info for this group!
|
||||
group.Go(func() (err error) {
|
||||
information[i], err = instance.Info().Information(ctx, true)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// gather all the observations
|
||||
flags := component.FetcherFlags{
|
||||
Context: ctx,
|
||||
}
|
||||
for _, o := range admin.Dependencies.Fetchers {
|
||||
o := o
|
||||
group.Go(func() error {
|
||||
return o.Fetch(flags, &target)
|
||||
})
|
||||
}
|
||||
|
||||
// wait for all the fetchers to finish
|
||||
if err := group.Wait(); err != nil {
|
||||
return status.Distillery{}, nil, err
|
||||
}
|
||||
|
||||
// count overall instances
|
||||
for _, i := range information {
|
||||
if i.Running {
|
||||
target.RunningCount++
|
||||
} else {
|
||||
target.StoppedCount++
|
||||
}
|
||||
}
|
||||
target.TotalCount = len(information)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distillery) error {
|
||||
target.Time = time.Now().UTC()
|
||||
target.Config = admin.Config
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed "html/index.html"
|
||||
var indexHTML []byte
|
||||
var indexTemplate = templates.Parse[indexContext]("index.html", indexHTML, assets.AssetsAdmin)
|
||||
|
||||
type indexContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
status.Distillery
|
||||
Instances []status.WissKI
|
||||
}
|
||||
|
||||
func (admin *Admin) index(ctx context.Context) http.Handler {
|
||||
tpl := indexTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
},
|
||||
Actions: []component.MenuItem{
|
||||
{Title: "Users", Path: "/admin/users/"},
|
||||
{Title: "Components", Path: "/admin/components/", Priority: component.SmallButton},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (idx indexContext, err error) {
|
||||
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
|
||||
return
|
||||
})
|
||||
}
|
||||
68
internal/dis/component/server/admin/instance.go
Normal file
68
internal/dis/component/server/admin/instance.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"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/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
//go:embed "html/instance.html"
|
||||
var instanceHTML []byte
|
||||
var instanceTemplate = templates.Parse[instanceContext]("instance.html", instanceHTML, assets.AssetsAdmin)
|
||||
|
||||
type instanceContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
Instance models.Instance
|
||||
Info status.WissKI
|
||||
}
|
||||
|
||||
func (admin *Admin) instance(ctx context.Context) http.Handler {
|
||||
tpl := instanceTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Instance", Path: "*to be replaced*"},
|
||||
},
|
||||
Actions: []component.MenuItem{
|
||||
{Title: "Grants", Path: "*to be replaced*"},
|
||||
{Title: "Ingredients", Path: "*to be replaced*", Priority: component.SmallButton},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandlerWithGaps(func(r *http.Request, gaps *templates.BaseContextGaps) (ic instanceContext, err error) {
|
||||
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
|
||||
|
||||
gaps.Crumbs[1] = component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}
|
||||
|
||||
gaps.Actions[0] = component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}
|
||||
gaps.Actions[1] = component.MenuItem{Title: "Ingredients", Path: template.URL("/admin/ingredients/" + slug), Priority: component.SmallButton}
|
||||
|
||||
// find the instance itself!
|
||||
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
return ic, httpx.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return ic, err
|
||||
}
|
||||
ic.Instance = instance.Instance
|
||||
|
||||
// get some more info about the wisski
|
||||
ic.Info, err = instance.Info().Information(r.Context(), false)
|
||||
if err != nil {
|
||||
return ic, err
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
153
internal/dis/component/server/admin/socket.go
Normal file
153
internal/dis/component/server/admin/socket.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||
"github.com/tkw1536/goprogram/status"
|
||||
)
|
||||
|
||||
type InstanceAction struct {
|
||||
NumParams int
|
||||
|
||||
HandleInteractive func(ctx context.Context, info *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error
|
||||
HandleResult func(ctx context.Context, info *Admin, instance *wisski.WissKI, params ...string) (value any, err error)
|
||||
}
|
||||
|
||||
var socketInstanceActions = map[string]InstanceAction{
|
||||
"snapshot": {
|
||||
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return admin.Dependencies.Exporter.MakeExport(
|
||||
ctx,
|
||||
out,
|
||||
exporter.ExportTask{
|
||||
Dest: "",
|
||||
Instance: instance,
|
||||
|
||||
StagingOnly: false,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
"rebuild": {
|
||||
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return instance.Barrel().Build(ctx, out, true)
|
||||
},
|
||||
},
|
||||
"update": {
|
||||
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return instance.Drush().Update(ctx, out)
|
||||
},
|
||||
},
|
||||
"cron": {
|
||||
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, str io.Writer, params ...string) error {
|
||||
return instance.Drush().Cron(ctx, str)
|
||||
},
|
||||
},
|
||||
"start": {
|
||||
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return instance.Barrel().Stack().Up(ctx, out)
|
||||
},
|
||||
},
|
||||
"stop": {
|
||||
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return instance.Barrel().Stack().Down(ctx, out)
|
||||
},
|
||||
},
|
||||
"purge": {
|
||||
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return admin.Dependencies.Purger.Purge(ctx, out, instance.Slug)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (admin *Admin) serveSocket(conn httpx.WebSocketConnection) {
|
||||
// read the next message to act on
|
||||
message, ok := <-conn.Read()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// perform an action if it exists!
|
||||
if action, ok := socketInstanceActions[string(message.Bytes)]; ok {
|
||||
admin.handleInstanceAction(conn, action)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var instanceParamsTimeout = time.Second
|
||||
|
||||
func (admin *Admin) handleInstanceAction(conn httpx.WebSocketConnection, action InstanceAction) {
|
||||
|
||||
// read the slug
|
||||
slug, ok := <-conn.Read()
|
||||
if !ok {
|
||||
<-conn.WriteText("Error reading slug")
|
||||
return
|
||||
}
|
||||
|
||||
// resolve the instance
|
||||
instance, err := admin.Dependencies.Instances.WissKI(conn.Context(), string(slug.Bytes))
|
||||
if err != nil {
|
||||
<-conn.WriteText("Instance not found")
|
||||
return
|
||||
}
|
||||
|
||||
// read the parameters
|
||||
params := make([]string, action.NumParams)
|
||||
for i := range params {
|
||||
select {
|
||||
case message, ok := <-conn.Read():
|
||||
if !ok {
|
||||
<-conn.WriteText("Insufficient parameters")
|
||||
return
|
||||
}
|
||||
params[i] = string(message.Bytes)
|
||||
case <-time.After(instanceParamsTimeout):
|
||||
<-conn.WriteText("Timed out reading parameters")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// build a stream
|
||||
writer := &status.LineBuffer{
|
||||
Line: func(line string) {
|
||||
<-conn.WriteText(line)
|
||||
},
|
||||
FlushLineOnClose: true,
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
// handle the interactive action
|
||||
if action.HandleInteractive != nil {
|
||||
err := action.HandleInteractive(conn.Context(), admin, instance, writer, params...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(writer, err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(writer, "done")
|
||||
}
|
||||
|
||||
// handle the result computation
|
||||
if action.HandleResult != nil {
|
||||
result, err := action.HandleResult(conn.Context(), admin, instance, params...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(writer, "false")
|
||||
return
|
||||
}
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
fmt.Fprintln(writer, "false")
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(writer, "true")
|
||||
fmt.Fprintln(writer, data)
|
||||
}
|
||||
|
||||
}
|
||||
251
internal/dis/component/server/admin/users.go
Normal file
251
internal/dis/component/server/admin/users.go
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
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/server/assets"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templates"
|
||||
"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 usersHTML []byte
|
||||
var usersTemplate = templates.Parse[usersContext]("user.html", usersHTML, assets.AssetsAdmin)
|
||||
|
||||
type usersContext struct {
|
||||
templates.BaseContext
|
||||
|
||||
Error string
|
||||
Users []*auth.AuthUser
|
||||
}
|
||||
|
||||
func (admin *Admin) users(ctx context.Context) http.Handler {
|
||||
tpl := usersTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Users", Path: "/admin/users/"},
|
||||
},
|
||||
Actions: []component.MenuItem{
|
||||
{Title: "Create New", Path: "/admin/users/create/"},
|
||||
},
|
||||
})
|
||||
|
||||
return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) {
|
||||
uc.Error = r.URL.Query().Get("error")
|
||||
uc.Users, err = admin.Dependencies.Auth.Users(r.Context())
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
//go:embed "html/user_create.html"
|
||||
var userCreateHTML []byte
|
||||
var userCreateTemplate = templates.ParseForm("user_create.html", userCreateHTML, assets.AssetsAdmin)
|
||||
|
||||
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 {
|
||||
tpl := userCreateTemplate.Prepare(admin.Dependencies.Templating, templates.BaseContextGaps{
|
||||
Crumbs: []component.MenuItem{
|
||||
{Title: "Admin", Path: "/admin/"},
|
||||
{Title: "Users", Path: "/admin/users"},
|
||||
{Title: "Create", 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: tpl.Template(),
|
||||
RenderTemplateContext: templates.FormTemplateContext(tpl),
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
func (admin *Admin) usersUnsetPasswordHandler(ctx context.Context) http.Handler {
|
||||
return admin.useraction(ctx, "unset password", func(r *http.Request, user *auth.AuthUser) error {
|
||||
user.PasswordHash = nil
|
||||
return user.Save(r.Context())
|
||||
})
|
||||
}
|
||||
|
||||
func (admin *Admin) usersImpersonateHandler(ctx context.Context) 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", "impersonate").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", "impersonate").Msg("failed to get user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// login the user into the session of the provided user
|
||||
if err := admin.Dependencies.Auth.Login(w, r, user); err != nil {
|
||||
logger.Err(err).Str("action", "impersonate").Msg("failed to login user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// and go there
|
||||
http.Redirect(w, r, "/user/", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue