From 200be5352fb4ac0ab1f05b1422f2ef9ceee9ff5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:15:00 +0100 Subject: [PATCH 1/9] Update role-based access tests to reflect OR semantics for multiple roles --- auth_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/auth_test.go b/auth_test.go index 4520527..d0c85e6 100644 --- a/auth_test.go +++ b/auth_test.go @@ -593,10 +593,10 @@ func TestRequiredRoles(t *testing.T) { return fiber.Map{"ok": true}, nil }, WithRoles(OpenAPIOptions{Summary: "User profile"}, "user")) - // Route requiring multiple roles (AND semantics): "admin" AND "user" + // Route requiring multiple roles (OR semantics): "admin" OR "editor" Get(oapi, "/admin/settings", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) { return fiber.Map{"ok": true}, nil - }, WithRoles(OpenAPIOptions{Summary: "Admin settings"}, "admin", "user")) + }, WithRoles(OpenAPIOptions{Summary: "Admin settings"}, "admin", "editor")) // Route with no required roles Get(oapi, "/public/info", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) { @@ -670,9 +670,9 @@ func TestRequiredRoles(t *testing.T) { } }) - // Multi-role AND semantics tests - // admin-token has roles ["admin", "user"] -> should pass - t.Run("multi-role: token with all roles accepted", func(t *testing.T) { + // Multi-role OR semantics tests + // admin-token has roles ["admin", "user"] -> has "admin", should pass + t.Run("multi-role: token with one matching role accepted", func(t *testing.T) { req := httptest.NewRequest("GET", "/admin/settings", nil) req.Header.Set("Authorization", "Bearer admin-token") resp, _ := app.Test(req) @@ -681,8 +681,8 @@ func TestRequiredRoles(t *testing.T) { } }) - // valid-token has roles ["user"] -> missing "admin", should be rejected - t.Run("multi-role: token missing one role rejected", func(t *testing.T) { + // valid-token has roles ["user"] -> has neither "admin" nor "editor", should be rejected + t.Run("multi-role: token with no matching role rejected", func(t *testing.T) { req := httptest.NewRequest("GET", "/admin/settings", nil) req.Header.Set("Authorization", "Bearer valid-token") resp, _ := app.Test(req) From 57b04d8ce04a2f5e4b2b7bc6ea37d8debfffcdf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:15:06 +0100 Subject: [PATCH 2/9] Update checkRequiredRoles to implement OR semantics for role verification --- auth.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/auth.go b/auth.go index f443744..17f6849 100644 --- a/auth.go +++ b/auth.go @@ -216,15 +216,18 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz return &AuthError{StatusCode: 401, Message: lastErr.Error()} } -// checkRequiredRoles checks that the authenticated user has all required roles. +// checkRequiredRoles checks that the authenticated user has at least one of the required roles (OR semantics). // Called inside validateAuthorization after auth context is established, before resource access checks. func checkRequiredRoles(authCtx *AuthContext, authService AuthorizationService, requiredRoles []string) error { + if len(requiredRoles) == 0 { + return nil + } for _, role := range requiredRoles { - if !authService.HasRole(authCtx, role) { - return &AuthError{StatusCode: 403, Message: fmt.Sprintf("required role missing: %s", role)} + if authService.HasRole(authCtx, role) { + return nil } } - return nil + return &AuthError{StatusCode: 403, Message: fmt.Sprintf("none of the required roles found: %v", requiredRoles)} } // validateResourceAccess validates resource access based on tags From a6a3e6504f3cb4fd50e4f91442107a5ce783c8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:26:39 +0100 Subject: [PATCH 3/9] Enhance WithRoles and WithAllRoles functions to clarify role semantics in route security --- auth_helpers.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/auth_helpers.go b/auth_helpers.go index 34944b0..a561d57 100644 --- a/auth_helpers.go +++ b/auth_helpers.go @@ -12,12 +12,19 @@ func WithSecurityDisabled(options OpenAPIOptions) OpenAPIOptions { return options } -// WithRoles adds required roles to a route (checked automatically during auth) +// WithRoles adds required roles to a route with OR semantics (user needs at least one) func WithRoles(options OpenAPIOptions, roles ...string) OpenAPIOptions { options.RequiredRoles = append(options.RequiredRoles, roles...) return options } +// WithAllRoles adds required roles to a route with AND semantics (user needs all of them) +func WithAllRoles(options OpenAPIOptions, roles ...string) OpenAPIOptions { + options.RequiredRoles = append(options.RequiredRoles, roles...) + options.RequireAllRoles = true + return options +} + // WithPermissions adds required permissions for documentation func WithPermissions(options OpenAPIOptions, permissions ...string) OpenAPIOptions { options.RequiredPermissions = append(options.RequiredPermissions, permissions...) From 990807d9b333cd1703202bf8485745fc34a23b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:26:44 +0100 Subject: [PATCH 4/9] Add tests for multi-role access and AND semantics in role-based authorization --- auth_test.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/auth_test.go b/auth_test.go index d0c85e6..7e2432c 100644 --- a/auth_test.go +++ b/auth_test.go @@ -38,6 +38,15 @@ func NewMockAuthService() *MockAuthService { "exp": time.Now().Add(time.Hour).Unix(), }, }, + "editor-token": { + UserID: "editor-321", + Roles: []string{"editor", "user"}, + Scopes: []string{"read", "write", "share"}, + Claims: map[string]interface{}{ + "sub": "editor-321", + "exp": time.Now().Add(time.Hour).Unix(), + }, + }, "readonly-token": { UserID: "readonly-789", Roles: []string{"user"}, @@ -668,6 +677,21 @@ func TestRequiredRoles(t *testing.T) { if len(roles) != 1 || roles[0] != "admin" { t.Errorf("Expected [admin], got %v", roles) } + mode, ok := getOp["x-required-roles-mode"].(string) + if !ok { + t.Fatal("Expected x-required-roles-mode in spec") + } + if mode != "any" { + t.Errorf("Expected mode 'any', got %s", mode) + } + + // Check OR route has mode "any" + settingsPath := paths["/admin/settings"].(map[string]interface{}) + settingsOp := settingsPath["get"].(map[string]interface{}) + settingsMode := settingsOp["x-required-roles-mode"].(string) + if settingsMode != "any" { + t.Errorf("Expected mode 'any' for /admin/settings, got %s", settingsMode) + } }) // Multi-role OR semantics tests @@ -681,6 +705,16 @@ func TestRequiredRoles(t *testing.T) { } }) + // editor-token has roles ["editor", "user"] -> has "editor" (second in RequiredRoles), should pass + t.Run("multi-role: token matching second role accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/admin/settings", nil) + req.Header.Set("Authorization", "Bearer editor-token") + resp, _ := app.Test(req) + if resp.StatusCode != 200 { + t.Errorf("Expected 200, got %d", resp.StatusCode) + } + }) + // valid-token has roles ["user"] -> has neither "admin" nor "editor", should be rejected t.Run("multi-role: token with no matching role rejected", func(t *testing.T) { req := httptest.NewRequest("GET", "/admin/settings", nil) @@ -768,6 +802,9 @@ func TestWithRolesHelper(t *testing.T) { if opts.RequiredRoles[0] != "admin" || opts.RequiredRoles[1] != "editor" { t.Errorf("Expected [admin, editor], got %v", opts.RequiredRoles) } + if opts.RequireAllRoles { + t.Error("WithRoles should not set RequireAllRoles") + } // Chaining opts = WithRoles(opts, "superadmin") @@ -775,3 +812,72 @@ func TestWithRolesHelper(t *testing.T) { t.Fatalf("Expected 3 roles after chaining, got %d", len(opts.RequiredRoles)) } } + +// TestWithAllRolesHelper tests the WithAllRoles helper function +func TestWithAllRolesHelper(t *testing.T) { + opts := WithAllRoles(OpenAPIOptions{Summary: "test"}, "admin", "editor") + if len(opts.RequiredRoles) != 2 { + t.Fatalf("Expected 2 roles, got %d", len(opts.RequiredRoles)) + } + if !opts.RequireAllRoles { + t.Error("WithAllRoles should set RequireAllRoles to true") + } +} + +// TestRequiredRoles_ANDSemantics tests RequireAllRoles=true (AND semantics) +func TestRequiredRoles_ANDSemantics(t *testing.T) { + mockAuth := NewMockAuthService() + + app := fiber.New() + oapi := New(app, Config{ + EnableOpenAPIDocs: true, + EnableValidation: true, + EnableAuthorization: true, + AuthService: mockAuth, + SecuritySchemes: map[string]SecurityScheme{ + "bearerAuth": {Type: "http", Scheme: "bearer", BearerFormat: "JWT"}, + }, + DefaultSecurity: []map[string][]string{ + {"bearerAuth": {}}, + }, + }) + + // Route requiring ALL of "admin" AND "user" (AND semantics) + Get(oapi, "/strict", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) { + return fiber.Map{"ok": true}, nil + }, WithAllRoles(OpenAPIOptions{Summary: "Strict route"}, "admin", "user")) + + // admin-token has roles ["admin", "user"] -> has both, should pass + t.Run("token with all roles accepted", func(t *testing.T) { + req := httptest.NewRequest("GET", "/strict", nil) + req.Header.Set("Authorization", "Bearer admin-token") + resp, _ := app.Test(req) + if resp.StatusCode != 200 { + t.Errorf("Expected 200, got %d", resp.StatusCode) + } + }) + + // valid-token has roles ["user"] -> missing "admin", should be rejected + t.Run("token missing one role rejected", func(t *testing.T) { + req := httptest.NewRequest("GET", "/strict", nil) + req.Header.Set("Authorization", "Bearer valid-token") + resp, _ := app.Test(req) + if resp.StatusCode != 403 { + t.Errorf("Expected 403, got %d", resp.StatusCode) + } + }) + + t.Run("x-required-roles-mode is 'all' in OpenAPI spec", func(t *testing.T) { + spec := oapi.GenerateOpenAPISpec() + paths := spec["paths"].(map[string]interface{}) + strictPath := paths["/strict"].(map[string]interface{}) + getOp := strictPath["get"].(map[string]interface{}) + mode, ok := getOp["x-required-roles-mode"].(string) + if !ok { + t.Fatal("Expected x-required-roles-mode in spec") + } + if mode != "all" { + t.Errorf("Expected mode 'all', got %s", mode) + } + }) +} From 74ce4d8cfee6708d7d2023e36da68810e961809e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:26:49 +0100 Subject: [PATCH 5/9] Refactor role verification logic to support AND/OR semantics in checkRequiredRoles function --- auth.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/auth.go b/auth.go index 17f6849..32ae888 100644 --- a/auth.go +++ b/auth.go @@ -142,7 +142,7 @@ func RoleGuard(validator AuthorizationService, requiredRoles ...string) fiber.Ha // validateAuthorization validates permissions based on configured security schemes. // When SecuritySchemes is empty, it falls back to Bearer-only validation for backward compatibility. -func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService, config *Config, requiredRoles []string) error { +func validateAuthorization(c *fiber.Ctx, input interface{}, authService AuthorizationService, config *Config, requiredRoles []string, requireAllRoles bool) error { if authService == nil { if len(requiredRoles) > 0 { return &AuthError{StatusCode: 500, Message: "authorization service not configured"} @@ -170,7 +170,7 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz c.Locals("auth", authCtx) // Check roles before resource access - if err := checkRequiredRoles(authCtx, authService, requiredRoles); err != nil { + if err := checkRequiredRoles(authCtx, authService, requiredRoles, requireAllRoles); err != nil { return err } return validateResourceAccess(c, authCtx, input, authService) @@ -192,7 +192,7 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz c.Locals("auth", authCtx) // Check roles before resource access - if err := checkRequiredRoles(authCtx, authService, requiredRoles); err != nil { + if err := checkRequiredRoles(authCtx, authService, requiredRoles, requireAllRoles); err != nil { return err } return validateResourceAccess(c, authCtx, input, authService) @@ -216,18 +216,27 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz return &AuthError{StatusCode: 401, Message: lastErr.Error()} } -// checkRequiredRoles checks that the authenticated user has at least one of the required roles (OR semantics). -// Called inside validateAuthorization after auth context is established, before resource access checks. -func checkRequiredRoles(authCtx *AuthContext, authService AuthorizationService, requiredRoles []string) error { +// checkRequiredRoles checks the authenticated user's roles against the required roles. +// By default (requireAll=false), the user needs at least one of the roles (OR semantics). +// When requireAll=true, the user must have all listed roles (AND semantics). +func checkRequiredRoles(authCtx *AuthContext, authService AuthorizationService, requiredRoles []string, requireAll bool) error { if len(requiredRoles) == 0 { return nil } + if requireAll { + for _, role := range requiredRoles { + if !authService.HasRole(authCtx, role) { + return &AuthError{StatusCode: 403, Message: fmt.Sprintf("required role missing: %s", role)} + } + } + return nil + } for _, role := range requiredRoles { if authService.HasRole(authCtx, role) { return nil } } - return &AuthError{StatusCode: 403, Message: fmt.Sprintf("none of the required roles found: %v", requiredRoles)} + return &AuthError{StatusCode: 403, Message: fmt.Sprintf("requires one of: %s", strings.Join(requiredRoles, ", "))} } // validateResourceAccess validates resource access based on tags From c8a552ca42d7f574497660405c1bf215c407e0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:26:53 +0100 Subject: [PATCH 6/9] Update parseInput to include RequireAllRoles in authorization validation --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index cf7c423..32efcf1 100644 --- a/common.go +++ b/common.go @@ -123,7 +123,7 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op if routeSecurity, ok := options.Security.([]map[string][]string); ok && len(routeSecurity) > 0 { cfg.DefaultSecurity = routeSecurity } - err = validateAuthorization(c, input, cfg.AuthService, &cfg, options.RequiredRoles) + err = validateAuthorization(c, input, cfg.AuthService, &cfg, options.RequiredRoles, options.RequireAllRoles) if err != nil { return input, err } From b61ffebee7457580e846b6a1db89725bb734b024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:26:58 +0100 Subject: [PATCH 7/9] Add support for role requirement modes in OpenAPI spec generation --- fiberoapi.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fiberoapi.go b/fiberoapi.go index a7319bb..daf4991 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -259,6 +259,11 @@ func (o *OApiApp) GenerateOpenAPISpec() map[string]interface{} { // Add required roles as OpenAPI extension if len(op.Options.RequiredRoles) > 0 { enhancedOptions["x-required-roles"] = op.Options.RequiredRoles + if op.Options.RequireAllRoles { + enhancedOptions["x-required-roles-mode"] = "all" + } else { + enhancedOptions["x-required-roles-mode"] = "any" + } } // Add descriptions for required permissions From 2e426e971a397db457461f6e629c6ed47f36d373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:27:03 +0100 Subject: [PATCH 8/9] Add RequireAllRoles field to OpenAPIOptions for AND semantics in role requirements --- types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types.go b/types.go index 8e28d49..c9917ce 100644 --- a/types.go +++ b/types.go @@ -84,7 +84,8 @@ type OpenAPIOptions struct { Description string `json:"description,omitempty"` Parameters []map[string]any `json:"parameters,omitempty"` Security any `json:"security,omitempty"` // Can be []map[string][]string or "disabled" - RequiredRoles []string `json:"-"` // Roles required to access this route (checked automatically) + RequiredRoles []string `json:"-"` // Roles required to access this route (OR semantics by default) + RequireAllRoles bool `json:"-"` // If true, all RequiredRoles must match (AND semantics) RequiredPermissions []string `json:"-"` // Ex: ["document:read", "workspace:admin"] ResourceType string `json:"-"` // Type de ressource concernée } From 153409d07196520f522297db2dea24f57bc4d45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Fri, 13 Mar 2026 19:28:38 +0100 Subject: [PATCH 9/9] Enhance README.md: Update features, installation instructions, and examples for improved clarity and completeness. Add detailed configuration options and examples for authentication and authorization. Revise HTTP methods section to include PATCH and HEAD. Improve documentation on validation, error handling, and role-based access control. --- README.md | 1128 ++++++++++++++--------------------------------------- 1 file changed, 303 insertions(+), 825 deletions(-) diff --git a/README.md b/README.md index dfb7c0f..8f5f22b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ # Fiber OpenAPI -A Go library that extends Fiber to add automatic OpenAPI documentation generation with built-in validation and group support. +A Go library that extends Fiber to add automatic OpenAPI documentation generation with built-in validation, authentication, and role-based access control. ## Features -- ✅ **Complete HTTP methods** (GET, POST, PUT, DELETE) with automatic validation -- ✅ **Group support** with OpenAPI methods available on both app and groups -- ✅ **Unified API** with interface-based approach for seamless app/group usage -- ✅ **Powerful validation** via `github.com/go-playground/validator/v10` -- ✅ **Authentication & Authorization** with JWT/Bearer token support and role-based access control -- ✅ **OpenAPI Security** documentation with automatic security scheme generation -- ✅ **Type safety** with Go generics -- ✅ **Custom error handling** -- ✅ **OpenAPI documentation generation** with automatic schema generation -- ✅ **Redoc documentation UI** for modern, responsive API documentation -- ✅ **Support for path, query, and body parameters** -- ✅ **Automatic documentation setup** with configurable paths +- **Complete HTTP methods** (GET, POST, PUT, PATCH, DELETE, HEAD) with automatic validation +- **Group support** with OpenAPI methods available on both app and groups +- **Unified API** with interface-based approach for seamless app/group usage +- **Powerful validation** via `github.com/go-playground/validator/v10` +- **Multiple authentication schemes**: Bearer, Basic Auth, API Key, AWS SigV4 +- **Declarative role-based access control** with OR/AND semantics +- **Custom error handlers** for validation and authentication errors +- **Per-route security overrides** and public routes +- **Type safety** with Go generics +- **OpenAPI 3.0 documentation** in JSON and YAML formats +- **Redoc documentation UI** for modern, responsive API documentation +- **OpenAPI extensions** (`x-required-roles`, `x-required-roles-mode`) +- **Conditional auth middleware** for flexible authentication strategies ## Installation @@ -25,8 +26,6 @@ go get github.com/labbs/fiber-oapi ## Quick Start -### Basic Usage with Default Configuration - ```go package main @@ -37,951 +36,430 @@ import ( func main() { app := fiber.New() - - // Create OApi app with default configuration - // Documentation will be available at /documentation (Redoc UI) and /openapi.json oapi := fiberoapi.New(app) - // Your routes here... + fiberoapi.Get(oapi, "/hello/:name", + func(c *fiber.Ctx, input struct { + Name string `path:"name" validate:"required,min=2"` + }) (fiber.Map, *fiberoapi.ErrorResponse) { + return fiber.Map{"message": "Hello " + input.Name}, nil + }, + fiberoapi.OpenAPIOptions{ + Summary: "Say hello", + Tags: []string{"greeting"}, + }) - oapi.Listen(":3000") + // Docs at /docs, spec at /openapi.json and /openapi.yaml + app.Listen(":3000") } ``` -### Using Groups +## Configuration ```go -func main() { - app := fiber.New() - oapi := fiberoapi.New(app) - - // Create groups with OpenAPI support - v1 := fiberoapi.Group(oapi, "/api/v1") - v2 := fiberoapi.Group(oapi, "/api/v2") - - // Nested groups - users := fiberoapi.Group(v1, "/users") - admin := fiberoapi.Group(v1, "/admin") - - // Routes work the same on app, groups, and nested groups - fiberoapi.Get(oapi, "/health", handler, options) // On app - fiberoapi.Get(v1, "/status", handler, options) // On group - fiberoapi.Post(users, "/", handler, options) // On nested group - - oapi.Listen(":3000") +type Config struct { + EnableValidation bool // Enable input validation (default: true) + EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) + EnableAuthorization bool // Enable auth validation (default: false) + OpenAPIDocsPath string // Path for docs UI (default: "/docs") + OpenAPIJSONPath string // Path for JSON spec (default: "/openapi.json") + OpenAPIYamlPath string // Path for YAML spec (default: "/openapi.yaml") + AuthService AuthorizationService // Service for handling auth + SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes + DefaultSecurity []map[string][]string // Default security requirements + ValidationErrorHandler ValidationErrorHandler // Custom handler for validation errors + AuthErrorHandler AuthErrorHandler // Custom handler for auth errors (401/403/5xx) } ``` -### Custom Configuration +Default config when none is provided: +- Validation: **enabled** +- Documentation: **enabled** +- Authorization: **disabled** +- Docs path: `/docs` +- JSON spec path: `/openapi.json` +- YAML spec path: `/openapi.yaml` -```go -func main() { - app := fiber.New() - - // Custom configuration - config := fiberoapi.Config{ - EnableValidation: true, // Enable input validation - EnableOpenAPIDocs: true, // Enable automatic docs setup - EnableAuthorization: false, // Enable authentication (default: false) - OpenAPIDocsPath: "/docs", // Custom docs path (default: /docs) - OpenAPIJSONPath: "/openapi.json", // Custom spec path (default: /openapi.json) - OpenAPIYamlPath: "/openapi.yaml", // Custom spec path (default: /openapi.yaml) - } - oapi := fiberoapi.New(app, config) +## HTTP Methods - // Your routes here... +All methods work with both the main app and groups: - oapi.Listen(":3000") -} +```go +fiberoapi.Get(router, path, handler, options) +fiberoapi.Post(router, path, handler, options) +fiberoapi.Put(router, path, handler, options) +fiberoapi.Patch(router, path, handler, options) +fiberoapi.Delete(router, path, handler, options) +fiberoapi.Head(router, path, handler, options) +fiberoapi.Method(method, router, path, handler, options) // Custom HTTP method ``` -## Usage Examples - -### GET with path parameters and validation +## Parameter Types ```go -type GetInput struct { - Name string `path:"name" validate:"required,min=2"` -} - -type GetOutput struct { - Message string `json:"message"` -} - -type GetError struct { - Code int `json:"code"` - Message string `json:"message"` +type MyInput struct { + ID string `path:"id" validate:"required"` // Path parameter + Filter string `query:"filter" validate:"omitempty"` // Query parameter + Auth string `header:"Authorization"` // Header parameter + Title string `json:"title" validate:"required,min=1"` // JSON body field } - -// Works on app -fiberoapi.Get(oapi, "/greeting/:name", - func(c *fiber.Ctx, input GetInput) (GetOutput, GetError) { - return GetOutput{Message: "Hello " + input.Name}, GetError{} - }, - fiberoapi.OpenAPIOptions{ - OperationID: "get-greeting", - Tags: []string{"greeting"}, - Summary: "Get a personalized greeting", - }) - -// Works on groups too -v1 := fiberoapi.Group(oapi, "/api/v1") -fiberoapi.Get(v1, "/greeting/:name", handler, options) ``` -### POST with JSON body and validation +Special tags: +- `openapi:"-"` — Exclude a field from the OpenAPI schema (the field still works in the handler) +- `description:"text"` — Add a description to the field in the spec +- `resource:"document"` — Mark field as a resource identifier for dynamic authorization +- `action:"write"` — Specify the action for resource access checks + +## Groups ```go -type CreateUserInput struct { - Username string `json:"username" validate:"required,min=3,max=20,alphanum"` - Email string `json:"email" validate:"required,email"` - Age int `json:"age" validate:"required,min=13,max=120"` -} +app := fiber.New() +oapi := fiberoapi.New(app) -type CreateUserOutput struct { - ID string `json:"id"` - Message string `json:"message"` -} +v1 := fiberoapi.Group(oapi, "/api/v1") +users := fiberoapi.Group(v1, "/users") -type CreateUserError struct { - Code int `json:"code"` - Message string `json:"message"` -} +// OpenAPI methods on groups +fiberoapi.Get(users, "/:id", getUser, options) // Registers as GET /api/v1/users/{id} +fiberoapi.Post(users, "/", createUser, options) -fiberoapi.Post(oapi, "/users", - func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, CreateUserError) { - if input.Username == "admin" { - return CreateUserOutput{}, CreateUserError{ - Code: 403, - Message: "Username 'admin' is reserved", - } - } - - return CreateUserOutput{ - ID: "user_" + input.Username, - Message: "User created successfully", - }, CreateUserError{} - }, - fiberoapi.OpenAPIOptions{ - OperationID: "create-user", - Tags: []string{"users"}, - Summary: "Create a new user", - }) +// Standard Fiber middleware still works +v1.Use(authMiddleware) ``` -### PUT with path parameters and JSON body - -```go -type UpdateUserInput struct { - ID string `path:"id" validate:"required"` - Username string `json:"username" validate:"omitempty,min=3,max=20,alphanum"` - Email string `json:"email" validate:"omitempty,email"` - Age int `json:"age" validate:"omitempty,min=13,max=120"` -} - -type UpdateUserOutput struct { - ID string `json:"id"` - Message string `json:"message"` - Updated bool `json:"updated"` -} - -fiberoapi.Put(oapi, "/users/:id", - func(c *fiber.Ctx, input UpdateUserInput) (UpdateUserOutput, CreateUserError) { - if input.ID == "notfound" { - return UpdateUserOutput{}, CreateUserError{ - Code: 404, - Message: "User not found", - } - } - - return UpdateUserOutput{ - ID: input.ID, - Message: "User updated successfully", - Updated: true, - }, CreateUserError{} - }, - fiberoapi.OpenAPIOptions{ - OperationID: "update-user", - Tags: []string{"users"}, - Summary: "Update an existing user", - }) -``` +## Validation -### DELETE with path parameters +Uses `validator/v10`. Common tags: ```go -type DeleteUserInput struct { - ID string `path:"id" validate:"required"` -} - -type DeleteUserOutput struct { - ID string `json:"id"` - Message string `json:"message"` - Deleted bool `json:"deleted"` +type Input struct { + Name string `json:"name" validate:"required,min=3,max=50"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=13,max=120"` + Role string `json:"role" validate:"oneof=admin user guest"` + Tags []string `json:"tags" validate:"dive,min=1"` } - -fiberoapi.Delete(oapi, "/users/:id", - func(c *fiber.Ctx, input DeleteUserInput) (DeleteUserOutput, CreateUserError) { - if input.ID == "protected" { - return DeleteUserOutput{}, CreateUserError{ - Code: 403, - Message: "User is protected and cannot be deleted", - } - } - - return DeleteUserOutput{ - ID: input.ID, - Message: "User deleted successfully", - Deleted: true, - }, CreateUserError{} - }, - fiberoapi.OpenAPIOptions{ - OperationID: "delete-user", - Tags: []string{"users"}, - Summary: "Delete a user", - }) ``` ## Authentication & Authorization -Fiber-oapi provides comprehensive authentication and authorization support with JWT/Bearer tokens, role-based access control, and automatic OpenAPI security documentation. +### Supported Security Schemes -### Basic Authentication Setup +| Scheme | Config | Validator Interface | +|--------|--------|-------------------| +| Bearer Token | `Type: "http", Scheme: "bearer"` | `AuthorizationService` (built-in) | +| HTTP Basic | `Type: "http", Scheme: "basic"` | `BasicAuthValidator` | +| API Key | `Type: "apiKey", In: "header"/"query"/"cookie"` | `APIKeyValidator` | +| AWS SigV4 | `Type: "http", Scheme: "AWS4-HMAC-SHA256"` | `AWSSignatureValidator` | -First, implement the `AuthorizationService` interface: - -```go -package main +### Setup -import ( - "fmt" - "time" - fiberoapi "github.com/labbs/fiber-oapi" - "github.com/gofiber/fiber/v2" -) +Implement `AuthorizationService` (required) and any additional validator interfaces: -// Implement the AuthorizationService interface +```go type MyAuthService struct{} -func (s *MyAuthService) ValidateToken(token string) (*fiberoapi.AuthContext, error) { - // Validate your JWT token here - switch token { - case "admin-token": - return &fiberoapi.AuthContext{ - UserID: "admin-123", - Roles: []string{"admin", "user"}, - Scopes: []string{"read", "write", "delete"}, - Claims: map[string]interface{}{ - "sub": "admin-123", - "exp": time.Now().Add(time.Hour).Unix(), - }, - }, nil - case "user-token": - return &fiberoapi.AuthContext{ - UserID: "user-789", - Roles: []string{"user"}, - Scopes: []string{"read", "write"}, - Claims: map[string]interface{}{ - "sub": "user-789", - "exp": time.Now().Add(time.Hour).Unix(), - }, - }, nil - default: - return nil, fmt.Errorf("invalid token") - } -} +// Required: AuthorizationService +func (s *MyAuthService) ValidateToken(token string) (*fiberoapi.AuthContext, error) { ... } +func (s *MyAuthService) HasRole(ctx *fiberoapi.AuthContext, role string) bool { ... } +func (s *MyAuthService) HasScope(ctx *fiberoapi.AuthContext, scope string) bool { ... } +func (s *MyAuthService) CanAccessResource(ctx *fiberoapi.AuthContext, resourceType, resourceID, action string) (bool, error) { ... } +func (s *MyAuthService) GetUserPermissions(ctx *fiberoapi.AuthContext, resourceType, resourceID string) (*fiberoapi.ResourcePermission, error) { ... } -func (s *MyAuthService) HasRole(ctx *fiberoapi.AuthContext, role string) bool { - for _, r := range ctx.Roles { - if r == role { - return true - } - } - return false -} +// Optional: BasicAuthValidator +func (s *MyAuthService) ValidateBasicAuth(username, password string) (*fiberoapi.AuthContext, error) { ... } -func (s *MyAuthService) HasScope(ctx *fiberoapi.AuthContext, scope string) bool { - for _, sc := range ctx.Scopes { - if sc == scope { - return true - } - } - return false -} +// Optional: APIKeyValidator +func (s *MyAuthService) ValidateAPIKey(key, location, paramName string) (*fiberoapi.AuthContext, error) { ... } -func (s *MyAuthService) CanAccessResource(ctx *fiberoapi.AuthContext, resourceType, resourceID, action string) (bool, error) { - // Admins can do everything - if s.HasRole(ctx, "admin") { - return true, nil - } - - // Custom resource-based logic - if resourceType == "document" && action == "delete" { - return false, nil // Only admins can delete - } - - return s.HasScope(ctx, action), nil -} +// Optional: AWSSignatureValidator +func (s *MyAuthService) ValidateAWSSignature(params *fiberoapi.AWSSignatureParams) (*fiberoapi.AuthContext, error) { ... } +``` -func (s *MyAuthService) GetUserPermissions(ctx *fiberoapi.AuthContext, resourceType, resourceID string) (*fiberoapi.ResourcePermission, error) { - actions := []string{} - if s.HasScope(ctx, "read") { - actions = append(actions, "read") - } - if s.HasScope(ctx, "write") { - actions = append(actions, "write") - } - if s.HasRole(ctx, "admin") { - actions = append(actions, "delete") - } - - return &fiberoapi.ResourcePermission{ - ResourceType: resourceType, - ResourceID: resourceID, - Actions: actions, - }, nil -} +Configure multiple security schemes (OR semantics between them): -func main() { - app := fiber.New() - authService := &MyAuthService{} - - // Configure with authentication - config := fiberoapi.Config{ - EnableValidation: true, - EnableOpenAPIDocs: true, - EnableAuthorization: true, - AuthService: authService, - SecuritySchemes: map[string]fiberoapi.SecurityScheme{ - "bearerAuth": { - Type: "http", - Scheme: "bearer", - BearerFormat: "JWT", - Description: "JWT Bearer token", - }, +```go +config := fiberoapi.Config{ + EnableAuthorization: true, + AuthService: &MyAuthService{}, + SecuritySchemes: map[string]fiberoapi.SecurityScheme{ + "bearerAuth": { + Type: "http", + Scheme: "bearer", + BearerFormat: "JWT", + Description: "JWT Bearer token", }, - DefaultSecurity: []map[string][]string{ - {"bearerAuth": {}}, + "basicAuth": { + Type: "http", + Scheme: "basic", + Description: "HTTP Basic authentication", }, - } - - oapi := fiberoapi.New(app, config) - - // Your authenticated routes here... - - oapi.Listen(":3000") + "apiKeyAuth": { + Type: "apiKey", + In: "header", + Name: "X-API-Key", + Description: "API Key via header", + }, + }, + // Any of these schemes can authenticate a request (OR semantics) + DefaultSecurity: []map[string][]string{ + {"bearerAuth": {}}, + {"basicAuth": {}}, + {"apiKeyAuth": {}}, + }, } +oapi := fiberoapi.New(app, config) ``` ### Public vs Protected Routes ```go -// Public route (no authentication required) -fiberoapi.Get(oapi, "/health", - func(c *fiber.Ctx, input struct{}) (map[string]string, *fiberoapi.ErrorResponse) { - return map[string]string{"status": "ok"}, nil - }, +// Public route — no authentication +fiberoapi.Get(oapi, "/health", handler, fiberoapi.OpenAPIOptions{ Summary: "Health check", - Security: "disabled", // Explicitly disable auth for this route + Security: "disabled", }) -// Protected route (authentication required by default) -fiberoapi.Get(oapi, "/profile", - func(c *fiber.Ctx, input struct{}) (UserProfile, *fiberoapi.ErrorResponse) { - // Get authenticated user context - authCtx, err := fiberoapi.GetAuthContext(c) - if err != nil { - return UserProfile{}, &fiberoapi.ErrorResponse{ - Code: 401, - Details: "Authentication required", - Type: "auth_error", - } - } - - return UserProfile{ - UserID: authCtx.UserID, - Roles: authCtx.Roles, - }, nil - }, +// Protected route — uses default security +fiberoapi.Get(oapi, "/profile", handler, fiberoapi.OpenAPIOptions{ - Summary: "Get user profile", - Tags: []string{"user"}, + Summary: "Get profile", }) -``` - -### Role-Based Access Control -```go -type DocumentRequest struct { - DocumentID string `path:"documentId" validate:"required"` -} - -type DocumentResponse struct { - ID string `json:"id"` - Title string `json:"title"` - Content string `json:"content"` -} - -// Route requiring specific role -fiberoapi.Get(oapi, "/documents/:documentId", - func(c *fiber.Ctx, input DocumentRequest) (DocumentResponse, *fiberoapi.ErrorResponse) { - authCtx, _ := fiberoapi.GetAuthContext(c) - - // Check if user has required role - if !authService.HasRole(authCtx, "user") { - return DocumentResponse{}, &fiberoapi.ErrorResponse{ - Code: 403, - Details: "User role required", - Type: "authorization_error", - } - } - - // Check if user has required scope - if !authService.HasScope(authCtx, "read") { - return DocumentResponse{}, &fiberoapi.ErrorResponse{ - Code: 403, - Details: "Read permission required", - Type: "authorization_error", - } - } - - return DocumentResponse{ - ID: input.DocumentID, - Title: "Document Title", - Content: "Document content", - }, nil - }, - fiberoapi.OpenAPIOptions{ - Summary: "Get document", - Description: "Requires 'user' role and 'read' scope", - Tags: []string{"documents"}, - }) - -// Admin-only route -fiberoapi.Delete(oapi, "/documents/:documentId", - func(c *fiber.Ctx, input DocumentRequest) (map[string]bool, *fiberoapi.ErrorResponse) { - authCtx, _ := fiberoapi.GetAuthContext(c) - - // Only admins can delete - if !authService.HasRole(authCtx, "admin") { - return nil, &fiberoapi.ErrorResponse{ - Code: 403, - Details: "Admin role required", - Type: "authorization_error", - } - } - - return map[string]bool{"deleted": true}, nil - }, - fiberoapi.OpenAPIOptions{ - Summary: "Delete document", - Description: "Admin only - requires 'admin' role", - Tags: []string{"documents", "admin"}, - }) +// Per-route security override +fiberoapi.Get(oapi, "/admin", handler, + fiberoapi.WithSecurity( + fiberoapi.OpenAPIOptions{Summary: "Admin endpoint"}, + []map[string][]string{{"bearerAuth": {}}}, // Only bearer, not API key + )) ``` -### Security Helpers +### Declarative Role-Based Access Control -Use the helper functions to simplify security configuration: +Roles are checked automatically before your handler runs. No manual checks needed. ```go -// Add security to existing options -options := fiberoapi.OpenAPIOptions{Summary: "Protected endpoint"} -options = fiberoapi.WithSecurity(options, map[string][]string{ - "bearerAuth": {}, -}) - -// Disable security for specific route -options = fiberoapi.WithSecurityDisabled(options) - -// Add required permissions for documentation -options = fiberoapi.WithPermissions(options, "document:read", "workspace:admin") - -// Set resource type for documentation -options = fiberoapi.WithResourceType(options, "document") -``` - -### Authentication Context - -Access the authenticated user's information in your handlers: - -```go -fiberoapi.Post(oapi, "/posts", - func(c *fiber.Ctx, input CreatePostInput) (PostResponse, *fiberoapi.ErrorResponse) { - // Get authenticated user context - authCtx, err := fiberoapi.GetAuthContext(c) - if err != nil { - return PostResponse{}, &fiberoapi.ErrorResponse{ - Code: 401, - Details: "Authentication required", - Type: "auth_error", - } - } - - // Use auth context - fmt.Printf("User %s with roles %v creating post\n", authCtx.UserID, authCtx.Roles) - - // Access JWT claims if needed - if exp, ok := authCtx.Claims["exp"].(int64); ok { - if time.Now().Unix() > exp { - return PostResponse{}, &fiberoapi.ErrorResponse{ - Code: 401, - Details: "Token expired", - Type: "auth_error", - } - } - } - - return PostResponse{ - ID: "post-123", - Author: authCtx.UserID, - Title: input.Title, - }, nil - }, +// OR semantics: user needs at least ONE of the listed roles +fiberoapi.Get(oapi, "/documents/:id", handler, + fiberoapi.WithRoles( + fiberoapi.OpenAPIOptions{Summary: "Get document", Tags: []string{"documents"}}, + "admin", "editor", // admin OR editor can access + )) + +// AND semantics: user needs ALL of the listed roles +fiberoapi.Delete(oapi, "/documents/:id", handler, + fiberoapi.WithAllRoles( + fiberoapi.OpenAPIOptions{Summary: "Delete document", Tags: []string{"documents"}}, + "admin", "moderator", // must be admin AND moderator + )) + +// Inline via OpenAPIOptions +fiberoapi.Get(oapi, "/settings", handler, fiberoapi.OpenAPIOptions{ - Summary: "Create a post", - Tags: []string{"posts"}, + Summary: "Settings", + RequiredRoles: []string{"admin", "superadmin"}, + RequireAllRoles: false, // OR semantics (default) }) ``` -### OpenAPI Security Documentation - -When authentication is enabled, the OpenAPI specification automatically includes: - -- **Security Schemes**: JWT Bearer token configuration -- **Security Requirements**: Applied to protected endpoints -- **Security Overrides**: Public endpoints marked with `security: []` +Roles appear in the OpenAPI spec as extensions: ```json { - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "JWT Bearer token" - } - } - }, - "security": [ - {"bearerAuth": []} - ], - "paths": { - "/health": { - "get": { - "security": [], - "summary": "Health check" - } - }, - "/profile": { - "get": { - "security": [{"bearerAuth": []}], - "summary": "Get user profile" - } - } - } + "x-required-roles": ["admin", "editor"], + "x-required-roles-mode": "any" } ``` -### Testing with Authentication - -```bash -# Public endpoint -curl http://localhost:3000/health - -# Protected endpoint -curl -H "Authorization: Bearer your-jwt-token" http://localhost:3000/profile - -# Admin endpoint -curl -H "Authorization: Bearer admin-token" -X DELETE http://localhost:3000/documents/123 -``` - -### Error Responses +### Permissions and Resource Access -Authentication errors are automatically formatted: +```go +// RequiredPermissions are documented in the OpenAPI spec description +fiberoapi.Put(oapi, "/documents/:id", handler, + fiberoapi.OpenAPIOptions{ + Summary: "Update document", + RequiredRoles: []string{"editor"}, + RequiredPermissions: []string{"document:write"}, + }) -```json -{ - "code": 401, - "details": "Invalid token", - "type": "auth_error" +// Resource-based access via struct tags +type UpdateDocInput struct { + DocumentID string `path:"documentId" validate:"required" resource:"document" action:"write"` + Title string `json:"title" validate:"required"` } -``` -```json -{ - "code": 403, - "details": "Admin role required", - "type": "authorization_error" -} +// Dynamic resource access check in handler +fiberoapi.RequireResourceAccess(c, authService, "document", docID, "delete") ``` -### Complete Authentication Example +### Authentication Context -See the complete working example in `_examples/auth/main.go` which demonstrates: -- Multiple user types with different roles and scopes -- Role-based access control -- Scope-based permissions -- Resource-level authorization -- OpenAPI security documentation -- Public and protected endpoints +Access the authenticated user in handlers: ```go -// Clone the repository and run the auth example -go run _examples/auth/main.go - -// Visit http://localhost:3002/docs to see the documentation -// Test with different tokens: admin-token, user-token, readonly-token -``` +fiberoapi.Get(oapi, "/me", func(c *fiber.Ctx, input struct{}) (fiber.Map, *fiberoapi.ErrorResponse) { + authCtx, err := fiberoapi.GetAuthContext(c) + if err != nil { + return nil, &fiberoapi.ErrorResponse{Code: 401, Details: "Not authenticated"} + } + return fiber.Map{ + "user_id": authCtx.UserID, + "roles": authCtx.Roles, + "scopes": authCtx.Scopes, + "claims": authCtx.Claims, + }, nil +}, fiberoapi.OpenAPIOptions{Summary: "Current user"}) ``` -## Configuration +## Custom Error Handlers -The library supports flexible configuration through the `Config` struct: +### Validation Errors ```go -type Config struct { - EnableValidation bool // Enable/disable input validation (default: true) - EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) - EnableAuthorization bool // Enable authentication/authorization (default: false) - OpenAPIDocsPath string // Path for documentation UI (default: "/docs") - OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") - AuthService AuthorizationService // Service for handling auth - SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes - DefaultSecurity []map[string][]string // Default security requirements -} +oapi := fiberoapi.New(app, fiberoapi.Config{ + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(400).JSON(fiber.Map{ + "success": false, + "error": err.Error(), + }) + }, +}) ``` -### Default Configuration - -If no configuration is provided, the library uses these defaults: -- Validation: **enabled** -- Documentation: **enabled** -- Authorization: **disabled** -- Docs path: `/docs` -- JSON spec path: `/openapi.json` - -### Disabling Features +### Authentication/Authorization Errors ```go -// Disable documentation but keep validation -config := fiberoapi.Config{ - EnableValidation: true, - EnableOpenAPIDocs: false, -} - -// Or disable validation but keep docs -config := fiberoapi.Config{ - EnableValidation: false, - EnableOpenAPIDocs: true, - OpenAPIDocsPath: "/api-docs", - OpenAPIJSONPath: "/openapi.json", -} - -// Enable authentication with custom paths -config := fiberoapi.Config{ - EnableValidation: true, - EnableOpenAPIDocs: true, +oapi := fiberoapi.New(app, fiberoapi.Config{ EnableAuthorization: true, - AuthService: &MyAuthService{}, - OpenAPIDocsPath: "/documentation", - OpenAPIJSONPath: "/openapi.json", -} -``` - -## Validation - -This library uses `validator/v10` for validation. You can use all supported validation tags: - -- `required` - Required field -- `min=3,max=20` - Min/max length -- `email` - Valid email format -- `alphanum` - Alphanumeric characters only -- `uuid4` - UUID version 4 -- `url` - Valid URL -- `oneof=admin user guest` - Value from a list -- `dive` - Validation for slice elements -- `gtfield=MinPrice` - Greater than another field - -## Supported Parameter Types - -- **Path parameters**: `path:"paramName"` (GET, POST, PUT, DELETE) -- **Query parameters**: `query:"paramName"` (GET, DELETE) -- **JSON body**: `json:"fieldName"` (POST, PUT) - -## Supported HTTP Methods - -All methods work with both the main app and groups through the unified API: - -- **GET**: `fiberoapi.Get()` - Retrieve resources with path/query parameters -- **POST**: `fiberoapi.Post()` - Create resources with JSON body + optional path parameters -- **PUT**: `fiberoapi.Put()` - Update resources with path parameters + JSON body -- **DELETE**: `fiberoapi.Delete()` - Delete resources with path parameters + optional query parameters - -### Legacy Method Names (Still Supported) - -For backward compatibility, the old method names are still available: -- `fiberoapi.GetOApi()` -- `fiberoapi.PostOApi()` -- `fiberoapi.PutOApi()` -- `fiberoapi.DeleteOApi()` - -## Groups - -Fiber-oapi provides full support for Fiber groups while maintaining access to OpenAPI methods: - -```go -// Create the main app -app := fiber.New() -oapi := fiberoapi.New(app) - -// Create groups - they have access to all Fiber Router methods AND OpenAPI methods -v1 := fiberoapi.Group(oapi, "/api/v1") -v2 := fiberoapi.Group(oapi, "/api/v2") - -// Nested groups work too -users := fiberoapi.Group(v1, "/users") -admin := fiberoapi.Group(v1, "/admin") - -// Use OpenAPI methods on any router (app or group) -fiberoapi.Get(oapi, "/health", healthHandler, options) // Main app -fiberoapi.Get(v1, "/status", statusHandler, options) // Group -fiberoapi.Post(users, "/", createUserHandler, options) // Nested group -fiberoapi.Put(users, "/:id", updateUserHandler, options) // Nested group - -// Use standard Fiber Router methods on groups (inherited via embedding) -v1.Use("/protected", authMiddleware) // Middleware -admin.Get("/stats", func(c *fiber.Ctx) error { // Regular Fiber handler - return c.JSON(fiber.Map{"stats": "data"}) + AuthService: authService, + AuthErrorHandler: func(c *fiber.Ctx, err *fiberoapi.AuthError) error { + // err.StatusCode: 401, 403, or 5xx + // err.Message: human-readable error message + return c.Status(err.StatusCode).JSON(fiber.Map{ + "error": err.Message, + "status": err.StatusCode, + }) + }, }) - -// For static files, use the main Fiber app -app.Static("/files", "./uploads") // Static files via main app - -// Groups preserve full path context for OpenAPI documentation -// fiberoapi.Get(users, "/:id", ...) registers as GET /api/v1/users/{id} ``` -### Group Features - -- **Fiber Router compatibility**: Groups embed `fiber.Router` so standard Router methods work (Use, Get, Post, etc.) -- **OpenAPI method support**: Use `fiberoapi.Get()`, `fiberoapi.Post()`, etc. on groups -- **Nested groups**: Create groups within groups with proper path handling -- **Path prefix handling**: OpenAPI paths are automatically constructed with full prefixes -- **Unified API**: Same function names work on both app and groups through interface polymorphism +Without custom handlers, default error responses are returned: -**Note**: For features like static file serving, use the main Fiber app: `app.Static("/path", "./dir")` - -## Error Handling +```json +// 401 - Authentication failure +{"code": 401, "details": "invalid token", "type": "authentication_error"} -Validation errors are automatically formatted and returned with HTTP status 400: +// 403 - Authorization failure +{"code": 403, "details": "requires one of: admin, editor", "type": "authorization_error"} -```json -{ - "error": "Validation failed", - "details": "Key: 'CreateUserInput.Username' Error:Field validation for 'Username' failed on the 'min' tag" -} +// 400 - Validation failure +{"code": 400, "details": "...", "type": "validation_error"} ``` -Custom errors use the `StatusCode` from your error struct. +## Conditional Auth Middleware -## Testing +Standalone middleware functions for use outside the declarative route system: -Run tests: - -```bash -go test -v +```go +// Smart middleware that auto-detects security schemes and excludes doc routes +app.Use(fiberoapi.SmartAuthMiddleware(authService, config)) + +// Skip auth for specific paths +app.Use(fiberoapi.ConditionalAuthMiddleware( + fiberoapi.BearerTokenMiddleware(authService), + "/health", "/docs", "/openapi.json", +)) + +// Individual scheme middleware +app.Use(fiberoapi.BearerTokenMiddleware(authService)) +app.Use(fiberoapi.BasicAuthMiddleware(authService)) +app.Use(fiberoapi.APIKeyMiddleware(authService, scheme)) +app.Use(fiberoapi.AWSSignatureMiddleware(authService)) + +// Role guard middleware +app.Use(fiberoapi.RoleGuard(authService, "admin")) ``` -## Complete Example with Groups +## Security Helpers ```go -package main +opts := fiberoapi.OpenAPIOptions{Summary: "My endpoint"} -import ( - "github.com/gofiber/fiber/v2" - fiberoapi "github.com/labbs/fiber-oapi" -) +// Security +opts = fiberoapi.WithSecurity(opts, []map[string][]string{{"bearerAuth": {}}}) +opts = fiberoapi.WithSecurityDisabled(opts) -type UserInput struct { - ID int `path:"id" validate:"required,min=1"` -} - -type UserOutput struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type UserError struct { - Message string `json:"message"` -} - -func main() { - app := fiber.New() - oapi := fiberoapi.New(app) +// Roles +opts = fiberoapi.WithRoles(opts, "admin", "editor") // OR semantics +opts = fiberoapi.WithAllRoles(opts, "admin", "moderator") // AND semantics - // Global routes - fiberoapi.Get(oapi, "/health", func(c *fiber.Ctx, input struct{}) (map[string]string, struct{}) { - return map[string]string{"status": "ok"}, struct{}{} - }, fiberoapi.OpenAPIOptions{ - Summary: "Health check", - Tags: []string{"health"}, - }) - - // API v1 group - v1 := fiberoapi.Group(oapi, "/api/v1") - - fiberoapi.Get(v1, "/users/:id", func(c *fiber.Ctx, input UserInput) (UserOutput, UserError) { - return UserOutput{ID: input.ID, Name: "User " + string(rune(input.ID))}, UserError{} - }, fiberoapi.OpenAPIOptions{ - Summary: "Get user by ID", - Tags: []string{"users"}, - }) - - fiberoapi.Post(v1, "/users", func(c *fiber.Ctx, input UserOutput) (UserOutput, UserError) { - return UserOutput{ID: 99, Name: input.Name}, UserError{} - }, fiberoapi.OpenAPIOptions{ - Summary: "Create a new user", - Tags: []string{"users"}, - }) - - // API v2 with nested groups - v2 := fiberoapi.Group(oapi, "/api/v2") - usersV2 := fiberoapi.Group(v2, "/users") +// Documentation +opts = fiberoapi.WithPermissions(opts, "document:read", "document:write") +opts = fiberoapi.WithResourceType(opts, "document") +``` - fiberoapi.Get(usersV2, "/:id", func(c *fiber.Ctx, input UserInput) (UserOutput, UserError) { - return UserOutput{ID: input.ID, Name: "User v2 " + string(rune(input.ID))}, UserError{} - }, fiberoapi.OpenAPIOptions{ - Summary: "Get user by ID (v2)", - Tags: []string{"users", "v2"}, - }) +## OpenAPI Spec Generation - // Mix with standard Fiber methods - v1.Use("/admin", func(c *fiber.Ctx) error { - return c.Next() // Auth middleware - }) +The spec is available in both JSON and YAML: - app.Listen(":3000") - // Visit http://localhost:3000/docs to see the Redoc documentation -} +```go +// Automatic endpoints +// GET /openapi.json +// GET /openapi.yaml +// GET /docs (Redoc UI) + +// Programmatic access +spec := oapi.GenerateOpenAPISpec() // map[string]interface{} +yamlSpec, err := oapi.GenerateOpenAPISpecYAML() // string ``` -## Advanced Usage - -### Custom Documentation Configuration +### Custom Documentation ```go -config := fiberoapi.DocConfig{ +oapi.SetupDocs(fiberoapi.DocConfig{ Title: "My API", Description: "My API description", Version: "2.0.0", DocsPath: "/documentation", JSONPath: "/api-spec.json", -} -oapi.SetupDocs(config) // Optional - docs are auto-configured by default -``` - -### OApiRouter Interface - -The library uses an `OApiRouter` interface that allows the same functions to work seamlessly with both apps and groups: - -```go -// This interface is implemented by both *OApiApp and *OApiGroup -type OApiRouter interface { - GetApp() *OApiApp - GetPrefix() string -} - -// So these functions work with both: -func Get[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions) -func Post[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions) -func Put[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions) -func Delete[T any, U any, E any](router OApiRouter, path string, handler HandlerFunc[T, U, E], options OpenAPIOptions) -func Group(router OApiRouter, prefix string, handlers ...fiber.Handler) *OApiGroup -``` - -## Documentation - -When `EnableOpenAPIDocs` is set to `true` (default), the library automatically sets up: - -- **Redoc UI**: Modern, responsive documentation interface available at the configured docs path (default: `/docs`) -- **OpenAPI JSON**: Complete OpenAPI 3.0 specification available at the configured JSON path (default: `/openapi.json`) -- **Automatic Schema Generation**: Input and output types are automatically converted to OpenAPI schemas -- **Components Section**: All schemas are properly organized in the `components/schemas` section - -No manual setup required! Just visit `http://localhost:3000/docs` to see your API documentation with Redoc. - -### Redoc vs Swagger UI - -This library uses **Redoc** for documentation UI instead of Swagger UI because: -- **Better performance** with large APIs -- **Responsive design** that works great on mobile -- **Clean, modern interface** -- **Better OpenAPI 3.0 support** -- **No JavaScript framework dependencies** - -### OpenAPI Schema Generation - -The library automatically generates OpenAPI 3.0 schemas from your Go types: - -```go -// This struct automatically becomes an OpenAPI schema -type User struct { - ID string `json:"id"` - Username string `json:"username" validate:"required,min=3"` - Email string `json:"email" validate:"required,email"` - Age int `json:"age" validate:"min=13,max=120"` -} +}) ``` -Generated OpenAPI spec will include: -- Complete path definitions with parameters -- Request/response schemas -- Validation rules as schema constraints -- Proper HTTP status codes -- Operation IDs, tags, and descriptions - -## Migration from v1 - -If you're migrating from a previous version, here are the key changes: - -### 1. New Unified API (Recommended) +## Testing -```go -fiberoapi.Get(oapi, "/users/:id", handler, options) // Works on app -fiberoapi.Post(oapi, "/users", handler, options) // Works on app +```bash +# Run all tests +go test -v ./... -// And seamlessly on groups -v1 := fiberoapi.Group(oapi, "/api/v1") -fiberoapi.Get(v1, "/users/:id", handler, options) // Works on groups -fiberoapi.Post(v1, "/users", handler, options) // Works on groups +# Run the auth example +go run _examples/auth/main.go +# Visit http://localhost:3002/docs ``` -### 2. Group Support +Testing with authentication: -```go -// New group functionality -v1 := fiberoapi.Group(oapi, "/api/v1") -users := fiberoapi.Group(v1, "/users") +```bash +# Bearer token +curl -H "Authorization: Bearer admin-token" http://localhost:3002/me -// All OpenAPI methods work on groups -fiberoapi.Get(users, "/:id", getUserHandler, options) -fiberoapi.Post(users, "/", createUserHandler, options) +# Basic auth +curl --user admin:admin-pass http://localhost:3002/me -// Standard Fiber Router methods work too (inherited via embedding) -users.Use(authMiddleware) // Middleware -users.Get("/legacy", func(c *fiber.Ctx) error { // Regular Fiber handler - return c.SendString("legacy endpoint") -}) +# API key +curl -H "X-API-Key: my-secret-api-key" http://localhost:3002/documents/doc-1 -// For static files, use the main Fiber app -app.Static("/avatars", "./uploads") // Static files via main app +# Public endpoint +curl http://localhost:3002/health ``` -### 3. Documentation UI +## Complete Example -- **Changed from Swagger UI to Redoc** for better performance and modern UI -- Same paths: `/docs` for UI, `/openapi.json` for spec -- No code changes required for existing documentation setup \ No newline at end of file +See `_examples/auth/main.go` for a full working example with: +- Multiple security schemes (Bearer, Basic, API Key, AWS SigV4) +- Declarative role-based access control +- Custom auth error handler +- Public and protected routes +- Resource-level authorization +- OpenAPI documentation with security schemes