Files
sm-user-plugin/classes/events.go
Jakub Zych 7900f47ba6 feat: password-change hook and the reset-2FA admin slot
The D-07 password-change hook is the existing AccessRevokedEvent, fired
inside the write's transaction by a reset, the user's own change and an
administrator's password; (*AccessRevokedEvent).PasswordChanged reports
those three reasons, and the login rehash fires nothing.

The Users screen declares the record action reset_two_factor (danger)
behind the new permission golem15.users.reset_two_factor, which no role
has by default. It is offered only when a two-factor plugin publishes a
classes.TwoFactorAdmin and the user has a second factor; Run calls the
service's Reset in the action's transaction.
2026-10-26 10:03:59 +01:00

147 lines
5.4 KiB
Go

package classes
import (
"time"
"git.golem15.com/golem15/sm-user-plugin/models"
"gorm.io/gorm"
)
// CreatedEventName is the festival name of CreatedEvent (D-13 / RESEARCH A6).
const CreatedEventName = "user.Created"
// DeletingEventName is the festival name of DeletingEvent (D-13 / RESEARCH A6).
const DeletingEventName = "user.Deleting"
// DeactivatingEventName is the festival name of DeactivatingEvent.
const DeactivatingEventName = "user.Deactivating"
// BackendLinkedEventName is the festival name of BackendLinkedEvent.
const BackendLinkedEventName = "user.BackendLinked"
// BackendUnlinkedEventName is the festival name of BackendUnlinkedEvent.
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
data map[string]any
}
// Collected returns the map listeners merge into the user payload.
func (e *GetApiArrayEvent) Collected() map[string]any {
if e.data == nil {
e.data = map[string]any{}
}
return e.data
}
// RegisterEvent is fired after a user row is created (Winter's
// golem15.user.register).
type RegisterEvent struct {
User *models.User
// Payload is a copy of the register input, mirroring the data array
// Winter passes with golem15.user.register, so a listener can read
// extra keys a client sent (an invitation token, for example).
// password and password_confirmation are never included, in plaintext
// or hashed. Listeners must treat it as read-only input.
Payload map[string]any
}
// CreatedEvent fires from every user create path (API register and admin
// create) so listeners can run inside the same write, including handle
// generation (D-13, D-15). Tx is the create transaction when there is one.
type CreatedEvent struct {
User *models.User
Tx *gorm.DB
}
// DeletingEvent fires from FormAfterDelete before ForceDeleteCleanup /
// unscoped delete, inside the same transaction, so listeners (chatchannels
// GDPR) can sever rows before the user row is gone (D-13, T-19-07).
type DeletingEvent struct {
User *models.User
Tx *gorm.DB
}
// DeactivatingEvent fires after a user is soft-deleted (deactivated, the
// PHP $user->delete()) by the admin deactivate action, inside the same
// transaction, so listeners can revoke what a deactivated account must not
// keep (OAuth grants, for example) while leaving what a restore needs. The
// user's session JWTs and personal API tokens are already revoked when it
// fires (DeactivateUsers calls RevokeUserAccess). A listener error rolls the
// deactivation back.
type DeactivatingEvent struct {
User *models.User
Tx *gorm.DB
}
// BackendLinkedEvent fires inside the write transaction after a link was
// stored, by the frontend user's confirmation (Link.LinkedBy nil) or by an
// administrator (Link.LinkedBy set). A listener error rolls the link back.
type BackendLinkedEvent struct {
Link *models.BackendLink
Tx *gorm.DB
}
// BackendUnlinkedEvent fires inside the write transaction after a link was
// removed, from My account, the Administrators screen or because one of the
// accounts was deleted or deactivated. A listener error rolls the removal
// back.
type BackendUnlinkedEvent struct {
Link *models.BackendLink
Tx *gorm.DB
}
// AccessRevokedEvent fires inside the write transaction when users lose
// every credential issued before Before: a ban, a password reset, a password
// an administrator set, or the user's own password change (FireAccessRevoked;
// Reason names which). Listeners revoke what such a credential still holds
// elsewhere, for example OAuth access and refresh tokens created before
// Before. Before is the untruncated instant of the revocation, while the
// users' tokens_valid_after keeps its whole-second value. A listener error
// rolls the write back. Deactivation and deletion keep their own events
// (DeactivatingEvent, DeletingEvent).
type AccessRevokedEvent struct {
UserIDs []uint
Before time.Time
Reason string
Tx *gorm.DB
}
// PasswordChanged reports whether the event comes from a new password the
// user or an administrator chose: a password reset, the user's own change or
// an administrator setting one (Reason AccessRevokedPasswordReset,
// AccessRevokedPasswordChanged or AccessRevokedPasswordSetByAdmin). The
// bcrypt rehash on login and a new account fire no AccessRevokedEvent. A
// two-factor plugin listens for these events to revoke the users' trusted
// devices inside the same transaction.
func (e *AccessRevokedEvent) PasswordChanged() bool {
if e == nil {
return false
}
switch e.Reason {
case AccessRevokedPasswordReset, AccessRevokedPasswordChanged, AccessRevokedPasswordSetByAdmin:
return true
}
return false
}
// 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
}