20260504110000_add_disabled_at_to_users.php 1.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. <?php
  2. declare(strict_types=1);
  3. use App\Infrastructure\Db\Migrations\BaseMigration;
  4. /**
  5. * SEC_REVIEW F11: add a `disabled_at` nullable timestamp to `users` so an
  6. * operator can revoke a user without deleting the audit-history row.
  7. *
  8. * `NULL` means active; any non-null value means the user is disabled (the
  9. * stamp is informational — only the predicate `IS NULL` matters at the
  10. * impersonation boundary). Indexed because both the impersonation
  11. * 403-check (`SELECT … WHERE id = ?`) and the admin-users list page filter
  12. * by it; the index is partial on SQLite to keep the live-user lookup
  13. * branch index-free.
  14. */
  15. final class AddDisabledAtToUsers extends BaseMigration
  16. {
  17. public function up(): void
  18. {
  19. $table = $this->table('users');
  20. $this->addTimestampColumn($table, 'disabled_at', ['null' => true, 'after' => 'is_local']);
  21. $table->update();
  22. // Partial index speeds up the "list disabled users" admin page; the
  23. // active-user lookup path doesn't need it (PK lookup already).
  24. if ($this->isMysql()) {
  25. $this->execute('CREATE INDEX idx_users_disabled_at ON users (disabled_at)');
  26. } else {
  27. $this->execute('CREATE INDEX idx_users_disabled_at ON users (disabled_at) WHERE disabled_at IS NOT NULL');
  28. }
  29. }
  30. public function down(): void
  31. {
  32. if ($this->isMysql()) {
  33. $this->execute('DROP INDEX idx_users_disabled_at ON users');
  34. } else {
  35. $this->execute('DROP INDEX IF EXISTS idx_users_disabled_at');
  36. }
  37. $this->table('users')
  38. ->removeColumn('disabled_at')
  39. ->update();
  40. }
  41. }