feat: login gates and the login event

Register publishes a *classes.LoginGates registry. POST /login consults
it through controllers.CheckLoginGates after the password, activation and
throttle checks and the bcrypt rehash, and before the restore of a
deactivated account, last_seen and the mint: a gate result replaces the
token response, a gate error answers the opaque 500. The restore and its
reactivate mail now run after the gates, so a password alone cannot
reactivate an account a second factor protects. Without a gate /login
answers as before; refresh never consults the gates.

classes.LoginEvent (golem15.user.login) fires after every password login
that minted a token; controllers.FireLoginEvent lets other plugins fire it.
This commit is contained in:
2026-10-26 09:55:48 +01:00
parent 50da92c1fb
commit 8501f3475a
8 changed files with 502 additions and 14 deletions

View File

@@ -60,9 +60,14 @@ type Plugin struct {
func (p *Plugin) ID() string { return "golem15.user" }
func (p *Plugin) Requires() []string { return nil }
// Register publishes the login-gate registry (*classes.LoginGates), so a
// plugin that requires this one can add its gate in its Boot.
func (p *Plugin) Register(app *backpack.App) error {
p.app = app
return nil
if gates, ok := app.Lookup[*classes.LoginGates](); ok && gates != nil {
return nil
}
return app.Publish(classes.NewLoginGates())
}
func (p *Plugin) Boot(app *backpack.App) error {