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

@@ -25,6 +25,10 @@ const BackendUnlinkedEventName = "user.BackendUnlinked"
// AccessRevokedEventName is the festival name of AccessRevokedEvent.
const AccessRevokedEventName = "user.AccessRevoked"
// LoginEventName is the festival name of LoginEvent, the PHP plugin's
// golem15.user.login.
const LoginEventName = "golem15.user.login"
// GetApiArrayEvent is the fire-and-collect payload other plugins extend.
type GetApiArrayEvent struct {
User *models.User
@@ -111,3 +115,14 @@ type AccessRevokedEvent struct {
Reason string
Tx *gorm.DB
}
// LoginEvent fires after a sign-in issued a session token: a password login,
// a sign-in completed by a second factor, a passkey sign-in or a device
// hand-over (Source is one of the LoginSource constants). It does not fire
// on refresh, on /register or for a sign-in a login gate held back. It fires
// after the token was minted and outside any transaction; a listener error
// is logged and never fails the sign-in (PHP's golem15.user.login).
type LoginEvent struct {
User *models.User
Source string
}