diff --git a/internal/dis/component/auth/panel/templates/token_created.html b/internal/dis/component/auth/panel/templates/token_created.html new file mode 100644 index 0000000..43a0f85 --- /dev/null +++ b/internal/dis/component/auth/panel/templates/token_created.html @@ -0,0 +1,30 @@ +
+

+ A new token has been created. + This token will only be shown once. + Please make sure to copy it now. +

+
+ +
+

+ {{ .Token.Token }} +

+
+ +
+

+ To test this token, you can use curl on the command line: +

+ + curl -H "Authorization: Bearer {{ .Token.Token }}" {{ .Domain }}api/v1/auth + +

+ You should receive a response with token: true inside of it. +

+
+ +
+ Back To Token List +
+ diff --git a/internal/dis/component/auth/panel/templates/tokens.html b/internal/dis/component/auth/panel/templates/tokens.html index 6c4172c..405444b 100644 --- a/internal/dis/component/auth/panel/templates/tokens.html +++ b/internal/dis/component/auth/panel/templates/tokens.html @@ -4,7 +4,7 @@

-
+

My Tokens

This table shows tokens currently associated with your account. @@ -16,6 +16,9 @@ + @@ -32,7 +35,10 @@ {{ range .Tokens }} +
+ ID + Token
- {{ .Token }} + {{ .TokenID }} + + (only shown once) {{ .Description }} @@ -40,7 +46,7 @@
- + {{ $csrf }}
@@ -54,3 +60,14 @@
+
+

+ To check if a token is working, you can use something like: +

+ + curl -H "Authorization: Bearer <token>" {{ .Domain }}api/v1/auth + +

+ When using a working token, you should get a response with Token: true in it. +

+
\ No newline at end of file diff --git a/internal/dis/component/auth/panel/tokens.go b/internal/dis/component/auth/panel/tokens.go index e32f0c5..5481577 100644 --- a/internal/dis/component/auth/panel/tokens.go +++ b/internal/dis/component/auth/panel/tokens.go @@ -2,6 +2,7 @@ package panel import ( "context" + "html/template" "net/http" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" @@ -27,6 +28,7 @@ var tokensTemplate = templating.Parse[TokenTemplateContext]( type TokenTemplateContext struct { templating.RuntimeFlags + Domain template.URL // server base URL Tokens []models.Token } @@ -49,6 +51,8 @@ func (panel *UserPanel) tokensRoute(ctx context.Context) http.Handler { return tc, err } + tc.Domain = template.URL(panel.Config.HTTP.JoinPath().String()) + // get the tokens tc.Tokens, err = panel.Dependencies.Tokens.Tokens(r.Context(), user.User.User) return tc, err @@ -70,14 +74,14 @@ func (panel *UserPanel) tokensDeleteRoute(ctx context.Context) http.Handler { return } - token := r.PostFormValue("token") - if token == "" { + id := r.PostFormValue("id") + if id == "" { 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 { + 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") httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) return @@ -101,8 +105,32 @@ type addTokenResult struct { 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 { - tpl := tokensAddTemplate.Prepare( + tplForm := tokensAddTemplate.Prepare( + panel.Dependencies.Templating, + templating.Crumbs( + menuUser, + menuTokens, + menuTokensAdd, + ), + ) + + tplDone := tokenCreateTemplate.Prepare( panel.Dependencies.Templating, templating.Crumbs( menuUser, @@ -117,8 +145,8 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler { }, FieldTemplate: field.PureCSSFieldTemplate, - RenderTemplate: tpl.Template(), - RenderTemplateContext: templating.FormTemplateContext(tpl), + RenderTemplate: tplForm.Template(), + RenderTemplateContext: templating.FormTemplateContext(tplForm), Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) { 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 { // 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 { 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 + + // render the created context + return httpx.WriteHTML(tplDone.Context(r, TokenCreateContext{ + Domain: template.URL(panel.Config.HTTP.JoinPath().String()), + Token: tok, + }), nil, tplDone.Template(), "", w, r) }, } } diff --git a/internal/dis/component/auth/panel/user.go b/internal/dis/component/auth/panel/user.go index 017db1f..cceb7c2 100644 --- a/internal/dis/component/auth/panel/user.go +++ b/internal/dis/component/auth/panel/user.go @@ -41,18 +41,22 @@ func (g GrantWithURL) AdminURL() template.URL { } 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( panel.Dependencies.Templating, templating.Crumbs( menuUser, ), - templating.Actions( - menuChangePassword, - menuTOTPAction, - menuSSH, - menuTokens, - ), + templating.Actions(actions...), ) return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) { diff --git a/internal/dis/component/auth/tokens/tokens.go b/internal/dis/component/auth/tokens/tokens.go index b77f8e2..52ca676 100644 --- a/internal/dis/component/auth/tokens/tokens.go +++ b/internal/dis/component/auth/tokens/tokens.go @@ -113,7 +113,16 @@ func (tok *Tokens) Add(ctx context.Context, user string, description string, sco } 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 mk.Token, err = NewToken() 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 -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 table, err := tok.table(ctx) if err != nil { @@ -144,5 +153,5 @@ func (tok *Tokens) Remove(ctx context.Context, user, token string) error { } // 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 } diff --git a/internal/models/token.go b/internal/models/token.go index 9c50b22..5123437 100644 --- a/internal/models/token.go +++ b/internal/models/token.go @@ -11,8 +11,10 @@ const TokensTable = "tokens" type Token struct { Pk uint `gorm:"column:pk;primaryKey"` - Token string `gorm:"column:token;unique:true;not null"` - User string `gorm:"column:user;not null"` // (distillery) username + 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 Description string `gorm:"column:description"`