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.
This commit is contained in:
2026-10-26 10:03:59 +01:00
parent 8501f3475a
commit 7900f47ba6
14 changed files with 419 additions and 12 deletions

View File

@@ -0,0 +1,38 @@
package classes
import (
"context"
"git.golem15.com/golem15/sm-user-plugin/models"
"gorm.io/gorm"
)
// PermissionResetTwoFactor is the backend permission an administrator needs
// to run the Users screen's "Reset 2FA" record action (reset_two_factor). No
// role has it by default: only a superuser, or a role granted it
// explicitly, may remove a user's second factors.
const PermissionResetTwoFactor = "golem15.users.reset_two_factor"
// TwoFactorAdmin is what a two-factor plugin publishes so the Users screen
// can offer its "Reset 2FA" record action. The screen looks it up on every
// request: without a published service the action is never offered, so an
// application that mounts no two-factor plugin never shows it.
//
// Publish it with an explicit type argument,
//
// app.Publish[classes.TwoFactorAdmin](svc)
//
// because a bare app.Publish(svc) keys the service on its concrete type and
// the lookup misses it.
type TwoFactorAdmin interface {
// HasTwoFactor reports whether the user has any second factor (an
// enabled method, a WebAuthn credential, recovery codes or a trusted
// device) to reset. It only reads, through tx.
HasTwoFactor(ctx context.Context, tx *gorm.DB, userID uint) (bool, error)
// Reset disables every second-factor method of user, deletes the
// WebAuthn credentials and recovery codes, revokes the trusted devices
// and fires the plugin's disabled event, all on tx (the record action's
// transaction): an error rolls the reset back. The user then signs in
// with the password alone until they set up a second factor again.
Reset(ctx context.Context, tx *gorm.DB, user *models.User) error
}