feat: fire user.Deactivating from the admin deactivate action
- DeactivatingEvent and DeactivatingEventName in classes - FireDeactivatingEvent modelled on FireDeletingEvent - the deactivate bulk action runs DeactivateUsers unchanged, then fires the event per newly deactivated user in the action's transaction
This commit is contained in:
@@ -164,6 +164,7 @@ Packages:
|
||||
- `FireRegisterEvent(ctx, app, user, fields)` fires `RegisterEvent` with `Payload` set to a copy of `fields` without `password` and `password_confirmation`, and returns the listeners' error (`/register` ignores it). Fire it after the user row is committed.
|
||||
- `FireCreatedEvent(ctx, app, user)` fires `CreatedEvent` (`user.Created`). `/register` ignores that error.
|
||||
- `FireDeletingEvent(ctx, app, user, tx)` fires `DeletingEvent` (`user.Deleting`) inside the delete transaction.
|
||||
- `FireDeactivatingEvent(ctx, app, user, tx)` fires `DeactivatingEvent` (`user.Deactivating`) inside the deactivation transaction. The admin deactivate bulk action fires it after `DeactivateUsers` for every selected user that was not deactivated yet; a listener error rolls the action back.
|
||||
- `IssueToken(app, user, r)` mints the session JWT as `/register` and `/login` do.
|
||||
- `APIArray(ctx, app, user)` builds the user payload, extended by `GetApiArrayEvent` listeners.
|
||||
- `classes`: `JWTSecret`, `GormUsers` (the `bouncer.UserProvider`), `LookupByEmail`, `UserPermissionGrants`, `UserGroupCodes`, `HasGroupCode`, `APITokenPrefix`, `MintAPIToken`, `HashAPIToken` and `RevokeAPIToken`, plus the activation/reset, throttle, mail locale and event helpers. `RegisterEvent` carries `User` and `Payload map[string]any`, a read-only copy of the register input without the password keys. For the admin and for other plugins it also has:
|
||||
@@ -173,7 +174,7 @@ Packages:
|
||||
- `TouchLastSeen(ctx, db, userID)`.
|
||||
- `RequestUser(ctx, app, r)` and `EndRequestSession(ctx, app, w, r)`, for handlers that serve signed-in and anonymous browsers on one route (an OAuth provider's authorize page). `RequestUser` reads and checks the session JWT exactly as the `jwt` guard does (bearer header, then the `token` and `auth_token` cookies, the blacklist, `tokens_valid_after`) and returns the user, or `nil` for an anonymous, invalid, expired or blacklisted request and for a soft-deleted user; only an infrastructure failure is an error. `EndRequestSession` blacklists the request's JWT until it expires (as `POST /logout` does) and writes the forget cookie of `bouncer.LoadCookieConfig`, best effort: a blacklist failure is logged, never returned.
|
||||
- `AvatarURL(ctx, app, user, size)`, the display picture rule of the user payload at any size: the avatar thumbnail (size by size, mode auto) when the user has an avatar and a file bucket is published, the file path when the thumbnail fails, otherwise the gravatar URL. The URL may be protocol-relative or relative. The payload's `avatar_url` comes from it at 128.
|
||||
- `HandleGenerator`, `AssignHandle(ctx, db, user)`, `CreatedEvent` / `CreatedEventName`, `DeletingEvent` / `DeletingEventName`, `AuthorizeUser` and `RegisterUserAuthorizer`.
|
||||
- `HandleGenerator`, `AssignHandle(ctx, db, user)`, `CreatedEvent` / `CreatedEventName`, `DeletingEvent` / `DeletingEventName`, `DeactivatingEvent` / `DeactivatingEventName`, `AuthorizeUser` and `RegisterUserAuthorizer`.
|
||||
- `middleware`: `APITokenGuard`, `Scope`, `Permission` and `BearerToken`.
|
||||
- `models`: `User`, `Throttle`, `Organisation`, `UserGroup`, `UsersGroup` (the `users_groups` pivot), `APIToken`, `OAuthIdentity`, `FrontendPermission` and `PermissionSet`. `PermissionSet` reads a stored permission column tolerantly (NULL, an empty string, `[]`, numeric strings) and writes a JSON object of integers; `ParsePermissionSet` and `PermissionSet.Text` do the same for a column that is a plain string on its model. `UserGroup.UsersCount` is filled only by a query that selects `users_count` (the admin list does). `Organisation.Members` is the relation behind the members manager, `Organisation.AttachRelations` declares its avatar (`OrganisationAvatarField`), and `Slugify` is the slug rule. `User.FilterScope` answers the admin list scope `FilterByGroup`, and `User.AttachRelations` declares the avatar (`AvatarField`).
|
||||
- `updates`: the gormigrate migrations, returned by `updates.All()`.
|
||||
|
||||
@@ -11,6 +11,9 @@ 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"
|
||||
|
||||
// GetApiArrayEvent is the fire-and-collect payload other plugins extend.
|
||||
type GetApiArrayEvent struct {
|
||||
User *models.User
|
||||
@@ -52,3 +55,13 @@ 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. A
|
||||
// listener error rolls the deactivation back.
|
||||
type DeactivatingEvent struct {
|
||||
User *models.User
|
||||
Tx *gorm.DB
|
||||
}
|
||||
|
||||
@@ -120,6 +120,16 @@ func FireDeletingEvent(ctx context.Context, app *backpack.App, user *models.User
|
||||
return app.Events.Fire(ctx, &classes.DeletingEvent{User: user, Tx: tx})
|
||||
}
|
||||
|
||||
// FireDeactivatingEvent fires classes.DeactivatingEvent (user.Deactivating)
|
||||
// for user inside the deactivation transaction and returns the listeners'
|
||||
// error, so a failed listener rolls the deactivation back.
|
||||
func FireDeactivatingEvent(ctx context.Context, app *backpack.App, user *models.User, tx *gorm.DB) error {
|
||||
if app == nil || app.Events == nil {
|
||||
return nil
|
||||
}
|
||||
return app.Events.Fire(ctx, &classes.DeactivatingEvent{User: user, Tx: tx})
|
||||
}
|
||||
|
||||
// IssueToken mints the session JWT for user exactly as /register and
|
||||
// /login do: the plugin's JWT secret, golem15.user.jwt.ttl and the request
|
||||
// URL as the issuer.
|
||||
|
||||
@@ -594,13 +594,35 @@ func (c usersAdminController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
}
|
||||
return []pact.AdminBulkAction{
|
||||
bulk("activate", classes.ActivateUsers),
|
||||
bulk("deactivate", classes.DeactivateUsers),
|
||||
bulk("deactivate", c.deactivateUsers),
|
||||
bulk("restore", classes.RestoreUsers),
|
||||
bulk("ban", classes.BanUsers),
|
||||
bulk("unban", classes.UnbanUsers),
|
||||
}
|
||||
}
|
||||
|
||||
// deactivateUsers runs classes.DeactivateUsers unchanged and then fires
|
||||
// user.Deactivating, in the action's transaction, for every selected user
|
||||
// that was not deactivated before. A listener error rolls the action back.
|
||||
func (c usersAdminController) deactivateUsers(ctx context.Context, tx *gorm.DB, users []*models.User) (int, error) {
|
||||
pending := make([]*models.User, 0, len(users))
|
||||
for _, user := range users {
|
||||
if user != nil && user.ID != 0 && !user.DeletedAt.Valid {
|
||||
pending = append(pending, user)
|
||||
}
|
||||
}
|
||||
changed, err := classes.DeactivateUsers(ctx, tx, users)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, user := range pending {
|
||||
if err := FireDeactivatingEvent(ctx, c.application(), user, tx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// throttleState reads whether one user is banned and whether they are
|
||||
// suspended, through the write's transaction when there is one.
|
||||
func (c usersAdminController) throttleState(ctx context.Context, userID uint) (banned, suspended bool, err error) {
|
||||
|
||||
Reference in New Issue
Block a user