feat(internal/lib): common oapi middleware for go services (#3663)

This commit is contained in:
David Barroso
2025-11-04 16:17:41 +01:00
committed by GitHub
parent 372c4e32d4
commit 184a3ed190
105 changed files with 1975 additions and 1741 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -17,6 +17,7 @@ on:
- '.golangci.yaml'
- 'go.mod'
- 'go.sum'
- 'internal/lib/**'
- 'vendor/**'
# auth

View File

@@ -17,6 +17,7 @@ on:
- '.golangci.yaml'
- 'go.mod'
- 'go.sum'
- 'internal/lib/**'
- 'vendor/**'
# storage

View File

@@ -119,6 +119,7 @@
gofumpt
golangci-lint
gqlgenc
oapi-codegen
# internal packages
self.packages.${system}.codegen

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,6 @@
package: api
generate:
gin-server: true
embedded-spec: true
strict-server: true
output: server.gen.go

View File

@@ -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
}

View File

@@ -0,0 +1,4 @@
package: api
generate:
models: true
output: types.gen.go

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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()

69
internal/lib/oapi/oapi.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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")

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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))

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;',

View File

@@ -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')

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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")
}
}
}

View File

@@ -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)
}

View File

@@ -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; [

View File

@@ -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

View File

@@ -11,7 +11,7 @@ let
"go.mod"
"go.sum"
(inDirectory "vendor")
"${submodule}/.golangci.yaml"
".golangci.yaml"
isDirectory
(and
(inDirectory submodule)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -1 +0,0 @@
bin/

View File

@@ -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.

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More