fix: logout revokes the session under serve: the jwt blacklist and GORM hooks are wired when the database is published

Boot runs before summer serve publishes the database, so the plugin's GORM
hooks, the Postgres jwt_blacklist store and its sweep now go through
lagoon.OnDatabase, and the jwt guard resolves the published store on every
request. A logged-out token is refused on jwt.auth, /fetch and /refresh.

The jwt guard reads the cookie names from classes.SessionCookieNames().
This commit is contained in:
Jakub Zych
2026-10-22 22:29:59 +02:00
parent 8e4b863a3f
commit 4bf44fc6ea
6 changed files with 294 additions and 22 deletions

View File

@@ -126,11 +126,10 @@ func adminHarnessDB(t *testing.T) (*gorm.DB, *sql.DB) {
return adminHarnessGDB, adminHarnessSQL
}
// newAdminEnv boots the plugin alone with the admin API mounted. configure
// functions adjust the config before the plugin is activated.
func newAdminEnv(t *testing.T, configure ...func(*compass.Config)) *adminEnv {
// adminEnvConfig is the config every harness boot starts from: the plugin's
// JWT secret, the admin secret, body limits and the log mail driver.
func adminEnvConfig(t *testing.T) *compass.Config {
t.Helper()
gdb, sqlDB := adminHarnessDB(t)
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: user-admin\nlocale: en\nfallback_locale: en\nurl: https://app.example.test\n"), 0o644); err != nil {
t.Fatal(err)
@@ -158,7 +157,15 @@ func newAdminEnv(t *testing.T, configure ...func(*compass.Config)) *adminEnv {
t.Fatal(err)
}
}
app := backpack.New(cfg)
return cfg
}
// newAdminEnv boots the plugin alone with the admin API mounted. configure
// functions adjust the config before the plugin is activated.
func newAdminEnv(t *testing.T, configure ...func(*compass.Config)) *adminEnv {
t.Helper()
gdb, sqlDB := adminHarnessDB(t)
app := backpack.New(adminEnvConfig(t))
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
t.Fatal(err)
}
@@ -191,6 +198,44 @@ func newAdminEnv(t *testing.T, configure ...func(*compass.Config)) *adminEnv {
return &adminEnv{h: h, app: app, gdb: gdb, bucket: bucket, mail: mail}
}
// newServeOrderEnv boots the plugin the way surf's serve command does after
// the generated main's party.Activate: every Boot runs with no database
// published, then lagoon.Publish, the uploads bucket and the router.
// beforePublish, when set, runs between Activate and Publish. Migrate is
// there only because the shared harness database may not be migrated yet.
func newServeOrderEnv(t *testing.T, beforePublish func(*backpack.App)) *adminEnv {
t.Helper()
gdb, sqlDB := adminHarnessDB(t)
app := backpack.New(adminEnvConfig(t))
mail := &mailCapture{next: slog.Default().Handler()}
if err := app.Publish(slog.New(mail)); err != nil {
t.Fatal(err)
}
plugins, err := party.Activate(app, []string{"golem15.user"})
if err != nil {
t.Fatal(err)
}
if beforePublish != nil {
beforePublish(app)
}
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
t.Fatal(err)
}
bucket := memblob.OpenBucket(nil)
t.Cleanup(func() { _ = bucket.Close() })
if err := attach.Publish(app, bucket); err != nil {
t.Fatal(err)
}
if err := lagoon.Migrate(gdb, plugins); err != nil {
t.Fatal(err)
}
h, err := surf.Assemble(app, plugins)
if err != nil {
t.Fatal(err)
}
return &adminEnv{h: h, app: app, gdb: gdb, bucket: bucket, mail: mail}
}
// admin inserts a backend role holding exactly permissions and a backend
// user in it, logs the user in and returns the bearer token. No permission
// at all gives an administrator who may sign in and open nothing.