Tokens: User improvements

This commit is contained in:
Tom 2023-06-29 09:44:04 +02:00
parent 8ccd490bed
commit 4f4fa2b3d7
6 changed files with 117 additions and 24 deletions

View file

@ -0,0 +1,30 @@
<div class="pure-u-1">
<p>
A new token has been created.
This token will only be shown once.
Please make sure to copy it now.
</p>
</div>
<div class="pure-u-1">
<p>
<span class="copy">{{ .Token.Token }}</span>
</p>
</div>
<div class="pure-u-1">
<p>
To test this token, you can use curl on the command line:
</p>
<code class="copy">
curl -H "Authorization: Bearer <b>{{ .Token.Token }}</b>" {{ .Domain }}api/v1/auth
</code>
<p>
You should receive a response with <code>token: true</code> inside of it.
</p>
</div>
<div class="pure-u-1">
<a href="/user/tokens/" class="pure-button">Back To Token List</a>
</div>

View file

@ -4,7 +4,7 @@
</p> </p>
</div> </div>
<div class="pure-u-1"> <div class="pure-u-2-3">
<h2>My Tokens</h2> <h2>My Tokens</h2>
<p> <p>
This table shows tokens currently associated with your account. This table shows tokens currently associated with your account.
@ -16,6 +16,9 @@
<table class="pure-table pure-table-bordered"> <table class="pure-table pure-table-bordered">
<thead> <thead>
<tr> <tr>
<th>
ID
</th>
<th> <th>
Token Token
</th> </th>
@ -32,7 +35,10 @@
{{ range .Tokens }} {{ range .Tokens }}
<tr> <tr>
<td> <td>
<code class="copy">{{ .Token }}</code> <code class="copy">{{ .TokenID }}</code>
</td>
<td>
(only shown once)
</td> </td>
<td> <td>
{{ .Description }} {{ .Description }}
@ -40,7 +46,7 @@
<td> <td>
<div class="pure-button-group" role="group"> <div class="pure-button-group" role="group">
<form action="/user/tokens/delete" method="POST" class="pure-form-group"> <form action="/user/tokens/delete" method="POST" class="pure-form-group">
<input type="hidden" name="token" value="{{ .Token }}"> <input type="hidden" name="id" value="{{ .TokenID }}">
<input type="submit" class="pure-button pure-button-danger" value="Delete"> <input type="submit" class="pure-button pure-button-danger" value="Delete">
{{ $csrf }} {{ $csrf }}
</form> </form>
@ -54,3 +60,14 @@
</div> </div>
</div> </div>
<div class="pure-u-1-3">
<p>
To check if a token is working, you can use something like:
</p>
<code class="copy">
curl -H "Authorization: Bearer <b>&lt;token&gt;</b>" {{ .Domain }}api/v1/auth
</code>
<p>
When using a working token, you should get a response with <code>Token: true</code> in it.
</p>
</div>

View file

@ -2,6 +2,7 @@ package panel
import ( import (
"context" "context"
"html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
@ -27,6 +28,7 @@ var tokensTemplate = templating.Parse[TokenTemplateContext](
type TokenTemplateContext struct { type TokenTemplateContext struct {
templating.RuntimeFlags templating.RuntimeFlags
Domain template.URL // server base URL
Tokens []models.Token Tokens []models.Token
} }
@ -49,6 +51,8 @@ func (panel *UserPanel) tokensRoute(ctx context.Context) http.Handler {
return tc, err return tc, err
} }
tc.Domain = template.URL(panel.Config.HTTP.JoinPath().String())
// get the tokens // get the tokens
tc.Tokens, err = panel.Dependencies.Tokens.Tokens(r.Context(), user.User.User) tc.Tokens, err = panel.Dependencies.Tokens.Tokens(r.Context(), user.User.User)
return tc, err return tc, err
@ -70,14 +74,14 @@ func (panel *UserPanel) tokensDeleteRoute(ctx context.Context) http.Handler {
return return
} }
token := r.PostFormValue("token") id := r.PostFormValue("id")
if token == "" { if id == "" {
logger.Err(err).Str("action", "delete token").Msg("failed to get token") logger.Err(err).Str("action", "delete token").Msg("failed to get token")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return return
} }
if err := panel.Dependencies.Tokens.Remove(r.Context(), user.User.User, token); err != nil { if err := panel.Dependencies.Tokens.Remove(r.Context(), user.User.User, id); err != nil {
logger.Err(err).Str("action", "delete token").Msg("failed to delete token") logger.Err(err).Str("action", "delete token").Msg("failed to delete token")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return return
@ -101,8 +105,32 @@ type addTokenResult struct {
Scopes []string Scopes []string
} }
//go:embed "templates/token_created.html"
var tokenCreatedHTML []byte
var tokenCreateTemplate = templating.Parse[TokenCreateContext](
"token_created.html", tokenCreatedHTML, httpx.FormTemplate,
templating.Title("Add Token"),
templating.Assets(assets.AssetsUser),
)
type TokenCreateContext struct {
templating.RuntimeFlags
Domain template.URL // server base URL
Token *models.Token
}
func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler { func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
tpl := tokensAddTemplate.Prepare( tplForm := tokensAddTemplate.Prepare(
panel.Dependencies.Templating,
templating.Crumbs(
menuUser,
menuTokens,
menuTokensAdd,
),
)
tplDone := tokenCreateTemplate.Prepare(
panel.Dependencies.Templating, panel.Dependencies.Templating,
templating.Crumbs( templating.Crumbs(
menuUser, menuUser,
@ -117,8 +145,8 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
}, },
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(), RenderTemplate: tplForm.Template(),
RenderTemplateContext: templating.FormTemplateContext(tpl), RenderTemplateContext: templating.FormTemplateContext(tplForm),
Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) { Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) {
at.User, err = panel.Dependencies.Auth.UserOfSession(r) at.User, err = panel.Dependencies.Auth.UserOfSession(r)
@ -138,16 +166,19 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
RenderSuccess: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error { RenderSuccess: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// add the key to the user // add the key to the user
_, err := panel.Dependencies.Tokens.Add(r.Context(), at.User.User.User, at.Description, at.Scopes) tok, err := panel.Dependencies.Tokens.Add(r.Context(), at.User.User.User, at.Description, at.Scopes)
if err != nil { if err != nil {
return err return err
} }
if err != nil { if err != nil {
return errAddToken return errAddToken
} }
// everything went fine, redirect the user back to the user page!
http.Redirect(w, r, string(menuTokens.Path), http.StatusSeeOther) // render the created context
return nil return httpx.WriteHTML(tplDone.Context(r, TokenCreateContext{
Domain: template.URL(panel.Config.HTTP.JoinPath().String()),
Token: tok,
}), nil, tplDone.Template(), "", w, r)
}, },
} }
} }

View file

@ -41,18 +41,22 @@ func (g GrantWithURL) AdminURL() template.URL {
} }
func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
actions := []component.MenuItem{
menuChangePassword,
menuTOTPAction,
menuSSH,
}
if panel.Config.HTTP.API.Value {
actions = append(actions, menuTokens)
}
tpl := userTemplate.Prepare( tpl := userTemplate.Prepare(
panel.Dependencies.Templating, panel.Dependencies.Templating,
templating.Crumbs( templating.Crumbs(
menuUser, menuUser,
), ),
templating.Actions( templating.Actions(actions...),
menuChangePassword,
menuTOTPAction,
menuSSH,
menuTokens,
),
) )
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) { return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {

View file

@ -113,7 +113,16 @@ func (tok *Tokens) Add(ctx context.Context, user string, description string, sco
} }
mk.SetScopes(scopes) mk.SetScopes(scopes)
// generate a new random password // generate a new id for the token
{
var err error
mk.TokenID, err = NewToken()
if err != nil {
return nil, err
}
}
// generate the actual token
var err error var err error
mk.Token, err = NewToken() mk.Token, err = NewToken()
if err != nil { if err != nil {
@ -136,7 +145,7 @@ func (tok *Tokens) Add(ctx context.Context, user string, description string, sco
} }
// Remove removes a token with the given token from the user // Remove removes a token with the given token from the user
func (tok *Tokens) Remove(ctx context.Context, user, token string) error { func (tok *Tokens) Remove(ctx context.Context, user, id string) error {
// get the table // get the table
table, err := tok.table(ctx) table, err := tok.table(ctx)
if err != nil { if err != nil {
@ -144,5 +153,5 @@ func (tok *Tokens) Remove(ctx context.Context, user, token string) error {
} }
// and do the delete // and do the delete
return table.Where("user = ? AND token = ?", user, token).Delete(&models.Token{}).Error return table.Where("user = ? AND id = ?", user, id).Delete(&models.Token{}).Error
} }

View file

@ -11,7 +11,9 @@ const TokensTable = "tokens"
type Token struct { type Token struct {
Pk uint `gorm:"column:pk;primaryKey"` Pk uint `gorm:"column:pk;primaryKey"`
Token string `gorm:"column:token;unique:true;not null"` Token string `json:"-" gorm:"column:token;unique:true;not null"` // token used by the actual api (shown only once)
TokenID string `gorm:"column:id;unique:true;not null"` // token id (displayed to user, used for finding it)
User string `gorm:"column:user;not null"` // (distillery) username User string `gorm:"column:user;not null"` // (distillery) username
Description string `gorm:"column:description"` Description string `gorm:"column:description"`