feat: wire the lighthouse realtime service
- Boot builds the realtime service and installs the user lookup - the Centrifugo driver ships with the app; config keeps the null driver - token and subscribe-proxy routes mount only with golem15.presentations, the token route behind presentations.realtime-owner - jz.bm.realtime per-user throttle bucket and websockets:health
This commit is contained in:
22
plugin.go
22
plugin.go
@@ -8,6 +8,8 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/lighthouse"
|
||||
"git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/console"
|
||||
@@ -49,6 +51,9 @@ type Plugin struct {
|
||||
// publicFS is the tree of the student app. Nil means the embedded
|
||||
// public/ directory; tests plant their own.
|
||||
publicFS fs.FS
|
||||
// realtime is the app's lighthouse service, built at Boot. Nil when
|
||||
// Boot did not run, as in tests that build routes on a bare plugin.
|
||||
realtime *lighthouse.Service
|
||||
}
|
||||
|
||||
func (p *Plugin) ID() string { return "jz.bm" }
|
||||
@@ -63,10 +68,16 @@ func (p *Plugin) Register(app *backpack.App) error {
|
||||
}
|
||||
|
||||
// Boot keeps the application, so route handlers and commands can reach its
|
||||
// services, such as the database, when they run. The management API uses the
|
||||
// shared user.api_token middleware owned by golem15.user.
|
||||
// services, such as the database, when they run, and wires the realtime
|
||||
// service. The management API uses the shared user.api_token middleware
|
||||
// owned by golem15.user.
|
||||
func (p *Plugin) Boot(app *backpack.App) error {
|
||||
p.app = app
|
||||
if app != nil {
|
||||
if err := p.wireRealtime(app); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,10 +95,11 @@ func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||
return generatedAdminControllers()
|
||||
}
|
||||
|
||||
// Commands returns the generated commands plus bm:seed. API tokens are
|
||||
// managed application-wide by golem15.user.
|
||||
// Commands returns the generated commands, bm:seed and websockets:health.
|
||||
// API tokens are managed application-wide by golem15.user.
|
||||
func (p *Plugin) Commands() []bonfire.Command {
|
||||
return append(generatedCommands(), console.SeedCommand(p.withDB))
|
||||
cmds := append(generatedCommands(), console.SeedCommand(p.withDB))
|
||||
return append(cmds, centrifugo.Commands(p.app)...)
|
||||
}
|
||||
|
||||
// AdminFS is the admin YAML the controllers read.
|
||||
|
||||
49
realtime.go
Normal file
49
realtime.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package bm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
usermodels "git.golem15.com/golem15/sm-user-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/lighthouse"
|
||||
"gorm.io/gorm"
|
||||
|
||||
// The app always ships the Centrifugo driver; config/realtime.yaml
|
||||
// keeps the null driver until rome's .env selects centrifugo.
|
||||
_ "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
|
||||
)
|
||||
|
||||
// errDatabaseUnavailable is the user lookup's answer before serve has
|
||||
// published the database.
|
||||
var errDatabaseUnavailable = errors.New("jz.bm: database unavailable")
|
||||
|
||||
// wireRealtime binds the app to the framework realtime service: it builds
|
||||
// the service selected by realtime.driver and installs the user lookup the
|
||||
// connection token reads its display name from. Channel authorizers belong
|
||||
// to the plugins that own the channels (golem15.presentations).
|
||||
func (p *Plugin) wireRealtime(app *backpack.App) error {
|
||||
svc, err := lighthouse.From(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.realtime = svc
|
||||
svc.SetUserLookup(func(ctx context.Context, id uint) (lighthouse.User, bool, error) {
|
||||
// Boot runs before serve publishes the database, so the handle is
|
||||
// resolved per call.
|
||||
gdb, ok := app.Lookup[*gorm.DB]()
|
||||
if !ok || gdb == nil {
|
||||
return lighthouse.User{}, false, errDatabaseUnavailable
|
||||
}
|
||||
var u usermodels.User
|
||||
err := gdb.WithContext(ctx).Select("id", "name").Where("id = ?", id).Take(&u).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return lighthouse.User{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return lighthouse.User{}, false, err
|
||||
}
|
||||
return lighthouse.User{ID: u.ID, Name: u.Name}, true, nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
41
routes.go
41
routes.go
@@ -2,10 +2,13 @@ package bm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
|
||||
usermiddleware "git.golem15.com/golem15/sm-user-plugin/middleware"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/lighthouse"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
@@ -27,9 +30,13 @@ const (
|
||||
// bucketManage throttles the management API per token, or per client IP
|
||||
// for a request without one.
|
||||
bucketManage = "throttle:jz.bm.manage"
|
||||
// bucketRealtime throttles the realtime token and subscribe-proxy
|
||||
// routes per user, or per client IP for a request without one.
|
||||
bucketRealtime = "throttle:jz.bm.realtime"
|
||||
)
|
||||
|
||||
// Buckets declares the rate limits of the public calendar feed.
|
||||
// Buckets declares the rate limits of the public calendar feed, the
|
||||
// management API and the realtime routes.
|
||||
func (p *Plugin) Buckets() map[string]surf.Bucket {
|
||||
trusted := surf.TrustedProxies(nil)
|
||||
if p.app != nil {
|
||||
@@ -56,6 +63,16 @@ func (p *Plugin) Buckets() map[string]surf.Bucket {
|
||||
return "jz.bm.manage.ip|" + surf.ClientIP(r, trusted)
|
||||
},
|
||||
},
|
||||
"jz.bm.realtime": {
|
||||
Max: 120,
|
||||
Decay: time.Minute,
|
||||
Key: func(r *http.Request) string {
|
||||
if u, ok := bouncer.User(r.Context()); ok && u.ID != 0 {
|
||||
return "jz.bm.realtime|u:" + strconv.FormatUint(uint64(u.ID), 10)
|
||||
}
|
||||
return "jz.bm.realtime|ip:" + surf.ClientIP(r, trusted)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +130,9 @@ func (p *Plugin) Routes(r pact.Router) error {
|
||||
r.Group(api.CalendarPath, surf.Use(bucketCalendarIP, bucketCalendarToken), func(g pact.Router) {
|
||||
g.Get("/{token}", h.CalendarICS)
|
||||
})
|
||||
if err := p.mountRealtime(r); err != nil {
|
||||
return err
|
||||
}
|
||||
// The student app: the least specific pattern, so every route above and
|
||||
// the admin's keep precedence. It also answers (404) the reserved paths.
|
||||
static, err := newStaticHandler(p.app, p.publicFS)
|
||||
@@ -128,6 +148,25 @@ func (p *Plugin) Routes(r pact.Router) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountRealtime mounts the realtime driver's routes: with the Centrifugo
|
||||
// driver the connection-token route (user) and the subscribe proxy
|
||||
// (server-to-server, authenticated by its shared secret). The phone remote
|
||||
// of golem15.presentations is the only realtime consumer, so nothing is
|
||||
// mounted without it, and the token route is limited to presentation owners
|
||||
// by its presentations.realtime-owner guard, named here without importing
|
||||
// the plugin. Routes built without Boot (bare tests) mount nothing; the
|
||||
// default null driver declares no routes.
|
||||
func (p *Plugin) mountRealtime(r pact.Router) error {
|
||||
if p.realtime == nil || p.app == nil || !p.app.HasPlugin("golem15.presentations") {
|
||||
return nil
|
||||
}
|
||||
return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{
|
||||
UserAuth: surf.Use("jwt.auth", "presentations.realtime-owner"),
|
||||
ServerToServer: surf.Use(),
|
||||
Middleware: surf.Use(bucketRealtime),
|
||||
})
|
||||
}
|
||||
|
||||
// handlers builds the API handlers on the application. The database is
|
||||
// looked up per request, because the binary boots plugins before serve
|
||||
// publishes it.
|
||||
|
||||
Reference in New Issue
Block a user