Files
summercms/modules/lighthouse/route.go
Jakub Zych cada7a4442 feat(11-03): add the lighthouse realtime package and its Centrifugo driver
- lighthouse: Service/From with realtime.driver selection, RegisterDriver
  registry, null/log/memory drivers, Route/Surface/Mount, users and actors
- centrifugo: HTTP API client (apikey header, 2xx success, no request
  without a key), five-generator HS256 TokenIssuer, TokenHandler with the
  WinterCMS 401/503 bodies
- module README and root modules table row
2026-09-30 12:18:11 +02:00

143 lines
3.8 KiB
Go

package lighthouse
import (
"fmt"
"net/http"
"strings"
"git.golem15.com/golem15/summercms/modules/pact"
)
// Surface says who calls a driver route, so the application can put it
// behind the right guard and group.
type Surface int
const (
// UserAuth routes are called by a signed-in browser user; they mount
// behind the application's user guard.
UserAuth Surface = iota + 1
// ServerToServer routes are called by the realtime server itself; they
// mount in a raw group without the house envelope.
ServerToServer
// Public routes need no authentication.
Public
)
// String returns the surface name.
func (s Surface) String() string {
switch s {
case UserAuth:
return "UserAuth"
case ServerToServer:
return "ServerToServer"
case Public:
return "Public"
default:
return fmt.Sprintf("Surface(%d)", int(s))
}
}
// Route is an HTTP endpoint declared by a driver.
type Route struct {
// Name identifies the route inside the driver (for example "token").
Name string
// Method is GET, POST, PUT, PATCH or DELETE.
Method string
// Path is the absolute request path.
Path string
Surface Surface
Handler http.HandlerFunc
}
// Surfaces holds the application's middleware for each surface. Middleware
// is appended after the surface middleware on every route, so a guard in
// UserAuth runs before a throttle in Middleware.
type Surfaces struct {
UserAuth []string
ServerToServer []string
Public []string
Middleware []string
}
// Mount registers the driver's routes on r. UserAuth and Public routes go
// through r.Group and ServerToServer routes through r.GroupRaw, each with
// the surface middleware followed by s.Middleware. A nil driver, or one
// without routes, mounts nothing. A UserAuth route with an empty
// s.UserAuth is an error: the application must never expose a user route
// without a guard. Every route is validated before any is registered.
func Mount(r pact.Router, d Driver, s Surfaces) error {
if r == nil {
return fmt.Errorf("lighthouse: router is nil")
}
if d == nil {
return nil
}
routes := d.Routes()
for _, rt := range routes {
if err := validateRoute(d, rt, s); err != nil {
return err
}
}
for _, rt := range routes {
mw := append(append([]string{}, surfaceMiddleware(rt.Surface, s)...), s.Middleware...)
add := func(g pact.Router) { addRoute(g, rt) }
if rt.Surface == ServerToServer {
r.GroupRaw("/", mw, add)
} else {
r.Group("/", mw, add)
}
}
return nil
}
func validateRoute(d Driver, rt Route, s Surfaces) error {
where := fmt.Sprintf("lighthouse: driver %s route %q", d.Name(), rt.Name)
if rt.Handler == nil {
return fmt.Errorf("%s has no handler", where)
}
if !strings.HasPrefix(rt.Path, "/") {
return fmt.Errorf("%s path %q must be absolute", where, rt.Path)
}
switch strings.ToUpper(rt.Method) {
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
default:
return fmt.Errorf("%s has unsupported method %q", where, rt.Method)
}
switch rt.Surface {
case UserAuth:
if len(s.UserAuth) == 0 {
return fmt.Errorf("%s is a UserAuth route but Surfaces.UserAuth is empty; mount it behind a user guard", where)
}
case ServerToServer, Public:
default:
return fmt.Errorf("%s has unknown surface %v", where, rt.Surface)
}
return nil
}
func surfaceMiddleware(surface Surface, s Surfaces) []string {
switch surface {
case UserAuth:
return s.UserAuth
case ServerToServer:
return s.ServerToServer
default:
return s.Public
}
}
func addRoute(g pact.Router, rt Route) {
switch strings.ToUpper(rt.Method) {
case http.MethodGet:
g.Get(rt.Path, rt.Handler)
case http.MethodPost:
g.Post(rt.Path, rt.Handler)
case http.MethodPut:
g.Put(rt.Path, rt.Handler)
case http.MethodPatch:
g.Patch(rt.Path, rt.Handler)
case http.MethodDelete:
g.Delete(rt.Path, rt.Handler)
}
}