Compare commits

..

9 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
David Barroso
a68d261d8e fix(nhost-js): improvements to Session guard to avoid conflict with ProviderSession (#3662) 2025-11-04 10:53:37 +01:00
David Barroso
55bda3f56b fix(auth): dont mutate client URL (#3660) 2025-11-03 10:56:14 +01:00
David Barroso
2311e1dd77 feat(auth): if the callback state is wrong send back to the redirectTo as provider_state (#3649) 2025-10-31 12:13:35 +01:00
David Barroso
824ee142c4 chore(nixops): set system libraries consistently on darwin (#3656) 2025-10-31 11:06:38 +01:00
113 changed files with 2073 additions and 1783 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

@@ -38,8 +38,6 @@ func graphql( //nolint:funlen
env[v.Name] = v.Value
}
env["HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT"] = "600"
return &Service{
Image: "nhost/graphql-engine:" + *cfg.GetHasura().GetVersion(),
DependsOn: map[string]DependsOn{
@@ -56,7 +54,7 @@ func graphql( //nolint:funlen
"CMD-SHELL",
"curl http://localhost:8080/healthz > /dev/null 2>&1",
},
Timeout: "600s",
Timeout: "60s",
Interval: "5s",
StartPeriod: "60s",
},
@@ -137,8 +135,6 @@ func console( //nolint:funlen
env[v.Name] = v.Value
}
env["HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT"] = "600"
return &Service{
Image: fmt.Sprintf(
"nhost/graphql-engine:%s.cli-migrations-v3",
@@ -169,7 +165,7 @@ func console( //nolint:funlen
"CMD-SHELL",
"timeout 1s bash -c ':> /dev/tcp/127.0.0.1/9695' || exit 1",
},
Timeout: "600s",
Timeout: "60s",
Interval: "5s",
StartPeriod: "60s",
},

View File

@@ -39,7 +39,6 @@ func expectedGraphql() *Service {
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE": "100",
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL": "1000",
"HASURA_GRAPHQL_LOG_LEVEL": "info",
"HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT": "600",
"HASURA_GRAPHQL_PG_CONNECTIONS": "50",
"HASURA_GRAPHQL_PG_TIMEOUT": "180",
"HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES": "false",
@@ -78,7 +77,7 @@ func expectedGraphql() *Service {
"CMD-SHELL",
"curl http://localhost:8080/healthz > /dev/null 2>&1",
},
Timeout: "600s",
Timeout: "60s",
Interval: "5s",
StartPeriod: "60s",
},
@@ -179,7 +178,6 @@ func expectedConsole() *Service {
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE": "100",
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL": "1000",
"HASURA_GRAPHQL_LOG_LEVEL": "info",
"HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT": "600",
"HASURA_GRAPHQL_PG_CONNECTIONS": "50",
"HASURA_GRAPHQL_PG_TIMEOUT": "180",
"HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES": "false",
@@ -218,7 +216,7 @@ func expectedConsole() *Service {
"CMD-SHELL",
"timeout 1s bash -c ':> /dev/tcp/127.0.0.1/9695' || exit 1",
},
Timeout: "600s",
Timeout: "60s",
Interval: "5s",
StartPeriod: "60s",
},

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

@@ -56,12 +56,17 @@ in
{ buildInputs ? [ ]
, shellHook ? ""
}: pkgs.mkShell {
inherit shellHook;
buildInputs = with pkgs; [
gnumake
nixpkgs-fmt
] ++ goCheckDeps ++ buildInputs;
shellHook = shellHook + pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
export SDKROOT=${pkgs.apple-sdk_12}
export SDKROOT_FOR_TARGET=${pkgs.apple-sdk_12}
export DEVELOPER_DIR=${pkgs.apple-sdk_12}
export DEVELOPER_DIR_FOR_TARGET=${pkgs.apple-sdk_12}
'';
};
check =
@@ -109,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

@@ -46,7 +46,7 @@ export const updateSessionFromResponseMiddleware = (
return body.session || null;
}
if ("accessToken" in body && "refreshToken" in body) {
if ("accessToken" in body && "refreshToken" in body && "user" in body) {
// Session
return body;
}

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")
@@ -525,7 +523,7 @@ func (ctrl *Controller) sendRedirectError(
) ErrorRedirectResponse {
errResponse := ctrl.getError(err)
redirectURL = generateRedirectURL(redirectURL, map[string]string{
redirectURL = appendURLValues(redirectURL, map[string]string{
"error": string(errResponse.Error),
"errorDescription": errResponse.Message,
})
@@ -567,17 +565,3 @@ func sqlIsDuplcateError(err error, fkey string) bool {
return strings.Contains(err.Error(), "SQLSTATE 23505") &&
strings.Contains(err.Error(), fkey)
}
func generateRedirectURL(
redirectTo *url.URL,
opts map[string]string,
) *url.URL {
q := redirectTo.Query()
for k, v := range opts {
q.Set(k, v)
}
redirectTo.RawQuery = q.Encode()
return redirectTo
}

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"
@@ -47,6 +47,20 @@ func (ctrl *Controller) getStateData(
return stateData, nil
}
func appendURLValues(u *url.URL, values map[string]string) *url.URL {
n := *u
u = &n
q := u.Query()
for k, v := range values {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u
}
func (ctrl *Controller) signinProviderProviderCallbackValidate(
ctx context.Context,
req providerCallbackData,
@@ -56,6 +70,10 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
stateData, apiErr := ctrl.getStateData(ctx, req.State, logger)
if apiErr != nil {
redirectTo = appendURLValues(redirectTo, map[string]string{
"provider_state": req.State,
})
return nil, nil, redirectTo, apiErr
}
@@ -72,16 +90,17 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
}
if req.Error != nil && *req.Error != "" {
values := redirectTo.Query()
values.Add("provider_error", deptr(req.Error))
values.Add("provider_error_description", deptr(req.ErrorDescription))
values.Add("provider_error_url", deptr(req.ErrorURI))
if stateData.State != nil && *stateData.State != "" {
values.Add("state", *stateData.State)
values := map[string]string{
"provider_error": deptr(req.Error),
"provider_error_description": deptr(req.ErrorDescription),
"provider_error_url": deptr(req.ErrorURI),
}
redirectTo.RawQuery = values.Encode()
if stateData.State != nil && *stateData.State != "" {
values["state"] = *stateData.State
}
redirectTo = appendURLValues(redirectTo, values)
return nil, nil, redirectTo, ErrOauthProviderError
}
@@ -93,9 +112,9 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
}
if stateData.State != nil && *stateData.State != "" {
values := optionsRedirectTo.Query()
values.Add("state", *stateData.State)
optionsRedirectTo.RawQuery = values.Encode()
optionsRedirectTo = appendURLValues(optionsRedirectTo, map[string]string{
"state": *stateData.State,
})
}
return stateData.Options, stateData.Connect, optionsRedirectTo, nil
@@ -201,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

@@ -666,7 +666,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `http://localhost:3000?error=invalid-state&errorDescription=Invalid+state`,
Location: `^http://localhost:3000\?error=invalid-state&errorDescription=Invalid\+state&provider_state=wrong-state$`, //nolint:lll
},
},
expectedJWT: nil,

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 {
@@ -80,7 +80,7 @@ func (ctrl *Controller) VerifyTicket( //nolint:ireturn
return ctrl.sendError(ErrInternalServerError), nil
}
redirectTo = generateRedirectURL(redirectTo, map[string]string{
redirectTo = appendURLValues(redirectTo, map[string]string{
"refreshToken": session.RefreshToken,
"type": string(ticketType),
})

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

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