Compare commits
3 Commits
hwsauth/v0
...
hws/v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c3d4ef095 | |||
| 5a3ed49ea4 | |||
| 2f49063432 |
@@ -51,6 +51,12 @@ func main() {
|
|||||||
Method: hws.MethodGET,
|
Method: hws.MethodGET,
|
||||||
Handler: http.HandlerFunc(getUserHandler),
|
Handler: http.HandlerFunc(getUserHandler),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Single route handling multiple HTTP methods
|
||||||
|
Path: "/api/resource",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: http.HandlerFunc(resourceHandler),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add routes and middleware
|
// Add routes and middleware
|
||||||
@@ -73,6 +79,18 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
w.Write([]byte("User ID: " + id))
|
w.Write([]byte("User ID: " + id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle GET, POST, and PUT for the same path
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
w.Write([]byte("Getting resource"))
|
||||||
|
case "POST":
|
||||||
|
w.Write([]byte("Creating resource"))
|
||||||
|
case "PUT":
|
||||||
|
w.Write([]byte("Updating resource"))
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
12
hws/doc.go
12
hws/doc.go
@@ -74,6 +74,18 @@
|
|||||||
// },
|
// },
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
// A single route can handle multiple HTTP methods using the Methods field:
|
||||||
|
//
|
||||||
|
// routes := []hws.Route{
|
||||||
|
// {
|
||||||
|
// Path: "/api/resource",
|
||||||
|
// Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
// Handler: http.HandlerFunc(resourceHandler),
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note: The Methods field takes precedence over Method if both are provided.
|
||||||
|
//
|
||||||
// Path parameters can be accessed using r.PathValue():
|
// Path parameters can be accessed using r.PathValue():
|
||||||
//
|
//
|
||||||
// func getUser(w http.ResponseWriter, r *http.Request) {
|
// func getUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ type MiddlewareFunc func(w http.ResponseWriter, r *http.Request) (*http.Request,
|
|||||||
|
|
||||||
// Server.AddMiddleware registers all the middleware.
|
// Server.AddMiddleware registers all the middleware.
|
||||||
// Middleware will be run in the order that they are provided.
|
// Middleware will be run in the order that they are provided.
|
||||||
|
// Can only be called once
|
||||||
func (server *Server) AddMiddleware(middleware ...Middleware) error {
|
func (server *Server) AddMiddleware(middleware ...Middleware) error {
|
||||||
if !server.routes {
|
if !server.routes {
|
||||||
return errors.New("Server.AddRoutes must be called before Server.AddMiddleware")
|
return errors.New("Server.AddRoutes must be called before Server.AddMiddleware")
|
||||||
}
|
}
|
||||||
|
if server.middleware {
|
||||||
|
return errors.New("Server.AddMiddleware already called")
|
||||||
|
}
|
||||||
// RUN LOGGING MIDDLEWARE FIRST
|
// RUN LOGGING MIDDLEWARE FIRST
|
||||||
server.server.Handler = logging(server.server.Handler, server.logger)
|
server.server.Handler = logging(server.server.Handler, server.logger)
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Path string // Absolute path to the requested resource
|
Path string // Absolute path to the requested resource
|
||||||
Method Method // HTTP Method
|
Method Method // HTTP Method
|
||||||
|
// Methods is an optional slice of Methods to use, if more than one can use the same handler.
|
||||||
|
// Will take precedence over the Method field if provided
|
||||||
|
Methods []Method
|
||||||
Handler http.Handler // Handler to use for the request
|
Handler http.Handler // Handler to use for the request
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,22 +32,34 @@ const (
|
|||||||
|
|
||||||
// Server.AddRoutes registers the page handlers for the server.
|
// Server.AddRoutes registers the page handlers for the server.
|
||||||
// At least one route must be provided.
|
// At least one route must be provided.
|
||||||
|
// If any route patterns (path + method) are defined multiple times, the first
|
||||||
|
// instance will be added and any additional conflicts will be discarded.
|
||||||
func (server *Server) AddRoutes(routes ...Route) error {
|
func (server *Server) AddRoutes(routes ...Route) error {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
return errors.New("No routes provided")
|
return errors.New("No routes provided")
|
||||||
}
|
}
|
||||||
|
patterns := []string{}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
|
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
if !validMethod(route.Method) {
|
if len(route.Methods) == 0 {
|
||||||
return fmt.Errorf("Invalid method %s for path %s", route.Method, route.Path)
|
route.Methods = []Method{route.Method}
|
||||||
|
}
|
||||||
|
for _, method := range route.Methods {
|
||||||
|
if !validMethod(method) {
|
||||||
|
return fmt.Errorf("Invalid method %s for path %s", method, route.Path)
|
||||||
}
|
}
|
||||||
if route.Handler == nil {
|
if route.Handler == nil {
|
||||||
return fmt.Errorf("No handler provided for %s %s", route.Method, route.Path)
|
return fmt.Errorf("No handler provided for %s %s", method, route.Path)
|
||||||
}
|
}
|
||||||
pattern := fmt.Sprintf("%s %s", route.Method, route.Path)
|
pattern := fmt.Sprintf("%s %s", method, route.Path)
|
||||||
|
if slices.Contains(patterns, pattern) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
patterns = append(patterns, pattern)
|
||||||
mux.Handle(pattern, route.Handler)
|
mux.Handle(pattern, route.Handler)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server.server.Handler = mux
|
server.server.Handler = mux
|
||||||
server.routes = true
|
server.routes = true
|
||||||
|
|||||||
@@ -122,6 +122,111 @@ func Test_AddRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_AddRoutes_MultipleMethods(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
t.Run("Single route with multiple methods", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(r.Method + " response"))
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/api/resource",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GET request
|
||||||
|
req := httptest.NewRequest("GET", "/api/resource", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "GET response", rr.Body.String())
|
||||||
|
|
||||||
|
// Test POST request
|
||||||
|
req = httptest.NewRequest("POST", "/api/resource", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "POST response", rr.Body.String())
|
||||||
|
|
||||||
|
// Test PUT request
|
||||||
|
req = httptest.NewRequest("PUT", "/api/resource", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "PUT response", rr.Body.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Methods field takes precedence over Method field", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Method: hws.MethodGET, // This should be ignored
|
||||||
|
Methods: []hws.Method{hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GET should not work (Method field ignored)
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
|
||||||
|
|
||||||
|
// POST should work (from Methods field)
|
||||||
|
req = httptest.NewRequest("POST", "/test", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
// PUT should work (from Methods field)
|
||||||
|
req = httptest.NewRequest("PUT", "/test", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid method in Methods slice", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.Method("INVALID")},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Invalid method")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Empty Methods slice falls back to Method field", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Methods: []hws.Method{}, // Empty slice
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GET should work (from Method field)
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_Routes_EndToEnd(t *testing.T) {
|
func Test_Routes_EndToEnd(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
server := createTestServer(t, &buf)
|
server := createTestServer(t, &buf)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hwsauth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
@@ -45,6 +46,9 @@ func (auth *Authenticator[T, TX]) getAuthenticatedUser(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return authenticatedModel[T]{}, errors.Wrap(err, "auth.load")
|
return authenticatedModel[T]{}, errors.Wrap(err, "auth.load")
|
||||||
}
|
}
|
||||||
|
if reflect.ValueOf(model).IsNil() {
|
||||||
|
return authenticatedModel[T]{}, errors.New("no user matching JWT in database")
|
||||||
|
}
|
||||||
authUser := authenticatedModel[T]{
|
authUser := authenticatedModel[T]{
|
||||||
model: model,
|
model: model,
|
||||||
fresh: aT.Fresh,
|
fresh: aT.Fresh,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hwsauth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -18,7 +19,9 @@ func (auth *Authenticator[T, TX]) refreshAuthTokens(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return getNil[T](), errors.Wrap(err, "auth.load")
|
return getNil[T](), errors.Wrap(err, "auth.load")
|
||||||
}
|
}
|
||||||
|
if reflect.ValueOf(model).IsNil() {
|
||||||
|
return getNil[T](), errors.New("no user matching JWT in database")
|
||||||
|
}
|
||||||
rememberMe := map[string]bool{
|
rememberMe := map[string]bool{
|
||||||
"session": false,
|
"session": false,
|
||||||
"exp": true,
|
"exp": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user