Compare commits

...

5 Commits

Author SHA1 Message Date
github-actions[bot]
7f72aadff9 release(packages/nhost-js): 4.1.0 (#3586)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:25 +01:00
github-actions[bot]
8faf9565bb release(services/storage): 0.9.0 (#3654)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:16 +01:00
github-actions[bot]
7ac3f12852 release(services/auth): 0.43.0 (#3667)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:10 +01:00
David Barroso
184a3ed190 feat(internal/lib): common oapi middleware for go services (#3663) 2025-11-04 16:17:41 +01:00
David Barroso
372c4e32d4 fix(ci): match the version exactly to avoid matching on pre-releases (#3666) 2025-11-04 15:54:01 +01:00
109 changed files with 2028 additions and 1742 deletions

View File

@@ -32,6 +32,7 @@ Where `PKG` is:
- `deps`: For changes to dependencies
- `docs`: For changes to the documentation
- `examples`: For changes to the examples
- `internal/lib`: For changes to Nhost's common libraries (internal)
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
- `nhost-js`: For changes to the Nhost JavaScript SDK
- `nixops`: For changes to the NixOps

View File

@@ -17,7 +17,7 @@ runs:
# Define valid types and packages
VALID_TYPES="feat|fix|chore"
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
# Check if title matches the pattern TYPE(PKG): SUMMARY
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then

View File

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

View File

@@ -40,7 +40,7 @@ jobs:
cd ${{ matrix.project }}
TAG_NAME=$(make release-tag-name)
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
if git tag | grep -q "$TAG_NAME@$VERSION"; then
if git tag | grep -qx "$TAG_NAME@$VERSION"; then
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
else
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"

View File

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

View File

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

2
go.mod
View File

@@ -16,7 +16,6 @@ require (
github.com/davidbyttow/govips/v2 v2.16.0
github.com/gabriel-vasile/mimetype v1.4.8
github.com/getkin/kin-openapi v0.133.0
github.com/gin-contrib/cors v1.7.3
github.com/gin-gonic/gin v1.11.0
github.com/go-git/go-git/v5 v5.16.2
github.com/go-webauthn/webauthn v0.12.2
@@ -30,7 +29,6 @@ require (
github.com/lmittmann/tint v1.0.7
github.com/mark3labs/mcp-go v0.41.1
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48
github.com/oapi-codegen/gin-middleware v1.0.2
github.com/oapi-codegen/runtime v1.1.1
github.com/pb33f/libopenapi v0.21.12
github.com/pelletier/go-toml/v2 v2.2.4

4
go.sum
View File

@@ -162,8 +162,6 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
@@ -341,8 +339,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk=
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oapi-codegen/gin-middleware v1.0.2 h1:/H99UzvHQAUxXK8pzdcGAZgjCVeXdFDAUUWaJT0k0eI=
github.com/oapi-codegen/gin-middleware v1.0.2/go.mod h1:2HJDQjH8jzK2/k/VKcWl+/T41H7ai2bKa6dN3AA2GpA=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=

View File

@@ -0,0 +1,13 @@
package oapi
import "fmt"
type AuthenticatorError struct {
Scheme string
Code string
Message string
}
func (e *AuthenticatorError) Error() string {
return fmt.Sprintf("security error [%s]: %s", e.Code, e.Message)
}

View File

@@ -0,0 +1,10 @@
//go:generate oapi-codegen -config server.cfg.yaml openapi.yaml
//go:generate oapi-codegen -config types.cfg.yaml openapi.yaml
package api
import (
_ "embed"
)
//go:embed openapi.yaml
var OpenAPISchema []byte

View File

@@ -0,0 +1,200 @@
openapi: "3.0.0"
paths:
/signin/email-password:
post:
summary: Sign in with email and password
description: Authenticate a user with their email and password. Returns a session object or MFA challenge if two-factor authentication is enabled.
operationId: signInEmailPassword
requestBody:
description: User credentials for email and password authentication
content:
application/json:
schema:
$ref: "#/components/schemas/SignInEmailPasswordRequest"
required: true
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/SignInEmailPasswordResponse"
description: "Authentication successful. If MFA is enabled, a challenge will be returned instead of a session."
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/user/email/change:
post:
summary: Change user email
description: Request to change the authenticated user's email address. A verification email will be sent to the new address to confirm the change. Requires elevated permissions.
operationId: changeUserEmail
tags:
- user
security:
- BearerAuthElevated: []
requestBody:
description: New email address and optional redirect URL for email change
content:
application/json:
schema:
$ref: "#/components/schemas/UserEmailChangeRequest"
required: true
responses:
"200":
description: >-
Email change requested. An email with a verification link has been sent to the new address
content:
application/json:
schema:
$ref: "#/components/schemas/OKResponse"
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: "Bearer authentication with JWT access token. Used to authenticate requests to protected endpoints."
BearerAuthElevated:
type: http
scheme: bearer
description: "Bearer authentication that requires elevated permissions. Used for sensitive operations that may require additional security measures such as recent authentication. For details see https://docs.nhost.io/products/auth/elevated-permissions"
schemas:
SignInEmailPasswordRequest:
type: object
description: "Request to authenticate using email and password"
additionalProperties: false
properties:
email:
description: "User's email address"
example: "john.smith@nhost.io"
format: email
type: string
password:
description: "User's password"
example: "Str0ngPassw#ord-94|%"
minLength: 3
maxLength: 50
type: string
required:
- email
- password
SignInEmailPasswordResponse:
type: object
description: "Response for email-password authentication that may include a session or MFA challenge"
additionalProperties: false
properties:
session:
$ref: "#/components/schemas/Session"
Session:
type: object
description: "User authentication session containing tokens and user information"
additionalProperties: false
properties:
accessToken:
type: string
description: "JWT token for authenticating API requests"
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
accessTokenExpiresIn:
type: integer
format: int64
description: "Expiration time of the access token in seconds"
example: 900
refreshTokenId:
description: "Identifier for the refresh token"
example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24"
pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
type: string
refreshToken:
description: "Token used to refresh the access token"
example: "2c35b6f3-c4b9-48e3-978a-d4d0f1d42e24"
pattern: \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
type: string
required:
- accessToken
- accessTokenExpiresIn
- refreshToken
- refreshTokenId
UserEmailChangeRequest:
type: object
additionalProperties: false
properties:
newEmail:
description: A valid email
example: john.smith@nhost.io
format: email
type: string
required:
- newEmail
OKResponse:
type: string
additionalProperties: false
enum:
- OK
ErrorResponse:
type: object
description: "Standardized error response"
additionalProperties: false
properties:
status:
description: "HTTP status error code"
type: integer
example: 400
message:
description: "Human-friendly error message"
type: string
example: "Invalid email format"
error:
description: "Error code identifying the specific application error"
type: string
enum:
- default-role-must-be-in-allowed-roles
- disabled-endpoint
- disabled-user
- email-already-in-use
- email-already-verified
- forbidden-anonymous
- internal-server-error
- invalid-email-password
- invalid-request
- locale-not-allowed
- password-too-short
- password-in-hibp-database
- redirectTo-not-allowed
- role-not-allowed
- signup-disabled
- unverified-user
- user-not-anonymous
- invalid-pat
- invalid-refresh-token
- invalid-ticket
- disabled-mfa-totp
- no-totp-secret
- invalid-totp
- mfa-type-not-found
- totp-already-active
- invalid-state
- oauth-token-echange-failed
- oauth-profile-fetch-failed
- oauth-provider-error
- invalid-otp
- cannot-send-sms
required:
- status
- message
- error

View File

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

View File

@@ -0,0 +1,351 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
package api
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gin-gonic/gin"
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Sign in with email and password
// (POST /signin/email-password)
SignInEmailPassword(c *gin.Context)
// Change user email
// (POST /user/email/change)
ChangeUserEmail(c *gin.Context)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandler func(*gin.Context, error, int)
}
type MiddlewareFunc func(c *gin.Context)
// SignInEmailPassword operation middleware
func (siw *ServerInterfaceWrapper) SignInEmailPassword(c *gin.Context) {
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.SignInEmailPassword(c)
}
// ChangeUserEmail operation middleware
func (siw *ServerInterfaceWrapper) ChangeUserEmail(c *gin.Context) {
c.Set(BearerAuthElevatedScopes, []string{})
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.ChangeUserEmail(c)
}
// GinServerOptions provides options for the Gin server.
type GinServerOptions struct {
BaseURL string
Middlewares []MiddlewareFunc
ErrorHandler func(*gin.Context, error, int)
}
// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
func RegisterHandlers(router gin.IRouter, si ServerInterface) {
RegisterHandlersWithOptions(router, si, GinServerOptions{})
}
// RegisterHandlersWithOptions creates http.Handler with additional options
func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) {
errorHandler := options.ErrorHandler
if errorHandler == nil {
errorHandler = func(c *gin.Context, err error, statusCode int) {
c.JSON(statusCode, gin.H{"msg": err.Error()})
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandler: errorHandler,
}
router.POST(options.BaseURL+"/signin/email-password", wrapper.SignInEmailPassword)
router.POST(options.BaseURL+"/user/email/change", wrapper.ChangeUserEmail)
}
type SignInEmailPasswordRequestObject struct {
Body *SignInEmailPasswordJSONRequestBody
}
type SignInEmailPasswordResponseObject interface {
VisitSignInEmailPasswordResponse(w http.ResponseWriter) error
}
type SignInEmailPassword200JSONResponse SignInEmailPasswordResponse
func (response SignInEmailPassword200JSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type SignInEmailPassworddefaultJSONResponse struct {
Body ErrorResponse
StatusCode int
}
func (response SignInEmailPassworddefaultJSONResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type ChangeUserEmailRequestObject struct {
Body *ChangeUserEmailJSONRequestBody
}
type ChangeUserEmailResponseObject interface {
VisitChangeUserEmailResponse(w http.ResponseWriter) error
}
type ChangeUserEmail200JSONResponse OKResponse
func (response ChangeUserEmail200JSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type ChangeUserEmaildefaultJSONResponse struct {
Body ErrorResponse
StatusCode int
}
func (response ChangeUserEmaildefaultJSONResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// Sign in with email and password
// (POST /signin/email-password)
SignInEmailPassword(ctx context.Context, request SignInEmailPasswordRequestObject) (SignInEmailPasswordResponseObject, error)
// Change user email
// (POST /user/email/change)
ChangeUserEmail(ctx context.Context, request ChangeUserEmailRequestObject) (ChangeUserEmailResponseObject, error)
}
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// SignInEmailPassword operation middleware
func (sh *strictHandler) SignInEmailPassword(ctx *gin.Context) {
var request SignInEmailPasswordRequestObject
var body SignInEmailPasswordJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.SignInEmailPassword(ctx, request.(SignInEmailPasswordRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "SignInEmailPassword")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(SignInEmailPasswordResponseObject); ok {
if err := validResponse.VisitSignInEmailPasswordResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// ChangeUserEmail operation middleware
func (sh *strictHandler) ChangeUserEmail(ctx *gin.Context) {
var request ChangeUserEmailRequestObject
var body ChangeUserEmailJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.ChangeUserEmail(ctx, request.(ChangeUserEmailRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "ChangeUserEmail")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(ChangeUserEmailResponseObject); ok {
if err := validResponse.VisitChangeUserEmailResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/9RYUXPbuBH+KxhcO30RJMVW0lhP9WV8rXLXS8Z22s7YfoCApYiYBHhY0IrO1X/vLEBK",
"pEQ1vs5dp32yTBCL3f2+b3fBZ65cWTkLNiCfP3NUOZQy/rzy3vlrwMpZBHogtTbBOCuLj95V4IMB5PNM",
"FggjrgGVNxWt8zm/CdJq6bX5GTQDMsR8a2nEq872Zx6X6UffRDyeKaeBGQ02mGxj7IqFHBhWoExmFJNV",
"VRglaUc6hY842Lrk8zuuIZN1EYR3BYiyxiCWIIwVsijcGnR8jnzEtUG5LEALsLpyxobusxoh2iylKYQs",
"PEi9ISN1jKP/+Am8yQxoPuKZ80ujNVghrbOb0tV0krEBvJWFQPBP4EXrsbFPsjBaJHOVRFw7rzsLHn6q",
"AcmxwilZgLAutHFQOpsdIjgnMHc+dB8aK3KzrISWQS5l9NuDNh5UuHUHlmKu+o/QrGxdiTYjfMRr20ba",
"pof+pG29aJPzlQy9UDIPmIvgHsF2ngejHqGX+jKTIrhQ8RG3Lv4SCMpD11qzHl/dVMn1zNWW3Iw7Wmyk",
"CuYJOjsxyED/O1mHxhsBKpd2BSKTJkWaFivvMlOAyCCofGDxyegBMJNnSlryCcFqgSXyhxEnR/mcY/DG",
"rvh2xEtAlCs4VsBf6lJakXkDVhebRkbt2yMOX2RZFWRrkc5kkUAsc76MOT86iYKuceCg29uPLC02p5Ds",
"ukfMptOdPaLxCjzfbolJP9XGgybBNdb3AY0aae+DdsvPoAK58uH7F1eWVtAfvh9M3w0gxjB+UYH6hOAZ",
"IUiVpakgmCwx5WyQxsZqQ8RAJq1mRHJmbMouGTmsY1IpQLyNxD5K8fu/3yZjBE/vYLtilx8XrNE49oCF",
"zft8+WdlPpj3i08/L179aBa4sNev1bvFm8Vj9Y+/vXt/MR6Ph7DueHP1pTIecDHgVlxK0QdTAnNZLLBp",
"c+OwocwoZ3XPtwtiREO1yIk3M35MEWJIFPyJtMTHlFrNgmPNu0cu9HJyps5fL99k50LNlhdi9hbOxcUf",
"30qhZ3qavdKzMzibxfoXqNryOb+/X95NxYUU2cPz2+39/VLs/p1tT/7u7np1RtuGstyNbqGP41ukzmXA",
"R9wpsF2Q/8uRHUi7S+0T1DpA+ig1Q0Xgxqzswl5R1frYtKvrptX9MjU3u4hDHWkBq5HklcoiabjTWw9m",
"EHrlGD6qEn/A1oDWHrAv0M8ut2MsTcj/ZHOHYWwc78gimR3gzc6RU0d2PN2fdhP81K5iqr6h1n4x++fv",
"qd7KLz+AXYWcz19PR7w0tv33/GvAtg7ujnsxTP/RWNhui2LojzuHxTjkMrBSbpixqqg1MLmrz86zv353",
"yVQuiwLs6niixH1L+J2HjM/5N5P9mDtpZtxJ2zkoKUdBEw4x5HdxKHgpL/uOWFhfDTPrknVa9q9AqQNc",
"dwcf40lzAKjam7C5oUQkT78F6cFf1sSaQ1/T2iFAaxNyRm2tW6nH7FNTy3s6bJsbLVTeBVCB7gXNwI3U",
"wCIo5OcynraPMA+hIkD2Hl4V8CQD6Jd6GqnUZAcZNLtZBb40kQHYuE2sRLBoaFhkBGQ0gHsyNlbYngOs",
"TSYrQWJNJ2CtciaReVBgw4E3Y/ad80xDkKZAhgCMAsT5ZKKdwnEL+aTyTtcq4IS2T1qnRcfpryeNsKaJ",
"hc9tXRQj7iqwsjJ8zs/H0/E09ZI84j+hOd/YycEdZP7MK5dof8DfLrwyDUeRESEH4wdK7phdQ6g9DVN7",
"IUdKHumZmYyFtROZVMEdoWmQgY1XBKLNDiTqvkOViidhAIZvnd5QIDTegU1S3t8fJ58xVYxUHb5aO063",
"rpj2gWlTeYjTgCxwX/96OToIlXc1HXwNUeSpgkbQzqbT3zagpsgPRHR5MDrXsQhkdTFmiyzCucdpxGQH",
"3LUpCrakmkB0AM2MxQBS0+y5o8aYxxPjDf5XC7H/QWMoqOYrAnNK1d6DZuvcFEAli6JrPz/4HdAjjnVZ",
"Sr9puEezclTBwMhBb09IJ0ljk3TZPK2wzkyTXk1jcUd36U5yOJ+M2SVLF/T2w0hcbdOOVJKCi8YsrNtd",
"8RhnM+PLuJSOJNH+u7J5JMDULHe98zcS34nePIDoj7DuZydi4qqmdrcfQtin6x86mmyg+W/Kr3MfHgjj",
"quNWSz/QY3a5RzfkTPZxL4x9ZLlEtgSwp3D/v9FZ02X5/O55cBS4e9g+dOWYqJEa025mkiuk6Sh+tnrY",
"brfbfwUAAP//3ciGL/8UAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

View File

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

View File

@@ -0,0 +1,112 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
package api
import (
openapi_types "github.com/oapi-codegen/runtime/types"
)
const (
BearerAuthElevatedScopes = "BearerAuthElevated.Scopes"
)
// Defines values for ErrorResponseError.
const (
CannotSendSms ErrorResponseError = "cannot-send-sms"
DefaultRoleMustBeInAllowedRoles ErrorResponseError = "default-role-must-be-in-allowed-roles"
DisabledEndpoint ErrorResponseError = "disabled-endpoint"
DisabledMfaTotp ErrorResponseError = "disabled-mfa-totp"
DisabledUser ErrorResponseError = "disabled-user"
EmailAlreadyInUse ErrorResponseError = "email-already-in-use"
EmailAlreadyVerified ErrorResponseError = "email-already-verified"
ForbiddenAnonymous ErrorResponseError = "forbidden-anonymous"
InternalServerError ErrorResponseError = "internal-server-error"
InvalidEmailPassword ErrorResponseError = "invalid-email-password"
InvalidOtp ErrorResponseError = "invalid-otp"
InvalidPat ErrorResponseError = "invalid-pat"
InvalidRefreshToken ErrorResponseError = "invalid-refresh-token"
InvalidRequest ErrorResponseError = "invalid-request"
InvalidState ErrorResponseError = "invalid-state"
InvalidTicket ErrorResponseError = "invalid-ticket"
InvalidTotp ErrorResponseError = "invalid-totp"
LocaleNotAllowed ErrorResponseError = "locale-not-allowed"
MfaTypeNotFound ErrorResponseError = "mfa-type-not-found"
NoTotpSecret ErrorResponseError = "no-totp-secret"
OauthProfileFetchFailed ErrorResponseError = "oauth-profile-fetch-failed"
OauthProviderError ErrorResponseError = "oauth-provider-error"
OauthTokenEchangeFailed ErrorResponseError = "oauth-token-echange-failed"
PasswordInHibpDatabase ErrorResponseError = "password-in-hibp-database"
PasswordTooShort ErrorResponseError = "password-too-short"
RedirectToNotAllowed ErrorResponseError = "redirectTo-not-allowed"
RoleNotAllowed ErrorResponseError = "role-not-allowed"
SignupDisabled ErrorResponseError = "signup-disabled"
TotpAlreadyActive ErrorResponseError = "totp-already-active"
UnverifiedUser ErrorResponseError = "unverified-user"
UserNotAnonymous ErrorResponseError = "user-not-anonymous"
)
// Defines values for OKResponse.
const (
OK OKResponse = "OK"
)
// ErrorResponse Standardized error response
type ErrorResponse struct {
// Error Error code identifying the specific application error
Error ErrorResponseError `json:"error"`
// Message Human-friendly error message
Message string `json:"message"`
// Status HTTP status error code
Status int `json:"status"`
}
// ErrorResponseError Error code identifying the specific application error
type ErrorResponseError string
// OKResponse defines model for OKResponse.
type OKResponse string
// Session User authentication session containing tokens and user information
type Session struct {
// AccessToken JWT token for authenticating API requests
AccessToken string `json:"accessToken"`
// AccessTokenExpiresIn Expiration time of the access token in seconds
AccessTokenExpiresIn int64 `json:"accessTokenExpiresIn"`
// RefreshToken Token used to refresh the access token
RefreshToken string `json:"refreshToken"`
// RefreshTokenId Identifier for the refresh token
RefreshTokenId string `json:"refreshTokenId"`
}
// SignInEmailPasswordRequest Request to authenticate using email and password
type SignInEmailPasswordRequest struct {
// Email User's email address
Email openapi_types.Email `json:"email"`
// Password User's password
Password string `json:"password"`
}
// SignInEmailPasswordResponse Response for email-password authentication that may include a session or MFA challenge
type SignInEmailPasswordResponse struct {
// Session User authentication session containing tokens and user information
Session *Session `json:"session,omitempty"`
}
// UserEmailChangeRequest defines model for UserEmailChangeRequest.
type UserEmailChangeRequest struct {
// NewEmail A valid email
NewEmail openapi_types.Email `json:"newEmail"`
}
// SignInEmailPasswordJSONRequestBody defines body for SignInEmailPassword for application/json ContentType.
type SignInEmailPasswordJSONRequestBody = SignInEmailPasswordRequest
// ChangeUserEmailJSONRequestBody defines body for ChangeUserEmail for application/json ContentType.
type ChangeUserEmailJSONRequestBody = UserEmailChangeRequest

View File

@@ -0,0 +1,49 @@
package controller
import (
"context"
"errors"
"net/http"
"github.com/nhost/nhost/internal/lib/oapi/example/api"
)
type Controller struct{}
func NewController() *Controller {
return &Controller{}
}
func (c *Controller) SignInEmailPassword( //nolint:ireturn
_ context.Context, req api.SignInEmailPasswordRequestObject,
) (api.SignInEmailPasswordResponseObject, error) {
switch req.Body.Email {
case "bad@email.com":
return api.SignInEmailPassworddefaultJSONResponse{
Body: api.ErrorResponse{
Error: api.DisabledUser,
Message: "The user account is disabled.",
Status: http.StatusConflict,
},
StatusCode: http.StatusConflict,
}, nil
case "crash@email.com":
return nil, errors.New("simulated server crash") //nolint:err113
}
return api.SignInEmailPassword200JSONResponse{
Session: &api.Session{
AccessToken: "access_token_example",
AccessTokenExpiresIn: 900, //nolint:mnd
RefreshToken: "refresh_token_example",
RefreshTokenId: "refresh_token_id_example",
},
}, nil
}
func (c *Controller) ChangeUserEmail( //nolint:ireturn
_ context.Context,
_ api.ChangeUserEmailRequestObject,
) (api.ChangeUserEmailResponseObject, error) {
return api.ChangeUserEmail200JSONResponse(api.OK), nil
}

View File

@@ -0,0 +1,109 @@
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"time"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/lmittmann/tint"
"github.com/nhost/nhost/internal/lib/oapi"
"github.com/nhost/nhost/internal/lib/oapi/example/api"
"github.com/nhost/nhost/internal/lib/oapi/example/controller"
"github.com/nhost/nhost/internal/lib/oapi/middleware"
)
const apiPrefix = "/"
func getLogger() *slog.Logger {
handler := tint.NewHandler(os.Stdout, &tint.Options{
AddSource: true,
Level: slog.LevelDebug,
TimeFormat: time.StampMilli,
NoColor: false,
ReplaceAttr: nil,
})
return slog.New(handler)
}
func authFn(
ctx context.Context,
input *openapi3filter.AuthenticationInput,
) error {
_, ok := ctx.Value(oapi.GinContextKey).(*gin.Context)
if !ok {
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "unable to get context",
}
}
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "your access token is invalid",
}
}
func setupRouter(logger *slog.Logger) (*gin.Engine, error) {
ctrl := controller.NewController()
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
router, mw, err := oapi.NewRouter(
api.OpenAPISchema,
apiPrefix,
authFn,
middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"*"},
},
logger,
)
if err != nil {
return nil, fmt.Errorf("failed to create oapi router: %w", err)
}
api.RegisterHandlersWithOptions(
router,
handler,
api.GinServerOptions{
BaseURL: apiPrefix,
Middlewares: []api.MiddlewareFunc{mw},
ErrorHandler: nil,
},
)
return router, nil
}
func run(ctx context.Context) error {
logger := getLogger()
router, err := setupRouter(logger) //nolint:contextcheck
if err != nil {
return err
}
server := &http.Server{ //nolint:exhaustruct
Addr: ":8080",
Handler: router,
ReadHeaderTimeout: 5 * time.Second, //nolint:mnd
}
if err := server.ListenAndServe(); err != nil {
logger.ErrorContext(ctx, "server failed", slog.String("error", err.Error()))
}
return nil
}
func main() {
if err := run(context.Background()); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,177 @@
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/google/go-cmp/cmp"
)
func makeRequest(
router *gin.Engine,
method, path string,
headers map[string]string,
body io.Reader,
) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, body)
for key, value := range headers {
req.Header.Set(key, value)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
func TestRequests(t *testing.T) {
t.Parallel()
logger := getLogger()
router, err := setupRouter(logger)
if err != nil {
t.Fatalf("Failed to set up router: %v", err)
}
cases := []struct {
name string
method string
path string
headers map[string]string
body io.Reader
expectedStatus int
expectedResponse string
}{
{
name: "success",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(`{"email": "asd@asd.com", "password": "p4ssw0rd"}`),
expectedStatus: http.StatusOK,
expectedResponse: "{\"session\":{\"accessToken\":\"access_token_example\",\"accessTokenExpiresIn\":900,\"refreshToken\":\"refresh_token_example\",\"refreshTokenId\":\"refresh_token_id_example\"}}\n", //nolint:lll
},
{
name: "expected error",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(
`{"email": "bad@email.com", "password": "p4ssw0rd"}`,
),
expectedStatus: http.StatusConflict,
expectedResponse: "{\"error\":\"disabled-user\",\"message\":\"The user account is disabled.\",\"status\":409}\n", //nolint:lll
},
{
name: "unexpected error",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(
`{"email": "crash@email.com", "password": "p4ssw0rd"}`,
),
expectedStatus: http.StatusInternalServerError,
expectedResponse: `{"errors":"internal-server-error","message":"simulated server crash"}`,
},
{
name: "missing body",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: nil,
expectedStatus: http.StatusBadRequest,
expectedResponse: `{"error":"request-validation-error","reason":"value is required but missing"}`,
},
{
name: "wrong param",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(
`{"wrong":"asd", "email": "asd@asd.com", "password": "p4ssw0rd"}`,
),
expectedStatus: http.StatusBadRequest,
expectedResponse: `{"error":"schema-validation-error","reason":"property \"wrong\" is unsupported"}`,
},
{
name: "missing param",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(`{"email": "asd@asd.com"}`),
expectedStatus: http.StatusBadRequest,
expectedResponse: `{"error":"schema-validation-error","reason":"property \"password\" is missing"}`,
},
{
name: "invalid param",
method: http.MethodPost,
path: "/signin/email-password",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(`{"email": "asdasd.com", "password": "p4ssw0rd"}`),
expectedStatus: http.StatusBadRequest,
expectedResponse: `{"errors":"bad-request","message":"email: failed to pass regex validation"}`,
},
{
name: "needs security",
method: http.MethodPost,
path: "/user/email/change",
headers: map[string]string{
"Content-Type": "application/json",
},
body: strings.NewReader(`{"newEmail": "new@asd.com"`),
expectedStatus: http.StatusUnauthorized,
expectedResponse: `{"error":"unauthorized","reason":"your access token is invalid","securityScheme":"BearerAuthElevated"}`, //nolint:lll
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
w := makeRequest(router, tc.method, tc.path, tc.headers, tc.body)
resp := w.Result()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != tc.expectedStatus {
t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
}
if diff := cmp.Diff(string(body), tc.expectedResponse); diff != "" {
t.Errorf("Response body mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -0,0 +1,140 @@
package middleware
import (
"net/http"
"slices"
"strings"
"github.com/gin-gonic/gin"
)
// CORSOptions configures the CORS middleware behavior.
//
// The middleware supports three strategies for handling Access-Control-Allow-Headers:
// - nil (default): Reflects the Access-Control-Request-Headers from the client
// - empty slice: Denies all headers (no Access-Control-Allow-Headers header is set)
// - non-empty slice: Uses the specified headers
type CORSOptions struct {
// AllowedOrigins is a list of origins permitted to make cross-origin requests.
// Use "*" or nil slice to allow all origins.
AllowedOrigins []string
// AllowedMethods is a list of HTTP methods the client is permitted to use.
// Common values: GET, POST, PUT, DELETE, PATCH, OPTIONS.
AllowedMethods []string
// AllowedHeaders controls which headers clients can use in requests.
// - nil: reflects client's Access-Control-Request-Headers (permissive)
// - empty slice: denies all headers
// - non-empty: allows only specified headers
AllowedHeaders []string
// ExposedHeaders lists headers that browsers are allowed to access.
// By default, browsers only expose simple response headers.
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials
// (cookies, authorization headers, or TLS client certificates).
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached. Empty string means no caching directive is sent.
MaxAge string
}
// CORS returns a Gin middleware handler that implements Cross-Origin Resource Sharing (CORS).
//
// The middleware handles both preflight (OPTIONS) requests and actual requests, setting
// appropriate CORS headers based on the provided configuration. It automatically adds
// the "Vary: Origin, Access-Control-Request-Method" header for proper cache behavior.
//
// For preflight requests (OPTIONS), the middleware responds with 204 No Content and
// prevents further request processing. For actual requests, it sets CORS headers and
// continues the middleware chain.
//
// Example usage:
//
// router.Use(middleware.CORS(middleware.CORSOptions{
// AllowedOrigins: []string{"https://example.com", "https://app.example.com"},
// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
// AllowedHeaders: nil, // reflects client headers
// AllowCredentials: true,
// MaxAge: "3600",
// }))
func CORS(opts CORSOptions) gin.HandlerFunc { //nolint:cyclop,funlen
allowedMethods := strings.Join(opts.AllowedMethods, ", ")
exposedHeaders := strings.Join(opts.ExposedHeaders, ", ")
allowCredentials := "false"
if opts.AllowCredentials {
allowCredentials = "true"
}
var (
headerStrategy string // "reflect", "specific", or "deny"
allowedHeaders string
)
switch {
case opts.AllowedHeaders == nil:
headerStrategy = "reflect"
case len(opts.AllowedHeaders) == 0:
headerStrategy = "deny"
default:
headerStrategy = "specific"
allowedHeaders = strings.Join(opts.AllowedHeaders, ", ")
}
f := func(c *gin.Context, origin string) {
if opts.AllowedOrigins != nil &&
!slices.Contains(opts.AllowedOrigins, origin) &&
!slices.Contains(opts.AllowedOrigins, "*") {
return
}
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", allowedMethods)
// Handle allowed headers based on strategy
switch headerStrategy {
case "specific":
c.Header("Access-Control-Allow-Headers", allowedHeaders)
case "reflect":
headers := c.Request.Header.Get("Access-Control-Request-Headers")
if headers != "" {
c.Header("Access-Control-Allow-Headers", headers)
}
case "deny":
// Don't set the header at all
}
if exposedHeaders != "" {
c.Header("Access-Control-Expose-Headers", exposedHeaders)
}
c.Header("Access-Control-Allow-Credentials", allowCredentials)
if opts.MaxAge != "" {
c.Header("Access-Control-Max-Age", opts.MaxAge)
}
c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method")
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if c.Request.Method == http.MethodOptions {
f(c, origin)
c.Header("Content-Length", "0")
c.AbortWithStatus(http.StatusNoContent)
return
}
if origin != "" {
f(c, origin)
}
c.Next()
}
}

View File

@@ -0,0 +1,323 @@
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/google/go-cmp/cmp"
"github.com/nhost/nhost/internal/lib/oapi/middleware"
)
func TestCORS(t *testing.T) { //nolint:maintidx
t.Parallel()
gin.SetMode(gin.TestMode)
cases := []struct {
name string
opts middleware.CORSOptions
requestMethod string
requestOrigin string
requestHeaders map[string]string
wantStatus int
wantHeaders http.Header
expectNext bool
}{
{
name: "OPTIONS request with allowed origin",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
},
requestMethod: "OPTIONS",
requestHeaders: map[string]string{},
requestOrigin: "https://example.com",
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Allow-Origin": []string{"https://example.com"},
"Access-Control-Allow-Methods": []string{"GET, POST"},
"Access-Control-Allow-Headers": []string{"Content-Type, Authorization"},
"Access-Control-Allow-Credentials": []string{"false"},
"Vary": []string{
"Origin, Access-Control-Request-Method",
},
"Content-Length": []string{"0"},
},
expectNext: false,
},
{
name: "OPTIONS request with wildcard origin",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
},
requestMethod: "OPTIONS",
requestHeaders: map[string]string{},
requestOrigin: "https://any-origin.com",
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Allow-Origin": []string{"https://any-origin.com"},
"Access-Control-Allow-Methods": []string{"GET, POST, PUT, DELETE"},
},
expectNext: false,
},
{
name: "OPTIONS request with disallowed origin",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
},
requestMethod: "OPTIONS",
requestHeaders: map[string]string{},
requestOrigin: "https://malicious.com",
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{},
expectNext: false,
},
{
name: "OPTIONS request with reflected headers (nil)",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"POST"},
AllowedHeaders: nil,
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{
"Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header",
},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Allow-Headers": []string{"X-Custom-Header, X-Another-Header"},
},
expectNext: false,
},
{
name: "OPTIONS request with denied headers (empty slice)",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"POST"},
AllowedHeaders: []string{},
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{
"Access-Control-Request-Headers": "X-Custom-Header, X-Another-Header",
},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{},
expectNext: false,
},
{
name: "OPTIONS request with nil headers and no request headers",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
AllowedHeaders: nil,
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{},
expectNext: false,
},
{
name: "OPTIONS request with credentials enabled",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
AllowCredentials: true,
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Allow-Credentials": []string{"true"},
},
expectNext: false,
},
{
name: "OPTIONS request with MaxAge",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
MaxAge: "3600",
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Max-Age": []string{"3600"},
},
expectNext: false,
},
{
name: "OPTIONS request with exposed headers",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
ExposedHeaders: []string{"X-Custom-Response", "X-Total-Count"},
},
requestMethod: "OPTIONS",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusNoContent,
wantHeaders: http.Header{
"Access-Control-Expose-Headers": []string{"X-Custom-Response, X-Total-Count"},
},
expectNext: false,
},
{
name: "GET request with allowed origin",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"Content-Type"},
},
requestMethod: "GET",
requestOrigin: "https://example.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusOK,
wantHeaders: http.Header{
"Access-Control-Allow-Origin": []string{"https://example.com"},
"Access-Control-Allow-Methods": []string{"GET, POST"},
"Access-Control-Allow-Headers": []string{"Content-Type"},
},
expectNext: true,
},
{
name: "POST request with disallowed origin",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
},
requestMethod: "POST",
requestOrigin: "https://malicious.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusOK,
wantHeaders: http.Header{},
expectNext: true,
},
{
name: "GET request without origin header",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET"},
},
requestMethod: "GET",
requestOrigin: "",
requestHeaders: map[string]string{},
wantStatus: http.StatusOK,
wantHeaders: http.Header{},
expectNext: true,
},
{
name: "GET request with empty allowed origins (denies all)",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{},
AllowedMethods: []string{"GET"},
},
requestMethod: "GET",
requestOrigin: "https://any-origin.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusOK,
wantHeaders: http.Header{},
expectNext: true,
},
{
name: "GET request with nil allowed origins (allows all)",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: nil,
AllowedMethods: []string{"GET"},
},
requestMethod: "GET",
requestOrigin: "https://any-origin.com",
requestHeaders: map[string]string{},
wantStatus: http.StatusOK,
wantHeaders: http.Header{
"Access-Control-Allow-Origin": []string{"https://any-origin.com"},
},
expectNext: true,
},
{
name: "GET request with multiple allowed origins",
opts: middleware.CORSOptions{ //nolint:exhaustruct
AllowedOrigins: []string{
"https://example.com",
"https://another-example.com",
"https://third-example.com",
},
AllowedMethods: []string{"GET"},
},
requestMethod: "GET",
requestHeaders: map[string]string{},
requestOrigin: "https://another-example.com",
wantStatus: http.StatusOK,
wantHeaders: http.Header{
"Access-Control-Allow-Origin": []string{"https://another-example.com"},
},
expectNext: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Setup router with CORS middleware
router := gin.New()
nextCalled := false
router.Use(middleware.CORS(tc.opts))
router.Any("/test", func(c *gin.Context) {
nextCalled = true
c.Status(http.StatusOK)
})
// Create request
req := httptest.NewRequest(tc.requestMethod, "/test", nil)
if tc.requestOrigin != "" {
req.Header.Set("Origin", tc.requestOrigin)
}
// Add any additional request headers
for key, value := range tc.requestHeaders {
req.Header.Set(key, value)
}
// Record response
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Check status code
if w.Code != tc.wantStatus {
t.Errorf("expected status %d, got %d", tc.wantStatus, w.Code)
}
// Check expected headers using cmp.Diff
// Only compare headers that are expected
gotHeaders := make(http.Header)
for key := range tc.wantHeaders {
if values := w.Header().Values(key); len(values) > 0 {
gotHeaders[key] = values
}
}
if diff := cmp.Diff(tc.wantHeaders, gotHeaders); diff != "" {
t.Errorf("response headers mismatch (-want +got):\n%s", diff)
}
// Check if Next() was called
if nextCalled != tc.expectNext {
t.Errorf("expected Next() called to be %v, got %v", tc.expectNext, nextCalled)
}
})
}
}

View File

@@ -30,6 +30,7 @@ func LoggerFromContext(ctx context.Context) *slog.Logger { //nolint:contextcheck
return logger
}
// Logger is a Gin middleware that logs HTTP requests and responses using slog.
func Logger(logger *slog.Logger) gin.HandlerFunc {
return func(ctx *gin.Context) {
startTime := time.Now()

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

@@ -0,0 +1,69 @@
package oapi
import (
"fmt"
"log/slog"
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/nhost/nhost/internal/lib/oapi/example/api"
"github.com/nhost/nhost/internal/lib/oapi/middleware"
)
func surfaceErrorsMiddleWare(c *gin.Context) {
// this captures two cases as far as I can see:
// 1. request validation errors where the strict generated code fails
// to bind the request to the struct (i.e. "invalid param" test)
// 2. when a handler returns an error instead of a response
c.Next()
if len(c.Errors) > 0 && !c.IsAborted() {
var errorCode string
switch c.Writer.Status() {
case http.StatusBadRequest:
errorCode = "bad-request"
default:
errorCode = "internal-server-error"
}
c.JSON(
c.Writer.Status(),
gin.H{"errors": errorCode, "message": c.Errors[0].Error()},
)
}
}
// NewRouter creates a Gin router with OpenAPI request validation middleware.
func NewRouter(
schema []byte,
apiPrefix string,
authenticationFunc openapi3filter.AuthenticationFunc,
corsOptions middleware.CORSOptions,
logger *slog.Logger,
) (*gin.Engine, func(c *gin.Context), error) {
router := gin.New()
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(schema)
if err != nil {
return nil, nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
}
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
URL: apiPrefix,
})
router.Use(
gin.Recovery(),
surfaceErrorsMiddleWare,
middleware.Logger(logger),
middleware.CORS(corsOptions),
)
mw := api.MiddlewareFunc(requestValidatorWithOptions(doc, authenticationFunc))
return router, mw, nil
}

View File

@@ -0,0 +1,128 @@
package oapi
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers"
"github.com/getkin/kin-openapi/routers/gorillamux"
"github.com/gin-gonic/gin"
)
type ContextKey string
const (
GinContextKey ContextKey = "nhost-oapi/gin-context"
)
func handleError(c *gin.Context, err error) {
var (
errReq *openapi3filter.RequestError
errSchema *openapi3.SchemaError
errAuth *AuthenticatorError
errSec *openapi3filter.SecurityRequirementsError
)
switch {
case errors.As(err, &errSchema):
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "schema-validation-error",
"reason": errSchema.Reason,
})
case errors.As(err, &errReq):
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "request-validation-error",
"reason": errReq.Err.Error(),
})
case errors.As(err, &errAuth):
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": errAuth.Code,
"reason": errAuth.Message,
"securityScheme": errAuth.Scheme,
})
case errors.As(err, &errSec):
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"reason": errSec.Error(),
})
default:
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
func requestValidatorWithOptions(
swagger *openapi3.T,
authFn openapi3filter.AuthenticationFunc,
) gin.HandlerFunc {
router, err := gorillamux.NewRouter(swagger)
if err != nil {
panic(err)
}
return func(c *gin.Context) {
if err := validateRequestFromContext(c, router, authFn); err != nil {
handleError(c, err)
}
c.Next()
}
}
func validateRequestFromContext(
c *gin.Context,
router routers.Router,
authFn openapi3filter.AuthenticationFunc,
) error {
route, pathParams, err := router.FindRoute(c.Request)
if err != nil {
var e *routers.RouteError
switch {
case errors.As(err, &e):
return e
default:
return fmt.Errorf("error validating route: %w", err)
}
}
validationInput := &openapi3filter.RequestValidationInput{ //nolint:exhaustruct
Request: c.Request,
PathParams: pathParams,
Route: route,
Options: &openapi3filter.Options{
AuthenticationFunc: authFn,
ExcludeRequestBody: false,
ExcludeRequestQueryParams: false,
ExcludeResponseBody: false,
ExcludeReadOnlyValidations: false,
ExcludeWriteOnlyValidations: false,
IncludeResponseStatus: false,
MultiError: false,
RegexCompiler: nil,
SkipSettingDefaults: false,
},
}
requestContext := context.WithValue(c.Request.Context(), GinContextKey, c)
if err := openapi3filter.ValidateRequest(requestContext, validationInput); err != nil {
return err //nolint:wrapcheck
}
return nil
}
func GetGinContext(c context.Context) *gin.Context {
v := c.Value(GinContextKey)
if v == nil {
return nil
}
ginCtx, ok := v.(*gin.Context)
if !ok {
return nil
}
return ginCtx
}

View File

@@ -114,13 +114,13 @@ in
echo " Running golangci-lint"
golangci-lint run \
--timeout 600s \
./${submodule}/...
./...
echo " Running tests"
richgo test \
-tags="${pkgs.lib.strings.concatStringsSep " " tags}" \
-ldflags="${pkgs.lib.strings.concatStringsSep " " ldflags}" \
-v ${goTestFlags} ./${submodule}/...
-v ${goTestFlags} ./...
${extraCheck}

View File

@@ -1,3 +1,24 @@
## [@nhost/nhost-js@4.1.0] - 2025-11-04
### 🚀 Features
- *(nhost-js)* Added pushChainFunction to functions and graphql clients (#3610)
- *(nhost-js)* Added various middlewares to work with headers and customizable createNhostClient (#3612)
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
### 🐛 Bug Fixes
- *(dashboard)* Run audit and lint in dashboard (#3578)
- *(nhost-js)* Improvements to Session guard to avoid conflict with ProviderSession (#3662)
### ⚙️ Miscellaneous Tasks
- *(nhost-js)* Generate code from local API definitions (#3583)
- *(docs)* Udpated README.md and CONTRIBUTING.md (#3587)
- *(nhost-js)* Regenerate types (#3648)
# Changelog
All notable changes to this project will be documented in this file.

View File

@@ -1,3 +1,23 @@
## [auth@0.43.0] - 2025-11-04
### 🚀 Features
- *(auth)* Encrypt TOTP secret (#3619)
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
- *(auth)* If the callback state is wrong send back to the redirectTo as provider_state (#3649)
- *(internal/lib)* Common oapi middleware for go services (#3663)
### 🐛 Bug Fixes
- *(auth)* Dont mutate client URL (#3660)
### ⚙️ Miscellaneous Tasks
- *(docs)* Fix broken link in openapi spec and minor mistakes in postmark integration info (#3621)
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
# Changelog
All notable changes to this project will be documented in this file.

View File

@@ -115,13 +115,13 @@ import (
"context"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
)
func (ctrl *Controller) YourEndpoint( //nolint:ireturn
ctx context.Context, request api.YourEndpointRequestObject,
) (api.YourEndpointResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
// Validate inputs
if apiErr := ctrl.wf.ValidateInput(request.Body.Field, logger); apiErr != nil {

View File

@@ -1,35 +0,0 @@
package cmd
import (
"net/http"
"github.com/gin-gonic/gin"
)
func cors() gin.HandlerFunc {
f := func(c *gin.Context, origin string) {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET")
headers := c.Request.Header.Get("Access-Control-Request-Headers")
c.Header("Access-Control-Allow-Headers", headers)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Max-Age", "86400")
c.Writer.Header().Add("Vary", "Origin, Access-Control-Request-Method")
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if c.Request.Method == http.MethodOptions {
f(c, origin)
c.Header("Content-Length", "0")
c.AbortWithStatus(http.StatusNoContent)
}
if origin != "" {
f(c, origin)
}
c.Next()
}
}

View File

@@ -8,9 +8,9 @@ import (
"time"
"github.com/bradfitz/gomemcache/memcache"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/nhost/nhost/internal/lib/oapi"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/docs"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/controller"
@@ -21,7 +21,6 @@ import (
"github.com/nhost/nhost/services/auth/go/oidc"
"github.com/nhost/nhost/services/auth/go/providers"
"github.com/nhost/nhost/services/auth/go/sql"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
"github.com/urfave/cli/v3"
)
@@ -1296,87 +1295,42 @@ func getDependencies( //nolint:ireturn
return emailer, sms, jwtGetter, idTokenValidator, nil
}
func getGoServer( //nolint:funlen
func getCORSOptions() oapimw.CORSOptions {
return oapimw.CORSOptions{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"POST", "GET"},
AllowedHeaders: nil,
ExposedHeaders: []string{},
AllowCredentials: true,
MaxAge: "86400",
}
}
func getGoServer(
ctx context.Context,
cmd *cli.Command,
db *sql.Queries,
encrypter *crypto.Encrypter,
logger *slog.Logger,
) (*http.Server, error) {
router := gin.New()
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(docs.OpenAPISchema)
if err != nil {
return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
}
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
URL: cmd.String(flagAPIPrefix),
})
handlers := []gin.HandlerFunc{
// ginmiddleware.OapiRequestValidator(doc),
gin.Recovery(),
cors(),
middleware.Logger(logger), //nolint:contextcheck
}
if cmd.Bool(flagRateLimitEnable) {
handlers = append(handlers, getRateLimiter(cmd, logger)) //nolint:contextcheck
}
if cmd.String(flagTurnstileSecret) != "" {
handlers = append(handlers, middleware.Tunrstile( //nolint:contextcheck
cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix)),
)
}
router.Use(handlers...)
config, err := getConfig(cmd)
if err != nil {
return nil, fmt.Errorf("problem creating config: %w", err)
}
emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger)
ctrl, jwtGetter, err := getController(ctx, cmd, db, encrypter, logger)
if err != nil {
return nil, err
}
oauthProviders, err := getOauth2Providers(ctx, cmd, logger)
if err != nil {
return nil, fmt.Errorf("problem creating oauth providers: %w", err)
}
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
ctrl, err := controller.New(
db,
config,
jwtGetter,
emailer,
smsClient,
hibp.NewClient(),
oauthProviders,
idTokenValidator,
controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now),
encrypter,
cmd.Root().Version,
router, mw, err := oapi.NewRouter( //nolint:contextcheck
docs.OpenAPISchema,
cmd.String(flagAPIPrefix),
jwtGetter.MiddlewareFunc,
getCORSOptions(),
logger,
)
if err != nil {
return nil, fmt.Errorf("failed to create controller: %w", err)
return nil, fmt.Errorf("failed to create router: %w", err)
}
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions(
doc,
&ginmiddleware.Options{ //nolint:exhaustruct
Options: openapi3filter.Options{ //nolint:exhaustruct
AuthenticationFunc: jwtGetter.MiddlewareFunc,
},
SilenceServersWarning: true,
},
))
api.RegisterHandlersWithOptions(
router,
handler,
@@ -1387,6 +1341,16 @@ func getGoServer( //nolint:funlen
},
)
if cmd.Bool(flagRateLimitEnable) {
router.Use(getRateLimiter(cmd, logger)) //nolint:contextcheck
}
if cmd.String(flagTurnstileSecret) != "" {
router.Use(middleware.Tunrstile( //nolint:contextcheck
cmd.String(flagTurnstileSecret), cmd.String(flagAPIPrefix),
))
}
if cmd.Bool(flagEnableChangeEnv) {
router.POST(cmd.String(flagAPIPrefix)+"/change-env", ctrl.PostChangeEnv)
}
@@ -1410,6 +1374,48 @@ func getGoServer( //nolint:funlen
return server, nil
}
func getController(
ctx context.Context,
cmd *cli.Command,
db *sql.Queries,
encrypter *crypto.Encrypter,
logger *slog.Logger,
) (*controller.Controller, *controller.JWTGetter, error) {
config, err := getConfig(cmd)
if err != nil {
return nil, nil, fmt.Errorf("problem creating config: %w", err)
}
emailer, smsClient, jwtGetter, idTokenValidator, err := getDependencies(ctx, cmd, db, logger)
if err != nil {
return nil, nil, err
}
oauthProviders, err := getOauth2Providers(ctx, cmd, logger)
if err != nil {
return nil, nil, fmt.Errorf("problem creating oauth providers: %w", err)
}
ctrl, err := controller.New(
db,
config,
jwtGetter,
emailer,
smsClient,
hibp.NewClient(),
oauthProviders,
idTokenValidator,
controller.NewTotp(cmd.String(flagMfaTotpIssuer), time.Now),
encrypter,
cmd.Root().Version,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create controller: %w", err)
}
return ctrl, jwtGetter, nil
}
func serve(ctx context.Context, cmd *cli.Command) error {
logger := getLogger(cmd.Bool(flagDebug), cmd.Bool(flagLogFormatTEXT))
logger.InfoContext(ctx, cmd.Root().Name+" v"+cmd.Root().Version)

View File

@@ -6,15 +6,15 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) AddSecurityKey( //nolint:ireturn
ctx context.Context,
_ api.AddSecurityKeyRequestObject,
) (api.AddSecurityKeyResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -3,15 +3,15 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
)
func (ctrl *Controller) ChangeUserEmail( //nolint:ireturn
ctx context.Context, request api.ChangeUserEmailRequestObject,
) (api.ChangeUserEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)
if apiErr != nil {

View File

@@ -3,15 +3,15 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
func (ctrl *Controller) ChangeUserMfa( //nolint:ireturn
ctx context.Context, _ api.ChangeUserMfaRequestObject,
) (api.ChangeUserMfaResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.MfaEnabled {
logger.WarnContext(ctx, "mfa disabled")

View File

@@ -5,8 +5,8 @@ import (
"log/slog"
"github.com/golang-jwt/jwt/v5"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) postUserPasswordAuthenticated( //nolint:ireturn
@@ -61,7 +61,7 @@ func (ctrl *Controller) ChangeUserPassword( //nolint:ireturn
ctx context.Context,
request api.ChangeUserPasswordRequestObject,
) (api.ChangeUserPasswordResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
jwtToken, ok := ctrl.wf.jwtGetter.FromContext(ctx)
if ok {

View File

@@ -4,15 +4,15 @@ import (
"context"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
func (ctrl *Controller) CreatePAT( //nolint:ireturn
ctx context.Context, request api.CreatePATRequestObject,
) (api.CreatePATResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
if apiErr != nil {

View File

@@ -6,8 +6,8 @@ import (
"time"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
)
@@ -77,7 +77,7 @@ func (ctrl *Controller) postUserDeanonymizeValidateRequest( //nolint:cyclop
func (ctrl *Controller) DeanonymizeUser( //nolint:funlen
ctx context.Context, request api.DeanonymizeUserRequestObject,
) (api.DeanonymizeUserResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
userID, password, options, apiError := ctrl.postUserDeanonymizeValidateRequest(

View File

@@ -3,15 +3,15 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) ElevateWebauthn( //nolint:ireturn
ctx context.Context,
_ api.ElevateWebauthnRequestObject,
) (api.ElevateWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -21,8 +21,6 @@ func (e *APIError) Error() string {
return fmt.Sprintf("API error: %s", e.t)
}
var ErrElevatedClaimRequired = errors.New("elevated-claim-required")
var (
ErrJWTConfiguration = errors.New("jwt-configuration")

View File

@@ -7,8 +7,8 @@ import (
"time"
"github.com/jackc/pgx/v5"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -16,7 +16,7 @@ func (ctrl *Controller) GetProviderTokens( //nolint:ireturn
ctx context.Context,
req api.GetProviderTokensRequestObject,
) (api.GetProviderTokensResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
logger = logger.With("provider", req.Provider)
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)

View File

@@ -4,15 +4,15 @@ import (
"context"
"encoding/json"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/oapi-codegen/runtime/types"
)
func (ctrl *Controller) GetUser( //nolint:ireturn
ctx context.Context, _ api.GetUserRequestObject,
) (api.GetUserResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
// Get authenticated user from JWT
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"reflect"
@@ -17,8 +16,8 @@ import (
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/nhost/nhost/internal/lib/oapi"
"github.com/nhost/nhost/services/auth/go/api"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
)
const JWTContextKey = "nhost/auth/jwt"
@@ -340,7 +339,7 @@ func (j *JWTGetter) Validate(accessToken string) (*jwt.Token, error) {
func (j *JWTGetter) FromContext(ctx context.Context) (*jwt.Token, bool) {
token, ok := ctx.Value(JWTContextKey).(*jwt.Token)
if !ok { //nolint:nestif
c := ginmiddleware.GetGinContext(ctx)
c := oapi.GetGinContext(ctx)
if c != nil {
a, ok := c.Get(JWTContextKey)
if !ok {
@@ -415,16 +414,28 @@ func (j *JWTGetter) MiddlewareFunc(
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
return errors.New("invalid authorization header") //nolint:err113
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "missing or malformed authorization header",
}
}
jwtToken, err := j.Validate(parts[1])
if err != nil {
return fmt.Errorf("error validating token: %w", err)
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: fmt.Sprintf("error validating token: %s", err),
}
}
if !jwtToken.Valid {
return errors.New("invalid token") //nolint:err113
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "invalid token",
}
}
if input.SecuritySchemeName == "BearerAuthElevated" {
@@ -435,15 +446,23 @@ func (j *JWTGetter) MiddlewareFunc(
found, err := j.verifyElevatedClaim(ctx, jwtToken, requestPath)
if err != nil {
return fmt.Errorf("error verifying elevated claim: %w", err)
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: fmt.Sprintf("error verifying elevated claim: %s", err),
}
}
if !found {
return ErrElevatedClaimRequired
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "elevated claim required",
}
}
}
c := ginmiddleware.GetGinContext(ctx)
c := oapi.GetGinContext(ctx)
c.Set(JWTContextKey, jwtToken)
return nil

View File

@@ -3,7 +3,6 @@ package controller_test
import (
"context"
"crypto"
"errors"
"log/slog"
"net/http"
"net/url"
@@ -16,9 +15,9 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/nhost/nhost/internal/lib/oapi"
"github.com/nhost/nhost/services/auth/go/controller"
"github.com/nhost/nhost/services/auth/go/controller/mock"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
"go.uber.org/mock/gomock"
)
@@ -535,8 +534,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
SecurityScheme: nil,
Scopes: []string{},
},
expected: nil,
expectedErr: controller.ErrElevatedClaimRequired,
expected: nil,
expectedErr: &oapi.AuthenticatorError{
Scheme: "BearerAuthElevated",
Code: "unauthorized",
Message: "elevated claim required",
},
},
{
@@ -559,8 +562,12 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
SecurityScheme: nil,
Scopes: []string{},
},
expected: nil,
expectedErr: controller.ErrElevatedClaimRequired,
expected: nil,
expectedErr: &oapi.AuthenticatorError{
Scheme: "BearerAuthElevated",
Code: "unauthorized",
Message: "elevated claim required",
},
},
{
@@ -763,13 +770,13 @@ func TestMiddlewareFunc(t *testing.T) { //nolint:maintidx
//nolint
ctx := context.WithValue(
context.Background(),
ginmiddleware.GinContextKey,
oapi.GinContextKey,
&gin.Context{},
)
err = jwtGetter.MiddlewareFunc(ctx, tc.request)
if !errors.Is(err, tc.expectedErr) {
t.Errorf("err = %v; want %v", err, tc.expectedErr)
if diff := cmp.Diff(err, tc.expectedErr); diff != "" {
t.Errorf("err mismatch (-want +got):\n%s", diff)
}
got, _ := jwtGetter.FromContext(ctx)

View File

@@ -3,14 +3,14 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) LinkIdToken( //nolint:ireturn,revive
ctx context.Context, req api.LinkIdTokenRequestObject,
) (api.LinkIdTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
profile, apiErr := ctrl.wf.GetOIDCProfileFromIDToken(
ctx,

View File

@@ -3,15 +3,15 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"golang.org/x/oauth2"
)
func (ctrl *Controller) RefreshProviderToken( //nolint:ireturn
ctx context.Context, req api.RefreshProviderTokenRequestObject,
) (api.RefreshProviderTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
logger = logger.With("provider", req.Provider)
provider := ctrl.Providers.Get(string(req.Provider))

View File

@@ -4,15 +4,15 @@ import (
"context"
"errors"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
func (ctrl *Controller) RefreshToken( //nolint:ireturn
ctx context.Context, request api.RefreshTokenRequestObject,
) (api.RefreshTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
ctx,

View File

@@ -6,8 +6,8 @@ import (
"log/slog"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
)
@@ -15,7 +15,7 @@ func (ctrl *Controller) SendPasswordResetEmail( //nolint:ireturn
ctx context.Context,
request api.SendPasswordResetEmailRequestObject,
) (api.SendPasswordResetEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
options, err := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)

View File

@@ -6,8 +6,8 @@ import (
"log/slog"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
)
@@ -15,7 +15,7 @@ func (ctrl *Controller) SendVerificationEmail( //nolint:ireturn
ctx context.Context,
request api.SendVerificationEmailRequestObject,
) (api.SendVerificationEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
options, apiErr := ctrl.wf.ValidateOptionsRedirectTo(ctx, request.Body.Options, logger)

View File

@@ -5,8 +5,8 @@ import (
"log/slog"
"slices"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) postSigninAnonymousValidateRequest(
@@ -49,7 +49,7 @@ func (ctrl *Controller) postSigninAnonymousValidateRequest(
func (ctrl *Controller) SignInAnonymous( //nolint:ireturn
ctx context.Context, req api.SignInAnonymousRequestObject,
) (api.SignInAnonymousResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
req, apiErr := ctrl.postSigninAnonymousValidateRequest(ctx, req, logger)
if apiErr != nil {

View File

@@ -6,8 +6,8 @@ import (
"time"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn
@@ -33,7 +33,7 @@ func (ctrl *Controller) postSigninEmailPasswordWithTOTP( //nolint:ireturn
func (ctrl *Controller) SignInEmailPassword( //nolint:ireturn
ctx context.Context, request api.SignInEmailPasswordRequestObject,
) (api.SignInEmailPasswordResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
user, apiErr := ctrl.wf.GetUserByEmail(ctx, string(request.Body.Email), logger)

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/oidc"
"github.com/nhost/nhost/services/auth/go/sql"
"github.com/oapi-codegen/runtime/types"
@@ -45,7 +45,7 @@ func (ctrl *Controller) postSigninIdtokenCheckUserExists(
func (ctrl *Controller) SignInIdToken( //nolint:ireturn,revive
ctx context.Context, req api.SignInIdTokenRequestObject,
) (api.SignInIdTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
profile, apiError := ctrl.wf.GetOIDCProfileFromIDToken(
ctx,

View File

@@ -8,8 +8,8 @@ import (
"math/big"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
)
@@ -26,7 +26,7 @@ func (ctrl *Controller) SignInOTPEmail( //nolint:ireturn
ctx context.Context,
request api.SignInOTPEmailRequestObject,
) (api.SignInOTPEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
if !ctrl.config.OTPEmailEnabled {

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/notifications"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -19,7 +19,7 @@ func (ctrl *Controller) SignInPasswordlessEmail( //nolint:ireturn
ctx context.Context,
request api.SignInPasswordlessEmailRequestObject,
) (api.SignInPasswordlessEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
if !ctrl.config.EmailPasswordlessEnabled {

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -18,7 +18,7 @@ func (ctrl *Controller) SignInPasswordlessSms( //nolint:ireturn
ctx context.Context,
request api.SignInPasswordlessSmsRequestObject,
) (api.SignInPasswordlessSmsResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("phoneNumber", request.Body.PhoneNumber))
if !ctrl.config.SMSPasswordlessEnabled {

View File

@@ -3,8 +3,8 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -12,7 +12,7 @@ func (ctrl *Controller) SignInPAT( //nolint:ireturn
ctx context.Context,
request api.SignInPATRequestObject,
) (api.SignInPATResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
user, apiErr := ctrl.wf.GetUserByRefreshTokenHash(
ctx,

View File

@@ -7,8 +7,8 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) getSigninProviderValidateRequest(
@@ -42,7 +42,7 @@ func (ctrl *Controller) SignInProvider( //nolint:ireturn
ctx context.Context,
req api.SignInProviderRequestObject,
) (api.SignInProviderResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("provider", string(req.Provider)))
redirectTo, apiErr := ctrl.getSigninProviderValidateRequest(ctx, req, logger)

View File

@@ -9,8 +9,8 @@ import (
"golang.org/x/oauth2"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/oidc"
"github.com/nhost/nhost/services/auth/go/providers"
"github.com/nhost/nhost/services/auth/go/sql"
@@ -220,7 +220,7 @@ func (ctrl *Controller) signinProviderProviderCallback(
ctx context.Context,
req providerCallbackData,
) (*url.URL, *APIError) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
options, connnect, redirectTo, apiErr := ctrl.signinProviderProviderCallbackValidate(
ctx,

View File

@@ -7,8 +7,8 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -54,7 +54,7 @@ func (ctrl *Controller) SignInWebauthn( //nolint:ireturn
ctx context.Context,
request api.SignInWebauthnRequestObject,
) (api.SignInWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -3,14 +3,14 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) SignOut( //nolint:ireturn
ctx context.Context, request api.SignOutRequestObject,
) (api.SignOutResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if deptr(request.Body.All) {
userID, apiErr := ctrl.wf.GetJWTInContext(ctx, logger)

View File

@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -45,7 +45,7 @@ func (ctrl *Controller) SignUpEmailPassword( //nolint:ireturn
ctx context.Context,
req api.SignUpEmailPasswordRequestObject,
) (api.SignUpEmailPasswordResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email)))
logger := oapimw.LoggerFromContext(ctx).With(slog.String("email", string(req.Body.Email)))
req, apiError := ctrl.postSignupEmailPasswordValidateRequest(ctx, req, logger)
if apiError != nil {

View File

@@ -5,8 +5,8 @@ import (
"log/slog"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) postSignupWebauthnValidateRequest(
@@ -42,7 +42,7 @@ func (ctrl *Controller) SignUpWebauthn( //nolint:ireturn
ctx context.Context,
request api.SignUpWebauthnRequestObject,
) (api.SignUpWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
options, apiErr := ctrl.postSignupWebauthnValidateRequest(ctx, request, logger)

View File

@@ -5,8 +5,8 @@ import (
"encoding/base64"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -14,7 +14,7 @@ func (ctrl *Controller) VerifyAddSecurityKey( //nolint:ireturn
ctx context.Context,
request api.VerifyAddSecurityKeyRequestObject,
) (api.VerifyAddSecurityKeyResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -5,8 +5,8 @@ import (
"log/slog"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -99,7 +99,7 @@ func (ctrl *Controller) postUserMfaActivate( //nolint:ireturn
func (ctrl *Controller) VerifyChangeUserMfa( //nolint:ireturn
ctx context.Context, req api.VerifyChangeUserMfaRequestObject,
) (api.VerifyChangeUserMfaResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.MfaEnabled {
logger.WarnContext(ctx, "mfa disabled")

View File

@@ -7,8 +7,8 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -16,7 +16,7 @@ func (ctrl *Controller) VerifyElevateWebauthn( //nolint:ireturn
ctx context.Context,
request api.VerifyElevateWebauthnRequestObject,
) (api.VerifyElevateWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -3,14 +3,14 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) VerifySignInMfaTotp( //nolint:ireturn
ctx context.Context, req api.VerifySignInMfaTotpRequestObject,
) (api.VerifySignInMfaTotpResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.MfaEnabled {
logger.WarnContext(ctx, "mfa disabled")

View File

@@ -4,15 +4,15 @@ import (
"context"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) VerifySignInOTPEmail( //nolint:ireturn
ctx context.Context,
request api.VerifySignInOTPEmailRequestObject,
) (api.VerifySignInOTPEmailResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("email", string(request.Body.Email)))
user, apiErr := ctrl.wf.GetUserByEmailAndTicket(

View File

@@ -4,15 +4,15 @@ import (
"context"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) VerifySignInPasswordlessSms( //nolint:ireturn
ctx context.Context,
request api.VerifySignInPasswordlessSmsRequestObject,
) (api.VerifySignInPasswordlessSmsResponseObject, error) {
logger := middleware.LoggerFromContext(ctx).
logger := oapimw.LoggerFromContext(ctx).
With(slog.String("phoneNumber", request.Body.PhoneNumber))
if !ctrl.config.SMSPasswordlessEnabled {

View File

@@ -10,8 +10,8 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) VerifySignInWebauthnUserHandle(
@@ -70,7 +70,7 @@ func (ctrl *Controller) VerifySignInWebauthn( //nolint:ireturn
ctx context.Context,
request api.VerifySignInWebauthnRequestObject,
) (api.VerifySignInWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if !ctrl.config.WebauthnEnabled {
logger.ErrorContext(ctx, "webauthn is disabled")

View File

@@ -11,8 +11,8 @@ import (
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -85,7 +85,7 @@ func (ctrl *Controller) VerifySignUpWebauthn( //nolint:ireturn
ctx context.Context,
request api.VerifySignUpWebauthnRequestObject,
) (api.VerifySignUpWebauthnResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
credData, options, nickname, apiErr := ctrl.postSignupWebauthnVerifyValidateRequest(
ctx,

View File

@@ -7,8 +7,8 @@ import (
"net/url"
"strings"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
@@ -59,7 +59,7 @@ func (ctrl *Controller) getVerifyHandleTicketType(
func (ctrl *Controller) VerifyTicket( //nolint:ireturn
ctx context.Context, req api.VerifyTicketRequestObject,
) (api.VerifyTicketResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
user, ticketType, redirectTo, apiErr := ctrl.getVerifyValidateRequest(ctx, req, logger)
switch {

View File

@@ -3,14 +3,14 @@ package controller
import (
"context"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
)
func (ctrl *Controller) VerifyToken( //nolint:ireturn
ctx context.Context, request api.VerifyTokenRequestObject,
) (api.VerifyTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
if request.Body != nil && request.Body.Token != nil {
if apiErr := ctrl.wf.VerifyJWTToken(ctx, *request.Body.Token, logger); apiErr != nil {

View File

@@ -24,6 +24,8 @@ let
./vacuum.yaml
./vacuum-ignore.yaml
(inDirectory ../../internal/lib/oapi)
./go/api/server.cfg.yaml
./go/api/types.cfg.yaml
./go/sql/schema.sh

View File

@@ -47,7 +47,7 @@ describe('personal access token', () => {
await request
.post('/pat')
.send({ expiresAt: new Date() })
.expect(StatusCodes.BAD_REQUEST);
.expect(StatusCodes.UNAUTHORIZED);
});
test('should be able to add metadata to a personal access token', async () => {
@@ -65,7 +65,6 @@ describe('personal access token', () => {
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
metadata: { name: 'Test PAT' },
})
.expect(StatusCodes.OK);
const { rows } = await client.query(
'SELECT * FROM auth.refresh_tokens WHERE refresh_token_hash=$1;',

View File

@@ -69,7 +69,7 @@ describe('user email', () => {
.post('/user/email/change')
// .set('Authorization', `Bearer ${accessToken}`)
.send({ newEmail })
.expect(StatusCodes.BAD_REQUEST);
.expect(StatusCodes.UNAUTHORIZED);
await request
.post('/user/email/change')

View File

@@ -27,7 +27,7 @@ describe('user password', () => {
});
it('should not get user data if not signed in', async () => {
await request.get('/user').expect(StatusCodes.BAD_REQUEST);
await request.get('/user').expect(StatusCodes.UNAUTHORIZED);
});
it('should get user data if signed in', async () => {

View File

@@ -1,3 +1,14 @@
## [storage@0.9.0] - 2025-11-04
### 🚀 Features
- *(internal/lib)* Common oapi middleware for go services (#3663)
### ⚙️ Miscellaneous Tasks
- *(nixops)* Bump go to 1.25.3 and nixpkgs due to CVEs (#3652)
# Changelog
All notable changes to this project will be documented in this file.

View File

@@ -11,10 +11,9 @@ import (
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/nhost/nhost/internal/lib/oapi"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/controller"
"github.com/nhost/nhost/services/storage/image"
@@ -23,7 +22,6 @@ import (
"github.com/nhost/nhost/services/storage/middleware/cdn/fastly"
"github.com/nhost/nhost/services/storage/migrations"
"github.com/nhost/nhost/services/storage/storage"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
"github.com/urfave/cli/v3"
)
@@ -53,88 +51,39 @@ const (
flagHasuraDBName = "hasura-db-name"
)
func getCorsMiddleware(
corsAllowOrigins []string,
corsAllowCredentials bool,
) gin.HandlerFunc {
return cors.New(cors.Config{ //nolint:exhaustruct
AllowOrigins: corsAllowOrigins,
AllowMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"},
AllowHeaders: []string{
func getCORSOptions(cmd *cli.Command) oapimw.CORSOptions {
return oapimw.CORSOptions{
AllowedOrigins: cmd.StringSlice(flagCorsAllowOrigins),
AllowedMethods: []string{"GET", "PUT", "POST", "HEAD", "DELETE"},
AllowedHeaders: []string{
"Authorization", "Origin", "if-match", "if-none-match", "if-modified-since", "if-unmodified-since",
"x-hasura-admin-secret", "x-nhost-bucket-id", "x-nhost-file-name", "x-nhost-file-id",
"x-hasura-role",
},
ExposeHeaders: []string{
ExposedHeaders: []string{
"Content-Length", "Content-Type", "Cache-Control", "ETag", "Last-Modified", "X-Error",
},
AllowCredentials: corsAllowCredentials,
MaxAge: 12 * time.Hour, //nolint: mnd
})
AllowCredentials: cmd.Bool(flagCorsAllowCredentials),
MaxAge: "86400",
}
}
func getGin( //nolint:funlen
bind string,
publicURL string,
apiRootPrefix string,
hasuraAdminSecret string,
func getServer(
cmd *cli.Command,
metadataStorage controller.MetadataStorage,
contentStorage controller.ContentStorage,
imageTransformer *image.Transformer,
logger *slog.Logger,
debug bool,
corsAllowOrigins []string,
corsAllowCredentials bool,
fastlyService string,
fastlyKey string,
clamavServer string,
) (*http.Server, error) {
router := gin.New()
router.GET("/healthz", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
if !debug {
gin.SetMode(gin.ReleaseMode)
}
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(controller.OpenAPISchema)
if err != nil {
return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err)
}
doc.AddServer(&openapi3.Server{ //nolint:exhaustruct
URL: apiRootPrefix,
})
handlers := []gin.HandlerFunc{
middleware.Logger(logger),
getCorsMiddleware(corsAllowOrigins, corsAllowCredentials),
gin.Recovery(),
}
if fastlyService != "" {
logger.InfoContext(context.Background(), "enabling fastly middleware")
handlers = append(
handlers,
fastly.New(fastlyService, fastlyKey, logger),
)
}
router.Use(handlers...)
av, err := getAv(clamavServer)
av, err := getAv(cmd.String(flagClamavServer))
if err != nil {
return nil, fmt.Errorf("problem trying to get av: %w", err)
}
ctrl := controller.New(
publicURL,
apiRootPrefix,
hasuraAdminSecret,
cmd.String(flagPublicURL),
cmd.String(flagAPIRootPrefix),
cmd.String(flagHasuraAdminSecret),
metadataStorage,
contentStorage,
imageTransformer,
@@ -143,27 +92,41 @@ func getGin( //nolint:funlen
)
handler := api.NewStrictHandler(ctrl, []api.StrictMiddlewareFunc{})
mw := api.MiddlewareFunc(ginmiddleware.OapiRequestValidatorWithOptions(
doc,
&ginmiddleware.Options{ //nolint:exhaustruct
Options: openapi3filter.Options{ //nolint:exhaustruct
AuthenticationFunc: middleware.AuthenticationFunc(hasuraAdminSecret),
},
SilenceServersWarning: true,
},
))
router, mw, err := oapi.NewRouter(
controller.OpenAPISchema,
cmd.String(flagAPIRootPrefix),
middleware.AuthenticationFunc(cmd.String(flagHasuraAdminSecret)),
getCORSOptions(cmd),
logger,
)
if err != nil {
return nil, fmt.Errorf("failed to create router: %w", err)
}
router.GET("/healthz", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
if cmd.String(flagFastlyService) != "" {
logger.InfoContext(context.Background(), "enabling fastly middleware")
router.Use(
fastly.New(cmd.String(flagFastlyService), cmd.String(flagFastlyKey), logger),
)
}
api.RegisterHandlersWithOptions(
router,
handler,
api.GinServerOptions{
BaseURL: apiRootPrefix,
BaseURL: cmd.String(flagAPIRootPrefix),
Middlewares: []api.MiddlewareFunc{mw},
ErrorHandler: nil,
},
)
server := &http.Server{ //nolint:exhaustruct
Addr: bind,
Addr: cmd.String(flagBind),
Handler: router,
ReadHeaderTimeout: 5 * time.Second, //nolint:mnd
}
@@ -453,21 +416,12 @@ func serve(ctx context.Context, cmd *cli.Command) error { //nolint:funlen
cmd.String(flagHasuraEndpoint) + "/graphql",
)
server, err := getGin( //nolint: contextcheck
cmd.String(flagBind),
cmd.String(flagPublicURL),
cmd.String(flagAPIRootPrefix),
cmd.String(flagHasuraAdminSecret),
server, err := getServer( //nolint: contextcheck
cmd,
metadataStorage,
contentStorage,
imageTransformer,
logger,
cmd.Bool(flagDebug),
cmd.StringSlice(flagCorsAllowOrigins),
cmd.Bool(flagCorsAllowCredentials),
cmd.String(flagFastlyService),
cmd.String(flagFastlyKey),
cmd.String(flagClamavServer),
)
if err != nil {
return err

View File

@@ -4,8 +4,8 @@ import (
"context"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
func (ctrl *Controller) deleteBrokenMetadata(
@@ -29,7 +29,7 @@ func (ctrl *Controller) DeleteBrokenMetadata( //nolint:ireturn
ctx context.Context,
_ api.DeleteBrokenMetadataRequestObject,
) (api.DeleteBrokenMetadataResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
files, apiErr := ctrl.deleteBrokenMetadata(ctx)
if apiErr != nil {

View File

@@ -4,6 +4,7 @@ import (
"context"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
"github.com/nhost/nhost/services/storage/middleware/cdn/fastly"
@@ -13,7 +14,7 @@ func (ctrl *Controller) DeleteFile( //nolint:ireturn
ctx context.Context,
request api.DeleteFileRequestObject,
) (api.DeleteFileResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
sessionHeaders := middleware.SessionHeadersFromContext(ctx)
apiErr := ctrl.metadataStorage.DeleteFileByID(ctx, request.Id, sessionHeaders)

View File

@@ -4,8 +4,8 @@ import (
"context"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
func (ctrl *Controller) deleteOrphans(ctx context.Context) ([]string, *APIError) {
@@ -27,7 +27,7 @@ func (ctrl *Controller) DeleteOrphanedFiles( //nolint:ireturn
ctx context.Context,
_ api.DeleteOrphanedFilesRequestObject,
) (api.DeleteOrphanedFilesResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
files, apiErr := ctrl.deleteOrphans(ctx)
if apiErr != nil {

View File

@@ -12,6 +12,7 @@ import (
"strings"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/image"
"github.com/nhost/nhost/services/storage/middleware"
@@ -324,7 +325,7 @@ func (ctrl *Controller) GetFile( //nolint:ireturn
ctx context.Context,
request api.GetFileRequestObject,
) (api.GetFileResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
sessionHeaders := middleware.SessionHeadersFromContext(ctx)
acceptHeader := middleware.AcceptHeaderFromContext(ctx)

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
@@ -24,7 +25,7 @@ type GetFilePresignedURLRequest struct {
func (ctrl *Controller) GetFilePresignedURL( //nolint:ireturn
ctx context.Context, request api.GetFilePresignedURLRequestObject,
) (api.GetFilePresignedURLResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
logger = logger.With("file_id", request.Id)
sessionHeaders := middleware.SessionHeadersFromContext(ctx)

View File

@@ -11,6 +11,7 @@ import (
"strconv"
"time"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
@@ -151,7 +152,7 @@ func (ctrl *Controller) GetFileWithPresignedURL( //nolint: ireturn
ctx context.Context,
request api.GetFileWithPresignedURLRequestObject,
) (api.GetFileWithPresignedURLResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
acceptHeader := middleware.AcceptHeaderFromContext(ctx)
fileMetadata, _, apiErr := ctrl.getFileMetadata(

View File

@@ -5,8 +5,8 @@ import (
"log/slog"
"path"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
type ListBrokenMetadataResponse struct {
@@ -60,7 +60,7 @@ func fileListSummary(files []FileSummary) *[]api.FileSummary {
func (ctrl *Controller) ListBrokenMetadata( //nolint:ireturn
ctx context.Context, _ api.ListBrokenMetadataRequestObject,
) (api.ListBrokenMetadataResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
files, apiErr := ctrl.listBrokenMetadata(ctx)
if apiErr != nil {

View File

@@ -7,8 +7,8 @@ import (
"net/http"
"github.com/gin-gonic/gin"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
func (ctrl *Controller) listNotUploaded(ctx context.Context) ([]FileSummary, *APIError) {
@@ -52,7 +52,7 @@ func (ctrl *Controller) ListNotUploaded(ctx *gin.Context) {
func (ctrl *Controller) ListFilesNotUploaded( //nolint:ireturn
ctx context.Context, _ api.ListFilesNotUploadedRequestObject,
) (api.ListFilesNotUploadedResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
files, apiErr := ctrl.listNotUploaded(ctx)
if apiErr != nil {

View File

@@ -6,8 +6,8 @@ import (
"net/http"
"path"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
func (ctrl *Controller) listOrphans(ctx context.Context) ([]string, *APIError) {
@@ -47,7 +47,7 @@ func (ctrl *Controller) listOrphans(ctx context.Context) ([]string, *APIError) {
func (ctrl *Controller) ListOrphanedFiles( //nolint:ireturn
ctx context.Context, _ api.ListOrphanedFilesRequestObject,
) (api.ListOrphanedFilesResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
files, apiErr := ctrl.listOrphans(ctx)
if apiErr != nil {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
"github.com/nhost/nhost/services/storage/middleware/cdn/fastly"
@@ -67,7 +68,7 @@ func (ctrl *Controller) ReplaceFile( //nolint:funlen,ireturn
ctx context.Context,
request api.ReplaceFileRequestObject,
) (api.ReplaceFileResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
sessionHeaders := middleware.SessionHeadersFromContext(ctx)
file, apiErr := replaceFileParseRequest(request)

View File

@@ -10,6 +10,7 @@ import (
"github.com/gabriel-vasile/mimetype"
"github.com/google/uuid"
oapimw "github.com/nhost/nhost/internal/lib/oapi/middleware"
"github.com/nhost/nhost/services/storage/api"
"github.com/nhost/nhost/services/storage/middleware"
)
@@ -249,7 +250,7 @@ func parseUploadRequest(form *multipart.Form) (uploadFileRequest, *APIError) {
func (ctrl *Controller) UploadFiles( //nolint:ireturn
ctx context.Context, request api.UploadFilesRequestObject,
) (api.UploadFilesResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger := oapimw.LoggerFromContext(ctx)
sessionHeaders := middleware.SessionHeadersFromContext(ctx)
form, err := request.Body.ReadForm(maxFormMemory)

View File

@@ -2,18 +2,15 @@ package middleware
import (
"context"
"errors"
"net/http"
"strings"
"github.com/getkin/kin-openapi/openapi3filter"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
"github.com/nhost/nhost/internal/lib/oapi"
)
const HeadersContextKey = "request.headers"
var ErrUnauthorized = errors.New("unauthorized")
func SessionHeadersFromContext(ctx context.Context) http.Header {
headers, _ := ctx.Value(HeadersContextKey).(http.Header)
@@ -51,11 +48,15 @@ func AuthenticationFunc(adminSecret string) openapi3filter.AuthenticationFunc {
"X-Hasura-Admin-Secret",
)
if adminSecretHeader != adminSecret {
return ErrUnauthorized
return &oapi.AuthenticatorError{
Scheme: input.SecuritySchemeName,
Code: "unauthorized",
Message: "invalid credentials",
}
}
}
c := ginmiddleware.GetGinContext(ctx)
c := oapi.GetGinContext(ctx)
c.Set(HeadersContextKey, input.RequestValidationInput.Request.Header)
return nil

View File

@@ -1,79 +0,0 @@
package middleware
import (
"context"
"log/slog"
"time"
"github.com/gin-gonic/gin"
)
type loggerCtxKey struct{}
// Stores the logger in the context.
func LoggerToContext(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerCtxKey{}, logger)
}
// Retrieves the logger from the context. It creates a new one if it can't be found.
func LoggerFromContext(ctx context.Context) *slog.Logger { //nolint:contextcheck
ginCtx, ok := ctx.(*gin.Context)
if ok {
ctx = ginCtx.Request.Context()
}
logger, ok := ctx.Value(loggerCtxKey{}).(*slog.Logger)
if !ok {
return slog.Default()
}
return logger
}
func Logger(logger *slog.Logger) gin.HandlerFunc {
return func(ctx *gin.Context) {
startTime := time.Now()
trace := TraceFromHTTPHeaders(ctx.Request.Header)
clientIP := ctx.ClientIP()
reqMethod := ctx.Request.Method
reqURL := ctx.Request.RequestURI
logger := logger.With(
slog.Group(
"trace",
slog.String("trace_id", trace.TraceID),
slog.String("span_id", trace.SpanID),
slog.String("parent_span_id", trace.ParentSpanID),
),
slog.Group(
"request",
slog.String("client_ip", clientIP),
slog.String("method", reqMethod),
slog.String("url", reqURL),
),
)
ctx.Request = ctx.Request.WithContext(
LoggerToContext(ctx.Request.Context(), logger),
)
ctx.Next()
latencyTime := time.Since(startTime)
statusCode := ctx.Writer.Status()
logger = logger.With(slog.Group(
"response",
slog.Int("status_code", statusCode),
slog.Duration("latency_time", latencyTime),
slog.Any("errors", ctx.Errors.Errors()),
))
TraceToHTTPHeaders(trace, ctx.Writer.Header())
if len(ctx.Errors.Errors()) > 0 {
logger.ErrorContext(ctx, "call completed with errors")
} else {
logger.InfoContext(ctx, "call completed")
}
}
}

View File

@@ -1,63 +0,0 @@
package middleware
import (
"net/http"
"github.com/google/uuid"
)
const (
headerTraceID = "X-B3-TraceId"
headerSpanID = "X-B3-SpanId"
headerParentSpanID = "X-B3-ParentSpanId"
)
type Trace struct {
TraceID string
ParentSpanID string
SpanID string
}
// NewTrace creates a new trace with a new `TraceID`.
func NewTrace() Trace {
return Trace{
TraceID: uuid.New().String(),
ParentSpanID: "",
SpanID: "",
}
}
// NewSpan create a new trace with the same `TraceID`, `ParentSpanID` set as the current `SpanID`
// and a new `SpanID`.
func (t Trace) NewSpan() Trace {
return Trace{
TraceID: t.TraceID,
ParentSpanID: t.SpanID,
SpanID: uuid.New().String(),
}
}
// FromHTTPHeaders extracts tracing information from HTTP headers.
// If no tracing information is found, a new trace is created with only `TraceID` set.
func TraceFromHTTPHeaders(headers http.Header) Trace {
traceID := headers.Get(headerTraceID)
if traceID == "" {
traceID = uuid.New().String()
}
spanID := headers.Get(headerSpanID)
parentSpanID := headers.Get(headerParentSpanID)
return Trace{
TraceID: traceID,
ParentSpanID: parentSpanID,
SpanID: spanID,
}
}
// ToHTTPHeaders adds tracing information to HTTP headers.
func TraceToHTTPHeaders(trace Trace, header http.Header) {
header.Set(headerTraceID, trace.TraceID)
header.Set(headerParentSpanID, trace.ParentSpanID)
header.Set(headerSpanID, trace.SpanID)
}

View File

@@ -33,6 +33,8 @@ let
(inDirectory "${submodule}/client/testdata")
(inDirectory "${submodule}/image/testdata")
(inDirectory "${submodule}/storage/testdata")
(inDirectory ../../internal/lib/oapi)
];
exclude = with nix-filter.lib; [

View File

@@ -1,55 +0,0 @@
version: "2"
linters:
default: all
settings:
funlen:
lines: 65
disable:
- canonicalheader
- depguard
- gomoddirectives
- musttag
- nlreturn
- tagliatelle
- varnamelen
- wsl
- noinlineerr
- funcorder
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- funlen
- ireturn
path: _test\.go
- linters:
- lll
source: '^//go:generate '
- linters:
- gochecknoglobals
text: Version is a global variable
- linters:
- ireturn
- lll
path: schema\.resolvers\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- schema\.resolvers\.go

View File

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

View File

@@ -1,25 +0,0 @@
*.o
*.a
*.so
_obj
_test
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
coverage.out
.idea

View File

@@ -1,39 +0,0 @@
linters:
enable-all: false
disable-all: true
fast: false
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- exportloopref
- exhaustive
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- noctx
- nolintlint
- rowserrcheck
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
- gofumpt
run:
timeout: 3m

View File

@@ -1,28 +0,0 @@
builds:
- # If true, skip the build.
# Useful for library projects.
# Default is false
skip: true
changelog:
use: github
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "Enhancements"
regexp: "^.*chore[(\\w)]*:+.*$"
order: 2
- title: "Refactor"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Build process updates"
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
order: 4
- title: "Documentation updates"
regexp: ^.*?docs?(\(.+\))??!?:.+$
order: 4
- title: Others

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Gin-Gonic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,95 +0,0 @@
# CORS gin's middleware
[![Run Tests](https://github.com/gin-contrib/cors/actions/workflows/go.yml/badge.svg)](https://github.com/gin-contrib/cors/actions/workflows/go.yml)
[![codecov](https://codecov.io/gh/gin-contrib/cors/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/cors)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/cors)](https://goreportcard.com/report/github.com/gin-contrib/cors)
[![GoDoc](https://godoc.org/github.com/gin-contrib/cors?status.svg)](https://godoc.org/github.com/gin-contrib/cors)
Gin middleware/handler to enable CORS support.
## Usage
### Start using it
Download and install it:
```sh
go get github.com/gin-contrib/cors
```
Import it in your code:
```go
import "github.com/gin-contrib/cors"
```
### Canonical example
```go
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// CORS for https://foo.com and https://github.com origins, allowing:
// - PUT and PATCH methods
// - Origin header
// - Credentials share
// - Preflight requests cached for 12 hours
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://foo.com"},
AllowMethods: []string{"PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour,
}))
router.Run()
}
```
### Using DefaultConfig as start point
```go
func main() {
router := gin.Default()
// - No origin allowed by default
// - GET,POST, PUT, HEAD methods
// - Credentials share disabled
// - Preflight requests cached for 12 hours
config := cors.DefaultConfig()
config.AllowOrigins = []string{"http://google.com"}
// config.AllowOrigins = []string{"http://google.com", "http://facebook.com"}
// config.AllowAllOrigins = true
router.Use(cors.New(config))
router.Run()
}
```
Note: while Default() allows all origins, DefaultConfig() does not and you will still have to use AllowAllOrigins.
### Default() allows all origins
```go
func main() {
router := gin.Default()
// same as
// config := cors.DefaultConfig()
// config.AllowAllOrigins = true
// router.Use(cors.New(config))
router.Use(cors.Default())
router.Run()
}
```
Using all origins disables the ability for Gin to set cookies for clients. When dealing with credentials, don't allow all origins.

View File

@@ -1,155 +0,0 @@
package cors
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
type cors struct {
allowAllOrigins bool
allowCredentials bool
allowOriginFunc func(string) bool
allowOriginWithContextFunc func(*gin.Context, string) bool
allowOrigins []string
normalHeaders http.Header
preflightHeaders http.Header
wildcardOrigins [][]string
optionsResponseStatusCode int
}
var (
DefaultSchemas = []string{
"http://",
"https://",
}
ExtensionSchemas = []string{
"chrome-extension://",
"safari-extension://",
"moz-extension://",
"ms-browser-extension://",
}
FileSchemas = []string{
"file://",
}
WebSocketSchemas = []string{
"ws://",
"wss://",
}
)
func newCors(config Config) *cors {
if err := config.Validate(); err != nil {
panic(err.Error())
}
for _, origin := range config.AllowOrigins {
if origin == "*" {
config.AllowAllOrigins = true
}
}
if config.OptionsResponseStatusCode == 0 {
config.OptionsResponseStatusCode = http.StatusNoContent
}
return &cors{
allowOriginFunc: config.AllowOriginFunc,
allowOriginWithContextFunc: config.AllowOriginWithContextFunc,
allowAllOrigins: config.AllowAllOrigins,
allowCredentials: config.AllowCredentials,
allowOrigins: normalize(config.AllowOrigins),
normalHeaders: generateNormalHeaders(config),
preflightHeaders: generatePreflightHeaders(config),
wildcardOrigins: config.parseWildcardRules(),
optionsResponseStatusCode: config.OptionsResponseStatusCode,
}
}
func (cors *cors) applyCors(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if len(origin) == 0 {
// request is not a CORS request
return
}
host := c.Request.Host
if origin == "http://"+host || origin == "https://"+host {
// request is not a CORS request but have origin header.
// for example, use fetch api
return
}
if !cors.isOriginValid(c, origin) {
c.AbortWithStatus(http.StatusForbidden)
return
}
if c.Request.Method == "OPTIONS" {
cors.handlePreflight(c)
defer c.AbortWithStatus(cors.optionsResponseStatusCode)
} else {
cors.handleNormal(c)
}
if !cors.allowAllOrigins {
c.Header("Access-Control-Allow-Origin", origin)
}
}
func (cors *cors) validateWildcardOrigin(origin string) bool {
for _, w := range cors.wildcardOrigins {
if w[0] == "*" && strings.HasSuffix(origin, w[1]) {
return true
}
if w[1] == "*" && strings.HasPrefix(origin, w[0]) {
return true
}
if strings.HasPrefix(origin, w[0]) && strings.HasSuffix(origin, w[1]) {
return true
}
}
return false
}
func (cors *cors) isOriginValid(c *gin.Context, origin string) bool {
valid := cors.validateOrigin(origin)
if !valid && cors.allowOriginWithContextFunc != nil {
valid = cors.allowOriginWithContextFunc(c, origin)
}
return valid
}
func (cors *cors) validateOrigin(origin string) bool {
if cors.allowAllOrigins {
return true
}
for _, value := range cors.allowOrigins {
if value == origin {
return true
}
}
if len(cors.wildcardOrigins) > 0 && cors.validateWildcardOrigin(origin) {
return true
}
if cors.allowOriginFunc != nil {
return cors.allowOriginFunc(origin)
}
return false
}
func (cors *cors) handlePreflight(c *gin.Context) {
header := c.Writer.Header()
for key, value := range cors.preflightHeaders {
header[key] = value
}
}
func (cors *cors) handleNormal(c *gin.Context) {
header := c.Writer.Header()
for key, value := range cors.normalHeaders {
header[key] = value
}
}

View File

@@ -1,198 +0,0 @@
package cors
import (
"errors"
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// Config represents all available options for the middleware.
type Config struct {
AllowAllOrigins bool
// AllowOrigins is a list of origins a cross-domain request can be executed from.
// If the special "*" value is present in the list, all origins will be allowed.
// Default value is []
AllowOrigins []string
// AllowOriginFunc is a custom function to validate the origin. It takes the origin
// as an argument and returns true if allowed or false otherwise. If this option is
// set, the content of AllowOrigins is ignored.
AllowOriginFunc func(origin string) bool
// Same as AllowOriginFunc except also receives the full request context.
// This function should use the context as a read only source and not
// have any side effects on the request, such as aborting or injecting
// values on the request.
AllowOriginWithContextFunc func(c *gin.Context, origin string) bool
// AllowMethods is a list of methods the client is allowed to use with
// cross-domain requests. Default value is simple methods (GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS)
AllowMethods []string
// AllowPrivateNetwork indicates whether the response should include allow private network header
AllowPrivateNetwork bool
// AllowHeaders is list of non simple headers the client is allowed to use with
// cross-domain requests.
AllowHeaders []string
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// ExposeHeaders indicates which headers are safe to expose to the API of a CORS
// API specification
ExposeHeaders []string
// MaxAge indicates how long (with second-precision) the results of a preflight request
// can be cached
MaxAge time.Duration
// Allows to add origins like http://some-domain/*, https://api.* or http://some.*.subdomain.com
AllowWildcard bool
// Allows usage of popular browser extensions schemas
AllowBrowserExtensions bool
// Allows to add custom schema like tauri://
CustomSchemas []string
// Allows usage of WebSocket protocol
AllowWebSockets bool
// Allows usage of file:// schema (dangerous!) use it only when you 100% sure it's needed
AllowFiles bool
// Allows to pass custom OPTIONS response status code for old browsers / clients
OptionsResponseStatusCode int
}
// AddAllowMethods is allowed to add custom methods
func (c *Config) AddAllowMethods(methods ...string) {
c.AllowMethods = append(c.AllowMethods, methods...)
}
// AddAllowHeaders is allowed to add custom headers
func (c *Config) AddAllowHeaders(headers ...string) {
c.AllowHeaders = append(c.AllowHeaders, headers...)
}
// AddExposeHeaders is allowed to add custom expose headers
func (c *Config) AddExposeHeaders(headers ...string) {
c.ExposeHeaders = append(c.ExposeHeaders, headers...)
}
func (c Config) getAllowedSchemas() []string {
allowedSchemas := DefaultSchemas
if c.AllowBrowserExtensions {
allowedSchemas = append(allowedSchemas, ExtensionSchemas...)
}
if c.AllowWebSockets {
allowedSchemas = append(allowedSchemas, WebSocketSchemas...)
}
if c.AllowFiles {
allowedSchemas = append(allowedSchemas, FileSchemas...)
}
if c.CustomSchemas != nil {
allowedSchemas = append(allowedSchemas, c.CustomSchemas...)
}
return allowedSchemas
}
func (c Config) validateAllowedSchemas(origin string) bool {
allowedSchemas := c.getAllowedSchemas()
for _, schema := range allowedSchemas {
if strings.HasPrefix(origin, schema) {
return true
}
}
return false
}
// Validate is check configuration of user defined.
func (c Config) Validate() error {
hasOriginFn := c.AllowOriginFunc != nil
hasOriginFn = hasOriginFn || c.AllowOriginWithContextFunc != nil
if c.AllowAllOrigins && (hasOriginFn || len(c.AllowOrigins) > 0) {
originFields := strings.Join([]string{
"AllowOriginFunc",
"AllowOriginFuncWithContext",
"AllowOrigins",
}, " or ")
return fmt.Errorf(
"conflict settings: all origins enabled. %s is not needed",
originFields,
)
}
if !c.AllowAllOrigins && !hasOriginFn && len(c.AllowOrigins) == 0 {
return errors.New("conflict settings: all origins disabled")
}
for _, origin := range c.AllowOrigins {
if !strings.Contains(origin, "*") && !c.validateAllowedSchemas(origin) {
return errors.New("bad origin: origins must contain '*' or include " + strings.Join(c.getAllowedSchemas(), ","))
}
}
return nil
}
func (c Config) parseWildcardRules() [][]string {
var wRules [][]string
if !c.AllowWildcard {
return wRules
}
for _, o := range c.AllowOrigins {
if !strings.Contains(o, "*") {
continue
}
if c := strings.Count(o, "*"); c > 1 {
panic(errors.New("only one * is allowed").Error())
}
i := strings.Index(o, "*")
if i == 0 {
wRules = append(wRules, []string{"*", o[1:]})
continue
}
if i == (len(o) - 1) {
wRules = append(wRules, []string{o[:i], "*"})
continue
}
wRules = append(wRules, []string{o[:i], o[i+1:]})
}
return wRules
}
// DefaultConfig returns a generic default configuration mapped to localhost.
func DefaultConfig() Config {
return Config{
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"},
AllowCredentials: false,
MaxAge: 12 * time.Hour,
}
}
// Default returns the location middleware with default configuration.
func Default() gin.HandlerFunc {
config := DefaultConfig()
config.AllowAllOrigins = true
return New(config)
}
// New returns the location middleware with user-defined custom configuration.
func New(config Config) gin.HandlerFunc {
cors := newCors(config)
return func(c *gin.Context) {
cors.applyCors(c)
}
}

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