WIPL tokens
This commit is contained in:
parent
c09c729157
commit
161e08fe1f
25 changed files with 716 additions and 63 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/tokens"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
|
||||
|
|
@ -24,6 +25,7 @@ type UserPanel struct {
|
|||
Auth *auth.Auth
|
||||
Templating *templating.Templating
|
||||
Policy *policy.Policy
|
||||
Tokens *tokens.Tokens
|
||||
Instances *instances.Instances
|
||||
Next *next.Next
|
||||
Keys *sshkeys.SSHKeys
|
||||
|
|
@ -40,14 +42,14 @@ func (panel *UserPanel) Routes() component.Routes {
|
|||
return component.Routes{
|
||||
Prefix: "/user/",
|
||||
CSRF: true,
|
||||
Decorator: panel.Dependencies.Auth.Require(scopes.ScopeUserLoggedIn, nil),
|
||||
Decorator: panel.Dependencies.Auth.Require(false, scopes.ScopeUserLoggedIn, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (panel *UserPanel) Menu(r *http.Request) []component.MenuItem {
|
||||
title := "Login"
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if user != nil && err == nil {
|
||||
title = user.User.User
|
||||
}
|
||||
|
|
@ -62,6 +64,9 @@ var (
|
|||
menuSSH = component.MenuItem{Title: "SSH Keys", Path: "/user/ssh/"}
|
||||
menuSSHAdd = component.MenuItem{Title: "Add New Key", Path: "/user/ssh/add/"}
|
||||
|
||||
menuTokens = component.MenuItem{Title: "Tokens", Path: "/user/tokens/"}
|
||||
menuTokensAdd = component.MenuItem{Title: "Add New Token", Path: "/user/tokens/add/"}
|
||||
|
||||
menuTOTPAction = component.DummyMenuItem()
|
||||
menuTOTPDisable = component.MenuItem{Title: "Disable Passcode (TOTP)", Path: "/user/totp/disable/"}
|
||||
menuTOTPEnable = component.MenuItem{Title: "Enable Passcode (TOTP)", Path: "/user/totp/enable/"}
|
||||
|
|
@ -115,8 +120,24 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
|
|||
router.Handler(http.MethodPost, route+"ssh/delete", delete)
|
||||
}
|
||||
|
||||
{
|
||||
tokens := panel.tokensRoute(ctx)
|
||||
router.Handler(http.MethodGet, route+"tokens", tokens)
|
||||
}
|
||||
|
||||
{
|
||||
add := panel.tokensAddRoute(ctx)
|
||||
router.Handler(http.MethodGet, route+"tokens/add", add)
|
||||
router.Handler(http.MethodPost, route+"tokens/add", add)
|
||||
}
|
||||
|
||||
{
|
||||
delete := panel.tokensDeleteRoute(ctx)
|
||||
router.Handler(http.MethodPost, route+"tokens/delete", delete)
|
||||
}
|
||||
|
||||
// ensure that the user is logged in!
|
||||
return panel.Dependencies.Auth.Protect(router, scopes.ScopeUserLoggedIn, nil), nil
|
||||
return panel.Dependencies.Auth.Protect(router, false, scopes.ScopeUserLoggedIn, nil), nil
|
||||
}
|
||||
|
||||
type userFormContext struct {
|
||||
|
|
@ -137,7 +158,7 @@ func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext
|
|||
|
||||
return func(ctx httpx.FormContext, r *http.Request) any {
|
||||
uctx := userFormContext{FormContext: ctx}
|
||||
if user, err := panel.Dependencies.Auth.UserOf(r); err == nil {
|
||||
if user, err := panel.Dependencies.Auth.UserOfSession(r); err == nil {
|
||||
uctx.User = &user.User
|
||||
}
|
||||
return tpl.Context(r, uctx, funcs...)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
|
|||
return struct{}{}, errPasswordsNotIdentical
|
||||
}
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
|
|||
)
|
||||
|
||||
return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
|
@ -89,6 +89,7 @@ var (
|
|||
errInvalidUser = errors.New("invalid user")
|
||||
errKeyParse = errors.New("unable to parse ssh key")
|
||||
errAddKey = errors.New("unable to add key")
|
||||
errAddToken = errors.New("unable to add token")
|
||||
)
|
||||
|
||||
func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
|
||||
|
|
@ -99,7 +100,7 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
|
|||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
logger.Err(err).Str("action", "delete ssh key").Msg("failed to get current user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
|
|
@ -119,7 +120,7 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/user/ssh/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, string(menuSSH.Path), http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
|
|||
RenderTemplateContext: templating.FormTemplateContext(tpl),
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) {
|
||||
ak.User, err = panel.Dependencies.Auth.UserOf(r)
|
||||
ak.User, err = panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil || ak.User == nil {
|
||||
return ak, errInvalidUser
|
||||
}
|
||||
|
|
|
|||
56
internal/dis/component/auth/panel/templates/tokens.html
Normal file
56
internal/dis/component/auth/panel/templates/tokens.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<div class="pure-u-1">
|
||||
<p>
|
||||
This page allows you to add, view and remove tokens from your distillery account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<h2>My Tokens</h2>
|
||||
<p>
|
||||
This table shows tokens currently associated with your account.
|
||||
Tokens can be used to access the API programatically.
|
||||
</p>
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Token
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ $csrf := .CSRF }}
|
||||
{{ range .Tokens }}
|
||||
<tr>
|
||||
<td>
|
||||
<code class="copy">{{ .Token }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{{ .Description }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="pure-button-group" role="group">
|
||||
<form action="/user/tokens/delete" method="POST" class="pure-form-group">
|
||||
<input type="hidden" name="token" value="{{ .Token }}">
|
||||
<input type="submit" class="pure-button pure-button-danger" value="Delete">
|
||||
{{ $csrf }}
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{{ template "form.html" . }}
|
||||
{{ define "form/button" }}Add{{ end }}
|
||||
{{ define "form/inside" }}
|
||||
<div>
|
||||
<p>
|
||||
Use this form to add a new <em>Token</em> to your account.
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
153
internal/dis/component/auth/panel/tokens.go
Normal file
153
internal/dis/component/auth/panel/tokens.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package panel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"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/templating"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/tkw1536/pkglib/httpx"
|
||||
"github.com/tkw1536/pkglib/httpx/field"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed "templates/tokens.html"
|
||||
var tokensHTML []byte
|
||||
var tokensTemplate = templating.Parse[TokenTemplateContext](
|
||||
"tokens.html", tokensHTML, nil,
|
||||
|
||||
templating.Title("Tokens"),
|
||||
templating.Assets(assets.AssetsUser),
|
||||
)
|
||||
|
||||
type TokenTemplateContext struct {
|
||||
templating.RuntimeFlags
|
||||
|
||||
Tokens []models.Token
|
||||
}
|
||||
|
||||
func (panel *UserPanel) tokensRoute(ctx context.Context) http.Handler {
|
||||
tpl := tokensTemplate.Prepare(
|
||||
panel.Dependencies.Templating,
|
||||
templating.Crumbs(
|
||||
menuUser,
|
||||
menuTokens,
|
||||
),
|
||||
templating.Actions(
|
||||
menuTokensAdd,
|
||||
),
|
||||
)
|
||||
|
||||
return tpl.HTMLHandler(func(r *http.Request) (tc TokenTemplateContext, err error) {
|
||||
// list the user
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil || user == nil {
|
||||
return tc, err
|
||||
}
|
||||
|
||||
// get the tokens
|
||||
tc.Tokens, err = panel.Dependencies.Tokens.Tokens(r.Context(), user.User.User)
|
||||
return tc, err
|
||||
})
|
||||
}
|
||||
|
||||
func (panel *UserPanel) tokensDeleteRoute(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", "delete token").Msg("failed to parse form")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
logger.Err(err).Str("action", "delete token").Msg("failed to get current user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token := r.PostFormValue("token")
|
||||
if token == "" {
|
||||
logger.Err(err).Str("action", "delete token").Msg("failed to get token")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := panel.Dependencies.Tokens.Remove(r.Context(), user.User.User, token); err != nil {
|
||||
logger.Err(err).Str("action", "delete token").Msg("failed to delete token")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, string(menuTokens.Path), http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
//go:embed "templates/tokens_add.html"
|
||||
var tokensAddHTML []byte
|
||||
var tokensAddTemplate = templating.ParseForm(
|
||||
"tokens_add.html", tokensAddHTML, httpx.FormTemplate,
|
||||
templating.Title("Add Token"),
|
||||
templating.Assets(assets.AssetsUser),
|
||||
)
|
||||
|
||||
type addTokenResult struct {
|
||||
User *auth.AuthUser
|
||||
Description string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
|
||||
tpl := tokensAddTemplate.Prepare(
|
||||
panel.Dependencies.Templating,
|
||||
templating.Crumbs(
|
||||
menuUser,
|
||||
menuTokens,
|
||||
menuTokensAdd,
|
||||
),
|
||||
)
|
||||
|
||||
return &httpx.Form[addTokenResult]{
|
||||
Fields: []field.Field{
|
||||
{Name: "description", Type: field.Text, Label: "Description"},
|
||||
},
|
||||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
RenderTemplate: tpl.Template(),
|
||||
RenderTemplateContext: templating.FormTemplateContext(tpl),
|
||||
|
||||
Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) {
|
||||
at.User, err = panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil || at.User == nil {
|
||||
return at, errInvalidUser
|
||||
}
|
||||
|
||||
at.Description = values["description"]
|
||||
if at.Description == "" {
|
||||
at.Description = "API Key"
|
||||
}
|
||||
|
||||
at.Scopes = nil
|
||||
|
||||
return at, nil
|
||||
},
|
||||
|
||||
RenderSuccess: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
|
||||
// add the key to the user
|
||||
_, err := panel.Dependencies.Tokens.Add(r.Context(), at.User.User.User, at.Description, at.Scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return errAddToken
|
||||
}
|
||||
// everything went fine, redirect the user back to the user page!
|
||||
http.Redirect(w, r, string(menuTokens.Path), http.StatusSeeOther)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
|
|||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
|
||||
},
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
|
|||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
password := values["password"]
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
}
|
||||
|
|
@ -105,11 +105,11 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
|
|||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
|
||||
},
|
||||
RenderTemplateContext: func(context httpx.FormContext, r *http.Request) any {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
|
||||
ctx := totpEnrollContext{
|
||||
userFormContext: userFormContext{
|
||||
|
|
@ -136,7 +136,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
|
|||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
password, otp := values["password"], values["otp"]
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
|
|||
FieldTemplate: field.PureCSSFieldTemplate,
|
||||
|
||||
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
|
||||
},
|
||||
RenderTemplate: tpl.Template(),
|
||||
|
|
@ -193,7 +193,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
|
|||
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
|
||||
password, otp := values["password"], values["otp"]
|
||||
|
||||
user, err := panel.Dependencies.Auth.UserOf(r)
|
||||
user, err := panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
return struct{}{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,12 +51,13 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
|
|||
menuChangePassword,
|
||||
menuTOTPAction,
|
||||
menuSSH,
|
||||
menuTokens,
|
||||
),
|
||||
)
|
||||
|
||||
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {
|
||||
// find the user
|
||||
uc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
|
||||
uc.AuthUser, err = panel.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil || uc.AuthUser == nil {
|
||||
return uc, nil, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue