# HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1718274359 -7200 # Thu Jun 13 12:25:59 2024 +0200 # Node ID 1cc6783208a506f3ec6af8e1ccb51fccf7c586e3 # Parent 2f2261ad2e7bc910b49e03ab5713de767ea27818 # EXP-Topic godox WIP add godox linter warns about TODOs and FIXMEs diff --git a/.golangci.yml b/.golangci.yml --- a/.golangci.yml +++ b/.golangci.yml @@ -57,7 +57,7 @@ - gocritic # Provides diagnostics that check for bugs, performance and style issues. style, metalinter v1.12.0 # - gocyclo # Computes and checks the cyclomatic complexity of functions complexity v1.0.0 - godot # Check if comments end in a period style, comment ✔ v1.25.0 - # - godox # Tool for detection of FIXME, TODO and other comment keywords style, comment v1.19.0 + - godox # Tool for detection of FIXME, TODO and other comment keywords style, comment v1.19.0 - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification format ✔ v1.0.0 - gofumpt # Gofumpt checks whether code was gofumpt-ed. format ✔ v1.28.0 - goheader # Checks is file header matches to pattern style v1.28.0 diff --git a/auth/auth.go b/auth/auth.go --- a/auth/auth.go +++ b/auth/auth.go @@ -93,6 +93,10 @@ cacheMissCollector prometheus.Counter } +func (auth *Auth) Expiration() time.Duration { + return auth.expiration +} + // NewAuth creates a Auth instance. func NewAuth(ctx context.Context, options TokenOptions, db *sqlx.DB, log zerolog.Logger) *Auth { if options.CacheMaxSize == 0 { diff --git a/restapi/auth_middleware.go b/restapi/auth_middleware.go --- a/restapi/auth_middleware.go +++ b/restapi/auth_middleware.go @@ -49,8 +49,7 @@ log.Err(err).Msg("failed to read the rednerd-auth cookie") } else { principal = pr - // TODO get the duration from the token options - if time.Until(time.Unix(principal.StandardClaims.ExpiresAt, 0)) < time.Minute*30 { + if time.Until(time.Unix(principal.StandardClaims.ExpiresAt, 0)) < auth.Expiration()*4/5 { _, cookie, err := auth.MakeToken(*principal, true) if err != nil { log.Err(err).Msg("failed to create the rednerd-auth cookie") diff --git a/restapi/configure_rednerd.go b/restapi/configure_rednerd.go --- a/restapi/configure_rednerd.go +++ b/restapi/configure_rednerd.go @@ -120,7 +120,7 @@ api.TemplateManagementTemplateVarlistHandler = handlers.NewTemplateVarlistHandler( config.DB, config.TemplateRenderingEngineRegistry) - api.RenderingRenderHandler = handlers.NewRenderHandler(renderer) + api.RenderingRenderHandler = handlers.NewRenderHandler(renderer, config.DB) api.PreServerShutdown = func() {} diff --git a/restapi/handlers/account-template-list.go b/restapi/handlers/account-template-list.go --- a/restapi/handlers/account-template-list.go +++ b/restapi/handlers/account-template-list.go @@ -80,7 +80,8 @@ NewAccountTemplateAddUnauthorized() } - templates, err := h.handle(params.HTTPRequest.Context(), params.Account) + templates, err := h.handle(ctx, params.Account) + if err != nil { return InternalError(err, log, false) } diff --git a/restapi/handlers/auth-refresh_token.go b/restapi/handlers/auth-refresh_token.go --- a/restapi/handlers/auth-refresh_token.go +++ b/restapi/handlers/auth-refresh_token.go @@ -1,8 +1,14 @@ package handlers import ( + "context" + "errors" + + "github.com/Masterminds/squirrel" "github.com/go-openapi/runtime/middleware" "github.com/jmoiron/sqlx" + "github.com/rs/zerolog" + "orus.io/orus-io/go-orusapi/database" "orus.io/orus-io/rednerd/auth" "orus.io/orus-io/rednerd/models" @@ -20,9 +26,37 @@ auth *auth.Auth } -func (h *AuthRefreshToken) handle(principal *models.Principal) (string, error) { - // TODO Query the db and check it the profile is still valid (ie has an api key set), - // is still connected to the account, and the account is still active +func (h *AuthRefreshToken) handle( + ctx context.Context, + log zerolog.Logger, + principal *models.Principal, +) (string, error) { + db := database.NewSQLHelper(ctx, h.db, log) + q := squirrel.Select(models.UserDBColumns...). + From(models.UserDBTable). + Where(squirrel.Eq{models.UserDBUsernameColumn: principal.Subject}) + var users []models.UserDB + if err := db.Select(&users, q); err != nil { + return "", err + } + if len(users) == 0 { + return "", errors.New("user does not exist in the database") + } + q = squirrel.Select(models.AccountDBColumns...). + From(models.AccountDBTable). + Where(squirrel.Eq{models.AccountDBNameColumn: principal.Subject}) + var accounts []models.AccountDB + if err := db.Select(&accounts, q); err != nil { + return "", err + } + if len(users) == 0 { + return "", errors.New("account does not exist in the database") + } + account := accounts[0] + if !account.IsUser { + return "", errors.New("the account but be a user") + } + token, _, err := h.auth.MakeToken(*principal, false) return token, err @@ -32,7 +66,9 @@ func (h *AuthRefreshToken) Handle( params op_auth.RefreshTokenParams, principal *models.Principal, ) middleware.Responder { - token, err := h.handle(principal) + ctx := params.HTTPRequest.Context() + log := zerolog.Ctx(ctx) + token, err := h.handle(ctx, *log, principal) if err != nil { return op_auth.NewRefreshTokenDefault(500).WithPayload(models.FromError(err)) } diff --git a/restapi/handlers/render.go b/restapi/handlers/render.go --- a/restapi/handlers/render.go +++ b/restapi/handlers/render.go @@ -3,9 +3,13 @@ import ( "context" "errors" + "fmt" + "github.com/Masterminds/squirrel" "github.com/go-openapi/runtime/middleware" + "github.com/jmoiron/sqlx" "github.com/rs/zerolog" + "orus.io/orus-io/go-orusapi/database" redner "orus.io/orus-io/rednerd/lib" "orus.io/orus-io/rednerd/models" @@ -13,22 +17,33 @@ "orus.io/orus-io/rednerd/utils" ) -// ErrNoTemplateNorDocuments is returned if the render request has no template -// nor a document. -var ErrNoTemplateNorDocument = errors.New("no template nor document. Must have one") +var ( + // ErrNoTemplateNorDocuments is returned if the render request has no template + // nor a document. + ErrNoTemplateNorDocument = errors.New( + "no template nor document. Must have one", + ) + // ErrBothTemplateAndDocument is returned if the render request has a template + // and a document. + ErrBothTemplateAndDocument = errors.New( + "render_request cannot accept both a Document and a Template", + ) +) // NewRenderHandler creates a RenderHandler. -func NewRenderHandler(renderer *redner.Renderer) *RenderHandler { - return &RenderHandler{renderer} +func NewRenderHandler(renderer *redner.Renderer, db *sqlx.DB) *RenderHandler { + return &RenderHandler{renderer, db} } // RenderHandler handles POST /api/v1/render. type RenderHandler struct { renderer *redner.Renderer + db *sqlx.DB } func (h *RenderHandler) handle( ctx context.Context, + log zerolog.Logger, principal *models.Principal, template *models.Template, data models.Dataset, @@ -36,8 +51,26 @@ metadata models.Metadata, toType string, ) ([]*models.Document, error) { - if template == nil && document == nil { + switch { + case template == nil && document == nil: return nil, utils.HTTPBadRequest(ErrNoTemplateNorDocument) + case template != nil && document != nil: + return nil, utils.HTTPBadRequest(ErrBothTemplateAndDocument) + default: + } + + if template != nil && template.Name != "" { + var tDB models.TemplateDB + db := database.NewSQLHelper(ctx, h.db, log) + q := squirrel.Select(models.TemplateDBColumns...). + From(models.TemplateDBTable). + Where(squirrel.Eq{models.TemplateDBNameColumn: template.Name}) + if err := db.Get(&tDB, q); err != nil { + return nil, err + } + if tDB.Account != principal.Subject { + return nil, errors.New("user can render on it's own templates only") + } } documents, err := h.renderer.Render( @@ -55,18 +88,23 @@ // Handle the incoming request. func (h *RenderHandler) Handle(params op.RenderParams, principal *models.Principal) middleware.Responder { - accept := params.Request.Accept - // TODO if accept is empty, get a 'accept' from the http header - - // TODO checks if the user has render access to the template - + req := params.Request + accept := req.Accept + if accept == "" { + acceptHeader := params.HTTPRequest.Header.Get("Accept") + fmt.Printf("hey %s", acceptHeader) + accept = models.Accept(acceptHeader) + } + ctx := params.HTTPRequest.Context() + log := zerolog.Ctx(ctx) docs, err := h.handle( - params.HTTPRequest.Context(), + ctx, + *log, principal, - params.Request.Template, - params.Request.Data, - params.Request.Document, - params.Request.Metadata, + req.Template, + req.Data, + req.Document, + req.Metadata, string(accept), ) if err != nil { @@ -80,12 +118,13 @@ return r } - if params.HTTPRequest.Context().Err() != nil { + if ctx.Err() != nil { return op.NewRenderDefault(499) } - return InternalError(err, zerolog.Ctx(params.HTTPRequest.Context()), false) + return InternalError(err, log, false) } + fmt.Print("heyyyyy") return op.NewRenderOK().WithPayload(docs) } diff --git a/tests/render_test.go b/restapi/handlers/render_test.go rename from tests/render_test.go rename to restapi/handlers/render_test.go --- a/tests/render_test.go +++ b/restapi/handlers/render_test.go @@ -1,7 +1,8 @@ -package tests +package handlers_test import ( "encoding/base64" + "fmt" "testing" "time" @@ -18,13 +19,20 @@ defer cancel() tester := apitester.NewAPITester(ctx, t, nil) - defer tester.Close() - - defer tester.Logout() + t.Cleanup(func() { + tester.Close() + tester.Logout() + }) tester.Login("admin", "admin") - tester.CreateUser("janedoe", "janedoe") + tester.Logout() tester.Login("janedoe", "janedoe") + tmpl := models.Template{ + Language: "mustache", + Produces: "text/mjml", + BodyFormat: "text", + Body: "<mjml> <mj-body> <mj-section> <mj-column> <mj-text>hello {{name}}</mj-text> </mj-column> </mj-section> </mj-body> </mjml>", + } t.Run("MJML-Template-to-html", func(t *testing.T) { defer tester.SetT(t)() @@ -33,20 +41,16 @@ tester.Render( &models.RenderRequest{ - Accept: "text/html", - Template: &models.Template{ - Language: "mustache", - Produces: "text/mjml", - BodyFormat: "text", - Body: "<mjml> <mj-body> <mj-section> <mj-column> <mj-text>hello {{name}}</mj-text> </mj-column> </mj-section> </mj-body> </mjml>", //nolint:lll - }, + Accept: "text/html", + Template: &tmpl, Data: models.Dataset{ apitester.JSONObj{"name": "Nath"}, }, }, &result, ) - require.Equal(t, 1, len(result)) + + require.Len(t, result, 1) if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { assert.Contains(t, string(body), "hello Nath") @@ -63,23 +67,18 @@ ), ) - assert.Equal(t, 1, len(logs)) + assert.Len(t, logs, 1) }) t.Run("MJML-Saved-Template-to-html", func(t *testing.T) { + tmpl.Name = "t1" defer tester.SetT(t)() var response models.Template tester.CreateTemplate( "janedoe", - &models.Template{ - Name: "t1", - Language: "mustache", - Produces: "text/mjml", - BodyFormat: "text", - Body: "<mjml> <mj-body> <mj-section> <mj-column> <mj-text>hello {{name}}</mj-text> </mj-column> </mj-section> </mj-body> </mjml>", //nolint:lll - }, + &tmpl, &response) var result []*models.Document @@ -96,7 +95,7 @@ }, }, &result) - require.Equal(t, 1, len(result)) + require.Len(t, result, 1) if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { assert.Contains(t, string(body), "hello Nath") @@ -127,14 +126,14 @@ }, &result, ) - require.Equal(t, 1, len(result)) + require.Len(t, result, 1) if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { assert.Contains(t, string(body), "hello world") } t.Log(result[0].RenderErrors) - require.Equal(t, 6, len(result[0].RenderErrors)) + require.Len(t, result[0].RenderErrors, 6) assert.Equal(t, "MJML", result[0].RenderErrors[0].Engine) assert.Equal(t, "MJML", result[0].RenderErrors[1].Engine) }) @@ -165,13 +164,12 @@ }, &result, ) - require.Equal(t, 1, len(result)) + require.Len(t, result, 1) if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { assert.Contains(t, string(body), "hello world") } - - require.Equal(t, 0, len(result[0].RenderErrors)) + require.Empty(t, result[0].RenderErrors) }) t.Run("MJML-specific-test", func(t *testing.T) { @@ -190,12 +188,33 @@ }, &result, ) - require.Equal(t, 1, len(result)) + require.Len(t, result, 1) if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { assert.Contains(t, string(body), "<strong>Client</strong> : client") } }) + t.Run("MJML-no-accept", func(t *testing.T) { + defer tester.SetT(t)() + tester.SetHeader("Accept", "text/html") + var result []*models.Document + tester.Render( + &models.RenderRequest{ + Accept: "", + Document: &models.Document{ + Type: "text/mjml", + BodyFormat: "text", + Body: mjmlSource, + }, + }, + &result, + ) + fmt.Print("I was HEERR") + require.Len(t, result, 1) + if body, err := base64.StdEncoding.DecodeString(result[0].Body); assert.NoError(t, err) { + assert.Contains(t, string(body), "<strong>Client</strong> : client") + } + }) } const mjmlSource = ` diff --git a/restapi/panic_middleware.go b/restapi/panic_middleware.go --- a/restapi/panic_middleware.go +++ b/restapi/panic_middleware.go @@ -1,7 +1,9 @@ package restapi import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "net/http" @@ -44,6 +46,12 @@ exc = exc.Str("plainstack", string(b)) } else { exc = exc.RawJSON("stacktrace", b) + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, b, "", "\t") + if err != nil { + log.Err(err).Msg("") + } + log.Info().Msgf("pretty %s", prettyJSON.String()) } log.Error().Dict("exception", exc).Msg("Caught a panic")