mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-30 00:04:25 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c5c7689e9 | ||
|
|
08050c960d |
@@ -47,8 +47,8 @@ func main() {
|
|||||||
handler.RegisterModel("public", modelNames[i], model)
|
handler.RegisterModel("public", modelNames[i], model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup routes using new SetupMuxRoutes function
|
// Setup routes using new SetupMuxRoutes function (without authentication)
|
||||||
resolvespec.SetupMuxRoutes(r, handler)
|
resolvespec.SetupMuxRoutes(r, handler, nil)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
logger.Info("Starting server on :8080")
|
logger.Info("Starting server on :8080")
|
||||||
|
|||||||
97
pkg/common/handler_example.go
Normal file
97
pkg/common/handler_example.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// Example showing how to use the common handler interfaces
|
||||||
|
// This file demonstrates the handler interface hierarchy and usage patterns
|
||||||
|
|
||||||
|
// ProcessWithAnyHandler demonstrates using the base SpecHandler interface
|
||||||
|
// which works with any handler type (resolvespec, restheadspec, or funcspec)
|
||||||
|
func ProcessWithAnyHandler(handler SpecHandler) Database {
|
||||||
|
// All handlers expose GetDatabase() through the SpecHandler interface
|
||||||
|
return handler.GetDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessCRUDRequest demonstrates using the CRUDHandler interface
|
||||||
|
// which works with resolvespec.Handler and restheadspec.Handler
|
||||||
|
func ProcessCRUDRequest(handler CRUDHandler, w ResponseWriter, r Request, params map[string]string) {
|
||||||
|
// Both resolvespec and restheadspec handlers implement Handle()
|
||||||
|
handler.Handle(w, r, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessMetadataRequest demonstrates getting metadata from CRUD handlers
|
||||||
|
func ProcessMetadataRequest(handler CRUDHandler, w ResponseWriter, r Request, params map[string]string) {
|
||||||
|
// Both resolvespec and restheadspec handlers implement HandleGet()
|
||||||
|
handler.HandleGet(w, r, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage patterns (not executable, just for documentation):
|
||||||
|
/*
|
||||||
|
// Example 1: Using with resolvespec.Handler
|
||||||
|
func ExampleResolveSpec() {
|
||||||
|
db := // ... get database
|
||||||
|
registry := // ... get registry
|
||||||
|
|
||||||
|
handler := resolvespec.NewHandler(db, registry)
|
||||||
|
|
||||||
|
// Can be used as SpecHandler
|
||||||
|
var specHandler SpecHandler = handler
|
||||||
|
database := specHandler.GetDatabase()
|
||||||
|
|
||||||
|
// Can be used as CRUDHandler
|
||||||
|
var crudHandler CRUDHandler = handler
|
||||||
|
crudHandler.Handle(w, r, params)
|
||||||
|
crudHandler.HandleGet(w, r, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Using with restheadspec.Handler
|
||||||
|
func ExampleRestHeadSpec() {
|
||||||
|
db := // ... get database
|
||||||
|
registry := // ... get registry
|
||||||
|
|
||||||
|
handler := restheadspec.NewHandler(db, registry)
|
||||||
|
|
||||||
|
// Can be used as SpecHandler
|
||||||
|
var specHandler SpecHandler = handler
|
||||||
|
database := specHandler.GetDatabase()
|
||||||
|
|
||||||
|
// Can be used as CRUDHandler
|
||||||
|
var crudHandler CRUDHandler = handler
|
||||||
|
crudHandler.Handle(w, r, params)
|
||||||
|
crudHandler.HandleGet(w, r, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Using with funcspec.Handler
|
||||||
|
func ExampleFuncSpec() {
|
||||||
|
db := // ... get database
|
||||||
|
|
||||||
|
handler := funcspec.NewHandler(db)
|
||||||
|
|
||||||
|
// Can be used as SpecHandler
|
||||||
|
var specHandler SpecHandler = handler
|
||||||
|
database := specHandler.GetDatabase()
|
||||||
|
|
||||||
|
// Can be used as QueryHandler
|
||||||
|
var queryHandler QueryHandler = handler
|
||||||
|
// funcspec has different methods: SqlQueryList() and SqlQuery()
|
||||||
|
// which return HTTP handler functions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Polymorphic handler processing
|
||||||
|
func ProcessHandlers(handlers []SpecHandler) {
|
||||||
|
for _, handler := range handlers {
|
||||||
|
// All handlers expose the database
|
||||||
|
db := handler.GetDatabase()
|
||||||
|
|
||||||
|
// Type switch for specific handler types
|
||||||
|
switch h := handler.(type) {
|
||||||
|
case CRUDHandler:
|
||||||
|
// This is resolvespec or restheadspec
|
||||||
|
// Can call Handle() and HandleGet()
|
||||||
|
_ = h
|
||||||
|
case QueryHandler:
|
||||||
|
// This is funcspec
|
||||||
|
// Can call SqlQueryList() and SqlQuery()
|
||||||
|
_ = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -246,3 +246,39 @@ type PrimaryKeyNameProvider interface {
|
|||||||
type SchemaProvider interface {
|
type SchemaProvider interface {
|
||||||
SchemaName() string
|
SchemaName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SpecHandler interface represents common functionality across all spec handlers
|
||||||
|
// This is the base interface implemented by:
|
||||||
|
// - resolvespec.Handler: Handles CRUD operations via request body with explicit operation field
|
||||||
|
// - restheadspec.Handler: Handles CRUD operations via HTTP methods (GET/POST/PUT/DELETE)
|
||||||
|
// - funcspec.Handler: Handles custom SQL query execution with dynamic parameters
|
||||||
|
//
|
||||||
|
// The interface hierarchy is:
|
||||||
|
//
|
||||||
|
// SpecHandler (base)
|
||||||
|
// ├── CRUDHandler (resolvespec, restheadspec)
|
||||||
|
// └── QueryHandler (funcspec)
|
||||||
|
type SpecHandler interface {
|
||||||
|
// GetDatabase returns the underlying database connection
|
||||||
|
GetDatabase() Database
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUDHandler interface for handlers that support CRUD operations
|
||||||
|
// This is implemented by resolvespec.Handler and restheadspec.Handler
|
||||||
|
type CRUDHandler interface {
|
||||||
|
SpecHandler
|
||||||
|
|
||||||
|
// Handle processes API requests through router-agnostic interface
|
||||||
|
Handle(w ResponseWriter, r Request, params map[string]string)
|
||||||
|
|
||||||
|
// HandleGet processes GET requests for metadata
|
||||||
|
HandleGet(w ResponseWriter, r Request, params map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryHandler interface for handlers that execute SQL queries
|
||||||
|
// This is implemented by funcspec.Handler
|
||||||
|
// Note: funcspec uses standard http.ResponseWriter and *http.Request instead of common interfaces
|
||||||
|
type QueryHandler interface {
|
||||||
|
SpecHandler
|
||||||
|
// Methods are defined in funcspec package due to different function signature requirements
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ func NewHandler(db common.Database) *Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDatabase returns the underlying database connection
|
||||||
|
// Implements common.SpecHandler interface
|
||||||
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
|
return h.db
|
||||||
|
}
|
||||||
|
|
||||||
// Hooks returns the hook registry for this handler
|
// Hooks returns the hook registry for this handler
|
||||||
// Use this to register custom hooks for operations
|
// Use this to register custom hooks for operations
|
||||||
func (h *Handler) Hooks() *HookRegistry {
|
func (h *Handler) Hooks() *HookRegistry {
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ func Init(dev bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateLoggerPath(path string, dev bool) {
|
||||||
|
defaultConfig := zap.NewProductionConfig()
|
||||||
|
if dev {
|
||||||
|
defaultConfig = zap.NewDevelopmentConfig()
|
||||||
|
}
|
||||||
|
defaultConfig.OutputPaths = []string{path}
|
||||||
|
UpdateLogger(&defaultConfig)
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateLogger(config *zap.Config) {
|
func UpdateLogger(config *zap.Config) {
|
||||||
defaultConfig := zap.NewProductionConfig()
|
defaultConfig := zap.NewProductionConfig()
|
||||||
defaultConfig.OutputPaths = []string{"resolvespec.log"}
|
defaultConfig.OutputPaths = []string{"resolvespec.log"}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
|||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDatabase returns the underlying database connection
|
||||||
|
// Implements common.SpecHandler interface
|
||||||
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
|
return h.db
|
||||||
|
}
|
||||||
|
|
||||||
// handlePanic is a helper function to handle panics with stack traces
|
// handlePanic is a helper function to handle panics with stack traces
|
||||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||||
stack := debug.Stack()
|
stack := debug.Stack()
|
||||||
|
|||||||
@@ -37,28 +37,46 @@ func NewStandardBunRouter() *router.StandardBunRouterAdapter {
|
|||||||
return router.NewStandardBunRouterAdapter()
|
return router.NewStandardBunRouterAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc is a function that wraps an http.Handler with additional functionality
|
||||||
|
type MiddlewareFunc func(http.Handler) http.Handler
|
||||||
|
|
||||||
// SetupMuxRoutes sets up routes for the ResolveSpec API with Mux
|
// SetupMuxRoutes sets up routes for the ResolveSpec API with Mux
|
||||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
|
// authMiddleware is optional - if provided, routes will be protected with the middleware
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
|
||||||
|
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
|
||||||
|
// Create handler functions
|
||||||
|
postEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.Handle(respAdapter, reqAdapter, vars)
|
handler.Handle(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("POST")
|
})
|
||||||
|
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
postEntityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.Handle(respAdapter, reqAdapter, vars)
|
handler.Handle(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("POST")
|
})
|
||||||
|
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
getEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.HandleGet(respAdapter, reqAdapter, vars)
|
handler.HandleGet(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("GET")
|
})
|
||||||
|
|
||||||
|
// Apply authentication middleware if provided
|
||||||
|
if authMiddleware != nil {
|
||||||
|
postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc)
|
||||||
|
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc)
|
||||||
|
getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
muxRouter.Handle("/{schema}/{entity}", postEntityHandler).Methods("POST")
|
||||||
|
muxRouter.Handle("/{schema}/{entity}/{id}", postEntityWithIDHandler).Methods("POST")
|
||||||
|
muxRouter.Handle("/{schema}/{entity}", getEntityHandler).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example usage functions for documentation:
|
// Example usage functions for documentation:
|
||||||
@@ -68,12 +86,20 @@ func ExampleWithGORM(db *gorm.DB) {
|
|||||||
// Create handler using GORM
|
// Create handler using GORM
|
||||||
handler := NewHandlerWithGORM(db)
|
handler := NewHandlerWithGORM(db)
|
||||||
|
|
||||||
// Setup router
|
// Setup router without authentication
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
SetupMuxRoutes(muxRouter, handler)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
|
|
||||||
// Register models
|
// Register models
|
||||||
// handler.RegisterModel("public", "users", &User{})
|
// handler.RegisterModel("public", "users", &User{})
|
||||||
|
|
||||||
|
// To add authentication, pass a middleware function:
|
||||||
|
// import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
// secList := security.NewSecurityList(myProvider)
|
||||||
|
// authMiddleware := func(h http.Handler) http.Handler {
|
||||||
|
// return security.NewAuthHandler(secList, h)
|
||||||
|
// }
|
||||||
|
// SetupMuxRoutes(muxRouter, handler, authMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExampleWithBun shows how to switch to Bun ORM
|
// ExampleWithBun shows how to switch to Bun ORM
|
||||||
@@ -88,9 +114,9 @@ func ExampleWithBun(bunDB *bun.DB) {
|
|||||||
// Create handler
|
// Create handler
|
||||||
handler := NewHandler(dbAdapter, registry)
|
handler := NewHandler(dbAdapter, registry)
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes without authentication
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
SetupMuxRoutes(muxRouter, handler)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
|
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
|||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDatabase returns the underlying database connection
|
||||||
|
// Implements common.SpecHandler interface
|
||||||
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
|
return h.db
|
||||||
|
}
|
||||||
|
|
||||||
// Hooks returns the hook registry for this handler
|
// Hooks returns the hook registry for this handler
|
||||||
// Use this to register custom hooks for operations
|
// Use this to register custom hooks for operations
|
||||||
func (h *Handler) Hooks() *HookRegistry {
|
func (h *Handler) Hooks() *HookRegistry {
|
||||||
|
|||||||
@@ -90,31 +90,51 @@ func NewStandardBunRouter() *router.StandardBunRouterAdapter {
|
|||||||
return router.NewStandardBunRouterAdapter()
|
return router.NewStandardBunRouterAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc is a function that wraps an http.Handler with additional functionality
|
||||||
|
type MiddlewareFunc func(http.Handler) http.Handler
|
||||||
|
|
||||||
// SetupMuxRoutes sets up routes for the RestHeadSpec API with Mux
|
// SetupMuxRoutes sets up routes for the RestHeadSpec API with Mux
|
||||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
|
// authMiddleware is optional - if provided, routes will be protected with the middleware
|
||||||
// GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}
|
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
|
||||||
|
// Create handler functions
|
||||||
|
entityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.Handle(respAdapter, reqAdapter, vars)
|
handler.Handle(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("GET", "POST")
|
})
|
||||||
|
|
||||||
// GET, PUT, PATCH, DELETE for /{schema}/{entity}/{id}
|
entityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.Handle(respAdapter, reqAdapter, vars)
|
handler.Handle(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("GET", "PUT", "PATCH", "DELETE", "POST")
|
})
|
||||||
|
|
||||||
// GET for metadata (using HandleGet)
|
metadataHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
muxRouter.HandleFunc("/{schema}/{entity}/metadata", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
reqAdapter := router.NewHTTPRequest(r)
|
reqAdapter := router.NewHTTPRequest(r)
|
||||||
respAdapter := router.NewHTTPResponseWriter(w)
|
respAdapter := router.NewHTTPResponseWriter(w)
|
||||||
handler.HandleGet(respAdapter, reqAdapter, vars)
|
handler.HandleGet(respAdapter, reqAdapter, vars)
|
||||||
}).Methods("GET")
|
})
|
||||||
|
|
||||||
|
// Apply authentication middleware if provided
|
||||||
|
if authMiddleware != nil {
|
||||||
|
entityHandler = authMiddleware(entityHandler).(http.HandlerFunc)
|
||||||
|
entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc)
|
||||||
|
metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
// GET, POST for /{schema}/{entity}
|
||||||
|
muxRouter.Handle("/{schema}/{entity}", entityHandler).Methods("GET", "POST")
|
||||||
|
|
||||||
|
// GET, PUT, PATCH, DELETE, POST for /{schema}/{entity}/{id}
|
||||||
|
muxRouter.Handle("/{schema}/{entity}/{id}", entityWithIDHandler).Methods("GET", "PUT", "PATCH", "DELETE", "POST")
|
||||||
|
|
||||||
|
// GET for metadata (using HandleGet)
|
||||||
|
muxRouter.Handle("/{schema}/{entity}/metadata", metadataHandler).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example usage functions for documentation:
|
// Example usage functions for documentation:
|
||||||
@@ -124,12 +144,20 @@ func ExampleWithGORM(db *gorm.DB) {
|
|||||||
// Create handler using GORM
|
// Create handler using GORM
|
||||||
handler := NewHandlerWithGORM(db)
|
handler := NewHandlerWithGORM(db)
|
||||||
|
|
||||||
// Setup router
|
// Setup router without authentication
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
SetupMuxRoutes(muxRouter, handler)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
|
|
||||||
// Register models
|
// Register models
|
||||||
// handler.registry.RegisterModel("public.users", &User{})
|
// handler.registry.RegisterModel("public.users", &User{})
|
||||||
|
|
||||||
|
// To add authentication, pass a middleware function:
|
||||||
|
// import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
// secList := security.NewSecurityList(myProvider)
|
||||||
|
// authMiddleware := func(h http.Handler) http.Handler {
|
||||||
|
// return security.NewAuthHandler(secList, h)
|
||||||
|
// }
|
||||||
|
// SetupMuxRoutes(muxRouter, handler, authMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExampleWithBun shows how to switch to Bun ORM
|
// ExampleWithBun shows how to switch to Bun ORM
|
||||||
@@ -144,9 +172,9 @@ func ExampleWithBun(bunDB *bun.DB) {
|
|||||||
// Create handler
|
// Create handler
|
||||||
handler := NewHandler(dbAdapter, registry)
|
handler := NewHandler(dbAdapter, registry)
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes without authentication
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
SetupMuxRoutes(muxRouter, handler)
|
SetupMuxRoutes(muxRouter, handler, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
|
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ security.UserContext{
|
|||||||
RemoteID: "remote_xyz", // Remote system ID
|
RemoteID: "remote_xyz", // Remote system ID
|
||||||
Roles: []string{"admin"}, // User roles
|
Roles: []string{"admin"}, // User roles
|
||||||
Email: "john@example.com", // User email
|
Email: "john@example.com", // User email
|
||||||
Claims: map[string]any{}, // Additional metadata
|
Claims: map[string]any{}, // Additional authentication claims
|
||||||
|
Meta: map[string]any{}, // Additional metadata (JSON-serializable)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -621,6 +622,67 @@ func main() {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Authentication Modes
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Required authentication (default)
|
||||||
|
// Authentication must succeed or returns 401
|
||||||
|
router.Use(security.NewAuthMiddleware(securityList))
|
||||||
|
|
||||||
|
// Skip authentication for specific routes
|
||||||
|
// Always sets guest user context
|
||||||
|
func PublicRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.SkipAuth(r.Context())
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
// Guest context will be set
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional authentication for specific routes
|
||||||
|
// Tries to authenticate, falls back to guest if it fails
|
||||||
|
func HomeRoute(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.OptionalAuth(r.Context())
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
userCtx, _ := security.GetUserContext(r.Context())
|
||||||
|
if userCtx.UserID == 0 {
|
||||||
|
// Guest user
|
||||||
|
} else {
|
||||||
|
// Authenticated user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comparison:**
|
||||||
|
- **Required**: Auth must succeed or return 401 (default)
|
||||||
|
- **SkipAuth**: Never tries to authenticate, always guest
|
||||||
|
- **OptionalAuth**: Tries to authenticate, guest on failure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standalone Handlers
|
||||||
|
|
||||||
|
```go
|
||||||
|
// NewAuthHandler - Required authentication (returns 401 on failure)
|
||||||
|
authHandler := security.NewAuthHandler(securityList, myHandler)
|
||||||
|
http.Handle("/api/protected", authHandler)
|
||||||
|
|
||||||
|
// NewOptionalAuthHandler - Optional authentication (guest on failure)
|
||||||
|
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
|
||||||
|
http.Handle("/home", optionalHandler)
|
||||||
|
|
||||||
|
// Example handler
|
||||||
|
func myHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userCtx, _ := security.GetUserContext(r.Context())
|
||||||
|
if userCtx.UserID == 0 {
|
||||||
|
// Guest user
|
||||||
|
} else {
|
||||||
|
// Authenticated user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Context Helpers
|
## Context Helpers
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -635,6 +697,7 @@ sessionID, ok := security.GetSessionID(ctx)
|
|||||||
remoteID, ok := security.GetRemoteID(ctx)
|
remoteID, ok := security.GetRemoteID(ctx)
|
||||||
roles, ok := security.GetUserRoles(ctx)
|
roles, ok := security.GetUserRoles(ctx)
|
||||||
email, ok := security.GetUserEmail(ctx)
|
email, ok := security.GetUserEmail(ctx)
|
||||||
|
meta, ok := security.GetUserMeta(ctx)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -118,14 +118,15 @@ Enhanced user context with complete user information:
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
UserID int // User's unique ID
|
UserID int // User's unique ID
|
||||||
UserName string // Username
|
UserName string // Username
|
||||||
UserLevel int // User privilege level
|
UserLevel int // User privilege level
|
||||||
SessionID string // Current session ID
|
SessionID string // Current session ID
|
||||||
RemoteID string // Remote system ID
|
RemoteID string // Remote system ID
|
||||||
Roles []string // User roles
|
Roles []string // User roles
|
||||||
Email string // User email
|
Email string // User email
|
||||||
Claims map[string]any // Additional metadata
|
Claims map[string]any // Additional authentication claims
|
||||||
|
Meta map[string]any // Additional metadata (can hold any JSON-serializable values)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -629,6 +630,142 @@ func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, tab
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Middleware and Handler API
|
||||||
|
|
||||||
|
### NewAuthMiddleware
|
||||||
|
Standard middleware that authenticates all requests:
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Use(security.NewAuthMiddleware(securityList))
|
||||||
|
```
|
||||||
|
|
||||||
|
Routes can skip authentication using the `SkipAuth` helper:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func PublicHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.SkipAuth(r.Context())
|
||||||
|
// This route will bypass authentication
|
||||||
|
// A guest user context will be set instead
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Handle("/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.SkipAuth(r.Context())
|
||||||
|
PublicHandler(w, r.WithContext(ctx))
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
When authentication is skipped, a guest user context is automatically set:
|
||||||
|
- UserID: 0
|
||||||
|
- UserName: "guest"
|
||||||
|
- Roles: ["guest"]
|
||||||
|
- RemoteID: Request's remote address
|
||||||
|
|
||||||
|
Routes can use optional authentication with the `OptionalAuth` helper:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func OptionalAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.OptionalAuth(r.Context())
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// This route will try to authenticate
|
||||||
|
// If authentication succeeds, authenticated user context is set
|
||||||
|
// If authentication fails, guest user context is set instead
|
||||||
|
|
||||||
|
userCtx, _ := security.GetUserContext(r.Context())
|
||||||
|
if userCtx.UserID == 0 {
|
||||||
|
// Guest user
|
||||||
|
fmt.Fprintf(w, "Welcome, guest!")
|
||||||
|
} else {
|
||||||
|
// Authenticated user
|
||||||
|
fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Handle("/home", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := security.OptionalAuth(r.Context())
|
||||||
|
OptionalAuthHandler(w, r.WithContext(ctx))
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication Modes Summary:**
|
||||||
|
- **Required (default)**: Authentication must succeed or returns 401
|
||||||
|
- **SkipAuth**: Bypasses authentication entirely, always sets guest context
|
||||||
|
- **OptionalAuth**: Tries authentication, falls back to guest context if it fails
|
||||||
|
|
||||||
|
### NewAuthHandler
|
||||||
|
|
||||||
|
Standalone authentication handler (without middleware wrapping):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use when you need authentication logic without middleware
|
||||||
|
authHandler := security.NewAuthHandler(securityList, myHandler)
|
||||||
|
http.Handle("/api/protected", authHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
### NewOptionalAuthHandler
|
||||||
|
|
||||||
|
Standalone optional authentication handler that tries to authenticate but falls back to guest:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use for routes that should work for both authenticated and guest users
|
||||||
|
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
|
||||||
|
http.Handle("/home", optionalHandler)
|
||||||
|
|
||||||
|
// Example handler that checks user context
|
||||||
|
func myHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userCtx, _ := security.GetUserContext(r.Context())
|
||||||
|
if userCtx.UserID == 0 {
|
||||||
|
fmt.Fprintf(w, "Welcome, guest!")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
|
||||||
|
Extract user information from context:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get full user context
|
||||||
|
userCtx, ok := security.GetUserContext(ctx)
|
||||||
|
|
||||||
|
// Get specific fields
|
||||||
|
userID, ok := security.GetUserID(ctx)
|
||||||
|
userName, ok := security.GetUserName(ctx)
|
||||||
|
userLevel, ok := security.GetUserLevel(ctx)
|
||||||
|
sessionID, ok := security.GetSessionID(ctx)
|
||||||
|
remoteID, ok := security.GetRemoteID(ctx)
|
||||||
|
roles, ok := security.GetUserRoles(ctx)
|
||||||
|
email, ok := security.GetUserEmail(ctx)
|
||||||
|
meta, ok := security.GetUserMeta(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Support
|
||||||
|
|
||||||
|
The `Meta` field in `UserContext` can hold any JSON-serializable values:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Set metadata during login
|
||||||
|
loginReq := security.LoginRequest{
|
||||||
|
Username: "user@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Meta: map[string]any{
|
||||||
|
"department": "engineering",
|
||||||
|
"location": "US",
|
||||||
|
"preferences": map[string]any{
|
||||||
|
"theme": "dark",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access metadata in handlers
|
||||||
|
meta, ok := security.GetUserMeta(ctx)
|
||||||
|
if ok {
|
||||||
|
department := meta["department"].(string)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Part of the ResolveSpec project.
|
Part of the ResolveSpec project.
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ func (a *HeaderAuthenticatorExample) Authenticate(r *http.Request) (*UserContext
|
|||||||
RemoteID: r.Header.Get("X-Remote-ID"),
|
RemoteID: r.Header.Get("X-Remote-ID"),
|
||||||
Email: r.Header.Get("X-User-Email"),
|
Email: r.Header.Get("X-User-Email"),
|
||||||
Roles: parseRoles(r.Header.Get("X-User-Roles")),
|
Roles: parseRoles(r.Header.Get("X-User-Roles")),
|
||||||
|
Claims: make(map[string]any),
|
||||||
|
Meta: make(map[string]any),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +127,8 @@ func (a *JWTAuthenticatorExample) Login(ctx context.Context, req LoginRequest) (
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
UserLevel: user.UserLevel,
|
UserLevel: user.UserLevel,
|
||||||
Roles: parseRoles(user.Roles),
|
Roles: parseRoles(user.Roles),
|
||||||
|
Claims: req.Claims,
|
||||||
|
Meta: req.Meta,
|
||||||
},
|
},
|
||||||
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -242,6 +246,9 @@ func (a *DatabaseAuthenticatorExample) Login(ctx context.Context, req LoginReque
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
UserLevel: user.UserLevel,
|
UserLevel: user.UserLevel,
|
||||||
Roles: parseRoles(user.Roles),
|
Roles: parseRoles(user.Roles),
|
||||||
|
SessionID: sessionToken,
|
||||||
|
Claims: req.Claims,
|
||||||
|
Meta: req.Meta,
|
||||||
},
|
},
|
||||||
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -320,6 +327,8 @@ func (a *DatabaseAuthenticatorExample) Authenticate(r *http.Request) (*UserConte
|
|||||||
UserLevel: session.UserLevel,
|
UserLevel: session.UserLevel,
|
||||||
SessionID: sessionToken,
|
SessionID: sessionToken,
|
||||||
Roles: parseRoles(session.Roles),
|
Roles: parseRoles(session.Roles),
|
||||||
|
Claims: make(map[string]any),
|
||||||
|
Meta: make(map[string]any),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +379,12 @@ func (a *DatabaseAuthenticatorExample) RefreshToken(ctx context.Context, refresh
|
|||||||
return &LoginResponse{
|
return &LoginResponse{
|
||||||
Token: newSessionToken,
|
Token: newSessionToken,
|
||||||
User: &UserContext{
|
User: &UserContext{
|
||||||
UserID: session.UserID,
|
UserID: session.UserID,
|
||||||
UserName: session.Username,
|
UserName: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
|
SessionID: newSessionToken,
|
||||||
|
Claims: make(map[string]any),
|
||||||
|
Meta: make(map[string]any),
|
||||||
},
|
},
|
||||||
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -7,35 +7,37 @@ import (
|
|||||||
|
|
||||||
// UserContext holds authenticated user information
|
// UserContext holds authenticated user information
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
UserID int
|
UserID int `json:"user_id"`
|
||||||
UserName string
|
UserName string `json:"user_name"`
|
||||||
UserLevel int
|
UserLevel int `json:"user_level"`
|
||||||
SessionID string
|
SessionID string `json:"session_id"`
|
||||||
RemoteID string
|
RemoteID string `json:"remote_id"`
|
||||||
Roles []string
|
Roles []string `json:"roles"`
|
||||||
Email string
|
Email string `json:"email"`
|
||||||
Claims map[string]any
|
Claims map[string]any `json:"claims"`
|
||||||
|
Meta map[string]any `json:"meta"` // Additional metadata that can hold any JSON-serializable values
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginRequest contains credentials for login
|
// LoginRequest contains credentials for login
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Username string
|
Username string `json:"username"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
Claims map[string]any // Additional login data
|
Claims map[string]any `json:"claims"` // Additional login data
|
||||||
|
Meta map[string]any `json:"meta"` // Additional metadata to be set on user context
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginResponse contains the result of a login attempt
|
// LoginResponse contains the result of a login attempt
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string
|
Token string `json:"token"`
|
||||||
RefreshToken string
|
RefreshToken string `json:"refresh_token"`
|
||||||
User *UserContext
|
User *UserContext `json:"user"`
|
||||||
ExpiresIn int64 // Token expiration in seconds
|
ExpiresIn int64 `json:"expires_in"` // Token expiration in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutRequest contains information for logout
|
// LogoutRequest contains information for logout
|
||||||
type LogoutRequest struct {
|
type LogoutRequest struct {
|
||||||
Token string
|
Token string `json:"token"`
|
||||||
UserID int
|
UserID int `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticator handles user authentication operations
|
// Authenticator handles user authentication operations
|
||||||
|
|||||||
@@ -10,21 +10,145 @@ type contextKey string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Context keys for user information
|
// Context keys for user information
|
||||||
UserIDKey contextKey = "user_id"
|
UserIDKey contextKey = "user_id"
|
||||||
UserNameKey contextKey = "user_name"
|
UserNameKey contextKey = "user_name"
|
||||||
UserLevelKey contextKey = "user_level"
|
UserLevelKey contextKey = "user_level"
|
||||||
SessionIDKey contextKey = "session_id"
|
SessionIDKey contextKey = "session_id"
|
||||||
RemoteIDKey contextKey = "remote_id"
|
RemoteIDKey contextKey = "remote_id"
|
||||||
UserRolesKey contextKey = "user_roles"
|
UserRolesKey contextKey = "user_roles"
|
||||||
UserEmailKey contextKey = "user_email"
|
UserEmailKey contextKey = "user_email"
|
||||||
UserContextKey contextKey = "user_context"
|
UserContextKey contextKey = "user_context"
|
||||||
|
UserMetaKey contextKey = "user_meta"
|
||||||
|
SkipAuthKey contextKey = "skip_auth"
|
||||||
|
OptionalAuthKey contextKey = "optional_auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SkipAuth returns a context with skip auth flag set to true
|
||||||
|
// Use this to mark routes that should bypass authentication middleware
|
||||||
|
func SkipAuth(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, SkipAuthKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalAuth returns a context with optional auth flag set to true
|
||||||
|
// Use this to mark routes that should try to authenticate, but fall back to guest if authentication fails
|
||||||
|
func OptionalAuth(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, OptionalAuthKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createGuestContext creates a guest user context for unauthenticated requests
|
||||||
|
func createGuestContext(r *http.Request) *UserContext {
|
||||||
|
return &UserContext{
|
||||||
|
UserID: 0,
|
||||||
|
UserName: "guest",
|
||||||
|
UserLevel: 0,
|
||||||
|
SessionID: "",
|
||||||
|
RemoteID: r.RemoteAddr,
|
||||||
|
Roles: []string{"guest"},
|
||||||
|
Email: "",
|
||||||
|
Claims: map[string]any{},
|
||||||
|
Meta: map[string]any{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setUserContext adds a user context to the request context
|
||||||
|
func setUserContext(r *http.Request, userCtx *UserContext) *http.Request {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, UserContextKey, userCtx)
|
||||||
|
ctx = context.WithValue(ctx, UserIDKey, userCtx.UserID)
|
||||||
|
ctx = context.WithValue(ctx, UserNameKey, userCtx.UserName)
|
||||||
|
ctx = context.WithValue(ctx, UserLevelKey, userCtx.UserLevel)
|
||||||
|
ctx = context.WithValue(ctx, SessionIDKey, userCtx.SessionID)
|
||||||
|
ctx = context.WithValue(ctx, RemoteIDKey, userCtx.RemoteID)
|
||||||
|
ctx = context.WithValue(ctx, UserRolesKey, userCtx.Roles)
|
||||||
|
|
||||||
|
if userCtx.Email != "" {
|
||||||
|
ctx = context.WithValue(ctx, UserEmailKey, userCtx.Email)
|
||||||
|
}
|
||||||
|
if len(userCtx.Meta) > 0 {
|
||||||
|
ctx = context.WithValue(ctx, UserMetaKey, userCtx.Meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateRequest performs authentication and adds user context to the request
|
||||||
|
// This is the shared authentication logic used by both handler and middleware
|
||||||
|
func authenticateRequest(w http.ResponseWriter, r *http.Request, provider SecurityProvider) (*http.Request, bool) {
|
||||||
|
// Call the provider's Authenticate method
|
||||||
|
userCtx, err := provider.Authenticate(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return setUserContext(r, userCtx), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthHandler creates an authentication handler that can be used standalone
|
||||||
|
// This handler performs authentication and returns 401 if authentication fails
|
||||||
|
// Use this when you need authentication logic without middleware wrapping
|
||||||
|
func NewAuthHandler(securityList *SecurityList, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the security provider
|
||||||
|
provider := securityList.Provider()
|
||||||
|
if provider == nil {
|
||||||
|
http.Error(w, "Security provider not configured", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate the request
|
||||||
|
authenticatedReq, ok := authenticateRequest(w, r, provider)
|
||||||
|
if !ok {
|
||||||
|
return // authenticateRequest already wrote the error response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with authenticated context
|
||||||
|
next.ServeHTTP(w, authenticatedReq)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptionalAuthHandler creates an optional authentication handler that can be used standalone
|
||||||
|
// This handler tries to authenticate but falls back to guest context if authentication fails
|
||||||
|
// Use this for routes that should show personalized content for authenticated users but still work for guests
|
||||||
|
func NewOptionalAuthHandler(securityList *SecurityList, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the security provider
|
||||||
|
provider := securityList.Provider()
|
||||||
|
if provider == nil {
|
||||||
|
http.Error(w, "Security provider not configured", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to authenticate
|
||||||
|
userCtx, err := provider.Authenticate(r)
|
||||||
|
if err != nil {
|
||||||
|
// Authentication failed - set guest context and continue
|
||||||
|
guestCtx := createGuestContext(r)
|
||||||
|
next.ServeHTTP(w, setUserContext(r, guestCtx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication succeeded - set user context
|
||||||
|
next.ServeHTTP(w, setUserContext(r, userCtx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// NewAuthMiddleware creates an authentication middleware with the given security list
|
// NewAuthMiddleware creates an authentication middleware with the given security list
|
||||||
// This middleware extracts user authentication from the request and adds it to context
|
// This middleware extracts user authentication from the request and adds it to context
|
||||||
|
// Routes can skip authentication by setting SkipAuthKey context value (use SkipAuth helper)
|
||||||
|
// Routes can use optional authentication by setting OptionalAuthKey context value (use OptionalAuth helper)
|
||||||
|
// When authentication is skipped or fails with optional auth, a guest user context is set instead
|
||||||
func NewAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handler {
|
func NewAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check if this route should skip authentication
|
||||||
|
if skip, ok := r.Context().Value(SkipAuthKey).(bool); ok && skip {
|
||||||
|
// Set guest user context for skipped routes
|
||||||
|
guestCtx := createGuestContext(r)
|
||||||
|
next.ServeHTTP(w, setUserContext(r, guestCtx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get the security provider
|
// Get the security provider
|
||||||
provider := securityList.Provider()
|
provider := securityList.Provider()
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
@@ -32,31 +156,25 @@ func NewAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handl
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the provider's Authenticate method
|
// Check if this route has optional authentication
|
||||||
|
optional, _ := r.Context().Value(OptionalAuthKey).(bool)
|
||||||
|
|
||||||
|
// Try to authenticate
|
||||||
userCtx, err := provider.Authenticate(r)
|
userCtx, err := provider.Authenticate(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if optional {
|
||||||
|
// Optional auth failed - set guest context and continue
|
||||||
|
guestCtx := createGuestContext(r)
|
||||||
|
next.ServeHTTP(w, setUserContext(r, guestCtx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Required auth failed - return error
|
||||||
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user information to context
|
// Authentication succeeded - set user context
|
||||||
ctx := r.Context()
|
next.ServeHTTP(w, setUserContext(r, userCtx))
|
||||||
ctx = context.WithValue(ctx, UserContextKey, userCtx)
|
|
||||||
ctx = context.WithValue(ctx, UserIDKey, userCtx.UserID)
|
|
||||||
ctx = context.WithValue(ctx, UserNameKey, userCtx.UserName)
|
|
||||||
ctx = context.WithValue(ctx, UserLevelKey, userCtx.UserLevel)
|
|
||||||
ctx = context.WithValue(ctx, SessionIDKey, userCtx.SessionID)
|
|
||||||
ctx = context.WithValue(ctx, RemoteIDKey, userCtx.RemoteID)
|
|
||||||
|
|
||||||
if len(userCtx.Roles) > 0 {
|
|
||||||
ctx = context.WithValue(ctx, UserRolesKey, userCtx.Roles)
|
|
||||||
}
|
|
||||||
if userCtx.Email != "" {
|
|
||||||
ctx = context.WithValue(ctx, UserEmailKey, userCtx.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with authenticated context
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,3 +237,164 @@ func GetUserEmail(ctx context.Context) (string, bool) {
|
|||||||
email, ok := ctx.Value(UserEmailKey).(string)
|
email, ok := ctx.Value(UserEmailKey).(string)
|
||||||
return email, ok
|
return email, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserMeta extracts user metadata from context
|
||||||
|
func GetUserMeta(ctx context.Context) (map[string]any, bool) {
|
||||||
|
meta, ok := ctx.Value(UserMetaKey).(map[string]any)
|
||||||
|
return meta, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Handler adapters for resolvespec/restheadspec compatibility
|
||||||
|
// // These functions allow using NewAuthHandler and NewOptionalAuthHandler with custom handler abstractions
|
||||||
|
|
||||||
|
// // SpecHandlerAdapter is an interface for handler adapters that need authentication
|
||||||
|
// // Implement this interface to create adapters for custom handler types
|
||||||
|
// type SpecHandlerAdapter interface {
|
||||||
|
// // AdaptToHTTPHandler converts the custom handler to a standard http.Handler
|
||||||
|
// AdaptToHTTPHandler() http.Handler
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ResolveSpecHandlerAdapter adapts a resolvespec/restheadspec handler method to http.Handler
|
||||||
|
// type ResolveSpecHandlerAdapter struct {
|
||||||
|
// // HandlerMethod is the method to call (e.g., handler.Handle, handler.HandleGet)
|
||||||
|
// HandlerMethod func(w any, r any, params map[string]string)
|
||||||
|
// // Params are the route parameters (e.g., {"schema": "public", "entity": "users"})
|
||||||
|
// Params map[string]string
|
||||||
|
// // RequestAdapter converts *http.Request to the custom Request interface
|
||||||
|
// // Use router.NewHTTPRequest from pkg/common/adapters/router
|
||||||
|
// RequestAdapter func(*http.Request) any
|
||||||
|
// // ResponseAdapter converts http.ResponseWriter to the custom ResponseWriter interface
|
||||||
|
// // Use router.NewHTTPResponseWriter from pkg/common/adapters/router
|
||||||
|
// ResponseAdapter func(http.ResponseWriter) any
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // AdaptToHTTPHandler implements SpecHandlerAdapter
|
||||||
|
// func (a *ResolveSpecHandlerAdapter) AdaptToHTTPHandler() http.Handler {
|
||||||
|
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// req := a.RequestAdapter(r)
|
||||||
|
// resp := a.ResponseAdapter(w)
|
||||||
|
// a.HandlerMethod(resp, req, a.Params)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // WrapSpecHandler wraps a spec handler adapter with authentication
|
||||||
|
// // Use this to apply NewAuthHandler or NewOptionalAuthHandler to resolvespec/restheadspec handlers
|
||||||
|
// //
|
||||||
|
// // Example with required auth:
|
||||||
|
// //
|
||||||
|
// // adapter := &security.ResolveSpecHandlerAdapter{
|
||||||
|
// // HandlerMethod: handler.Handle,
|
||||||
|
// // Params: map[string]string{"schema": "public", "entity": "users"},
|
||||||
|
// // RequestAdapter: func(r *http.Request) any { return router.NewHTTPRequest(r) },
|
||||||
|
// // ResponseAdapter: func(w http.ResponseWriter) any { return router.NewHTTPResponseWriter(w) },
|
||||||
|
// // }
|
||||||
|
// // authHandler := security.WrapSpecHandler(securityList, adapter, false)
|
||||||
|
// // muxRouter.Handle("/api/users", authHandler)
|
||||||
|
// func WrapSpecHandler(securityList *SecurityList, adapter SpecHandlerAdapter, optional bool) http.Handler {
|
||||||
|
// httpHandler := adapter.AdaptToHTTPHandler()
|
||||||
|
// if optional {
|
||||||
|
// return NewOptionalAuthHandler(securityList, httpHandler)
|
||||||
|
// }
|
||||||
|
// return NewAuthHandler(securityList, httpHandler)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // MuxRouteBuilder helps build authenticated routes with Gorilla Mux
|
||||||
|
// type MuxRouteBuilder struct {
|
||||||
|
// securityList *SecurityList
|
||||||
|
// requestAdapter func(*http.Request) any
|
||||||
|
// responseAdapter func(http.ResponseWriter) any
|
||||||
|
// paramExtractor func(*http.Request) map[string]string
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // NewMuxRouteBuilder creates a route builder for Gorilla Mux with standard router adapters
|
||||||
|
// // Usage:
|
||||||
|
// //
|
||||||
|
// // builder := security.NewMuxRouteBuilder(securityList, router.NewHTTPRequest, router.NewHTTPResponseWriter)
|
||||||
|
// func NewMuxRouteBuilder(
|
||||||
|
// securityList *SecurityList,
|
||||||
|
// requestAdapter func(*http.Request) any,
|
||||||
|
// responseAdapter func(http.ResponseWriter) any,
|
||||||
|
// ) *MuxRouteBuilder {
|
||||||
|
// return &MuxRouteBuilder{
|
||||||
|
// securityList: securityList,
|
||||||
|
// requestAdapter: requestAdapter,
|
||||||
|
// responseAdapter: responseAdapter,
|
||||||
|
// paramExtractor: nil, // Will be set per route using mux.Vars
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // HandleAuth creates an authenticated route handler
|
||||||
|
// // pattern: the route pattern (e.g., "/{schema}/{entity}")
|
||||||
|
// // handler: the handler method to call (e.g., handler.Handle)
|
||||||
|
// // optional: true for optional auth (guest fallback), false for required auth (401 on failure)
|
||||||
|
// // methods: HTTP methods (e.g., "GET", "POST")
|
||||||
|
// //
|
||||||
|
// // Usage:
|
||||||
|
// //
|
||||||
|
// // builder.HandleAuth(router, "/{schema}/{entity}", handler.Handle, false, "POST")
|
||||||
|
// func (b *MuxRouteBuilder) HandleAuth(
|
||||||
|
// router interface {
|
||||||
|
// HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) interface{ Methods(...string) interface{} }
|
||||||
|
// },
|
||||||
|
// pattern string,
|
||||||
|
// handlerMethod func(w any, r any, params map[string]string),
|
||||||
|
// optional bool,
|
||||||
|
// methods ...string,
|
||||||
|
// ) {
|
||||||
|
// router.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// // Extract params using the registered extractor or default to empty map
|
||||||
|
// var params map[string]string
|
||||||
|
// if b.paramExtractor != nil {
|
||||||
|
// params = b.paramExtractor(r)
|
||||||
|
// } else {
|
||||||
|
// params = make(map[string]string)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// adapter := &ResolveSpecHandlerAdapter{
|
||||||
|
// HandlerMethod: handlerMethod,
|
||||||
|
// Params: params,
|
||||||
|
// RequestAdapter: b.requestAdapter,
|
||||||
|
// ResponseAdapter: b.responseAdapter,
|
||||||
|
// }
|
||||||
|
// authHandler := WrapSpecHandler(b.securityList, adapter, optional)
|
||||||
|
// authHandler.ServeHTTP(w, r)
|
||||||
|
// }).Methods(methods...)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // SetParamExtractor sets a custom parameter extractor function
|
||||||
|
// // For Gorilla Mux, you would use: builder.SetParamExtractor(mux.Vars)
|
||||||
|
// func (b *MuxRouteBuilder) SetParamExtractor(extractor func(*http.Request) map[string]string) {
|
||||||
|
// b.paramExtractor = extractor
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // SetupAuthenticatedSpecRoutes sets up all standard resolvespec/restheadspec routes with authentication
|
||||||
|
// // This is a convenience function that sets up the common route patterns
|
||||||
|
// //
|
||||||
|
// // Usage:
|
||||||
|
// //
|
||||||
|
// // security.SetupAuthenticatedSpecRoutes(router, handler, securityList, router.NewHTTPRequest, router.NewHTTPResponseWriter, mux.Vars)
|
||||||
|
// func SetupAuthenticatedSpecRoutes(
|
||||||
|
// router interface {
|
||||||
|
// HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) interface{ Methods(...string) interface{} }
|
||||||
|
// },
|
||||||
|
// handler interface {
|
||||||
|
// Handle(w any, r any, params map[string]string)
|
||||||
|
// HandleGet(w any, r any, params map[string]string)
|
||||||
|
// },
|
||||||
|
// securityList *SecurityList,
|
||||||
|
// requestAdapter func(*http.Request) any,
|
||||||
|
// responseAdapter func(http.ResponseWriter) any,
|
||||||
|
// paramExtractor func(*http.Request) map[string]string,
|
||||||
|
// ) {
|
||||||
|
// builder := NewMuxRouteBuilder(securityList, requestAdapter, responseAdapter)
|
||||||
|
// builder.SetParamExtractor(paramExtractor)
|
||||||
|
|
||||||
|
// // POST /{schema}/{entity}
|
||||||
|
// builder.HandleAuth(router, "/{schema}/{entity}", handler.Handle, false, "POST")
|
||||||
|
|
||||||
|
// // POST /{schema}/{entity}/{id}
|
||||||
|
// builder.HandleAuth(router, "/{schema}/{entity}/{id}", handler.Handle, false, "POST")
|
||||||
|
|
||||||
|
// // GET /{schema}/{entity}
|
||||||
|
// builder.HandleAuth(router, "/{schema}/{entity}", handler.HandleGet, false, "GET")
|
||||||
|
// }
|
||||||
|
|||||||
@@ -15,26 +15,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ColumnSecurity struct {
|
type ColumnSecurity struct {
|
||||||
Schema string
|
Schema string `json:"schema"`
|
||||||
Tablename string
|
Tablename string `json:"tablename"`
|
||||||
Path []string
|
Path []string `json:"path"`
|
||||||
ExtraFilters map[string]string
|
ExtraFilters map[string]string `json:"extra_filters"`
|
||||||
UserID int
|
UserID int `json:"user_id"`
|
||||||
Accesstype string `json:"accesstype"`
|
Accesstype string `json:"accesstype"`
|
||||||
MaskStart int
|
MaskStart int `json:"mask_start"`
|
||||||
MaskEnd int
|
MaskEnd int `json:"mask_end"`
|
||||||
MaskInvert bool
|
MaskInvert bool `json:"mask_invert"`
|
||||||
MaskChar string
|
MaskChar string `json:"mask_char"`
|
||||||
Control string `json:"control"`
|
Control string `json:"control"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowSecurity struct {
|
type RowSecurity struct {
|
||||||
Schema string
|
Schema string `json:"schema"`
|
||||||
Tablename string
|
Tablename string `json:"tablename"`
|
||||||
Template string
|
Template string `json:"template"`
|
||||||
HasBlock bool
|
HasBlock bool `json:"has_block"`
|
||||||
UserID int
|
UserID int `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Type) string {
|
func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Type) string {
|
||||||
|
|||||||
@@ -68,12 +68,14 @@ func ExampleDatabaseSecurity(gormDB interface{}, sqlDB *sql.DB) (http.Handler, e
|
|||||||
// Step 5: Setup security
|
// Step 5: Setup security
|
||||||
securityList := SetupSecurityProvider(handler, provider)
|
securityList := SetupSecurityProvider(handler, provider)
|
||||||
|
|
||||||
// Step 6: Create router and setup routes
|
// Step 6: Create router and setup routes with authentication
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
restheadspec.SetupMuxRoutes(router, handler)
|
authMiddleware := func(h http.Handler) http.Handler {
|
||||||
|
return NewAuthHandler(securityList, h)
|
||||||
|
}
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
|
||||||
|
|
||||||
// Step 7: Apply middleware in correct order
|
// Step 7: Apply additional security middleware
|
||||||
router.Use(NewAuthMiddleware(securityList))
|
|
||||||
router.Use(SetSecurityMiddleware(securityList))
|
router.Use(SetSecurityMiddleware(securityList))
|
||||||
|
|
||||||
return router, nil
|
return router, nil
|
||||||
@@ -95,9 +97,11 @@ func ExampleHeaderAuthentication(gormDB interface{}, sqlDB *sql.DB) (*mux.Router
|
|||||||
securityList := SetupSecurityProvider(handler, provider)
|
securityList := SetupSecurityProvider(handler, provider)
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
restheadspec.SetupMuxRoutes(router, handler)
|
authMiddleware := func(h http.Handler) http.Handler {
|
||||||
|
return NewAuthHandler(securityList, h)
|
||||||
|
}
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
|
||||||
|
|
||||||
router.Use(NewAuthMiddleware(securityList))
|
|
||||||
router.Use(SetSecurityMiddleware(securityList))
|
router.Use(SetSecurityMiddleware(securityList))
|
||||||
|
|
||||||
return router, nil
|
return router, nil
|
||||||
@@ -150,9 +154,11 @@ func ExampleConfigSecurity(gormDB interface{}) (*mux.Router, error) {
|
|||||||
securityList := SetupSecurityProvider(handler, provider)
|
securityList := SetupSecurityProvider(handler, provider)
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
restheadspec.SetupMuxRoutes(router, handler)
|
authMiddleware := func(h http.Handler) http.Handler {
|
||||||
|
return NewAuthHandler(securityList, h)
|
||||||
|
}
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
|
||||||
|
|
||||||
router.Use(NewAuthMiddleware(securityList))
|
|
||||||
router.Use(SetSecurityMiddleware(securityList))
|
router.Use(SetSecurityMiddleware(securityList))
|
||||||
|
|
||||||
return router, nil
|
return router, nil
|
||||||
@@ -282,10 +288,12 @@ func CompleteServerExample(gormDB interface{}, sqlDB *sql.DB) http.Handler {
|
|||||||
// Add auth routes (login/logout)
|
// Add auth routes (login/logout)
|
||||||
SetupAuthRoutes(router, securityList)
|
SetupAuthRoutes(router, securityList)
|
||||||
|
|
||||||
// Add API routes with security middleware
|
// Add API routes with authentication
|
||||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||||
restheadspec.SetupMuxRoutes(apiRouter, handler)
|
authMiddleware := func(h http.Handler) http.Handler {
|
||||||
apiRouter.Use(NewAuthMiddleware(securityList))
|
return NewAuthHandler(securityList, h)
|
||||||
|
}
|
||||||
|
restheadspec.SetupMuxRoutes(apiRouter, handler, authMiddleware)
|
||||||
apiRouter.Use(SetSecurityMiddleware(securityList))
|
apiRouter.Use(SetSecurityMiddleware(securityList))
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
Reference in New Issue
Block a user