feat(internal/lib): common oapi middleware for go services (#3663)
This commit is contained in:
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
.github/workflows/auth_checks.yaml
vendored
1
.github/workflows/auth_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# auth
|
||||
|
||||
1
.github/workflows/storage_checks.yaml
vendored
1
.github/workflows/storage_checks.yaml
vendored
@@ -17,6 +17,7 @@ on:
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# storage
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
gofumpt
|
||||
golangci-lint
|
||||
gqlgenc
|
||||
oapi-codegen
|
||||
|
||||
# internal packages
|
||||
self.packages.${system}.codegen
|
||||
|
||||
2
go.mod
2
go.mod
@@ -16,7 +16,6 @@ require (
|
||||
github.com/davidbyttow/govips/v2 v2.16.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/getkin/kin-openapi v0.133.0
|
||||
github.com/gin-contrib/cors v1.7.3
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-webauthn/webauthn v0.12.2
|
||||
@@ -30,7 +29,6 @@ require (
|
||||
github.com/lmittmann/tint v1.0.7
|
||||
github.com/mark3labs/mcp-go v0.41.1
|
||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48
|
||||
github.com/oapi-codegen/gin-middleware v1.0.2
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/pb33f/libopenapi v0.21.12
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
|
||||
4
go.sum
4
go.sum
@@ -162,8 +162,6 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
|
||||
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
@@ -341,8 +339,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk=
|
||||
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oapi-codegen/gin-middleware v1.0.2 h1:/H99UzvHQAUxXK8pzdcGAZgjCVeXdFDAUUWaJT0k0eI=
|
||||
github.com/oapi-codegen/gin-middleware v1.0.2/go.mod h1:2HJDQjH8jzK2/k/VKcWl+/T41H7ai2bKa6dN3AA2GpA=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
|
||||
13
internal/lib/oapi/errors.go
Normal file
13
internal/lib/oapi/errors.go
Normal 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)
|
||||
}
|
||||
10
internal/lib/oapi/example/api/api.go
Normal file
10
internal/lib/oapi/example/api/api.go
Normal 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
|
||||
200
internal/lib/oapi/example/api/openapi.yaml
Normal file
200
internal/lib/oapi/example/api/openapi.yaml
Normal 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
|
||||
6
internal/lib/oapi/example/api/server.cfg.yaml
Normal file
6
internal/lib/oapi/example/api/server.cfg.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
embedded-spec: true
|
||||
strict-server: true
|
||||
output: server.gen.go
|
||||
351
internal/lib/oapi/example/api/server.gen.go
Normal file
351
internal/lib/oapi/example/api/server.gen.go
Normal 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
|
||||
}
|
||||
4
internal/lib/oapi/example/api/types.cfg.yaml
Normal file
4
internal/lib/oapi/example/api/types.cfg.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
package: api
|
||||
generate:
|
||||
models: true
|
||||
output: types.gen.go
|
||||
112
internal/lib/oapi/example/api/types.gen.go
Normal file
112
internal/lib/oapi/example/api/types.gen.go
Normal 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
|
||||
49
internal/lib/oapi/example/controller/controller.go
Normal file
49
internal/lib/oapi/example/controller/controller.go
Normal 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
|
||||
}
|
||||
109
internal/lib/oapi/example/main.go
Normal file
109
internal/lib/oapi/example/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
177
internal/lib/oapi/example/main_test.go
Normal file
177
internal/lib/oapi/example/main_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
140
internal/lib/oapi/middleware/cors.go
Normal file
140
internal/lib/oapi/middleware/cors.go
Normal 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()
|
||||
}
|
||||
}
|
||||
323
internal/lib/oapi/middleware/cors_test.go
Normal file
323
internal/lib/oapi/middleware/cors_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
69
internal/lib/oapi/oapi.go
Normal 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
|
||||
}
|
||||
128
internal/lib/oapi/request.go
Normal file
128
internal/lib/oapi/request.go
Normal 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
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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; [
|
||||
|
||||
@@ -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
|
||||
@@ -11,7 +11,7 @@ let
|
||||
"go.mod"
|
||||
"go.sum"
|
||||
(inDirectory "vendor")
|
||||
"${submodule}/.golangci.yaml"
|
||||
".golangci.yaml"
|
||||
isDirectory
|
||||
(and
|
||||
(inDirectory submodule)
|
||||
|
||||
25
vendor/github.com/gin-contrib/cors/.gitignore
generated
vendored
25
vendor/github.com/gin-contrib/cors/.gitignore
generated
vendored
@@ -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
|
||||
39
vendor/github.com/gin-contrib/cors/.golangci.yml
generated
vendored
39
vendor/github.com/gin-contrib/cors/.golangci.yml
generated
vendored
@@ -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
|
||||
28
vendor/github.com/gin-contrib/cors/.goreleaser.yaml
generated
vendored
28
vendor/github.com/gin-contrib/cors/.goreleaser.yaml
generated
vendored
@@ -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
|
||||
21
vendor/github.com/gin-contrib/cors/LICENSE
generated
vendored
21
vendor/github.com/gin-contrib/cors/LICENSE
generated
vendored
@@ -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.
|
||||
95
vendor/github.com/gin-contrib/cors/README.md
generated
vendored
95
vendor/github.com/gin-contrib/cors/README.md
generated
vendored
@@ -1,95 +0,0 @@
|
||||
# CORS gin's middleware
|
||||
|
||||
[](https://github.com/gin-contrib/cors/actions/workflows/go.yml)
|
||||
[](https://codecov.io/gh/gin-contrib/cors)
|
||||
[](https://goreportcard.com/report/github.com/gin-contrib/cors)
|
||||
[](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.
|
||||
155
vendor/github.com/gin-contrib/cors/config.go
generated
vendored
155
vendor/github.com/gin-contrib/cors/config.go
generated
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
198
vendor/github.com/gin-contrib/cors/cors.go
generated
vendored
198
vendor/github.com/gin-contrib/cors/cors.go
generated
vendored
@@ -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)
|
||||
}
|
||||
}
|
||||
90
vendor/github.com/gin-contrib/cors/utils.go
generated
vendored
90
vendor/github.com/gin-contrib/cors/utils.go
generated
vendored
@@ -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
|
||||
}
|
||||
1
vendor/github.com/oapi-codegen/gin-middleware/.gitignore
generated
vendored
1
vendor/github.com/oapi-codegen/gin-middleware/.gitignore
generated
vendored
@@ -1 +0,0 @@
|
||||
bin/
|
||||
201
vendor/github.com/oapi-codegen/gin-middleware/LICENSE
generated
vendored
201
vendor/github.com/oapi-codegen/gin-middleware/LICENSE
generated
vendored
@@ -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.
|
||||
32
vendor/github.com/oapi-codegen/gin-middleware/Makefile
generated
vendored
32
vendor/github.com/oapi-codegen/gin-middleware/Makefile
generated
vendored
@@ -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
Reference in New Issue
Block a user