From 1fcac1b2f73f9d589c2673b84eecd8d12a323fba Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 30 Jun 2026 17:56:48 +0300 Subject: [PATCH] feat: automatically generate context ignore paths --- .github/workflows/ci.yml | 6 +- Makefile | 3 +- gen/context_paths/context_paths.go | 131 ++++++++++++++++++ gen/context_paths/paths.tmpl | 6 + gen/{gen.go => docs/docs_gen.go} | 6 + gen/{ => docs}/gen_env.go | 0 gen/{ => docs}/gen_md.go | 0 .../sqlc_wrapper.go | 6 +- gen/{sqlc-wrapper => sqlc_wrapper}/store.tmpl | 2 +- generate.go | 3 + internal/controller/context_controller.go | 1 + internal/controller/health_controller.go | 1 + internal/controller/oauth_controller.go | 2 + internal/controller/oidc_controller.go | 2 + internal/controller/resources_controller.go | 1 + internal/controller/user_controller.go | 1 + internal/controller/well_known_controller.go | 3 + internal/middleware/context_middleware.go | 20 --- internal/middleware/context_paths.go | 18 +++ internal/middleware/generate.go | 3 + internal/repository/postgres/generate.go | 2 +- internal/repository/postgres/store.go | 2 +- internal/repository/sqlite/generate.go | 2 +- internal/repository/sqlite/store.go | 2 +- 24 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 gen/context_paths/context_paths.go create mode 100644 gen/context_paths/paths.tmpl rename gen/{gen.go => docs/docs_gen.go} (75%) rename gen/{ => docs}/gen_env.go (100%) rename gen/{ => docs}/gen_md.go (100%) rename gen/{sqlc-wrapper => sqlc_wrapper}/sqlc_wrapper.go (98%) rename gen/{sqlc-wrapper => sqlc_wrapper}/store.tmpl (94%) create mode 100644 generate.go create mode 100644 internal/middleware/context_paths.go create mode 100644 internal/middleware/generate.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0ceceb0..e338afa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,9 +36,9 @@ jobs: - name: Check codegen is up to date run: | sqlc generate - go generate ./internal/repository/... - git diff --exit-code -- internal/repository/ - git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true + go generate ./... + git diff --exit-code + git status --porcelain | grep -q . && echo "untracked files in git diff" && exit 1 || true - name: Install frontend dependencies working-directory: ./frontend diff --git a/Makefile b/Makefile index b50b7bec..07094777 100644 --- a/Makefile +++ b/Makefile @@ -94,5 +94,4 @@ sql: # Go gen generate: - go run ./gen - go generate ./internal/repository/... + go generate ./... diff --git a/gen/context_paths/context_paths.go b/gen/context_paths/context_paths.go new file mode 100644 index 00000000..5a8e089a --- /dev/null +++ b/gen/context_paths/context_paths.go @@ -0,0 +1,131 @@ +// gen/context_paths generates the ignore paths for the user context since +// gin will not less apply the middleware to only specific paths. +// +// The generator reads every controller and looks for the //context:ignore comment. +// The format for the context ignore comment is: +// +// //contxt:ignore /api/mypath GET,POST +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" + "strings" + "text/template" + + _ "embed" + + "golang.org/x/tools/go/packages" +) + +//go:embed paths.tmpl +var pathsTmplSrc string + +var pathsTmpl = template.Must(template.New("paths").Parse(pathsTmplSrc)) + +func main() { + if err := run(); err != nil { + fmt.Printf("Failed to generate: %s", err.Error()) + os.Exit(1) + } +} + +func run() error { + // load pkg + pkgConfig := &packages.Config{ + Mode: packages.NeedFiles, + } + + pkgs, err := packages.Load(pkgConfig, "github.com/tinyauthapp/tinyauth/internal/controller") + + if err != nil { + return fmt.Errorf("failed to load pkg: %w", err) + } + + if len(pkgs) == 0 { + return fmt.Errorf("failed to get controllers package") + } + + pkg := pkgs[0] + + // for each file we check the comments and either add or remove the context + var contextIgnorePaths []string + + for _, gofile := range pkg.GoFiles { + // read the file + file, err := os.ReadFile(gofile) + + if err != nil { + fmt.Printf("Failed to read %s, ignoring", gofile) + continue + } + + // get the comment lines + lines := strings.SplitSeq(string(file), "\n") + + for line := range lines { + if !strings.HasPrefix(strings.TrimSpace(line), "//context:ignore") { + continue + } + + path, methods, ok := parseContextIgnoreLine(line) + + if !ok { + fmt.Printf("Failed to parse %s rule, ignore", line) + continue + } + + for _, m := range methods { + contextIgnorePaths = append(contextIgnorePaths, m+" "+path) + } + } + } + + // generate out + type tmplData struct { + IgnorePaths []string + } + + var buf bytes.Buffer + + if err := pathsTmpl.Execute(&buf, tmplData{ + IgnorePaths: contextIgnorePaths, + }); err != nil { + return err + } + + formatted, err := format.Source(buf.Bytes()) + + if err != nil { + return fmt.Errorf("gofmt failed: %w", err) + } + + // write out + err = os.WriteFile("context_paths.go", formatted, 0666) + + if err != nil { + return fmt.Errorf("failed to write out: %w", err) + } + + return nil +} + +func parseContextIgnoreLine(line string) (string, []string, bool) { + line = strings.TrimPrefix(line, "//context:ignore ") + path, methodStr, ok := strings.Cut(line, " ") + if !ok { + return "", []string{}, false + } + var methodsParsed []string + methodParts := strings.SplitSeq(methodStr, ",") + for m := range methodParts { + if strings.TrimSpace(m) == "" { + continue + } + m = strings.ToUpper(m) + methodsParsed = append(methodsParsed, m) + } + return path, methodsParsed, true +} diff --git a/gen/context_paths/paths.tmpl b/gen/context_paths/paths.tmpl new file mode 100644 index 00000000..fb4c7078 --- /dev/null +++ b/gen/context_paths/paths.tmpl @@ -0,0 +1,6 @@ +// Code generated by gen/context_paths. DO NOT EDIT. +package middleware + +var contextSkipPathsPrefix = []string{ +{{range .IgnorePaths}}"{{.}}", +{{end}}} diff --git a/gen/gen.go b/gen/docs/docs_gen.go similarity index 75% rename from gen/gen.go rename to gen/docs/docs_gen.go index f84bc4d0..ef0a435c 100644 --- a/gen/gen.go +++ b/gen/docs/docs_gen.go @@ -1,3 +1,9 @@ +// gen/docs generates the .env.example and config.gen.md +// files for the configuration of Tinyauth. Run via: +// +// The generator reads the Tinyauth configuration package and using reflection it generates the +// example files. The .env.example is used in this repo while the config.gen.md is used in the +// documentaton alongside some warnings that are added later. package main import ( diff --git a/gen/gen_env.go b/gen/docs/gen_env.go similarity index 100% rename from gen/gen_env.go rename to gen/docs/gen_env.go diff --git a/gen/gen_md.go b/gen/docs/gen_md.go similarity index 100% rename from gen/gen_md.go rename to gen/docs/gen_md.go diff --git a/gen/sqlc-wrapper/sqlc_wrapper.go b/gen/sqlc_wrapper/sqlc_wrapper.go similarity index 98% rename from gen/sqlc-wrapper/sqlc_wrapper.go rename to gen/sqlc_wrapper/sqlc_wrapper.go index a7a75eb4..21804570 100644 --- a/gen/sqlc-wrapper/sqlc_wrapper.go +++ b/gen/sqlc_wrapper/sqlc_wrapper.go @@ -1,7 +1,5 @@ -// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under -// internal/repository//. Run via: -// -// go generate ./internal/repository/... +// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under +// internal/repository//. // // The generator introspects *Queries methods and the model/params types in the // driver package, then emits a store.go that wraps *Queries so it satisfies diff --git a/gen/sqlc-wrapper/store.tmpl b/gen/sqlc_wrapper/store.tmpl similarity index 94% rename from gen/sqlc-wrapper/store.tmpl rename to gen/sqlc_wrapper/store.tmpl index fa4acf01..607d1b3a 100644 --- a/gen/sqlc-wrapper/store.tmpl +++ b/gen/sqlc_wrapper/store.tmpl @@ -1,4 +1,4 @@ -// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT. +// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT. package {{.PkgName}} import ( diff --git a/generate.go b/generate.go new file mode 100644 index 00000000..77906e66 --- /dev/null +++ b/generate.go @@ -0,0 +1,3 @@ +package docs + +//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index abfabaad..113a2022 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -147,6 +147,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { c.JSON(200, userContext) } +//context:ignore /api/context/app GET func (controller *ContextController) appContextHandler(c *gin.Context) { c.JSON(200, AppContextResponse{ Status: 200, diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 2b578978..c9cb8da4 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -23,6 +23,7 @@ func NewHealthController(i HealthControllerInput) *HealthController { return controller } +//context:ignore /api/healthz GET,HEAD func (controller *HealthController) healthHandler(c *gin.Context) { c.JSON(200, gin.H{ "status": 200, diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 27fca206..6bd06ca5 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -54,6 +54,7 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController { return controller } +//context:ignore /api/oauth/url GET func (controller *OAuthController) oauthURLHandler(c *gin.Context) { var req OAuthRequest @@ -118,6 +119,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { }) } +//context:ignore /api/oauth/callback GET func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var req OAuthRequest diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index c4049953..d691c9b9 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -367,6 +367,7 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { }) } +//context:ignore /api/oidc/token POST func (controller *OIDCController) Token(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured") @@ -538,6 +539,7 @@ func (controller *OIDCController) Token(c *gin.Context) { c.JSON(200, tokenResponse) } +//context:ignore /api/oidc/userinfo GET,POST func (controller *OIDCController) Userinfo(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured") diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go index f4b720ed..afef24aa 100644 --- a/internal/controller/resources_controller.go +++ b/internal/controller/resources_controller.go @@ -33,6 +33,7 @@ func NewResourcesController(i ResourcesControllerInput) *ResourcesController { return controller } +//context:ignore /resources GET func (controller *ResourcesController) resourcesHandler(c *gin.Context) { if controller.config.Resources.Path == "" { c.JSON(404, gin.H{ diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index ae6c23bf..6c21e4a0 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -57,6 +57,7 @@ func NewUserController(i UserControllerInput) *UserController { return controller } +//context:ignore /api/user/login POST func (controller *UserController) loginHandler(c *gin.Context) { var req LoginRequest diff --git a/internal/controller/well_known_controller.go b/internal/controller/well_known_controller.go index a32c3a06..f299fff3 100644 --- a/internal/controller/well_known_controller.go +++ b/internal/controller/well_known_controller.go @@ -65,6 +65,7 @@ func NewWellKnownController(i WellKnownControllerInput) *WellKnownController { return controller } +//context:ignore /.well-known/openid-configuration GET func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) { if controller.oidc == nil { c.JSON(500, gin.H{ @@ -94,6 +95,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context }) } +//context:ignore /.well-known/jwks.json GET func (controller *WellKnownController) JWKS(c *gin.Context) { if controller.oidc == nil { c.JSON(500, gin.H{ @@ -122,6 +124,7 @@ func (controller *WellKnownController) JWKS(c *gin.Context) { c.Status(http.StatusOK) } +//context:ignore /.well-known/webfinger GET func (controller *WellKnownController) WebFinger(c *gin.Context) { c.Header("Content-Type", "application/jrd+json") c.Header("Access-Control-Allow-Origin", "*") diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 0620f275..6ed955f8 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -16,26 +16,6 @@ import ( "github.com/gin-gonic/gin" ) -// Gin won't let us set a middleware on a specific route (at least it doesn't work, -// see https://github.com/gin-gonic/gin/issues/531) so we have to do some hackery -var ( - contextSkipPathsPrefix = []string{ - "GET /api/context/app", - "GET /api/healthz", - "HEAD /api/healthz", - "GET /api/oauth/url", - "GET /api/oauth/callback", - "GET /api/oidc/clients", - "POST /api/oidc/token", - "GET /api/oidc/userinfo", - "POST /api/oidc/userinfo", - "GET /resources", - "POST /api/user/login", - "GET /.well-known/openid-configuration", - "GET /.well-known/jwks.json", - } -) - type ContextMiddleware struct { log *logger.Logger runtime *model.RuntimeConfig diff --git a/internal/middleware/context_paths.go b/internal/middleware/context_paths.go new file mode 100644 index 00000000..98132adf --- /dev/null +++ b/internal/middleware/context_paths.go @@ -0,0 +1,18 @@ +// Code generated by gen/context_paths. DO NOT EDIT. +package middleware + +var contextSkipPathsPrefix = []string{ + "GET /api/context/app", + "GET /api/healthz", + "HEAD /api/healthz", + "GET /api/oauth/url", + "GET /api/oauth/callback", + "POST /api/oidc/token", + "GET /api/oidc/userinfo", + "POST /api/oidc/userinfo", + "GET /resources", + "POST /api/user/login", + "GET /.well-known/openid-configuration", + "GET /.well-known/jwks.json", + "GET /.well-known/webfinger", +} diff --git a/internal/middleware/generate.go b/internal/middleware/generate.go new file mode 100644 index 00000000..0c33cda7 --- /dev/null +++ b/internal/middleware/generate.go @@ -0,0 +1,3 @@ +package middleware + +//go:generate go run github.com/tinyauthapp/tinyauth/gen/context_paths diff --git a/internal/repository/postgres/generate.go b/internal/repository/postgres/generate.go index dcd23be9..21c85a50 100644 --- a/internal/repository/postgres/generate.go +++ b/internal/repository/postgres/generate.go @@ -1,3 +1,3 @@ package postgres -//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres +//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres diff --git a/internal/repository/postgres/store.go b/internal/repository/postgres/store.go index b3e79c80..8c94a3c7 100644 --- a/internal/repository/postgres/store.go +++ b/internal/repository/postgres/store.go @@ -1,4 +1,4 @@ -// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT. +// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT. package postgres import ( diff --git a/internal/repository/sqlite/generate.go b/internal/repository/sqlite/generate.go index ed695567..b16fb27d 100644 --- a/internal/repository/sqlite/generate.go +++ b/internal/repository/sqlite/generate.go @@ -1,3 +1,3 @@ package sqlite -//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite +//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite diff --git a/internal/repository/sqlite/store.go b/internal/repository/sqlite/store.go index a567c871..2d22088a 100644 --- a/internal/repository/sqlite/store.go +++ b/internal/repository/sqlite/store.go @@ -1,4 +1,4 @@ -// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT. +// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT. package sqlite import (