diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f6066e69..0762fa54d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,6 +32,7 @@ Where `PKG` is: - `deps`: For changes to dependencies - `docs`: For changes to the documentation - `examples`: For changes to the examples +- `internal/lib`: For changes to Nhost's common libraries (internal) - `mintlify-openapi`: For changes to the Mintlify OpenAPI tool - `nhost-js`: For changes to the Nhost JavaScript SDK - `nixops`: For changes to the NixOps diff --git a/.github/actions/validate-pr-title/action.yaml b/.github/actions/validate-pr-title/action.yaml index fb82ec198..049fba2e3 100644 --- a/.github/actions/validate-pr-title/action.yaml +++ b/.github/actions/validate-pr-title/action.yaml @@ -17,7 +17,7 @@ runs: # Define valid types and packages VALID_TYPES="feat|fix|chore" - VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage" + VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage" # Check if title matches the pattern TYPE(PKG): SUMMARY if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then diff --git a/.github/workflows/auth_checks.yaml b/.github/workflows/auth_checks.yaml index 660928e4b..f41981c9b 100644 --- a/.github/workflows/auth_checks.yaml +++ b/.github/workflows/auth_checks.yaml @@ -17,6 +17,7 @@ on: - '.golangci.yaml' - 'go.mod' - 'go.sum' + - 'internal/lib/**' - 'vendor/**' # auth diff --git a/.github/workflows/storage_checks.yaml b/.github/workflows/storage_checks.yaml index 4d10ab223..213ee6477 100644 --- a/.github/workflows/storage_checks.yaml +++ b/.github/workflows/storage_checks.yaml @@ -17,6 +17,7 @@ on: - '.golangci.yaml' - 'go.mod' - 'go.sum' + - 'internal/lib/**' - 'vendor/**' # storage diff --git a/flake.nix b/flake.nix index 9e7f5a77b..568567728 100644 --- a/flake.nix +++ b/flake.nix @@ -119,6 +119,7 @@ gofumpt golangci-lint gqlgenc + oapi-codegen # internal packages self.packages.${system}.codegen diff --git a/go.mod b/go.mod index c79785739..a4e210069 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/davidbyttow/govips/v2 v2.16.0 github.com/gabriel-vasile/mimetype v1.4.8 github.com/getkin/kin-openapi v0.133.0 - github.com/gin-contrib/cors v1.7.3 github.com/gin-gonic/gin v1.11.0 github.com/go-git/go-git/v5 v5.16.2 github.com/go-webauthn/webauthn v0.12.2 @@ -30,7 +29,6 @@ require ( github.com/lmittmann/tint v1.0.7 github.com/mark3labs/mcp-go v0.41.1 github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 - github.com/oapi-codegen/gin-middleware v1.0.2 github.com/oapi-codegen/runtime v1.1.1 github.com/pb33f/libopenapi v0.21.12 github.com/pelletier/go-toml/v2 v2.2.4 diff --git a/go.sum b/go.sum index 27f5446b6..f73e50e15 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,6 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= -github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -341,8 +339,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+ github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk= github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oapi-codegen/gin-middleware v1.0.2 h1:/H99UzvHQAUxXK8pzdcGAZgjCVeXdFDAUUWaJT0k0eI= -github.com/oapi-codegen/gin-middleware v1.0.2/go.mod h1:2HJDQjH8jzK2/k/VKcWl+/T41H7ai2bKa6dN3AA2GpA= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= diff --git a/internal/lib/oapi/errors.go b/internal/lib/oapi/errors.go new file mode 100644 index 000000000..bb588fbb2 --- /dev/null +++ b/internal/lib/oapi/errors.go @@ -0,0 +1,13 @@ +package oapi + +import "fmt" + +type AuthenticatorError struct { + Scheme string + Code string + Message string +} + +func (e *AuthenticatorError) Error() string { + return fmt.Sprintf("security error [%s]: %s", e.Code, e.Message) +} diff --git a/internal/lib/oapi/example/api/api.go b/internal/lib/oapi/example/api/api.go new file mode 100644 index 000000000..42f162a8e --- /dev/null +++ b/internal/lib/oapi/example/api/api.go @@ -0,0 +1,10 @@ +//go:generate oapi-codegen -config server.cfg.yaml openapi.yaml +//go:generate oapi-codegen -config types.cfg.yaml openapi.yaml +package api + +import ( + _ "embed" +) + +//go:embed openapi.yaml +var OpenAPISchema []byte diff --git a/internal/lib/oapi/example/api/openapi.yaml b/internal/lib/oapi/example/api/openapi.yaml new file mode 100644 index 000000000..8bbae1558 --- /dev/null +++ b/internal/lib/oapi/example/api/openapi.yaml @@ -0,0 +1,200 @@ +openapi: "3.0.0" + +paths: + /signin/email-password: + post: + summary: Sign in with email and password + description: Authenticate a user with their email and password. Returns a session object or MFA challenge if two-factor authentication is enabled. + operationId: signInEmailPassword + requestBody: + description: User credentials for email and password authentication + content: + application/json: + schema: + $ref: "#/components/schemas/SignInEmailPasswordRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SignInEmailPasswordResponse" + description: "Authentication successful. If MFA is enabled, a challenge will be returned instead of a session." + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: "An error occurred while processing the request" + + /user/email/change: + post: + summary: Change user email + description: Request to change the authenticated user's email address. A verification email will be sent to the new address to confirm the change. Requires elevated permissions. + operationId: changeUserEmail + tags: + - user + security: + - BearerAuthElevated: [] + requestBody: + description: New email address and optional redirect URL for email change + content: + application/json: + schema: + $ref: "#/components/schemas/UserEmailChangeRequest" + required: true + responses: + "200": + description: >- + Email change requested. An email with a verification link has been sent to the new address + content: + application/json: + schema: + $ref: "#/components/schemas/OKResponse" + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: "An error occurred while processing the request" + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + description: "Bearer authentication with JWT access token. Used to authenticate requests to protected endpoints." + BearerAuthElevated: + type: http + scheme: bearer + description: "Bearer authentication that requires elevated permissions. Used for sensitive operations that may require additional security measures such as recent authentication. For details see https://docs.nhost.io/products/auth/elevated-permissions" + + schemas: + SignInEmailPasswordRequest: + type: object + description: "Request to authenticate using email and password" + additionalProperties: false + properties: + email: + description: "User's email address" + example: "john.smith@nhost.io" + format: email + type: string + password: + description: "User's password" + example: "Str0ngPassw#ord-94|%" + minLength: 3 + maxLength: 50 + type: string + required: + - email + - password + + SignInEmailPasswordResponse: + type: object + description: "Response for email-password authentication that may include a session or MFA challenge" + additionalProperties: false + properties: + session: + $ref: "#/components/schemas/Session" + + Session: + type: object + description: "User authentication session containing tokens and user information" + additionalProperties: false + properties: + accessToken: + type: string + description: "JWT token for authenticating API requests" + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + accessTokenExpiresIn: + type: integer + format: int64 + description: "Expiration time of the access token in seconds" + example: 900 + refreshTokenId: + description: "Identifier for the refresh token" + example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24" + pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b + type: string + refreshToken: + description: "Token used to refresh the access token" + example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24" + pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b + type: string + required: + - accessToken + - accessTokenExpiresIn + - refreshToken + - refreshTokenId + + UserEmailChangeRequest: + type: object + additionalProperties: false + properties: + newEmail: + description: A valid email + example: john.smith@nhost.io + format: email + type: string + required: + - newEmail + + OKResponse: + type: string + additionalProperties: false + enum: + - OK + + ErrorResponse: + type: object + description: "Standardized error response" + additionalProperties: false + properties: + status: + description: "HTTP status error code" + type: integer + example: 400 + message: + description: "Human-friendly error message" + type: string + example: "Invalid email format" + error: + description: "Error code identifying the specific application error" + type: string + enum: + - default-role-must-be-in-allowed-roles + - disabled-endpoint + - disabled-user + - email-already-in-use + - email-already-verified + - forbidden-anonymous + - internal-server-error + - invalid-email-password + - invalid-request + - locale-not-allowed + - password-too-short + - password-in-hibp-database + - redirectTo-not-allowed + - role-not-allowed + - signup-disabled + - unverified-user + - user-not-anonymous + - invalid-pat + - invalid-refresh-token + - invalid-ticket + - disabled-mfa-totp + - no-totp-secret + - invalid-totp + - mfa-type-not-found + - totp-already-active + - invalid-state + - oauth-token-echange-failed + - oauth-profile-fetch-failed + - oauth-provider-error + - invalid-otp + - cannot-send-sms + required: + - status + - message + - error diff --git a/internal/lib/oapi/example/api/server.cfg.yaml b/internal/lib/oapi/example/api/server.cfg.yaml new file mode 100644 index 000000000..ab5b43329 --- /dev/null +++ b/internal/lib/oapi/example/api/server.cfg.yaml @@ -0,0 +1,6 @@ +package: api +generate: + gin-server: true + embedded-spec: true + strict-server: true +output: server.gen.go diff --git a/internal/lib/oapi/example/api/server.gen.go b/internal/lib/oapi/example/api/server.gen.go new file mode 100644 index 000000000..fe7861657 --- /dev/null +++ b/internal/lib/oapi/example/api/server.gen.go @@ -0,0 +1,351 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT. +package api + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" + strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Sign in with email and password + // (POST /signin/email-password) + SignInEmailPassword(c *gin.Context) + // Change user email + // (POST /user/email/change) + ChangeUserEmail(c *gin.Context) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +type MiddlewareFunc func(c *gin.Context) + +// SignInEmailPassword operation middleware +func (siw *ServerInterfaceWrapper) SignInEmailPassword(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.SignInEmailPassword(c) +} + +// ChangeUserEmail operation middleware +func (siw *ServerInterfaceWrapper) ChangeUserEmail(c *gin.Context) { + + c.Set(BearerAuthElevatedScopes, []string{}) + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.ChangeUserEmail(c) +} + +// GinServerOptions provides options for the Gin server. +type GinServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router gin.IRouter, si ServerInterface) { + RegisterHandlersWithOptions(router, si, GinServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { + errorHandler := options.ErrorHandler + if errorHandler == nil { + errorHandler = func(c *gin.Context, err error, statusCode int) { + c.JSON(statusCode, gin.H{"msg": err.Error()}) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandler: errorHandler, + } + + router.POST(options.BaseURL+"/signin/email-password", wrapper.SignInEmailPassword) + router.POST(options.BaseURL+"/user/email/change", wrapper.ChangeUserEmail) +} + +type SignInEmailPasswordRequestObject struct { + Body *SignInEmailPasswordJSONRequestBody +} + +type SignInEmailPasswordResponseObject interface { + VisitSignInEmailPasswordResponse(w http.ResponseWriter) error +} + +type SignInEmailPassword200JSONResponse SignInEmailPasswordResponse + +func (response SignInEmailPassword200JSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SignInEmailPassworddefaultJSONResponse struct { + Body ErrorResponse + StatusCode int +} + +func (response SignInEmailPassworddefaultJSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type ChangeUserEmailRequestObject struct { + Body *ChangeUserEmailJSONRequestBody +} + +type ChangeUserEmailResponseObject interface { + VisitChangeUserEmailResponse(w http.ResponseWriter) error +} + +type ChangeUserEmail200JSONResponse OKResponse + +func (response ChangeUserEmail200JSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ChangeUserEmaildefaultJSONResponse struct { + Body ErrorResponse + StatusCode int +} + +func (response ChangeUserEmaildefaultJSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Sign in with email and password + // (POST /signin/email-password) + SignInEmailPassword(ctx context.Context, request SignInEmailPasswordRequestObject) (SignInEmailPasswordResponseObject, error) + // Change user email + // (POST /user/email/change) + ChangeUserEmail(ctx context.Context, request ChangeUserEmailRequestObject) (ChangeUserEmailResponseObject, error) +} + +type StrictHandlerFunc = strictgin.StrictGinHandlerFunc +type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// SignInEmailPassword operation middleware +func (sh *strictHandler) SignInEmailPassword(ctx *gin.Context) { + var request SignInEmailPasswordRequestObject + + var body SignInEmailPasswordJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.SignInEmailPassword(ctx, request.(SignInEmailPasswordRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SignInEmailPassword") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(SignInEmailPasswordResponseObject); ok { + if err := validResponse.VisitSignInEmailPasswordResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ChangeUserEmail operation middleware +func (sh *strictHandler) ChangeUserEmail(ctx *gin.Context) { + var request ChangeUserEmailRequestObject + + var body ChangeUserEmailJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.ChangeUserEmail(ctx, request.(ChangeUserEmailRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ChangeUserEmail") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(ChangeUserEmailResponseObject); ok { + if err := validResponse.VisitChangeUserEmailResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/9RYUXPbuBH+KxhcO30RJMVW0lhP9WV8rXLXS8Z22s7YfoCApYiYBHhY0IrO1X/vLEBK", + "pEQ1vs5dp32yTBCL3f2+b3fBZ65cWTkLNiCfP3NUOZQy/rzy3vlrwMpZBHogtTbBOCuLj95V4IMB5PNM", + "FggjrgGVNxWt8zm/CdJq6bX5GTQDMsR8a2nEq872Zx6X6UffRDyeKaeBGQ02mGxj7IqFHBhWoExmFJNV", + "VRglaUc6hY842Lrk8zuuIZN1EYR3BYiyxiCWIIwVsijcGnR8jnzEtUG5LEALsLpyxobusxoh2iylKYQs", + "PEi9ISN1jKP/+Am8yQxoPuKZ80ujNVghrbOb0tV0krEBvJWFQPBP4EXrsbFPsjBaJHOVRFw7rzsLHn6q", + "AcmxwilZgLAutHFQOpsdIjgnMHc+dB8aK3KzrISWQS5l9NuDNh5UuHUHlmKu+o/QrGxdiTYjfMRr20ba", + "pof+pG29aJPzlQy9UDIPmIvgHsF2ngejHqGX+jKTIrhQ8RG3Lv4SCMpD11qzHl/dVMn1zNWW3Iw7Wmyk", + "CuYJOjsxyED/O1mHxhsBKpd2BSKTJkWaFivvMlOAyCCofGDxyegBMJNnSlryCcFqgSXyhxEnR/mcY/DG", + "rvh2xEtAlCs4VsBf6lJakXkDVhebRkbt2yMOX2RZFWRrkc5kkUAsc76MOT86iYKuceCg29uPLC02p5Ds", + "ukfMptOdPaLxCjzfbolJP9XGgybBNdb3AY0aae+DdsvPoAK58uH7F1eWVtAfvh9M3w0gxjB+UYH6hOAZ", + "IUiVpakgmCwx5WyQxsZqQ8RAJq1mRHJmbMouGTmsY1IpQLyNxD5K8fu/3yZjBE/vYLtilx8XrNE49oCF", + "zft8+WdlPpj3i08/L179aBa4sNev1bvFm8Vj9Y+/vXt/MR6Ph7DueHP1pTIecDHgVlxK0QdTAnNZLLBp", + "c+OwocwoZ3XPtwtiREO1yIk3M35MEWJIFPyJtMTHlFrNgmPNu0cu9HJyps5fL99k50LNlhdi9hbOxcUf", + "30qhZ3qavdKzMzibxfoXqNryOb+/X95NxYUU2cPz2+39/VLs/p1tT/7u7np1RtuGstyNbqGP41ukzmXA", + "R9wpsF2Q/8uRHUi7S+0T1DpA+ig1Q0Xgxqzswl5R1frYtKvrptX9MjU3u4hDHWkBq5HklcoiabjTWw9m", + "EHrlGD6qEn/A1oDWHrAv0M8ut2MsTcj/ZHOHYWwc78gimR3gzc6RU0d2PN2fdhP81K5iqr6h1n4x++fv", + "qd7KLz+AXYWcz19PR7w0tv33/GvAtg7ujnsxTP/RWNhui2LojzuHxTjkMrBSbpixqqg1MLmrz86zv353", + "yVQuiwLs6niixH1L+J2HjM/5N5P9mDtpZtxJ2zkoKUdBEw4x5HdxKHgpL/uOWFhfDTPrknVa9q9AqQNc", + "dwcf40lzAKjam7C5oUQkT78F6cFf1sSaQ1/T2iFAaxNyRm2tW6nH7FNTy3s6bJsbLVTeBVCB7gXNwI3U", + "wCIo5OcynraPMA+hIkD2Hl4V8CQD6Jd6GqnUZAcZNLtZBb40kQHYuE2sRLBoaFhkBGQ0gHsyNlbYngOs", + "TSYrQWJNJ2CtciaReVBgw4E3Y/ad80xDkKZAhgCMAsT5ZKKdwnEL+aTyTtcq4IS2T1qnRcfpryeNsKaJ", + "hc9tXRQj7iqwsjJ8zs/H0/E09ZI84j+hOd/YycEdZP7MK5dof8DfLrwyDUeRESEH4wdK7phdQ6g9DVN7", + "IUdKHumZmYyFtROZVMEdoWmQgY1XBKLNDiTqvkOViidhAIZvnd5QIDTegU1S3t8fJ58xVYxUHb5aO063", + "rpj2gWlTeYjTgCxwX/96OToIlXc1HXwNUeSpgkbQzqbT3zagpsgPRHR5MDrXsQhkdTFmiyzCucdpxGQH", + "3LUpCrakmkB0AM2MxQBS0+y5o8aYxxPjDf5XC7H/QWMoqOYrAnNK1d6DZuvcFEAli6JrPz/4HdAjjnVZ", + "Sr9puEezclTBwMhBb09IJ0ljk3TZPK2wzkyTXk1jcUd36U5yOJ+M2SVLF/T2w0hcbdOOVJKCi8YsrNtd", + "8RhnM+PLuJSOJNH+u7J5JMDULHe98zcS34nePIDoj7DuZydi4qqmdrcfQtin6x86mmyg+W/Kr3MfHgjj", + "quNWSz/QY3a5RzfkTPZxL4x9ZLlEtgSwp3D/v9FZ02X5/O55cBS4e9g+dOWYqJEa025mkiuk6Sh+tnrY", + "brfbfwUAAP//3ciGL/8UAAA=", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/lib/oapi/example/api/types.cfg.yaml b/internal/lib/oapi/example/api/types.cfg.yaml new file mode 100644 index 000000000..4ea1d8aa5 --- /dev/null +++ b/internal/lib/oapi/example/api/types.cfg.yaml @@ -0,0 +1,4 @@ +package: api +generate: + models: true +output: types.gen.go diff --git a/internal/lib/oapi/example/api/types.gen.go b/internal/lib/oapi/example/api/types.gen.go new file mode 100644 index 000000000..5ba810b42 --- /dev/null +++ b/internal/lib/oapi/example/api/types.gen.go @@ -0,0 +1,112 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT. +package api + +import ( + openapi_types "github.com/oapi-codegen/runtime/types" +) + +const ( + BearerAuthElevatedScopes = "BearerAuthElevated.Scopes" +) + +// Defines values for ErrorResponseError. +const ( + CannotSendSms ErrorResponseError = "cannot-send-sms" + DefaultRoleMustBeInAllowedRoles ErrorResponseError = "default-role-must-be-in-allowed-roles" + DisabledEndpoint ErrorResponseError = "disabled-endpoint" + DisabledMfaTotp ErrorResponseError = "disabled-mfa-totp" + DisabledUser ErrorResponseError = "disabled-user" + EmailAlreadyInUse ErrorResponseError = "email-already-in-use" + EmailAlreadyVerified ErrorResponseError = "email-already-verified" + ForbiddenAnonymous ErrorResponseError = "forbidden-anonymous" + InternalServerError ErrorResponseError = "internal-server-error" + InvalidEmailPassword ErrorResponseError = "invalid-email-password" + InvalidOtp ErrorResponseError = "invalid-otp" + InvalidPat ErrorResponseError = "invalid-pat" + InvalidRefreshToken ErrorResponseError = "invalid-refresh-token" + InvalidRequest ErrorResponseError = "invalid-request" + InvalidState ErrorResponseError = "invalid-state" + InvalidTicket ErrorResponseError = "invalid-ticket" + InvalidTotp ErrorResponseError = "invalid-totp" + LocaleNotAllowed ErrorResponseError = "locale-not-allowed" + MfaTypeNotFound ErrorResponseError = "mfa-type-not-found" + NoTotpSecret ErrorResponseError = "no-totp-secret" + OauthProfileFetchFailed ErrorResponseError = "oauth-profile-fetch-failed" + OauthProviderError ErrorResponseError = "oauth-provider-error" + OauthTokenEchangeFailed ErrorResponseError = "oauth-token-echange-failed" + PasswordInHibpDatabase ErrorResponseError = "password-in-hibp-database" + PasswordTooShort ErrorResponseError = "password-too-short" + RedirectToNotAllowed ErrorResponseError = "redirectTo-not-allowed" + RoleNotAllowed ErrorResponseError = "role-not-allowed" + SignupDisabled ErrorResponseError = "signup-disabled" + TotpAlreadyActive ErrorResponseError = "totp-already-active" + UnverifiedUser ErrorResponseError = "unverified-user" + UserNotAnonymous ErrorResponseError = "user-not-anonymous" +) + +// Defines values for OKResponse. +const ( + OK OKResponse = "OK" +) + +// ErrorResponse Standardized error response +type ErrorResponse struct { + // Error Error code identifying the specific application error + Error ErrorResponseError `json:"error"` + + // Message Human-friendly error message + Message string `json:"message"` + + // Status HTTP status error code + Status int `json:"status"` +} + +// ErrorResponseError Error code identifying the specific application error +type ErrorResponseError string + +// OKResponse defines model for OKResponse. +type OKResponse string + +// Session User authentication session containing tokens and user information +type Session struct { + // AccessToken JWT token for authenticating API requests + AccessToken string `json:"accessToken"` + + // AccessTokenExpiresIn Expiration time of the access token in seconds + AccessTokenExpiresIn int64 `json:"accessTokenExpiresIn"` + + // RefreshToken Token used to refresh the access token + RefreshToken string `json:"refreshToken"` + + // RefreshTokenId Identifier for the refresh token + RefreshTokenId string `json:"refreshTokenId"` +} + +// SignInEmailPasswordRequest Request to authenticate using email and password +type SignInEmailPasswordRequest struct { + // Email User's email address + Email openapi_types.Email `json:"email"` + + // Password User's password + Password string `json:"password"` +} + +// SignInEmailPasswordResponse Response for email-password authentication that may include a session or MFA challenge +type SignInEmailPasswordResponse struct { + // Session User authentication session containing tokens and user information + Session *Session `json:"session,omitempty"` +} + +// UserEmailChangeRequest defines model for UserEmailChangeRequest. +type UserEmailChangeRequest struct { + // NewEmail A valid email + NewEmail openapi_types.Email `json:"newEmail"` +} + +// SignInEmailPasswordJSONRequestBody defines body for SignInEmailPassword for application/json ContentType. +type SignInEmailPasswordJSONRequestBody = SignInEmailPasswordRequest + +// ChangeUserEmailJSONRequestBody defines body for ChangeUserEmail for application/json ContentType. +type ChangeUserEmailJSONRequestBody = UserEmailChangeRequest diff --git a/internal/lib/oapi/example/controller/controller.go b/internal/lib/oapi/example/controller/controller.go new file mode 100644 index 000000000..5bf620f8b --- /dev/null +++ b/internal/lib/oapi/example/controller/controller.go @@ -0,0 +1,49 @@ +package controller + +import ( + "context" + "errors" + "net/http" + + "github.com/nhost/nhost/internal/lib/oapi/example/api" +) + +type Controller struct{} + +func NewController() *Controller { + return &Controller{} +} + +func (c *Controller) SignInEmailPassword( //nolint:ireturn + _ context.Context, req api.SignInEmailPasswordRequestObject, +) (api.SignInEmailPasswordResponseObject, error) { + switch req.Body.Email { + case "bad@email.com": + return api.SignInEmailPassworddefaultJSONResponse{ + Body: api.ErrorResponse{ + Error: api.DisabledUser, + Message: "The user account is disabled.", + Status: http.StatusConflict, + }, + StatusCode: http.StatusConflict, + }, nil + case "crash@email.com": + return nil, errors.New("simulated server crash") //nolint:err113 + } + + return api.SignInEmailPassword200JSONResponse{ + Session: &api.Session{ + AccessToken: "access_token_example", + AccessTokenExpiresIn: 900, //nolint:mnd + RefreshToken: "refresh_token_example", + RefreshTokenId: "refresh_token_id_example", + }, + }, nil +} + +func (c *Controller) ChangeUserEmail( //nolint:ireturn + _ context.Context, + _ api.ChangeUserEmailRequestObject, +) (api.ChangeUserEmailResponseObject, error) { + return api.ChangeUserEmail200JSONResponse(api.OK), nil +} diff --git a/internal/lib/oapi/example/main.go b/internal/lib/oapi/example/main.go new file mode 100644 index 000000000..d429827e9 --- /dev/null +++ b/internal/lib/oapi/example/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "time" + + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/gin-gonic/gin" + "github.com/lmittmann/tint" + "github.com/nhost/nhost/internal/lib/oapi" + "github.com/nhost/nhost/internal/lib/oapi/example/api" + "github.com/nhost/nhost/internal/lib/oapi/example/controller" + "github.com/nhost/nhost/internal/lib/oapi/middleware" +) + +const apiPrefix = "/" + +func getLogger() *slog.Logger { + handler := tint.NewHandler(os.Stdout, &tint.Options{ + AddSource: true, + Level: slog.LevelDebug, + TimeFormat: time.StampMilli, + NoColor: false, + ReplaceAttr: nil, + }) + + return slog.New(handler) +} + +func authFn( + ctx context.Context, + input *openapi3filter.AuthenticationInput, +) error { + _, ok := ctx.Value(oapi.GinContextKey).(*gin.Context) + if !ok { + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "unable to get context", + } + } + + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "your access token is invalid", + } +} + +func setupRouter(logger *slog.Logger) (*gin.Engine, error) { + ctrl := controller.NewController() + handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{}) + + router, mw, err := oapi.NewRouter( + api.OpenAPISchema, + apiPrefix, + authFn, + middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"*"}, + }, + logger, + ) + if err != nil { + return nil, fmt.Errorf("failed to create oapi router: %w", err) + } + + api.RegisterHandlersWithOptions( + router, + handler, + api.GinServerOptions{ + BaseURL: apiPrefix, + Middlewares: []api.MiddlewareFunc{mw}, + ErrorHandler: nil, + }, + ) + + return router, nil +} + +func run(ctx context.Context) error { + logger := getLogger() + + router, err := setupRouter(logger) //nolint:contextcheck + if err != nil { + return err + } + + server := &http.Server{ //nolint:exhaustruct + Addr: ":8080", + Handler: router, + ReadHeaderTimeout: 5 * time.Second, //nolint:mnd + } + + if err := server.ListenAndServe(); err != nil { + logger.ErrorContext(ctx, "server failed", slog.String("error", err.Error())) + } + + return nil +} + +func main() { + if err := run(context.Background()); err != nil { + panic(err) + } +} diff --git a/internal/lib/oapi/example/main_test.go b/internal/lib/oapi/example/main_test.go new file mode 100644 index 000000000..8d0ee4f81 --- /dev/null +++ b/internal/lib/oapi/example/main_test.go @@ -0,0 +1,177 @@ +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" +) + +func makeRequest( + router *gin.Engine, + method, path string, + headers map[string]string, + body io.Reader, +) *httptest.ResponseRecorder { + req := httptest.NewRequest(method, path, body) + + for key, value := range headers { + req.Header.Set(key, value) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + return w +} + +func TestRequests(t *testing.T) { + t.Parallel() + + logger := getLogger() + + router, err := setupRouter(logger) + if err != nil { + t.Fatalf("Failed to set up router: %v", err) + } + + cases := []struct { + name string + method string + path string + headers map[string]string + body io.Reader + expectedStatus int + expectedResponse string + }{ + { + name: "success", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"email": "asd@asd.com", "password": "p4ssw0rd"}`), + expectedStatus: http.StatusOK, + expectedResponse: "{\"session\":{\"accessToken\":\"access_token_example\",\"accessTokenExpiresIn\":900,\"refreshToken\":\"refresh_token_example\",\"refreshTokenId\":\"refresh_token_id_example\"}}\n", //nolint:lll + }, + + { + name: "expected error", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader( + `{"email": "bad@email.com", "password": "p4ssw0rd"}`, + ), + expectedStatus: http.StatusConflict, + expectedResponse: "{\"error\":\"disabled-user\",\"message\":\"The user account is disabled.\",\"status\":409}\n", //nolint:lll + }, + + { + name: "unexpected error", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader( + `{"email": "crash@email.com", "password": "p4ssw0rd"}`, + ), + expectedStatus: http.StatusInternalServerError, + expectedResponse: `{"errors":"internal-server-error","message":"simulated server crash"}`, + }, + + { + name: "missing body", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: nil, + expectedStatus: http.StatusBadRequest, + expectedResponse: `{"error":"request-validation-error","reason":"value is required but missing"}`, + }, + + { + name: "wrong param", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader( + `{"wrong":"asd", "email": "asd@asd.com", "password": "p4ssw0rd"}`, + ), + expectedStatus: http.StatusBadRequest, + expectedResponse: `{"error":"schema-validation-error","reason":"property \"wrong\" is unsupported"}`, + }, + + { + name: "missing param", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"email": "asd@asd.com"}`), + expectedStatus: http.StatusBadRequest, + expectedResponse: `{"error":"schema-validation-error","reason":"property \"password\" is missing"}`, + }, + + { + name: "invalid param", + method: http.MethodPost, + path: "/signin/email-password", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"email": "asdasd.com", "password": "p4ssw0rd"}`), + expectedStatus: http.StatusBadRequest, + expectedResponse: `{"errors":"bad-request","message":"email: failed to pass regex validation"}`, + }, + + { + name: "needs security", + method: http.MethodPost, + path: "/user/email/change", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"newEmail": "new@asd.com"`), + expectedStatus: http.StatusUnauthorized, + expectedResponse: `{"error":"unauthorized","reason":"your access token is invalid","securityScheme":"BearerAuthElevated"}`, //nolint:lll + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + w := makeRequest(router, tc.method, tc.path, tc.headers, tc.body) + + resp := w.Result() + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + if resp.StatusCode != tc.expectedStatus { + t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode) + } + + if diff := cmp.Diff(string(body), tc.expectedResponse); diff != "" { + t.Errorf("Response body mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/lib/oapi/middleware/cors.go b/internal/lib/oapi/middleware/cors.go new file mode 100644 index 000000000..5b7443d7d --- /dev/null +++ b/internal/lib/oapi/middleware/cors.go @@ -0,0 +1,140 @@ +package middleware + +import ( + "net/http" + "slices" + "strings" + + "github.com/gin-gonic/gin" +) + +// CORSOptions configures the CORS middleware behavior. +// +// The middleware supports three strategies for handling Access-Control-Allow-Headers: +// - nil (default): Reflects the Access-Control-Request-Headers from the client +// - empty slice: Denies all headers (no Access-Control-Allow-Headers header is set) +// - non-empty slice: Uses the specified headers +type CORSOptions struct { + // AllowedOrigins is a list of origins permitted to make cross-origin requests. + // Use "*" or nil slice to allow all origins. + AllowedOrigins []string + + // AllowedMethods is a list of HTTP methods the client is permitted to use. + // Common values: GET, POST, PUT, DELETE, PATCH, OPTIONS. + AllowedMethods []string + + // AllowedHeaders controls which headers clients can use in requests. + // - nil: reflects client's Access-Control-Request-Headers (permissive) + // - empty slice: denies all headers + // - non-empty: allows only specified headers + AllowedHeaders []string + + // ExposedHeaders lists headers that browsers are allowed to access. + // By default, browsers only expose simple response headers. + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include credentials + // (cookies, authorization headers, or TLS client certificates). + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. Empty string means no caching directive is sent. + MaxAge string +} + +// CORS returns a Gin middleware handler that implements Cross-Origin Resource Sharing (CORS). +// +// The middleware handles both preflight (OPTIONS) requests and actual requests, setting +// appropriate CORS headers based on the provided configuration. It automatically adds +// the "Vary: Origin, Access-Control-Request-Method" header for proper cache behavior. +// +// For preflight requests (OPTIONS), the middleware responds with 204 No Content and +// prevents further request processing. For actual requests, it sets CORS headers and +// continues the middleware chain. +// +// Example usage: +// +// router.Use(middleware.CORS(middleware.CORSOptions{ +// AllowedOrigins: []string{"https://example.com", "https://app.example.com"}, +// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, +// AllowedHeaders: nil, // reflects client headers +// AllowCredentials: true, +// MaxAge: "3600", +// })) +func CORS(opts CORSOptions) gin.HandlerFunc { //nolint:cyclop,funlen + allowedMethods := strings.Join(opts.AllowedMethods, ", ") + exposedHeaders := strings.Join(opts.ExposedHeaders, ", ") + + allowCredentials := "false" + if opts.AllowCredentials { + allowCredentials = "true" + } + + var ( + headerStrategy string // "reflect", "specific", or "deny" + allowedHeaders string + ) + switch { + case opts.AllowedHeaders == nil: + headerStrategy = "reflect" + case len(opts.AllowedHeaders) == 0: + headerStrategy = "deny" + default: + headerStrategy = "specific" + allowedHeaders = strings.Join(opts.AllowedHeaders, ", ") + } + + f := func(c *gin.Context, origin string) { + if opts.AllowedOrigins != nil && + !slices.Contains(opts.AllowedOrigins, origin) && + !slices.Contains(opts.AllowedOrigins, "*") { + return + } + + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Methods", allowedMethods) + + // Handle allowed headers based on strategy + switch headerStrategy { + case "specific": + c.Header("Access-Control-Allow-Headers", allowedHeaders) + case "reflect": + headers := c.Request.Header.Get("Access-Control-Request-Headers") + if headers != "" { + c.Header("Access-Control-Allow-Headers", headers) + } + case "deny": + // Don't set the header at all + } + + if exposedHeaders != "" { + c.Header("Access-Control-Expose-Headers", exposedHeaders) + } + + c.Header("Access-Control-Allow-Credentials", allowCredentials) + + if opts.MaxAge != "" { + c.Header("Access-Control-Max-Age", opts.MaxAge) + } + + c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method") + } + + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + if c.Request.Method == http.MethodOptions { + f(c, origin) + + c.Header("Content-Length", "0") + c.AbortWithStatus(http.StatusNoContent) + + return + } + + if origin != "" { + f(c, origin) + } + + c.Next() + } +} diff --git a/internal/lib/oapi/middleware/cors_test.go b/internal/lib/oapi/middleware/cors_test.go new file mode 100644 index 000000000..6f16217c8 --- /dev/null +++ b/internal/lib/oapi/middleware/cors_test.go @@ -0,0 +1,323 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/go-cmp/cmp" + "github.com/nhost/nhost/internal/lib/oapi/middleware" +) + +func TestCORS(t *testing.T) { //nolint:maintidx + t.Parallel() + + gin.SetMode(gin.TestMode) + + cases := []struct { + name string + opts middleware.CORSOptions + requestMethod string + requestOrigin string + requestHeaders map[string]string + wantStatus int + wantHeaders http.Header + expectNext bool + }{ + { + name: "OPTIONS request with allowed origin", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + }, + requestMethod: "OPTIONS", + requestHeaders: map[string]string{}, + requestOrigin: "https://example.com", + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Allow-Origin": []string{"https://example.com"}, + "Access-Control-Allow-Methods": []string{"GET, POST"}, + "Access-Control-Allow-Headers": []string{"Content-Type, Authorization"}, + "Access-Control-Allow-Credentials": []string{"false"}, + "Vary": []string{ + "Origin, Access-Control-Request-Method", + }, + "Content-Length": []string{"0"}, + }, + expectNext: false, + }, + { + name: "OPTIONS request with wildcard origin", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + }, + requestMethod: "OPTIONS", + requestHeaders: map[string]string{}, + requestOrigin: "https://any-origin.com", + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Allow-Origin": []string{"https://any-origin.com"}, + "Access-Control-Allow-Methods": []string{"GET, POST, PUT, DELETE"}, + }, + expectNext: false, + }, + { + name: "OPTIONS request with disallowed origin", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST"}, + }, + requestMethod: "OPTIONS", + requestHeaders: map[string]string{}, + requestOrigin: "https://malicious.com", + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{}, + expectNext: false, + }, + { + name: "OPTIONS request with reflected headers (nil)", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"POST"}, + AllowedHeaders: nil, + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{ + "Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header", + }, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Allow-Headers": []string{"X-Custom-Header, X-Another-Header"}, + }, + expectNext: false, + }, + { + name: "OPTIONS request with denied headers (empty slice)", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"POST"}, + AllowedHeaders: []string{}, + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{ + "Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header", + }, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{}, + expectNext: false, + }, + { + name: "OPTIONS request with nil headers and no request headers", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET"}, + AllowedHeaders: nil, + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{}, + expectNext: false, + }, + { + name: "OPTIONS request with credentials enabled", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET"}, + AllowCredentials: true, + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Allow-Credentials": []string{"true"}, + }, + expectNext: false, + }, + { + name: "OPTIONS request with MaxAge", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET"}, + MaxAge: "3600", + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Max-Age": []string{"3600"}, + }, + expectNext: false, + }, + { + name: "OPTIONS request with exposed headers", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET"}, + ExposedHeaders: []string{"X-Custom-Response", "X-Total-Count"}, + }, + requestMethod: "OPTIONS", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusNoContent, + wantHeaders: http.Header{ + "Access-Control-Expose-Headers": []string{"X-Custom-Response, X-Total-Count"}, + }, + expectNext: false, + }, + { + name: "GET request with allowed origin", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST"}, + AllowedHeaders: []string{"Content-Type"}, + }, + requestMethod: "GET", + requestOrigin: "https://example.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusOK, + wantHeaders: http.Header{ + "Access-Control-Allow-Origin": []string{"https://example.com"}, + "Access-Control-Allow-Methods": []string{"GET, POST"}, + "Access-Control-Allow-Headers": []string{"Content-Type"}, + }, + expectNext: true, + }, + { + name: "POST request with disallowed origin", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST"}, + }, + requestMethod: "POST", + requestOrigin: "https://malicious.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusOK, + wantHeaders: http.Header{}, + expectNext: true, + }, + { + name: "GET request without origin header", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET"}, + }, + requestMethod: "GET", + requestOrigin: "", + requestHeaders: map[string]string{}, + wantStatus: http.StatusOK, + wantHeaders: http.Header{}, + expectNext: true, + }, + { + name: "GET request with empty allowed origins (denies all)", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{}, + AllowedMethods: []string{"GET"}, + }, + requestMethod: "GET", + requestOrigin: "https://any-origin.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusOK, + wantHeaders: http.Header{}, + expectNext: true, + }, + { + name: "GET request with nil allowed origins (allows all)", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: nil, + AllowedMethods: []string{"GET"}, + }, + requestMethod: "GET", + requestOrigin: "https://any-origin.com", + requestHeaders: map[string]string{}, + wantStatus: http.StatusOK, + wantHeaders: http.Header{ + "Access-Control-Allow-Origin": []string{"https://any-origin.com"}, + }, + expectNext: true, + }, + { + name: "GET request with multiple allowed origins", + opts: middleware.CORSOptions{ //nolint:exhaustruct + AllowedOrigins: []string{ + "https://example.com", + "https://another-example.com", + "https://third-example.com", + }, + AllowedMethods: []string{"GET"}, + }, + requestMethod: "GET", + requestHeaders: map[string]string{}, + requestOrigin: "https://another-example.com", + wantStatus: http.StatusOK, + wantHeaders: http.Header{ + "Access-Control-Allow-Origin": []string{"https://another-example.com"}, + }, + expectNext: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Setup router with CORS middleware + router := gin.New() + nextCalled := false + + router.Use(middleware.CORS(tc.opts)) + router.Any("/test", func(c *gin.Context) { + nextCalled = true + + c.Status(http.StatusOK) + }) + + // Create request + req := httptest.NewRequest(tc.requestMethod, "/test", nil) + if tc.requestOrigin != "" { + req.Header.Set("Origin", tc.requestOrigin) + } + + // Add any additional request headers + for key, value := range tc.requestHeaders { + req.Header.Set(key, value) + } + + // Record response + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Check status code + if w.Code != tc.wantStatus { + t.Errorf("expected status %d, got %d", tc.wantStatus, w.Code) + } + + // Check expected headers using cmp.Diff + // Only compare headers that are expected + gotHeaders := make(http.Header) + for key := range tc.wantHeaders { + if values := w.Header().Values(key); len(values) > 0 { + gotHeaders[key] = values + } + } + + if diff := cmp.Diff(tc.wantHeaders, gotHeaders); diff != "" { + t.Errorf("response headers mismatch (-want +got):\n%s", diff) + } + + // Check if Next() was called + if nextCalled != tc.expectNext { + t.Errorf("expected Next() called to be %v, got %v", tc.expectNext, nextCalled) + } + }) + } +} diff --git a/services/auth/go/middleware/logger.go b/internal/lib/oapi/middleware/logger.go similarity index 95% rename from services/auth/go/middleware/logger.go rename to internal/lib/oapi/middleware/logger.go index fb06e76ad..33efebaaa 100644 --- a/services/auth/go/middleware/logger.go +++ b/internal/lib/oapi/middleware/logger.go @@ -30,6 +30,7 @@ func LoggerFromContext(ctx context.Context) *slog.Logger { //nolint:contextcheck return logger } +// Logger is a Gin middleware that logs HTTP requests and responses using slog. func Logger(logger *slog.Logger) gin.HandlerFunc { return func(ctx *gin.Context) { startTime := time.Now() diff --git a/services/auth/go/middleware/tracing.go b/internal/lib/oapi/middleware/tracing.go similarity index 100% rename from services/auth/go/middleware/tracing.go rename to internal/lib/oapi/middleware/tracing.go diff --git a/internal/lib/oapi/oapi.go b/internal/lib/oapi/oapi.go new file mode 100644 index 000000000..daa719efc --- /dev/null +++ b/internal/lib/oapi/oapi.go @@ -0,0 +1,69 @@ +package oapi + +import ( + "fmt" + "log/slog" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/gin-gonic/gin" + "github.com/nhost/nhost/internal/lib/oapi/example/api" + "github.com/nhost/nhost/internal/lib/oapi/middleware" +) + +func surfaceErrorsMiddleWare(c *gin.Context) { + // this captures two cases as far as I can see: + // 1. request validation errors where the strict generated code fails + // to bind the request to the struct (i.e. "invalid param" test) + // 2. when a handler returns an error instead of a response + c.Next() + + if len(c.Errors) > 0 && !c.IsAborted() { + var errorCode string + switch c.Writer.Status() { + case http.StatusBadRequest: + errorCode = "bad-request" + default: + errorCode = "internal-server-error" + } + + c.JSON( + c.Writer.Status(), + gin.H{"errors": errorCode, "message": c.Errors[0].Error()}, + ) + } +} + +// NewRouter creates a Gin router with OpenAPI request validation middleware. +func NewRouter( + schema []byte, + apiPrefix string, + authenticationFunc openapi3filter.AuthenticationFunc, + corsOptions middleware.CORSOptions, + logger *slog.Logger, +) (*gin.Engine, func(c *gin.Context), error) { + router := gin.New() + + loader := openapi3.NewLoader() + + doc, err := loader.LoadFromData(schema) + if err != nil { + return nil, nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) + } + + doc.AddServer(&openapi3.Server{ //nolint:exhaustruct + URL: apiPrefix, + }) + + router.Use( + gin.Recovery(), + surfaceErrorsMiddleWare, + middleware.Logger(logger), + middleware.CORS(corsOptions), + ) + + mw := api.MiddlewareFunc(requestValidatorWithOptions(doc, authenticationFunc)) + + return router, mw, nil +} diff --git a/internal/lib/oapi/request.go b/internal/lib/oapi/request.go new file mode 100644 index 000000000..67f54129e --- /dev/null +++ b/internal/lib/oapi/request.go @@ -0,0 +1,128 @@ +package oapi + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/gin-gonic/gin" +) + +type ContextKey string + +const ( + GinContextKey ContextKey = "nhost-oapi/gin-context" +) + +func handleError(c *gin.Context, err error) { + var ( + errReq *openapi3filter.RequestError + errSchema *openapi3.SchemaError + errAuth *AuthenticatorError + errSec *openapi3filter.SecurityRequirementsError + ) + switch { + case errors.As(err, &errSchema): + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "schema-validation-error", + "reason": errSchema.Reason, + }) + case errors.As(err, &errReq): + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "request-validation-error", + "reason": errReq.Err.Error(), + }) + case errors.As(err, &errAuth): + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": errAuth.Code, + "reason": errAuth.Message, + "securityScheme": errAuth.Scheme, + }) + case errors.As(err, &errSec): + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "reason": errSec.Error(), + }) + default: + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func requestValidatorWithOptions( + swagger *openapi3.T, + authFn openapi3filter.AuthenticationFunc, +) gin.HandlerFunc { + router, err := gorillamux.NewRouter(swagger) + if err != nil { + panic(err) + } + + return func(c *gin.Context) { + if err := validateRequestFromContext(c, router, authFn); err != nil { + handleError(c, err) + } + + c.Next() + } +} + +func validateRequestFromContext( + c *gin.Context, + router routers.Router, + authFn openapi3filter.AuthenticationFunc, +) error { + route, pathParams, err := router.FindRoute(c.Request) + if err != nil { + var e *routers.RouteError + switch { + case errors.As(err, &e): + return e + default: + return fmt.Errorf("error validating route: %w", err) + } + } + + validationInput := &openapi3filter.RequestValidationInput{ //nolint:exhaustruct + Request: c.Request, + PathParams: pathParams, + Route: route, + Options: &openapi3filter.Options{ + AuthenticationFunc: authFn, + ExcludeRequestBody: false, + ExcludeRequestQueryParams: false, + ExcludeResponseBody: false, + ExcludeReadOnlyValidations: false, + ExcludeWriteOnlyValidations: false, + IncludeResponseStatus: false, + MultiError: false, + RegexCompiler: nil, + SkipSettingDefaults: false, + }, + } + + requestContext := context.WithValue(c.Request.Context(), GinContextKey, c) + if err := openapi3filter.ValidateRequest(requestContext, validationInput); err != nil { + return err //nolint:wrapcheck + } + + return nil +} + +func GetGinContext(c context.Context) *gin.Context { + v := c.Value(GinContextKey) + if v == nil { + return nil + } + + ginCtx, ok := v.(*gin.Context) + if !ok { + return nil + } + + return ginCtx +} diff --git a/nixops/lib/go/go.nix b/nixops/lib/go/go.nix index 2c6acb729..b698364e5 100644 --- a/nixops/lib/go/go.nix +++ b/nixops/lib/go/go.nix @@ -114,13 +114,13 @@ in echo "➜ Running golangci-lint" golangci-lint run \ --timeout 600s \ - ./${submodule}/... + ./... echo "➜ Running tests" richgo test \ -tags="${pkgs.lib.strings.concatStringsSep " " tags}" \ -ldflags="${pkgs.lib.strings.concatStringsSep " " ldflags}" \ - -v ${goTestFlags} ./${submodule}/... + -v ${goTestFlags} ./... ${extraCheck} diff --git a/services/auth/CLAUDE.md b/services/auth/CLAUDE.md index 4c9ed712a..17b850abb 100644 --- a/services/auth/CLAUDE.md +++ b/services/auth/CLAUDE.md @@ -115,13 +115,13 @@ import ( "context" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" ) func (ctrl *Controller) YourEndpoint( //nolint:ireturn ctx context.Context, request api.YourEndpointRequestObject, ) (api.YourEndpointResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) // Validate inputs if apiErr := ctrl.wf.ValidateInput(request.Body.Field, logger); apiErr != nil { diff --git a/services/auth/go/cmd/cors.go b/services/auth/go/cmd/cors.go deleted file mode 100644 index e4b19b6da..000000000 --- a/services/auth/go/cmd/cors.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmd - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func cors() gin.HandlerFunc { - f := func(c *gin.Context, origin string) { - c.Header("Access-Control-Allow-Origin", origin) - c.Header("Access-Control-Allow-Methods", "POST, GET") - headers := c.Request.Header.Get("Access-Control-Request-Headers") - c.Header("Access-Control-Allow-Headers", headers) - c.Header("Access-Control-Allow-Credentials", "true") - c.Header("Access-Control-Max-Age", "86400") - c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method") - } - - return func(c *gin.Context) { - origin := c.Request.Header.Get("Origin") - if c.Request.Method == http.MethodOptions { - f(c, origin) - - c.Header("Content-Length", "0") - c.AbortWithStatus(http.StatusNoContent) - } - - if origin != "" { - f(c, origin) - } - - c.Next() - } -} diff --git a/services/auth/go/cmd/serve.go b/services/auth/go/cmd/serve.go index 46e92b15e..01c6be8c7 100644 --- a/services/auth/go/cmd/serve.go +++ b/services/auth/go/cmd/serve.go @@ -8,9 +8,9 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" "github.com/gin-gonic/gin" + "github.com/nhost/nhost/internal/lib/oapi" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/docs" "github.com/nhost/nhost/services/auth/go/api" "github.com/nhost/nhost/services/auth/go/controller" @@ -21,7 +21,6 @@ import ( "github.com/nhost/nhost/services/auth/go/oidc" "github.com/nhost/nhost/services/auth/go/providers" "github.com/nhost/nhost/services/auth/go/sql" - ginmiddleware "github.com/oapi-codegen/gin-middleware" "github.com/urfave/cli/v3" ) @@ -1296,87 +1295,42 @@ func getDependencies( //nolint:ireturn return emailer, sms, jwtGetter, idTokenValidator, nil } -func getGoServer( //nolint:funlen +func getCORSOptions() oapimw.CORSOptions { + return oapimw.CORSOptions{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"POST", "GET"}, + AllowedHeaders: nil, + ExposedHeaders: []string{}, + AllowCredentials: true, + MaxAge: "86400", + } +} + +func getGoServer( ctx context.Context, cmd *cli.Command, db *sql.Queries, encrypter *crypto.Encrypter, logger *slog.Logger, ) (*http.Server, error) { - router := gin.New() - - loader := openapi3.NewLoader() - - doc, err := loader.LoadFromData(docs.OpenAPISchema) - if err != nil { - return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) - } - - doc.AddServer(&openapi3.Server{ //nolint:exhaustruct - URL: cmd.String(flagAPIPrefix), - }) - - handlers := []gin.HandlerFunc{ - // ginmiddleware.OapiRequestValidator(doc), - gin.Recovery(), - cors(), - middleware.Logger(logger), //nolint:contextcheck - } - - if cmd.Bool(flagRateLimitEnable) { - handlers = append(handlers, getRateLimiter(cmd, logger)) //nolint:contextcheck - } - - if cmd.String(flagTurnstileSecret) != "" { - handlers = append(handlers, middleware.Tunrstile( //nolint:contextcheck - cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix)), - ) - } - - router.Use(handlers...) - - config, err := getConfig(cmd) - if err != nil { - return nil, fmt.Errorf("problem creating config: %w", err) - } - - emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger) + ctrl, jwtGetter, err := getController(ctx, cmd, db, encrypter, logger) if err != nil { return nil, err } - oauthProviders, err := getOauth2Providers(ctx, cmd, logger) - if err != nil { - return nil, fmt.Errorf("problem creating oauth providers: %w", err) - } + handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{}) - ctrl, err := controller.New( - db, - config, - jwtGetter, - emailer, - smsClient, - hibp.NewClient(), - oauthProviders, - idTokenValidator, - controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now), - encrypter, - cmd.Root().Version, + router, mw, err := oapi.NewRouter( //nolint:contextcheck + docs.OpenAPISchema, + cmd.String(flagAPIPrefix), + jwtGetter.MiddlewareFunc, + getCORSOptions(), + logger, ) if err != nil { - return nil, fmt.Errorf("failed to create controller: %w", err) + return nil, fmt.Errorf("failed to create router: %w", err) } - handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{}) - mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions( - doc, - &ginmiddleware.Options{ //nolint:exhaustruct - Options: openapi3filter.Options{ //nolint:exhaustruct - AuthenticationFunc: jwtGetter.MiddlewareFunc, - }, - SilenceServersWarning: true, - }, - )) api.RegisterHandlersWithOptions( router, handler, @@ -1387,6 +1341,16 @@ func getGoServer( //nolint:funlen }, ) + if cmd.Bool(flagRateLimitEnable) { + router.Use(getRateLimiter(cmd, logger)) //nolint:contextcheck + } + + if cmd.String(flagTurnstileSecret) != "" { + router.Use(middleware.Tunrstile( //nolint:contextcheck + cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix), + )) + } + if cmd.Bool(flagEnableChangeEnv) { router.POST(cmd.String(flagAPIPrefix)+"/change-env", ctrl.PostChangeEnv) } @@ -1410,6 +1374,48 @@ func getGoServer( //nolint:funlen return server, nil } +func getController( + ctx context.Context, + cmd *cli.Command, + db *sql.Queries, + encrypter *crypto.Encrypter, + logger *slog.Logger, +) (*controller.Controller, *controller.JWTGetter, error) { + config, err := getConfig(cmd) + if err != nil { + return nil, nil, fmt.Errorf("problem creating config: %w", err) + } + + emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger) + if err != nil { + return nil, nil, err + } + + oauthProviders, err := getOauth2Providers(ctx, cmd, logger) + if err != nil { + return nil, nil, fmt.Errorf("problem creating oauth providers: %w", err) + } + + ctrl, err := controller.New( + db, + config, + jwtGetter, + emailer, + smsClient, + hibp.NewClient(), + oauthProviders, + idTokenValidator, + controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now), + encrypter, + cmd.Root().Version, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to create controller: %w", err) + } + + return ctrl, jwtGetter, nil +} + func serve(ctx context.Context, cmd *cli.Command) error { logger := getLogger(cmd.Bool(flagDebug), cmd.Bool(flagLogFormatTEXT)) logger.InfoContext(ctx, cmd.Root().Name+" v"+cmd.Root().Version) diff --git a/services/auth/go/controller/add_security_key.go b/services/auth/go/controller/add_security_key.go index b43298c97..dc47d1895 100644 --- a/services/auth/go/controller/add_security_key.go +++ b/services/auth/go/controller/add_security_key.go @@ -6,15 +6,15 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) AddSecurityKey( //nolint:ireturn ctx context.Context, _ api.AddSecurityKeyRequestObject, ) (api.AddSecurityKeyResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/change_user_email.go b/services/auth/go/controller/change_user_email.go index 5f8f81dc7..7dd8f0d24 100644 --- a/services/auth/go/controller/change_user_email.go +++ b/services/auth/go/controller/change_user_email.go @@ -3,15 +3,15 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" ) func (ctrl *Controller) ChangeUserEmail( //nolint:ireturn ctx context.Context, request api.ChangeUserEmailRequestObject, ) (api.ChangeUserEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger) if apiErr != nil { diff --git a/services/auth/go/controller/change_user_mfa_get.go b/services/auth/go/controller/change_user_mfa_get.go index 48b2ea8be..2a110f33c 100644 --- a/services/auth/go/controller/change_user_mfa_get.go +++ b/services/auth/go/controller/change_user_mfa_get.go @@ -3,15 +3,15 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) func (ctrl *Controller) ChangeUserMfa( //nolint:ireturn ctx context.Context, _ api.ChangeUserMfaRequestObject, ) (api.ChangeUserMfaResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.MfaEnabled { logger.WarnContext(ctx, "mfa disabled") diff --git a/services/auth/go/controller/change_user_password.go b/services/auth/go/controller/change_user_password.go index 8b2247f60..e4cb5cbca 100644 --- a/services/auth/go/controller/change_user_password.go +++ b/services/auth/go/controller/change_user_password.go @@ -5,8 +5,8 @@ import ( "log/slog" "github.com/golang-jwt/jwt/v5" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) postUserPasswordAuthenticated( //nolint:ireturn @@ -61,7 +61,7 @@ func (ctrl *Controller) ChangeUserPassword( //nolint:ireturn ctx context.Context, request api.ChangeUserPasswordRequestObject, ) (api.ChangeUserPasswordResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) jwtToken, ok := ctrl.wf.jwtGetter.FromContext(ctx) if ok { diff --git a/services/auth/go/controller/create_pat.go b/services/auth/go/controller/create_pat.go index 88cde01e1..497f11852 100644 --- a/services/auth/go/controller/create_pat.go +++ b/services/auth/go/controller/create_pat.go @@ -4,15 +4,15 @@ import ( "context" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) func (ctrl *Controller) CreatePAT( //nolint:ireturn ctx context.Context, request api.CreatePATRequestObject, ) (api.CreatePATResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger) if apiErr != nil { diff --git a/services/auth/go/controller/deanonymize_user.go b/services/auth/go/controller/deanonymize_user.go index fd1484537..b7df6cdc5 100644 --- a/services/auth/go/controller/deanonymize_user.go +++ b/services/auth/go/controller/deanonymize_user.go @@ -6,8 +6,8 @@ import ( "time" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" ) @@ -77,7 +77,7 @@ func (ctrl *Controller) postUserDeanonymizeValidateRequest( //nolint:cyclop func (ctrl *Controller) DeanonymizeUser( //nolint:funlen ctx context.Context, request api.DeanonymizeUserRequestObject, ) (api.DeanonymizeUserResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) userID, password, options, apiError := ctrl.postUserDeanonymizeValidateRequest( diff --git a/services/auth/go/controller/elevate_webauthn.go b/services/auth/go/controller/elevate_webauthn.go index 2ffa0b3db..343fe6056 100644 --- a/services/auth/go/controller/elevate_webauthn.go +++ b/services/auth/go/controller/elevate_webauthn.go @@ -3,15 +3,15 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) ElevateWebauthn( //nolint:ireturn ctx context.Context, _ api.ElevateWebauthnRequestObject, ) (api.ElevateWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/errors.go b/services/auth/go/controller/errors.go index 48d63e825..58f311677 100644 --- a/services/auth/go/controller/errors.go +++ b/services/auth/go/controller/errors.go @@ -21,8 +21,6 @@ func (e *APIError) Error() string { return fmt.Sprintf("API error: %s", e.t) } -var ErrElevatedClaimRequired = errors.New("elevated-claim-required") - var ( ErrJWTConfiguration = errors.New("jwt-configuration") diff --git a/services/auth/go/controller/get_provider_tokens.go b/services/auth/go/controller/get_provider_tokens.go index 6096941f3..90d9bba61 100644 --- a/services/auth/go/controller/get_provider_tokens.go +++ b/services/auth/go/controller/get_provider_tokens.go @@ -7,8 +7,8 @@ import ( "time" "github.com/jackc/pgx/v5" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -16,7 +16,7 @@ func (ctrl *Controller) GetProviderTokens( //nolint:ireturn ctx context.Context, req api.GetProviderTokensRequestObject, ) (api.GetProviderTokensResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) logger = logger.With("provider", req.Provider) user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger) diff --git a/services/auth/go/controller/get_user.go b/services/auth/go/controller/get_user.go index 6f76b4741..3b0a98cbf 100644 --- a/services/auth/go/controller/get_user.go +++ b/services/auth/go/controller/get_user.go @@ -4,15 +4,15 @@ import ( "context" "encoding/json" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/oapi-codegen/runtime/types" ) func (ctrl *Controller) GetUser( //nolint:ireturn ctx context.Context, _ api.GetUserRequestObject, ) (api.GetUserResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) // Get authenticated user from JWT user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger) diff --git a/services/auth/go/controller/jwt.go b/services/auth/go/controller/jwt.go index b3fa00e9f..3a386fb64 100644 --- a/services/auth/go/controller/jwt.go +++ b/services/auth/go/controller/jwt.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "log/slog" "reflect" @@ -17,8 +16,8 @@ import ( "github.com/getkin/kin-openapi/openapi3filter" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" + "github.com/nhost/nhost/internal/lib/oapi" "github.com/nhost/nhost/services/auth/go/api" - ginmiddleware "github.com/oapi-codegen/gin-middleware" ) const JWTContextKey = "nhost/auth/jwt" @@ -340,7 +339,7 @@ func (j *JWTGetter) Validate(accessToken string) (*jwt.Token, error) { func (j *JWTGetter) FromContext(ctx context.Context) (*jwt.Token, bool) { token, ok := ctx.Value(JWTContextKey).(*jwt.Token) if !ok { //nolint:nestif - c := ginmiddleware.GetGinContext(ctx) + c := oapi.GetGinContext(ctx) if c != nil { a, ok := c.Get(JWTContextKey) if !ok { @@ -415,16 +414,28 @@ func (j *JWTGetter) MiddlewareFunc( parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { - return errors.New("invalid authorization header") //nolint:err113 + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "missing or malformed authorization header", + } } jwtToken, err := j.Validate(parts[1]) if err != nil { - return fmt.Errorf("error validating token: %w", err) + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: fmt.Sprintf("error validating token: %s", err), + } } if !jwtToken.Valid { - return errors.New("invalid token") //nolint:err113 + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "invalid token", + } } if input.SecuritySchemeName == "BearerAuthElevated" { @@ -435,15 +446,23 @@ func (j *JWTGetter) MiddlewareFunc( found, err := j.verifyElevatedClaim(ctx, jwtToken, requestPath) if err != nil { - return fmt.Errorf("error verifying elevated claim: %w", err) + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: fmt.Sprintf("error verifying elevated claim: %s", err), + } } if !found { - return ErrElevatedClaimRequired + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "elevated claim required", + } } } - c := ginmiddleware.GetGinContext(ctx) + c := oapi.GetGinContext(ctx) c.Set(JWTContextKey, jwtToken) return nil diff --git a/services/auth/go/controller/jwt_test.go b/services/auth/go/controller/jwt_test.go index 32015ab85..afc50c4e8 100644 --- a/services/auth/go/controller/jwt_test.go +++ b/services/auth/go/controller/jwt_test.go @@ -3,7 +3,6 @@ package controller_test import ( "context" "crypto" - "errors" "log/slog" "net/http" "net/url" @@ -16,9 +15,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + "github.com/nhost/nhost/internal/lib/oapi" "github.com/nhost/nhost/services/auth/go/controller" "github.com/nhost/nhost/services/auth/go/controller/mock" - ginmiddleware "github.com/oapi-codegen/gin-middleware" "go.uber.org/mock/gomock" ) @@ -535,8 +534,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx SecurityScheme: nil, Scopes: []string{}, }, - expected: nil, - expectedErr: controller.ErrElevatedClaimRequired, + expected: nil, + expectedErr: &oapi.AuthenticatorError{ + Scheme: "BearerAuthElevated", + Code: "unauthorized", + Message: "elevated claim required", + }, }, { @@ -559,8 +562,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx SecurityScheme: nil, Scopes: []string{}, }, - expected: nil, - expectedErr: controller.ErrElevatedClaimRequired, + expected: nil, + expectedErr: &oapi.AuthenticatorError{ + Scheme: "BearerAuthElevated", + Code: "unauthorized", + Message: "elevated claim required", + }, }, { @@ -763,13 +770,13 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx //nolint ctx := context.WithValue( context.Background(), - ginmiddleware.GinContextKey, + oapi.GinContextKey, &gin.Context{}, ) err = jwtGetter.MiddlewareFunc(ctx, tc.request) - if !errors.Is(err, tc.expectedErr) { - t.Errorf("err = %v; want %v", err, tc.expectedErr) + if diff := cmp.Diff(err, tc.expectedErr); diff != "" { + t.Errorf("err mismatch (-want +got):\n%s", diff) } got, _ := jwtGetter.FromContext(ctx) diff --git a/services/auth/go/controller/link_id_token.go b/services/auth/go/controller/link_id_token.go index e1a47ba5f..06cb4c53f 100644 --- a/services/auth/go/controller/link_id_token.go +++ b/services/auth/go/controller/link_id_token.go @@ -3,14 +3,14 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive ctx context.Context, req api.LinkIdTokenRequestObject, ) (api.LinkIdTokenResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) profile, apiErr := ctrl.wf.GetOIDCProfileFromIDToken( ctx, diff --git a/services/auth/go/controller/refresh_provider_token.go b/services/auth/go/controller/refresh_provider_token.go index 227d53088..c86353efb 100644 --- a/services/auth/go/controller/refresh_provider_token.go +++ b/services/auth/go/controller/refresh_provider_token.go @@ -3,15 +3,15 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "golang.org/x/oauth2" ) func (ctrl *Controller) RefreshProviderToken( //nolint:ireturn ctx context.Context, req api.RefreshProviderTokenRequestObject, ) (api.RefreshProviderTokenResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) logger = logger.With("provider", req.Provider) provider := ctrl.Providers.Get(string(req.Provider)) diff --git a/services/auth/go/controller/refresh_token.go b/services/auth/go/controller/refresh_token.go index 453e1fcdf..1848f0ab9 100644 --- a/services/auth/go/controller/refresh_token.go +++ b/services/auth/go/controller/refresh_token.go @@ -4,15 +4,15 @@ import ( "context" "errors" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) func (ctrl *Controller) RefreshToken( //nolint:ireturn ctx context.Context, request api.RefreshTokenRequestObject, ) (api.RefreshTokenResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) user, apiErr := ctrl.wf.GetUserByRefreshTokenHash( ctx, diff --git a/services/auth/go/controller/send_password_reset_email.go b/services/auth/go/controller/send_password_reset_email.go index ad70065d6..7a5a3e4b3 100644 --- a/services/auth/go/controller/send_password_reset_email.go +++ b/services/auth/go/controller/send_password_reset_email.go @@ -6,8 +6,8 @@ import ( "log/slog" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" ) @@ -15,7 +15,7 @@ func (ctrl *Controller) SendPasswordResetEmail( //nolint:ireturn ctx context.Context, request api.SendPasswordResetEmailRequestObject, ) (api.SendPasswordResetEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) options, err := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger) diff --git a/services/auth/go/controller/send_verification_email.go b/services/auth/go/controller/send_verification_email.go index 87c47889e..2a23fbd94 100644 --- a/services/auth/go/controller/send_verification_email.go +++ b/services/auth/go/controller/send_verification_email.go @@ -6,8 +6,8 @@ import ( "log/slog" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" ) @@ -15,7 +15,7 @@ func (ctrl *Controller) SendVerificationEmail( //nolint:ireturn ctx context.Context, request api.SendVerificationEmailRequestObject, ) (api.SendVerificationEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger) diff --git a/services/auth/go/controller/sign_in_anonymous.go b/services/auth/go/controller/sign_in_anonymous.go index 0fa07fc3f..889aee055 100644 --- a/services/auth/go/controller/sign_in_anonymous.go +++ b/services/auth/go/controller/sign_in_anonymous.go @@ -5,8 +5,8 @@ import ( "log/slog" "slices" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) postSigninAnonymousValidateRequest( @@ -49,7 +49,7 @@ func (ctrl *Controller) postSigninAnonymousValidateRequest( func (ctrl *Controller) SignInAnonymous( //nolint:ireturn ctx context.Context, req api.SignInAnonymousRequestObject, ) (api.SignInAnonymousResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) req, apiErr := ctrl.postSigninAnonymousValidateRequest(ctx, req, logger) if apiErr != nil { diff --git a/services/auth/go/controller/sign_in_email_password.go b/services/auth/go/controller/sign_in_email_password.go index 477d76c9b..bbf906581 100644 --- a/services/auth/go/controller/sign_in_email_password.go +++ b/services/auth/go/controller/sign_in_email_password.go @@ -6,8 +6,8 @@ import ( "time" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn @@ -33,7 +33,7 @@ func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn func (ctrl *Controller) SignInEmailPassword( //nolint:ireturn ctx context.Context, request api.SignInEmailPasswordRequestObject, ) (api.SignInEmailPasswordResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) user, apiErr := ctrl.wf.GetUserByEmail(ctx, string(request.Body.Email), logger) diff --git a/services/auth/go/controller/sign_in_id_token.go b/services/auth/go/controller/sign_in_id_token.go index e3c19eb2c..f385f8fed 100644 --- a/services/auth/go/controller/sign_in_id_token.go +++ b/services/auth/go/controller/sign_in_id_token.go @@ -9,8 +9,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/oidc" "github.com/nhost/nhost/services/auth/go/sql" "github.com/oapi-codegen/runtime/types" @@ -45,7 +45,7 @@ func (ctrl *Controller) postSigninIdtokenCheckUserExists( func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive ctx context.Context, req api.SignInIdTokenRequestObject, ) (api.SignInIdTokenResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) profile, apiError := ctrl.wf.GetOIDCProfileFromIDToken( ctx, diff --git a/services/auth/go/controller/sign_in_otp_email.go b/services/auth/go/controller/sign_in_otp_email.go index 2136cc000..77fd83c46 100644 --- a/services/auth/go/controller/sign_in_otp_email.go +++ b/services/auth/go/controller/sign_in_otp_email.go @@ -8,8 +8,8 @@ import ( "math/big" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" ) @@ -26,7 +26,7 @@ func (ctrl *Controller) SignInOTPEmail( //nolint:ireturn ctx context.Context, request api.SignInOTPEmailRequestObject, ) (api.SignInOTPEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) if !ctrl.config.OTPEmailEnabled { diff --git a/services/auth/go/controller/sign_in_passwordless_email.go b/services/auth/go/controller/sign_in_passwordless_email.go index a49c9ed2d..7579bd4a8 100644 --- a/services/auth/go/controller/sign_in_passwordless_email.go +++ b/services/auth/go/controller/sign_in_passwordless_email.go @@ -9,8 +9,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/notifications" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -19,7 +19,7 @@ func (ctrl *Controller) SignInPasswordlessEmail( //nolint:ireturn ctx context.Context, request api.SignInPasswordlessEmailRequestObject, ) (api.SignInPasswordlessEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) if !ctrl.config.EmailPasswordlessEnabled { diff --git a/services/auth/go/controller/sign_in_passwordless_sms.go b/services/auth/go/controller/sign_in_passwordless_sms.go index ed2db174d..e4cd9ac60 100644 --- a/services/auth/go/controller/sign_in_passwordless_sms.go +++ b/services/auth/go/controller/sign_in_passwordless_sms.go @@ -9,8 +9,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -18,7 +18,7 @@ func (ctrl *Controller) SignInPasswordlessSms( //nolint:ireturn ctx context.Context, request api.SignInPasswordlessSmsRequestObject, ) (api.SignInPasswordlessSmsResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("phoneNumber", request.Body.PhoneNumber)) if !ctrl.config.SMSPasswordlessEnabled { diff --git a/services/auth/go/controller/sign_in_pat.go b/services/auth/go/controller/sign_in_pat.go index 3457a0caf..2ea8d220e 100644 --- a/services/auth/go/controller/sign_in_pat.go +++ b/services/auth/go/controller/sign_in_pat.go @@ -3,8 +3,8 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -12,7 +12,7 @@ func (ctrl *Controller) SignInPAT( //nolint:ireturn ctx context.Context, request api.SignInPATRequestObject, ) (api.SignInPATResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) user, apiErr := ctrl.wf.GetUserByRefreshTokenHash( ctx, diff --git a/services/auth/go/controller/sign_in_provider.go b/services/auth/go/controller/sign_in_provider.go index 503317470..1a358778f 100644 --- a/services/auth/go/controller/sign_in_provider.go +++ b/services/auth/go/controller/sign_in_provider.go @@ -7,8 +7,8 @@ import ( "time" "github.com/golang-jwt/jwt/v5" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) getSigninProviderValidateRequest( @@ -42,7 +42,7 @@ func (ctrl *Controller) SignInProvider( //nolint:ireturn ctx context.Context, req api.SignInProviderRequestObject, ) (api.SignInProviderResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("provider", string(req.Provider))) redirectTo, apiErr := ctrl.getSigninProviderValidateRequest(ctx, req, logger) diff --git a/services/auth/go/controller/sign_in_provider_callback_get.go b/services/auth/go/controller/sign_in_provider_callback_get.go index 51522ee43..543ae82e2 100644 --- a/services/auth/go/controller/sign_in_provider_callback_get.go +++ b/services/auth/go/controller/sign_in_provider_callback_get.go @@ -9,8 +9,8 @@ import ( "golang.org/x/oauth2" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/oidc" "github.com/nhost/nhost/services/auth/go/providers" "github.com/nhost/nhost/services/auth/go/sql" @@ -220,7 +220,7 @@ func (ctrl *Controller) signinProviderProviderCallback( ctx context.Context, req providerCallbackData, ) (*url.URL, *APIError) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) options, connnect, redirectTo, apiErr := ctrl.signinProviderProviderCallbackValidate( ctx, diff --git a/services/auth/go/controller/sign_in_webauthn.go b/services/auth/go/controller/sign_in_webauthn.go index d9f37ab0f..2e2d4ee35 100644 --- a/services/auth/go/controller/sign_in_webauthn.go +++ b/services/auth/go/controller/sign_in_webauthn.go @@ -7,8 +7,8 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -54,7 +54,7 @@ func (ctrl *Controller) SignInWebauthn( //nolint:ireturn ctx context.Context, request api.SignInWebauthnRequestObject, ) (api.SignInWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/sign_out.go b/services/auth/go/controller/sign_out.go index 75c80e507..ba07dfda2 100644 --- a/services/auth/go/controller/sign_out.go +++ b/services/auth/go/controller/sign_out.go @@ -3,14 +3,14 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) SignOut( //nolint:ireturn ctx context.Context, request api.SignOutRequestObject, ) (api.SignOutResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if deptr(request.Body.All) { userID, apiErr := ctrl.wf.GetJWTInContext(ctx, logger) diff --git a/services/auth/go/controller/sign_up_email_password.go b/services/auth/go/controller/sign_up_email_password.go index 2be8dae59..9f6c1bd9b 100644 --- a/services/auth/go/controller/sign_up_email_password.go +++ b/services/auth/go/controller/sign_up_email_password.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -45,7 +45,7 @@ func (ctrl *Controller) SignUpEmailPassword( //nolint:ireturn ctx context.Context, req api.SignUpEmailPasswordRequestObject, ) (api.SignUpEmailPasswordResponseObject, error) { - logger := middleware.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email))) + logger := oapimw.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email))) req, apiError := ctrl.postSignupEmailPasswordValidateRequest(ctx, req, logger) if apiError != nil { diff --git a/services/auth/go/controller/sign_up_webauthn.go b/services/auth/go/controller/sign_up_webauthn.go index 744398507..bec10f050 100644 --- a/services/auth/go/controller/sign_up_webauthn.go +++ b/services/auth/go/controller/sign_up_webauthn.go @@ -5,8 +5,8 @@ import ( "log/slog" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) postSignupWebauthnValidateRequest( @@ -42,7 +42,7 @@ func (ctrl *Controller) SignUpWebauthn( //nolint:ireturn ctx context.Context, request api.SignUpWebauthnRequestObject, ) (api.SignUpWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) options, apiErr := ctrl.postSignupWebauthnValidateRequest(ctx, request, logger) diff --git a/services/auth/go/controller/verify_add_security_key.go b/services/auth/go/controller/verify_add_security_key.go index a71d4d097..6266ced3f 100644 --- a/services/auth/go/controller/verify_add_security_key.go +++ b/services/auth/go/controller/verify_add_security_key.go @@ -5,8 +5,8 @@ import ( "encoding/base64" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -14,7 +14,7 @@ func (ctrl *Controller) VerifyAddSecurityKey( //nolint:ireturn ctx context.Context, request api.VerifyAddSecurityKeyRequestObject, ) (api.VerifyAddSecurityKeyResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/verify_change_user_mfa.go b/services/auth/go/controller/verify_change_user_mfa.go index 70527c59f..1046d5311 100644 --- a/services/auth/go/controller/verify_change_user_mfa.go +++ b/services/auth/go/controller/verify_change_user_mfa.go @@ -5,8 +5,8 @@ import ( "log/slog" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -99,7 +99,7 @@ func (ctrl *Controller) postUserMfaActivate( //nolint:ireturn func (ctrl *Controller) VerifyChangeUserMfa( //nolint:ireturn ctx context.Context, req api.VerifyChangeUserMfaRequestObject, ) (api.VerifyChangeUserMfaResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.MfaEnabled { logger.WarnContext(ctx, "mfa disabled") diff --git a/services/auth/go/controller/verify_elevate_webauthn.go b/services/auth/go/controller/verify_elevate_webauthn.go index 3e6ef94ba..9c3e5b129 100644 --- a/services/auth/go/controller/verify_elevate_webauthn.go +++ b/services/auth/go/controller/verify_elevate_webauthn.go @@ -7,8 +7,8 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -16,7 +16,7 @@ func (ctrl *Controller) VerifyElevateWebauthn( //nolint:ireturn ctx context.Context, request api.VerifyElevateWebauthnRequestObject, ) (api.VerifyElevateWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/verify_sign_in_mfa_totp.go b/services/auth/go/controller/verify_sign_in_mfa_totp.go index d8bd29e5e..94c1cf701 100644 --- a/services/auth/go/controller/verify_sign_in_mfa_totp.go +++ b/services/auth/go/controller/verify_sign_in_mfa_totp.go @@ -3,14 +3,14 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) VerifySignInMfaTotp( //nolint:ireturn ctx context.Context, req api.VerifySignInMfaTotpRequestObject, ) (api.VerifySignInMfaTotpResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.MfaEnabled { logger.WarnContext(ctx, "mfa disabled") diff --git a/services/auth/go/controller/verify_sign_in_otp_email.go b/services/auth/go/controller/verify_sign_in_otp_email.go index 1b3a62271..2df595f67 100644 --- a/services/auth/go/controller/verify_sign_in_otp_email.go +++ b/services/auth/go/controller/verify_sign_in_otp_email.go @@ -4,15 +4,15 @@ import ( "context" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) VerifySignInOTPEmail( //nolint:ireturn ctx context.Context, request api.VerifySignInOTPEmailRequestObject, ) (api.VerifySignInOTPEmailResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("email", string(request.Body.Email))) user, apiErr := ctrl.wf.GetUserByEmailAndTicket( diff --git a/services/auth/go/controller/verify_sign_in_passwordless_sms.go b/services/auth/go/controller/verify_sign_in_passwordless_sms.go index 587688943..df67e4aea 100644 --- a/services/auth/go/controller/verify_sign_in_passwordless_sms.go +++ b/services/auth/go/controller/verify_sign_in_passwordless_sms.go @@ -4,15 +4,15 @@ import ( "context" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) VerifySignInPasswordlessSms( //nolint:ireturn ctx context.Context, request api.VerifySignInPasswordlessSmsRequestObject, ) (api.VerifySignInPasswordlessSmsResponseObject, error) { - logger := middleware.LoggerFromContext(ctx). + logger := oapimw.LoggerFromContext(ctx). With(slog.String("phoneNumber", request.Body.PhoneNumber)) if !ctrl.config.SMSPasswordlessEnabled { diff --git a/services/auth/go/controller/verify_sign_in_webauthn.go b/services/auth/go/controller/verify_sign_in_webauthn.go index 8ef984337..bd116bc9d 100644 --- a/services/auth/go/controller/verify_sign_in_webauthn.go +++ b/services/auth/go/controller/verify_sign_in_webauthn.go @@ -10,8 +10,8 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) VerifySignInWebauthnUserHandle( @@ -70,7 +70,7 @@ func (ctrl *Controller) VerifySignInWebauthn( //nolint:ireturn ctx context.Context, request api.VerifySignInWebauthnRequestObject, ) (api.VerifySignInWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if !ctrl.config.WebauthnEnabled { logger.ErrorContext(ctx, "webauthn is disabled") diff --git a/services/auth/go/controller/verify_sign_up_webauthn.go b/services/auth/go/controller/verify_sign_up_webauthn.go index f35776ce7..c8581c172 100644 --- a/services/auth/go/controller/verify_sign_up_webauthn.go +++ b/services/auth/go/controller/verify_sign_up_webauthn.go @@ -11,8 +11,8 @@ import ( "github.com/go-webauthn/webauthn/webauthn" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -85,7 +85,7 @@ func (ctrl *Controller) VerifySignUpWebauthn( //nolint:ireturn ctx context.Context, request api.VerifySignUpWebauthnRequestObject, ) (api.VerifySignUpWebauthnResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) credData, options, nickname, apiErr := ctrl.postSignupWebauthnVerifyValidateRequest( ctx, diff --git a/services/auth/go/controller/verify_ticket.go b/services/auth/go/controller/verify_ticket.go index 4392c42a7..b53148536 100644 --- a/services/auth/go/controller/verify_ticket.go +++ b/services/auth/go/controller/verify_ticket.go @@ -7,8 +7,8 @@ import ( "net/url" "strings" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" "github.com/nhost/nhost/services/auth/go/sql" ) @@ -59,7 +59,7 @@ func (ctrl *Controller) getVerifyHandleTicketType( func (ctrl *Controller) VerifyTicket( //nolint:ireturn ctx context.Context, req api.VerifyTicketRequestObject, ) (api.VerifyTicketResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) user, ticketType, redirectTo, apiErr := ctrl.getVerifyValidateRequest(ctx, req, logger) switch { diff --git a/services/auth/go/controller/verify_token.go b/services/auth/go/controller/verify_token.go index 193812f35..932437708 100644 --- a/services/auth/go/controller/verify_token.go +++ b/services/auth/go/controller/verify_token.go @@ -3,14 +3,14 @@ package controller import ( "context" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/auth/go/api" - "github.com/nhost/nhost/services/auth/go/middleware" ) func (ctrl *Controller) VerifyToken( //nolint:ireturn ctx context.Context, request api.VerifyTokenRequestObject, ) (api.VerifyTokenResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) if request.Body != nil && request.Body.Token != nil { if apiErr := ctrl.wf.VerifyJWTToken(ctx, *request.Body.Token, logger); apiErr != nil { diff --git a/services/auth/project.nix b/services/auth/project.nix index 9fab4aa4f..298aecf7e 100644 --- a/services/auth/project.nix +++ b/services/auth/project.nix @@ -24,6 +24,8 @@ let ./vacuum.yaml ./vacuum-ignore.yaml + (inDirectory ../../internal/lib/oapi) + ./go/api/server.cfg.yaml ./go/api/types.cfg.yaml ./go/sql/schema.sh diff --git a/services/auth/test/routes/pat/pat.test.ts b/services/auth/test/routes/pat/pat.test.ts index 48d15fd1e..73b489b13 100644 --- a/services/auth/test/routes/pat/pat.test.ts +++ b/services/auth/test/routes/pat/pat.test.ts @@ -47,7 +47,7 @@ describe('personal access token', () => { await request .post('/pat') .send({ expiresAt: new Date() }) - .expect(StatusCodes.BAD_REQUEST); + .expect(StatusCodes.UNAUTHORIZED); }); test('should be able to add metadata to a personal access token', async () => { @@ -65,7 +65,6 @@ describe('personal access token', () => { expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), metadata: { name: 'Test PAT' }, }) - .expect(StatusCodes.OK); const { rows } = await client.query( 'SELECT * FROM auth.refresh_tokens WHERE refresh_token_hash=$1;', diff --git a/services/auth/test/routes/user/email.test.ts b/services/auth/test/routes/user/email.test.ts index f46ee6008..91e92d11e 100644 --- a/services/auth/test/routes/user/email.test.ts +++ b/services/auth/test/routes/user/email.test.ts @@ -69,7 +69,7 @@ describe('user email', () => { .post('/user/email/change') // .set('Authorization', `Bearer ${accessToken}`) .send({ newEmail }) - .expect(StatusCodes.BAD_REQUEST); + .expect(StatusCodes.UNAUTHORIZED); await request .post('/user/email/change') diff --git a/services/auth/test/routes/user/user.test.ts b/services/auth/test/routes/user/user.test.ts index ffe8d1f57..7c210a209 100644 --- a/services/auth/test/routes/user/user.test.ts +++ b/services/auth/test/routes/user/user.test.ts @@ -27,7 +27,7 @@ describe('user password', () => { }); it('should not get user data if not signed in', async () => { - await request.get('/user').expect(StatusCodes.BAD_REQUEST); + await request.get('/user').expect(StatusCodes.UNAUTHORIZED); }); it('should get user data if signed in', async () => { diff --git a/services/storage/cmd/serve.go b/services/storage/cmd/serve.go index a0ae07149..3c8474f67 100644 --- a/services/storage/cmd/serve.go +++ b/services/storage/cmd/serve.go @@ -11,10 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "github.com/nhost/nhost/internal/lib/oapi" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/controller" "github.com/nhost/nhost/services/storage/image" @@ -23,7 +22,6 @@ import ( "github.com/nhost/nhost/services/storage/middleware/cdn/fastly" "github.com/nhost/nhost/services/storage/migrations" "github.com/nhost/nhost/services/storage/storage" - ginmiddleware "github.com/oapi-codegen/gin-middleware" "github.com/urfave/cli/v3" ) @@ -53,88 +51,39 @@ const ( flagHasuraDBName = "hasura-db-name" ) -func getCorsMiddleware( - corsAllowOrigins []string, - corsAllowCredentials bool, -) gin.HandlerFunc { - return cors.New(cors.Config{ //nolint:exhaustruct - AllowOrigins: corsAllowOrigins, - AllowMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"}, - AllowHeaders: []string{ +func getCORSOptions(cmd *cli.Command) oapimw.CORSOptions { + return oapimw.CORSOptions{ + AllowedOrigins: cmd.StringSlice(flagCorsAllowOrigins), + AllowedMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"}, + AllowedHeaders: []string{ "Authorization", "Origin", "if-match", "if-none-match", "if-modified-since", "if-unmodified-since", "x-hasura-admin-secret", "x-nhost-bucket-id", "x-nhost-file-name", "x-nhost-file-id", "x-hasura-role", }, - ExposeHeaders: []string{ + ExposedHeaders: []string{ "Content-Length", "Content-Type", "Cache-Control", "ETag", "Last-Modified", "X-Error", }, - AllowCredentials: corsAllowCredentials, - MaxAge: 12 * time.Hour, //nolint: mnd - }) + AllowCredentials: cmd.Bool(flagCorsAllowCredentials), + MaxAge: "86400", + } } -func getGin( //nolint:funlen - bind string, - publicURL string, - apiRootPrefix string, - hasuraAdminSecret string, +func getServer( + cmd *cli.Command, metadataStorage controller.MetadataStorage, contentStorage controller.ContentStorage, imageTransformer *image.Transformer, logger *slog.Logger, - debug bool, - corsAllowOrigins []string, - corsAllowCredentials bool, - fastlyService string, - fastlyKey string, - clamavServer string, ) (*http.Server, error) { - router := gin.New() - - router.GET("/healthz", func(c *gin.Context) { - c.String(http.StatusOK, "ok") - }) - - if !debug { - gin.SetMode(gin.ReleaseMode) - } - - loader := openapi3.NewLoader() - - doc, err := loader.LoadFromData(controller.OpenAPISchema) - if err != nil { - return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) - } - - doc.AddServer(&openapi3.Server{ //nolint:exhaustruct - URL: apiRootPrefix, - }) - - handlers := []gin.HandlerFunc{ - middleware.Logger(logger), - getCorsMiddleware(corsAllowOrigins, corsAllowCredentials), - gin.Recovery(), - } - - if fastlyService != "" { - logger.InfoContext(context.Background(), "enabling fastly middleware") - handlers = append( - handlers, - fastly.New(fastlyService, fastlyKey, logger), - ) - } - - router.Use(handlers...) - - av, err := getAv(clamavServer) + av, err := getAv(cmd.String(flagClamavServer)) if err != nil { return nil, fmt.Errorf("problem trying to get av: %w", err) } ctrl := controller.New( - publicURL, - apiRootPrefix, - hasuraAdminSecret, + cmd.String(flagPublicURL), + cmd.String(flagAPIRootPrefix), + cmd.String(flagHasuraAdminSecret), metadataStorage, contentStorage, imageTransformer, @@ -143,27 +92,41 @@ func getGin( //nolint:funlen ) handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{}) - mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions( - doc, - &ginmiddleware.Options{ //nolint:exhaustruct - Options: openapi3filter.Options{ //nolint:exhaustruct - AuthenticationFunc: middleware.AuthenticationFunc(hasuraAdminSecret), - }, - SilenceServersWarning: true, - }, - )) + + router, mw, err := oapi.NewRouter( + controller.OpenAPISchema, + cmd.String(flagAPIRootPrefix), + middleware.AuthenticationFunc(cmd.String(flagHasuraAdminSecret)), + getCORSOptions(cmd), + logger, + ) + if err != nil { + return nil, fmt.Errorf("failed to create router: %w", err) + } + + router.GET("/healthz", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + if cmd.String(flagFastlyService) != "" { + logger.InfoContext(context.Background(), "enabling fastly middleware") + router.Use( + fastly.New(cmd.String(flagFastlyService), cmd.String(flagFastlyKey), logger), + ) + } + api.RegisterHandlersWithOptions( router, handler, api.GinServerOptions{ - BaseURL: apiRootPrefix, + BaseURL: cmd.String(flagAPIRootPrefix), Middlewares: []api.MiddlewareFunc{mw}, ErrorHandler: nil, }, ) server := &http.Server{ //nolint:exhaustruct - Addr: bind, + Addr: cmd.String(flagBind), Handler: router, ReadHeaderTimeout: 5 * time.Second, //nolint:mnd } @@ -453,21 +416,12 @@ func serve(ctx context.Context, cmd *cli.Command) error { //nolint:funlen cmd.String(flagHasuraEndpoint) + "/graphql", ) - server, err := getGin( //nolint: contextcheck - cmd.String(flagBind), - cmd.String(flagPublicURL), - cmd.String(flagAPIRootPrefix), - cmd.String(flagHasuraAdminSecret), + server, err := getServer( //nolint: contextcheck + cmd, metadataStorage, contentStorage, imageTransformer, logger, - cmd.Bool(flagDebug), - cmd.StringSlice(flagCorsAllowOrigins), - cmd.Bool(flagCorsAllowCredentials), - cmd.String(flagFastlyService), - cmd.String(flagFastlyKey), - cmd.String(flagClamavServer), ) if err != nil { return err diff --git a/services/storage/controller/delete_broken_metadata.go b/services/storage/controller/delete_broken_metadata.go index 20e0a2217..c56301e97 100644 --- a/services/storage/controller/delete_broken_metadata.go +++ b/services/storage/controller/delete_broken_metadata.go @@ -4,8 +4,8 @@ import ( "context" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" - "github.com/nhost/nhost/services/storage/middleware" ) func (ctrl *Controller) deleteBrokenMetadata( @@ -29,7 +29,7 @@ func (ctrl *Controller) DeleteBrokenMetadata( //nolint:ireturn ctx context.Context, _ api.DeleteBrokenMetadataRequestObject, ) (api.DeleteBrokenMetadataResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) files, apiErr := ctrl.deleteBrokenMetadata(ctx) if apiErr != nil { diff --git a/services/storage/controller/delete_file.go b/services/storage/controller/delete_file.go index d3934b4cc..d5c0687f3 100644 --- a/services/storage/controller/delete_file.go +++ b/services/storage/controller/delete_file.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/middleware" "github.com/nhost/nhost/services/storage/middleware/cdn/fastly" @@ -13,7 +14,7 @@ func (ctrl *Controller) DeleteFile( //nolint:ireturn ctx context.Context, request api.DeleteFileRequestObject, ) (api.DeleteFileResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) sessionHeaders := middleware.SessionHeadersFromContext(ctx) apiErr := ctrl.metadataStorage.DeleteFileByID(ctx, request.Id, sessionHeaders) diff --git a/services/storage/controller/delete_orphaned_files.go b/services/storage/controller/delete_orphaned_files.go index 71fd83336..d51be3529 100644 --- a/services/storage/controller/delete_orphaned_files.go +++ b/services/storage/controller/delete_orphaned_files.go @@ -4,8 +4,8 @@ import ( "context" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" - "github.com/nhost/nhost/services/storage/middleware" ) func (ctrl *Controller) deleteOrphans(ctx context.Context) ([]string, *APIError) { @@ -27,7 +27,7 @@ func (ctrl *Controller) DeleteOrphanedFiles( //nolint:ireturn ctx context.Context, _ api.DeleteOrphanedFilesRequestObject, ) (api.DeleteOrphanedFilesResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) files, apiErr := ctrl.deleteOrphans(ctx) if apiErr != nil { diff --git a/services/storage/controller/get_file.go b/services/storage/controller/get_file.go index 5d4f2e797..b4b20fb3b 100644 --- a/services/storage/controller/get_file.go +++ b/services/storage/controller/get_file.go @@ -12,6 +12,7 @@ import ( "strings" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/image" "github.com/nhost/nhost/services/storage/middleware" @@ -324,7 +325,7 @@ func (ctrl *Controller) GetFile( //nolint:ireturn ctx context.Context, request api.GetFileRequestObject, ) (api.GetFileResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) sessionHeaders := middleware.SessionHeadersFromContext(ctx) acceptHeader := middleware.AcceptHeaderFromContext(ctx) diff --git a/services/storage/controller/get_file_presigned_url.go b/services/storage/controller/get_file_presigned_url.go index 4b2b61c0f..136460134 100644 --- a/services/storage/controller/get_file_presigned_url.go +++ b/services/storage/controller/get_file_presigned_url.go @@ -7,6 +7,7 @@ import ( "log/slog" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/middleware" ) @@ -24,7 +25,7 @@ type GetFilePresignedURLRequest struct { func (ctrl *Controller) GetFilePresignedURL( //nolint:ireturn ctx context.Context, request api.GetFilePresignedURLRequestObject, ) (api.GetFilePresignedURLResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) logger = logger.With("file_id", request.Id) sessionHeaders := middleware.SessionHeadersFromContext(ctx) diff --git a/services/storage/controller/get_file_with_presigned_url.go b/services/storage/controller/get_file_with_presigned_url.go index 5059af6f2..908af091b 100644 --- a/services/storage/controller/get_file_with_presigned_url.go +++ b/services/storage/controller/get_file_with_presigned_url.go @@ -11,6 +11,7 @@ import ( "strconv" "time" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/middleware" ) @@ -151,7 +152,7 @@ func (ctrl *Controller) GetFileWithPresignedURL( //nolint: ireturn ctx context.Context, request api.GetFileWithPresignedURLRequestObject, ) (api.GetFileWithPresignedURLResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) acceptHeader := middleware.AcceptHeaderFromContext(ctx) fileMetadata, _, apiErr := ctrl.getFileMetadata( diff --git a/services/storage/controller/list_broken_metadata.go b/services/storage/controller/list_broken_metadata.go index 176a7c4a8..951c40b70 100644 --- a/services/storage/controller/list_broken_metadata.go +++ b/services/storage/controller/list_broken_metadata.go @@ -5,8 +5,8 @@ import ( "log/slog" "path" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" - "github.com/nhost/nhost/services/storage/middleware" ) type ListBrokenMetadataResponse struct { @@ -60,7 +60,7 @@ func fileListSummary(files []FileSummary) *[]api.FileSummary { func (ctrl *Controller) ListBrokenMetadata( //nolint:ireturn ctx context.Context, _ api.ListBrokenMetadataRequestObject, ) (api.ListBrokenMetadataResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) files, apiErr := ctrl.listBrokenMetadata(ctx) if apiErr != nil { diff --git a/services/storage/controller/list_files_not_uploaded.go b/services/storage/controller/list_files_not_uploaded.go index 8a252f21c..41f019c93 100644 --- a/services/storage/controller/list_files_not_uploaded.go +++ b/services/storage/controller/list_files_not_uploaded.go @@ -7,8 +7,8 @@ import ( "net/http" "github.com/gin-gonic/gin" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" - "github.com/nhost/nhost/services/storage/middleware" ) func (ctrl *Controller) listNotUploaded(ctx context.Context) ([]FileSummary, *APIError) { @@ -52,7 +52,7 @@ func (ctrl *Controller) ListNotUploaded(ctx *gin.Context) { func (ctrl *Controller) ListFilesNotUploaded( //nolint:ireturn ctx context.Context, _ api.ListFilesNotUploadedRequestObject, ) (api.ListFilesNotUploadedResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) files, apiErr := ctrl.listNotUploaded(ctx) if apiErr != nil { diff --git a/services/storage/controller/list_orphaned_files.go b/services/storage/controller/list_orphaned_files.go index ee961cc10..90553a11a 100644 --- a/services/storage/controller/list_orphaned_files.go +++ b/services/storage/controller/list_orphaned_files.go @@ -6,8 +6,8 @@ import ( "net/http" "path" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" - "github.com/nhost/nhost/services/storage/middleware" ) func (ctrl *Controller) listOrphans(ctx context.Context) ([]string, *APIError) { @@ -47,7 +47,7 @@ func (ctrl *Controller) listOrphans(ctx context.Context) ([]string, *APIError) { func (ctrl *Controller) ListOrphanedFiles( //nolint:ireturn ctx context.Context, _ api.ListOrphanedFilesRequestObject, ) (api.ListOrphanedFilesResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) files, apiErr := ctrl.listOrphans(ctx) if apiErr != nil { diff --git a/services/storage/controller/replace_file.go b/services/storage/controller/replace_file.go index 5d430580c..22dd90c1a 100644 --- a/services/storage/controller/replace_file.go +++ b/services/storage/controller/replace_file.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/middleware" "github.com/nhost/nhost/services/storage/middleware/cdn/fastly" @@ -67,7 +68,7 @@ func (ctrl *Controller) ReplaceFile( //nolint:funlen,ireturn ctx context.Context, request api.ReplaceFileRequestObject, ) (api.ReplaceFileResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) sessionHeaders := middleware.SessionHeadersFromContext(ctx) file, apiErr := replaceFileParseRequest(request) diff --git a/services/storage/controller/upload_files.go b/services/storage/controller/upload_files.go index 8cff145fc..20dd476c9 100644 --- a/services/storage/controller/upload_files.go +++ b/services/storage/controller/upload_files.go @@ -10,6 +10,7 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/google/uuid" + oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware" "github.com/nhost/nhost/services/storage/api" "github.com/nhost/nhost/services/storage/middleware" ) @@ -249,7 +250,7 @@ func parseUploadRequest(form *multipart.Form) (uploadFileRequest, *APIError) { func (ctrl *Controller) UploadFiles( //nolint:ireturn ctx context.Context, request api.UploadFilesRequestObject, ) (api.UploadFilesResponseObject, error) { - logger := middleware.LoggerFromContext(ctx) + logger := oapimw.LoggerFromContext(ctx) sessionHeaders := middleware.SessionHeadersFromContext(ctx) form, err := request.Body.ReadForm(maxFormMemory) diff --git a/services/storage/middleware/headers.go b/services/storage/middleware/headers.go index e99c16a1b..34db14e6c 100644 --- a/services/storage/middleware/headers.go +++ b/services/storage/middleware/headers.go @@ -2,18 +2,15 @@ package middleware import ( "context" - "errors" "net/http" "strings" "github.com/getkin/kin-openapi/openapi3filter" - ginmiddleware "github.com/oapi-codegen/gin-middleware" + "github.com/nhost/nhost/internal/lib/oapi" ) const HeadersContextKey = "request.headers" -var ErrUnauthorized = errors.New("unauthorized") - func SessionHeadersFromContext(ctx context.Context) http.Header { headers, _ := ctx.Value(HeadersContextKey).(http.Header) @@ -51,11 +48,15 @@ func AuthenticationFunc(adminSecret string) openapi3filter.AuthenticationFunc { "X-Hasura-Admin-Secret", ) if adminSecretHeader != adminSecret { - return ErrUnauthorized + return &oapi.AuthenticatorError{ + Scheme: input.SecuritySchemeName, + Code: "unauthorized", + Message: "invalid credentials", + } } } - c := ginmiddleware.GetGinContext(ctx) + c := oapi.GetGinContext(ctx) c.Set(HeadersContextKey, input.RequestValidationInput.Request.Header) return nil diff --git a/services/storage/middleware/logger.go b/services/storage/middleware/logger.go deleted file mode 100644 index fb06e76ad..000000000 --- a/services/storage/middleware/logger.go +++ /dev/null @@ -1,79 +0,0 @@ -package middleware - -import ( - "context" - "log/slog" - "time" - - "github.com/gin-gonic/gin" -) - -type loggerCtxKey struct{} - -// Stores the logger in the context. -func LoggerToContext(ctx context.Context, logger *slog.Logger) context.Context { - return context.WithValue(ctx, loggerCtxKey{}, logger) -} - -// Retrieves the logger from the context. It creates a new one if it can't be found. -func LoggerFromContext(ctx context.Context) *slog.Logger { //nolint:contextcheck - ginCtx, ok := ctx.(*gin.Context) - if ok { - ctx = ginCtx.Request.Context() - } - - logger, ok := ctx.Value(loggerCtxKey{}).(*slog.Logger) - if !ok { - return slog.Default() - } - - return logger -} - -func Logger(logger *slog.Logger) gin.HandlerFunc { - return func(ctx *gin.Context) { - startTime := time.Now() - - trace := TraceFromHTTPHeaders(ctx.Request.Header) - - clientIP := ctx.ClientIP() - reqMethod := ctx.Request.Method - reqURL := ctx.Request.RequestURI - logger := logger.With( - slog.Group( - "trace", - slog.String("trace_id", trace.TraceID), - slog.String("span_id", trace.SpanID), - slog.String("parent_span_id", trace.ParentSpanID), - ), - slog.Group( - "request", - slog.String("client_ip", clientIP), - slog.String("method", reqMethod), - slog.String("url", reqURL), - ), - ) - ctx.Request = ctx.Request.WithContext( - LoggerToContext(ctx.Request.Context(), logger), - ) - ctx.Next() - - latencyTime := time.Since(startTime) - statusCode := ctx.Writer.Status() - - logger = logger.With(slog.Group( - "response", - slog.Int("status_code", statusCode), - slog.Duration("latency_time", latencyTime), - slog.Any("errors", ctx.Errors.Errors()), - )) - - TraceToHTTPHeaders(trace, ctx.Writer.Header()) - - if len(ctx.Errors.Errors()) > 0 { - logger.ErrorContext(ctx, "call completed with errors") - } else { - logger.InfoContext(ctx, "call completed") - } - } -} diff --git a/services/storage/middleware/tracing.go b/services/storage/middleware/tracing.go deleted file mode 100644 index e0dd790ec..000000000 --- a/services/storage/middleware/tracing.go +++ /dev/null @@ -1,63 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/google/uuid" -) - -const ( - headerTraceID = "X-B3-TraceId" - headerSpanID = "X-B3-SpanId" - headerParentSpanID = "X-B3-ParentSpanId" -) - -type Trace struct { - TraceID string - ParentSpanID string - SpanID string -} - -// NewTrace creates a new trace with a new `TraceID`. -func NewTrace() Trace { - return Trace{ - TraceID: uuid.New().String(), - ParentSpanID: "", - SpanID: "", - } -} - -// NewSpan create a new trace with the same `TraceID`, `ParentSpanID` set as the current `SpanID` -// and a new `SpanID`. -func (t Trace) NewSpan() Trace { - return Trace{ - TraceID: t.TraceID, - ParentSpanID: t.SpanID, - SpanID: uuid.New().String(), - } -} - -// FromHTTPHeaders extracts tracing information from HTTP headers. -// If no tracing information is found, a new trace is created with only `TraceID` set. -func TraceFromHTTPHeaders(headers http.Header) Trace { - traceID := headers.Get(headerTraceID) - if traceID == "" { - traceID = uuid.New().String() - } - - spanID := headers.Get(headerSpanID) - parentSpanID := headers.Get(headerParentSpanID) - - return Trace{ - TraceID: traceID, - ParentSpanID: parentSpanID, - SpanID: spanID, - } -} - -// ToHTTPHeaders adds tracing information to HTTP headers. -func TraceToHTTPHeaders(trace Trace, header http.Header) { - header.Set(headerTraceID, trace.TraceID) - header.Set(headerParentSpanID, trace.ParentSpanID) - header.Set(headerSpanID, trace.SpanID) -} diff --git a/services/storage/project.nix b/services/storage/project.nix index 99fb4e486..eb2b27e8f 100644 --- a/services/storage/project.nix +++ b/services/storage/project.nix @@ -33,6 +33,8 @@ let (inDirectory "${submodule}/client/testdata") (inDirectory "${submodule}/image/testdata") (inDirectory "${submodule}/storage/testdata") + + (inDirectory ../../internal/lib/oapi) ]; exclude = with nix-filter.lib; [ diff --git a/tools/codegen/.golangci.yaml b/tools/codegen/.golangci.yaml deleted file mode 100644 index eb20287cb..000000000 --- a/tools/codegen/.golangci.yaml +++ /dev/null @@ -1,55 +0,0 @@ -version: "2" -linters: - default: all - settings: - funlen: - lines: 65 - disable: - - canonicalheader - - depguard - - gomoddirectives - - musttag - - nlreturn - - tagliatelle - - varnamelen - - wsl - - noinlineerr - - funcorder - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - rules: - - linters: - - funlen - - ireturn - path: _test\.go - - linters: - - lll - source: '^//go:generate ' - - linters: - - gochecknoglobals - text: Version is a global variable - - linters: - - ireturn - - lll - path: schema\.resolvers\.go - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - enable: - - gofmt - - gofumpt - - goimports - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ - - schema\.resolvers\.go diff --git a/tools/codegen/project.nix b/tools/codegen/project.nix index 7c368e08a..da2ef5e28 100644 --- a/tools/codegen/project.nix +++ b/tools/codegen/project.nix @@ -11,7 +11,7 @@ let "go.mod" "go.sum" (inDirectory "vendor") - "${submodule}/.golangci.yaml" + ".golangci.yaml" isDirectory (and (inDirectory submodule) diff --git a/vendor/github.com/gin-contrib/cors/.gitignore b/vendor/github.com/gin-contrib/cors/.gitignore deleted file mode 100644 index 002df848c..000000000 --- a/vendor/github.com/gin-contrib/cors/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -*.o -*.a -*.so - -_obj -_test - -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof - -coverage.out - -.idea diff --git a/vendor/github.com/gin-contrib/cors/.golangci.yml b/vendor/github.com/gin-contrib/cors/.golangci.yml deleted file mode 100644 index d59c99bd4..000000000 --- a/vendor/github.com/gin-contrib/cors/.golangci.yml +++ /dev/null @@ -1,39 +0,0 @@ -linters: - enable-all: false - disable-all: true - fast: false - enable: - - bodyclose - - dogsled - - dupl - - errcheck - - exportloopref - - exhaustive - - gochecknoinits - - goconst - - gocritic - - gocyclo - - gofmt - - goimports - - goprintffuncname - - gosec - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - noctx - - nolintlint - - rowserrcheck - - staticcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - whitespace - - gofumpt - -run: - timeout: 3m diff --git a/vendor/github.com/gin-contrib/cors/.goreleaser.yaml b/vendor/github.com/gin-contrib/cors/.goreleaser.yaml deleted file mode 100644 index 01b1081cf..000000000 --- a/vendor/github.com/gin-contrib/cors/.goreleaser.yaml +++ /dev/null @@ -1,28 +0,0 @@ -builds: - - # If true, skip the build. - # Useful for library projects. - # Default is false - skip: true - -changelog: - use: github - groups: - - title: Features - regexp: "^.*feat[(\\w)]*:+.*$" - order: 0 - - title: "Bug fixes" - regexp: "^.*fix[(\\w)]*:+.*$" - order: 1 - - title: "Enhancements" - regexp: "^.*chore[(\\w)]*:+.*$" - order: 2 - - title: "Refactor" - regexp: "^.*refactor[(\\w)]*:+.*$" - order: 3 - - title: "Build process updates" - regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ - order: 4 - - title: "Documentation updates" - regexp: ^.*?docs?(\(.+\))??!?:.+$ - order: 4 - - title: Others diff --git a/vendor/github.com/gin-contrib/cors/LICENSE b/vendor/github.com/gin-contrib/cors/LICENSE deleted file mode 100644 index 4e2cfb015..000000000 --- a/vendor/github.com/gin-contrib/cors/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Gin-Gonic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/gin-contrib/cors/README.md b/vendor/github.com/gin-contrib/cors/README.md deleted file mode 100644 index d43523295..000000000 --- a/vendor/github.com/gin-contrib/cors/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# CORS gin's middleware - -[![Run Tests](https://github.com/gin-contrib/cors/actions/workflows/go.yml/badge.svg)](https://github.com/gin-contrib/cors/actions/workflows/go.yml) -[![codecov](https://codecov.io/gh/gin-contrib/cors/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/cors) -[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/cors)](https://goreportcard.com/report/github.com/gin-contrib/cors) -[![GoDoc](https://godoc.org/github.com/gin-contrib/cors?status.svg)](https://godoc.org/github.com/gin-contrib/cors) - -Gin middleware/handler to enable CORS support. - -## Usage - -### Start using it - -Download and install it: - -```sh -go get github.com/gin-contrib/cors -``` - -Import it in your code: - -```go -import "github.com/gin-contrib/cors" -``` - -### Canonical example - -```go -package main - -import ( - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -func main() { - router := gin.Default() - // CORS for https://foo.com and https://github.com origins, allowing: - // - PUT and PATCH methods - // - Origin header - // - Credentials share - // - Preflight requests cached for 12 hours - router.Use(cors.New(cors.Config{ - AllowOrigins: []string{"https://foo.com"}, - AllowMethods: []string{"PUT", "PATCH"}, - AllowHeaders: []string{"Origin"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - AllowOriginFunc: func(origin string) bool { - return origin == "https://github.com" - }, - MaxAge: 12 * time.Hour, - })) - router.Run() -} -``` - -### Using DefaultConfig as start point - -```go -func main() { - router := gin.Default() - // - No origin allowed by default - // - GET,POST, PUT, HEAD methods - // - Credentials share disabled - // - Preflight requests cached for 12 hours - config := cors.DefaultConfig() - config.AllowOrigins = []string{"http://google.com"} - // config.AllowOrigins = []string{"http://google.com", "http://facebook.com"} - // config.AllowAllOrigins = true - - router.Use(cors.New(config)) - router.Run() -} -``` - -Note: while Default() allows all origins, DefaultConfig() does not and you will still have to use AllowAllOrigins. - -### Default() allows all origins - -```go -func main() { - router := gin.Default() - // same as - // config := cors.DefaultConfig() - // config.AllowAllOrigins = true - // router.Use(cors.New(config)) - router.Use(cors.Default()) - router.Run() -} -``` - -Using all origins disables the ability for Gin to set cookies for clients. When dealing with credentials, don't allow all origins. diff --git a/vendor/github.com/gin-contrib/cors/config.go b/vendor/github.com/gin-contrib/cors/config.go deleted file mode 100644 index 8a295e3db..000000000 --- a/vendor/github.com/gin-contrib/cors/config.go +++ /dev/null @@ -1,155 +0,0 @@ -package cors - -import ( - "net/http" - "strings" - - "github.com/gin-gonic/gin" -) - -type cors struct { - allowAllOrigins bool - allowCredentials bool - allowOriginFunc func(string) bool - allowOriginWithContextFunc func(*gin.Context, string) bool - allowOrigins []string - normalHeaders http.Header - preflightHeaders http.Header - wildcardOrigins [][]string - optionsResponseStatusCode int -} - -var ( - DefaultSchemas = []string{ - "http://", - "https://", - } - ExtensionSchemas = []string{ - "chrome-extension://", - "safari-extension://", - "moz-extension://", - "ms-browser-extension://", - } - FileSchemas = []string{ - "file://", - } - WebSocketSchemas = []string{ - "ws://", - "wss://", - } -) - -func newCors(config Config) *cors { - if err := config.Validate(); err != nil { - panic(err.Error()) - } - - for _, origin := range config.AllowOrigins { - if origin == "*" { - config.AllowAllOrigins = true - } - } - - if config.OptionsResponseStatusCode == 0 { - config.OptionsResponseStatusCode = http.StatusNoContent - } - - return &cors{ - allowOriginFunc: config.AllowOriginFunc, - allowOriginWithContextFunc: config.AllowOriginWithContextFunc, - allowAllOrigins: config.AllowAllOrigins, - allowCredentials: config.AllowCredentials, - allowOrigins: normalize(config.AllowOrigins), - normalHeaders: generateNormalHeaders(config), - preflightHeaders: generatePreflightHeaders(config), - wildcardOrigins: config.parseWildcardRules(), - optionsResponseStatusCode: config.OptionsResponseStatusCode, - } -} - -func (cors *cors) applyCors(c *gin.Context) { - origin := c.Request.Header.Get("Origin") - if len(origin) == 0 { - // request is not a CORS request - return - } - host := c.Request.Host - - if origin == "http://"+host || origin == "https://"+host { - // request is not a CORS request but have origin header. - // for example, use fetch api - return - } - - if !cors.isOriginValid(c, origin) { - c.AbortWithStatus(http.StatusForbidden) - return - } - - if c.Request.Method == "OPTIONS" { - cors.handlePreflight(c) - defer c.AbortWithStatus(cors.optionsResponseStatusCode) - } else { - cors.handleNormal(c) - } - - if !cors.allowAllOrigins { - c.Header("Access-Control-Allow-Origin", origin) - } -} - -func (cors *cors) validateWildcardOrigin(origin string) bool { - for _, w := range cors.wildcardOrigins { - if w[0] == "*" && strings.HasSuffix(origin, w[1]) { - return true - } - if w[1] == "*" && strings.HasPrefix(origin, w[0]) { - return true - } - if strings.HasPrefix(origin, w[0]) && strings.HasSuffix(origin, w[1]) { - return true - } - } - - return false -} - -func (cors *cors) isOriginValid(c *gin.Context, origin string) bool { - valid := cors.validateOrigin(origin) - if !valid && cors.allowOriginWithContextFunc != nil { - valid = cors.allowOriginWithContextFunc(c, origin) - } - return valid -} - -func (cors *cors) validateOrigin(origin string) bool { - if cors.allowAllOrigins { - return true - } - for _, value := range cors.allowOrigins { - if value == origin { - return true - } - } - if len(cors.wildcardOrigins) > 0 && cors.validateWildcardOrigin(origin) { - return true - } - if cors.allowOriginFunc != nil { - return cors.allowOriginFunc(origin) - } - return false -} - -func (cors *cors) handlePreflight(c *gin.Context) { - header := c.Writer.Header() - for key, value := range cors.preflightHeaders { - header[key] = value - } -} - -func (cors *cors) handleNormal(c *gin.Context) { - header := c.Writer.Header() - for key, value := range cors.normalHeaders { - header[key] = value - } -} diff --git a/vendor/github.com/gin-contrib/cors/cors.go b/vendor/github.com/gin-contrib/cors/cors.go deleted file mode 100644 index 2261df759..000000000 --- a/vendor/github.com/gin-contrib/cors/cors.go +++ /dev/null @@ -1,198 +0,0 @@ -package cors - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -// Config represents all available options for the middleware. -type Config struct { - AllowAllOrigins bool - - // AllowOrigins is a list of origins a cross-domain request can be executed from. - // If the special "*" value is present in the list, all origins will be allowed. - // Default value is [] - AllowOrigins []string - - // AllowOriginFunc is a custom function to validate the origin. It takes the origin - // as an argument and returns true if allowed or false otherwise. If this option is - // set, the content of AllowOrigins is ignored. - AllowOriginFunc func(origin string) bool - - // Same as AllowOriginFunc except also receives the full request context. - // This function should use the context as a read only source and not - // have any side effects on the request, such as aborting or injecting - // values on the request. - AllowOriginWithContextFunc func(c *gin.Context, origin string) bool - - // AllowMethods is a list of methods the client is allowed to use with - // cross-domain requests. Default value is simple methods (GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS) - AllowMethods []string - - // AllowPrivateNetwork indicates whether the response should include allow private network header - AllowPrivateNetwork bool - - // AllowHeaders is list of non simple headers the client is allowed to use with - // cross-domain requests. - AllowHeaders []string - - // AllowCredentials indicates whether the request can include user credentials like - // cookies, HTTP authentication or client side SSL certificates. - AllowCredentials bool - - // ExposeHeaders indicates which headers are safe to expose to the API of a CORS - // API specification - ExposeHeaders []string - - // MaxAge indicates how long (with second-precision) the results of a preflight request - // can be cached - MaxAge time.Duration - - // Allows to add origins like http://some-domain/*, https://api.* or http://some.*.subdomain.com - AllowWildcard bool - - // Allows usage of popular browser extensions schemas - AllowBrowserExtensions bool - - // Allows to add custom schema like tauri:// - CustomSchemas []string - - // Allows usage of WebSocket protocol - AllowWebSockets bool - - // Allows usage of file:// schema (dangerous!) use it only when you 100% sure it's needed - AllowFiles bool - - // Allows to pass custom OPTIONS response status code for old browsers / clients - OptionsResponseStatusCode int -} - -// AddAllowMethods is allowed to add custom methods -func (c *Config) AddAllowMethods(methods ...string) { - c.AllowMethods = append(c.AllowMethods, methods...) -} - -// AddAllowHeaders is allowed to add custom headers -func (c *Config) AddAllowHeaders(headers ...string) { - c.AllowHeaders = append(c.AllowHeaders, headers...) -} - -// AddExposeHeaders is allowed to add custom expose headers -func (c *Config) AddExposeHeaders(headers ...string) { - c.ExposeHeaders = append(c.ExposeHeaders, headers...) -} - -func (c Config) getAllowedSchemas() []string { - allowedSchemas := DefaultSchemas - if c.AllowBrowserExtensions { - allowedSchemas = append(allowedSchemas, ExtensionSchemas...) - } - if c.AllowWebSockets { - allowedSchemas = append(allowedSchemas, WebSocketSchemas...) - } - if c.AllowFiles { - allowedSchemas = append(allowedSchemas, FileSchemas...) - } - if c.CustomSchemas != nil { - allowedSchemas = append(allowedSchemas, c.CustomSchemas...) - } - return allowedSchemas -} - -func (c Config) validateAllowedSchemas(origin string) bool { - allowedSchemas := c.getAllowedSchemas() - for _, schema := range allowedSchemas { - if strings.HasPrefix(origin, schema) { - return true - } - } - return false -} - -// Validate is check configuration of user defined. -func (c Config) Validate() error { - hasOriginFn := c.AllowOriginFunc != nil - hasOriginFn = hasOriginFn || c.AllowOriginWithContextFunc != nil - - if c.AllowAllOrigins && (hasOriginFn || len(c.AllowOrigins) > 0) { - originFields := strings.Join([]string{ - "AllowOriginFunc", - "AllowOriginFuncWithContext", - "AllowOrigins", - }, " or ") - return fmt.Errorf( - "conflict settings: all origins enabled. %s is not needed", - originFields, - ) - } - if !c.AllowAllOrigins && !hasOriginFn && len(c.AllowOrigins) == 0 { - return errors.New("conflict settings: all origins disabled") - } - for _, origin := range c.AllowOrigins { - if !strings.Contains(origin, "*") && !c.validateAllowedSchemas(origin) { - return errors.New("bad origin: origins must contain '*' or include " + strings.Join(c.getAllowedSchemas(), ",")) - } - } - return nil -} - -func (c Config) parseWildcardRules() [][]string { - var wRules [][]string - - if !c.AllowWildcard { - return wRules - } - - for _, o := range c.AllowOrigins { - if !strings.Contains(o, "*") { - continue - } - - if c := strings.Count(o, "*"); c > 1 { - panic(errors.New("only one * is allowed").Error()) - } - - i := strings.Index(o, "*") - if i == 0 { - wRules = append(wRules, []string{"*", o[1:]}) - continue - } - if i == (len(o) - 1) { - wRules = append(wRules, []string{o[:i], "*"}) - continue - } - - wRules = append(wRules, []string{o[:i], o[i+1:]}) - } - - return wRules -} - -// DefaultConfig returns a generic default configuration mapped to localhost. -func DefaultConfig() Config { - return Config{ - AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, - AllowCredentials: false, - MaxAge: 12 * time.Hour, - } -} - -// Default returns the location middleware with default configuration. -func Default() gin.HandlerFunc { - config := DefaultConfig() - config.AllowAllOrigins = true - return New(config) -} - -// New returns the location middleware with user-defined custom configuration. -func New(config Config) gin.HandlerFunc { - cors := newCors(config) - return func(c *gin.Context) { - cors.applyCors(c) - } -} diff --git a/vendor/github.com/gin-contrib/cors/utils.go b/vendor/github.com/gin-contrib/cors/utils.go deleted file mode 100644 index b98e90b8c..000000000 --- a/vendor/github.com/gin-contrib/cors/utils.go +++ /dev/null @@ -1,90 +0,0 @@ -package cors - -import ( - "net/http" - "strconv" - "strings" - "time" -) - -type converter func(string) string - -func generateNormalHeaders(c Config) http.Header { - headers := make(http.Header) - if c.AllowCredentials { - headers.Set("Access-Control-Allow-Credentials", "true") - } - if len(c.ExposeHeaders) > 0 { - exposeHeaders := convert(normalize(c.ExposeHeaders), http.CanonicalHeaderKey) - headers.Set("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ",")) - } - if c.AllowAllOrigins { - headers.Set("Access-Control-Allow-Origin", "*") - } else { - headers.Set("Vary", "Origin") - } - return headers -} - -func generatePreflightHeaders(c Config) http.Header { - headers := make(http.Header) - if c.AllowCredentials { - headers.Set("Access-Control-Allow-Credentials", "true") - } - if len(c.AllowMethods) > 0 { - allowMethods := convert(normalize(c.AllowMethods), strings.ToUpper) - value := strings.Join(allowMethods, ",") - headers.Set("Access-Control-Allow-Methods", value) - } - if len(c.AllowHeaders) > 0 { - allowHeaders := convert(normalize(c.AllowHeaders), http.CanonicalHeaderKey) - value := strings.Join(allowHeaders, ",") - headers.Set("Access-Control-Allow-Headers", value) - } - if c.MaxAge > time.Duration(0) { - value := strconv.FormatInt(int64(c.MaxAge/time.Second), 10) - headers.Set("Access-Control-Max-Age", value) - } - - if c.AllowPrivateNetwork { - headers.Set("Access-Control-Allow-Private-Network", "true") - } - - if c.AllowAllOrigins { - headers.Set("Access-Control-Allow-Origin", "*") - } else { - // Always set Vary headers - // see https://github.com/rs/cors/issues/10, - // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 - - headers.Add("Vary", "Origin") - headers.Add("Vary", "Access-Control-Request-Method") - headers.Add("Vary", "Access-Control-Request-Headers") - } - return headers -} - -func normalize(values []string) []string { - if values == nil { - return nil - } - distinctMap := make(map[string]bool, len(values)) - normalized := make([]string, 0, len(values)) - for _, value := range values { - value = strings.TrimSpace(value) - value = strings.ToLower(value) - if _, seen := distinctMap[value]; !seen { - normalized = append(normalized, value) - distinctMap[value] = true - } - } - return normalized -} - -func convert(s []string, c converter) []string { - var out []string - for _, i := range s { - out = append(out, c(i)) - } - return out -} diff --git a/vendor/github.com/oapi-codegen/gin-middleware/.gitignore b/vendor/github.com/oapi-codegen/gin-middleware/.gitignore deleted file mode 100644 index e660fd93d..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/vendor/github.com/oapi-codegen/gin-middleware/LICENSE b/vendor/github.com/oapi-codegen/gin-middleware/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/github.com/oapi-codegen/gin-middleware/Makefile b/vendor/github.com/oapi-codegen/gin-middleware/Makefile deleted file mode 100644 index 91dd690ee..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -GOBASE=$(shell pwd) -GOBIN=$(GOBASE)/bin - -help: - @echo "This is a helper makefile for oapi-codegen" - @echo "Targets:" - @echo " generate: regenerate all generated files" - @echo " test: run all tests" - @echo " gin_example generate gin example server code" - @echo " tidy tidy go mod" - -$(GOBIN)/golangci-lint: - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.59.0 - -.PHONY: tools -tools: $(GOBIN)/golangci-lint - -lint: tools - $(GOBIN)/golangci-lint run ./... - -lint-ci: tools - $(GOBIN)/golangci-lint run ./... --out-format=github-actions --timeout=5m - -generate: - go generate ./... - -test: - go test -cover ./... - -tidy: - @echo "tidy..." - go mod tidy diff --git a/vendor/github.com/oapi-codegen/gin-middleware/README.md b/vendor/github.com/oapi-codegen/gin-middleware/README.md deleted file mode 100644 index 08dc2cae4..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Gin Middleware - -⚠️ This README may be for the latest development version, which may contain unreleased changes. Please ensure you're looking at the README for the latest release version. - -Middleware for the [Gin web server](https://github.com/gin-gonic/gin) for use with [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen). - -Licensed under the Apache-2.0. diff --git a/vendor/github.com/oapi-codegen/gin-middleware/oapi_validate.go b/vendor/github.com/oapi-codegen/gin-middleware/oapi_validate.go deleted file mode 100644 index 871d8ffad..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/oapi_validate.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2021 DeepMap, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ginmiddleware - -import ( - "context" - "errors" - "fmt" - "log" - "net/http" - "os" - "strings" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/getkin/kin-openapi/routers" - "github.com/getkin/kin-openapi/routers/gorillamux" - "github.com/gin-gonic/gin" -) - -const ( - GinContextKey = "oapi-codegen/gin-context" - UserDataKey = "oapi-codegen/user-data" -) - -// OapiValidatorFromYamlFile creates a validator middleware from a YAML file path -func OapiValidatorFromYamlFile(path string) (gin.HandlerFunc, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("error reading %s: %s", path, err) - } - - swagger, err := openapi3.NewLoader().LoadFromData(data) - if err != nil { - return nil, fmt.Errorf("error parsing %s as Swagger YAML: %s", - path, err) - } - return OapiRequestValidator(swagger), nil -} - -// OapiRequestValidator is an gin middleware function which validates incoming HTTP requests -// to make sure that they conform to the given OAPI 3.0 specification. When -// OAPI validation fails on the request, we return an HTTP/400 with error message -func OapiRequestValidator(swagger *openapi3.T) gin.HandlerFunc { - return OapiRequestValidatorWithOptions(swagger, nil) -} - -// ErrorHandler is called when there is an error in validation -type ErrorHandler func(c *gin.Context, message string, statusCode int) - -// MultiErrorHandler is called when oapi returns a MultiError type -type MultiErrorHandler func(openapi3.MultiError) error - -// Options to customize request validation. These are passed through to -// openapi3filter. -type Options struct { - ErrorHandler ErrorHandler - Options openapi3filter.Options - ParamDecoder openapi3filter.ContentParameterDecoder - UserData interface{} - MultiErrorHandler MultiErrorHandler - // SilenceServersWarning allows silencing a warning for https://github.com/deepmap/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil` - SilenceServersWarning bool -} - -// OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options -func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) gin.HandlerFunc { - if swagger.Servers != nil && (options == nil || !options.SilenceServersWarning) { - log.Println("WARN: OapiRequestValidatorWithOptions called with an OpenAPI spec that has `Servers` set. This may lead to an HTTP 400 with `no matching operation was found` when sending a valid request, as the validator performs `Host` header validation. If you're expecting `Host` header validation, you can silence this warning by setting `Options.SilenceServersWarning = true`. See https://github.com/deepmap/oapi-codegen/issues/882 for more information.") - } - - router, err := gorillamux.NewRouter(swagger) - if err != nil { - panic(err) - } - return func(c *gin.Context) { - err := ValidateRequestFromContext(c, router, options) - if err != nil { - // using errors.Is did not work - if options != nil && options.ErrorHandler != nil && err.Error() == routers.ErrPathNotFound.Error() { - options.ErrorHandler(c, err.Error(), http.StatusNotFound) - // in case the handler didn't internally call Abort, stop the chain - c.Abort() - } else if options != nil && options.ErrorHandler != nil { - options.ErrorHandler(c, err.Error(), http.StatusBadRequest) - // in case the handler didn't internally call Abort, stop the chain - c.Abort() - } else if err.Error() == routers.ErrPathNotFound.Error() { - // note: i am not sure if this is the best way to handle this - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) - } else { - // note: i am not sure if this is the best way to handle this - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - } - c.Next() - } -} - -// ValidateRequestFromContext is called from the middleware above and actually does the work -// of validating a request. -func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *Options) error { - req := c.Request - route, pathParams, err := router.FindRoute(req) - - // We failed to find a matching route for the request. - if err != nil { - switch e := err.(type) { - case *routers.RouteError: - // We've got a bad request, the path requested doesn't match - // either server, or path, or something. - return errors.New(e.Reason) - default: - // This should never happen today, but if our upstream code changes, - // we don't want to crash the server, so handle the unexpected error. - return fmt.Errorf("error validating route: %s", err.Error()) - } - } - - validationInput := &openapi3filter.RequestValidationInput{ - Request: req, - PathParams: pathParams, - Route: route, - } - - // Pass the gin context into the request validator, so that any callbacks - // which it invokes make it available. - requestContext := context.WithValue(context.Background(), GinContextKey, c) //nolint:staticcheck - - if options != nil { - validationInput.Options = &options.Options - validationInput.ParamDecoder = options.ParamDecoder - requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck - } - - err = openapi3filter.ValidateRequest(requestContext, validationInput) - if err != nil { - me := openapi3.MultiError{} - if errors.As(err, &me) { - errFunc := getMultiErrorHandlerFromOptions(options) - return errFunc(me) - } - - switch e := err.(type) { - case *openapi3filter.RequestError: - // We've got a bad request - // Split up the verbose error by lines and return the first one - // openapi errors seem to be multi-line with a decent message on the first - errorLines := strings.Split(e.Error(), "\n") - return fmt.Errorf("error in openapi3filter.RequestError: %s", errorLines[0]) - case *openapi3filter.SecurityRequirementsError: - return fmt.Errorf("error in openapi3filter.SecurityRequirementsError: %s", e.Error()) - default: - // This should never happen today, but if our upstream code changes, - // we don't want to crash the server, so handle the unexpected error. - return fmt.Errorf("error validating request: %w", err) - } - } - return nil -} - -// GetGinContext gets the gin context from within requests. It returns -// nil if not found or wrong type. -func GetGinContext(c context.Context) *gin.Context { - iface := c.Value(GinContextKey) - if iface == nil { - return nil - } - ginCtx, ok := iface.(*gin.Context) - if !ok { - return nil - } - return ginCtx -} - -func GetUserData(c context.Context) interface{} { - return c.Value(UserDataKey) -} - -// attempt to get the MultiErrorHandler from the options. If it is not set, -// return a default handler -func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler { - if options == nil { - return defaultMultiErrorHandler - } - - if options.MultiErrorHandler == nil { - return defaultMultiErrorHandler - } - - return options.MultiErrorHandler -} - -// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list -// of all of the errors. This method is called if there are no other -// methods defined on the options. -func defaultMultiErrorHandler(me openapi3.MultiError) error { - return fmt.Errorf("multiple errors encountered: %s", me) -} diff --git a/vendor/github.com/oapi-codegen/gin-middleware/renovate.json b/vendor/github.com/oapi-codegen/gin-middleware/renovate.json deleted file mode 100644 index c4e1abbd1..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "local>oapi-codegen/renovate-config" - ] -} diff --git a/vendor/github.com/oapi-codegen/gin-middleware/test_spec.yaml b/vendor/github.com/oapi-codegen/gin-middleware/test_spec.yaml deleted file mode 100644 index 6e0a2415d..000000000 --- a/vendor/github.com/oapi-codegen/gin-middleware/test_spec.yaml +++ /dev/null @@ -1,103 +0,0 @@ -openapi: "3.0.0" -info: - version: 1.0.0 - title: TestServer -servers: - - url: http://deepmap.ai/ -paths: - /resource: - get: - operationId: getResource - parameters: - - name: id - in: query - schema: - type: integer - minimum: 10 - maximum: 100 - responses: - '200': - description: success - content: - application/json: - schema: - properties: - name: - type: string - id: - type: integer - post: - operationId: createResource - responses: - '204': - description: No content - requestBody: - required: true - content: - application/json: - schema: - properties: - name: - type: string - /protected_resource: - get: - operationId: getProtectedResource - security: - - BearerAuth: - - someScope - responses: - '204': - description: no content - /protected_resource2: - get: - operationId: getProtectedResource - security: - - BearerAuth: - - otherScope - responses: - '204': - description: no content - /protected_resource_401: - get: - operationId: getProtectedResource - security: - - BearerAuth: - - unauthorized - responses: - '401': - description: no content - /multiparamresource: - get: - operationId: getResource - parameters: - - name: id - in: query - required: true - schema: - type: integer - minimum: 10 - maximum: 100 - - name: id2 - required: true - in: query - schema: - type: integer - minimum: 10 - maximum: 100 - responses: - '200': - description: success - content: - application/json: - schema: - properties: - name: - type: string - id: - type: integer -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT diff --git a/vendor/modules.txt b/vendor/modules.txt index 013998606..db7fbda9b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -409,9 +409,6 @@ github.com/getkin/kin-openapi/routers github.com/getkin/kin-openapi/routers/gorillamux github.com/getkin/kin-openapi/routers/legacy github.com/getkin/kin-openapi/routers/legacy/pathpattern -# github.com/gin-contrib/cors v1.7.3 -## explicit; go 1.21.0 -github.com/gin-contrib/cors # github.com/gin-contrib/sse v1.1.0 ## explicit; go 1.23 github.com/gin-contrib/sse @@ -733,9 +730,6 @@ github.com/nhost/be/services/mimir/nhost github.com/nhost/be/services/mimir/schema github.com/nhost/be/services/mimir/schema/appconfig github.com/nhost/be/tools/cuegraph/types -# github.com/oapi-codegen/gin-middleware v1.0.2 -## explicit; go 1.20 -github.com/oapi-codegen/gin-middleware # github.com/oapi-codegen/runtime v1.1.1 ## explicit; go 1.20 github.com/oapi-codegen/runtime