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) } }