How do I let users cancel their account with JWT / JSON:API? I am making an Ionic app with Drupal as the backend.  Users log in via JWT.  I'm using JSON:API to send data to Ionic.  Now I want to allow users to cancel their accounts in the app (by JSON:API) instead of forcing them to log into the Drupal website and cancel there.
How can I do that?
This method uses the JSON:API Resources module.
It assumes:

Upon cancellation, blocking the user is OK.  (Admins will manually delete the accounts completely later.)
Logged-in users will simply make a GET request to www.example.com/jsonapi/MYMODULE/cancelaccount to cancel their accounts (the app will provide an &quot;Are you sure?&quot; dialog and so forth).

There is probably a better way to do this but I am sharing my solution here because a Google search didn't reveal anything on this topic.
What to do
First, create a custom module.
Then, in MYMODULE.routing.yml:
MYMODULE.user_cancel:
  path: '/%jsonapi%/MYMODULE/cancelaccount'
  defaults:
    _jsonapi_resource: Drupal\MYMODULE\Resource\MYMODULEUserCancel
  requirements:
    # Give all authenticated users permission.
    _permission: 'view own commerce_order'
  options:
    no_cache: 'TRUE

Next, in MYMODULE/src/Resource/MYMODULEUserCancel.php:
&lt;?php

namespace Drupal\MYMODULE\Resource;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceResponse;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\ResourceType\ResourceTypeAttribute;
use Drupal\jsonapi_resources\Resource\EntityQueryResourceBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;

/**
 * Processes a request for a collection containing a user's article nodes.
 *
 * @internal
 */
class MYMODULEUserCancel extends EntityQueryResourceBase implements ContainerInjectionInterface {

  /**
   * The current user account.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected AccountInterface $currentUser;

  /**
   * Logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactory
   */
  protected $loggerFactory;

  /**
   * Constructs a new EntityResourceBase object.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   * @param \Drupal\Core\Logger\LoggerChannelFactory $logger_factory
   *   The Watchdog logger.
   */
  public function __construct(AccountInterface $account, LoggerChannelFactory $logger_factory) {
    $this-&gt;currentUser = $account;
    $this-&gt;loggerFactory = $logger_factory-&gt;get('MYMODULE');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container-&gt;get('current_user'),
      $container-&gt;get('logger.factory'),
    );
  }

  /**
   * Process the resource request.
   *
   * Code example:
   * https://git.drupalcode.org/project/jsonapi_resources/tree/9.x-1.x/tests/modules/jsonapi_resources_test/src/Resource.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
   *   The route resource types.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function process(Request $request, array $resource_types): ResourceResponse {
    $uid = $this-&gt;currentUser-&gt;id();
    // Make sure that we do not try to cancel user 1 or anonymous.
    if ($uid !== 1 &amp;&amp; $uid !== 0) {
      $user_storage = $this-&gt;entityTypeManager-&gt;getStorage('user');
      $current_user = $user_storage-&gt;load($uid);
      // See: https://drupal.stackexchange.com/a/204422.
      // Send the user the &quot;account blocked&quot; confirmation email.
      _user_mail_notify('status_blocked', $current_user);
      $current_user-&gt;set('status', FALSE);
      $current_user-&gt;save();
      $this-&gt;loggerFactory-&gt;notice(&quot;User account for UID @uid cancelled by JSON:API.&quot;, [
        '@uid' =&gt; $current_user-&gt;id(),
      ]);
    }

    // Some data to return with JSON:API.
    $custom_field_data = [
      'cancelled' =&gt; TRUE,
    ];

    $links = new LinkCollection([]);
    $resource_type = reset($resource_types);
    // To disable caching, set metadata.
    $cacheability = new CacheableMetadata();
    $cacheability-&gt;setCacheMaxAge(1);
    $primary_data = new ResourceObject(
      $cacheability,
      $resource_type,
      $current_user-&gt;uuid(),
      NULL,
      $custom_field_data,
      $links
    );
    $top_level_data = new ResourceObjectData([$primary_data], 2);
    $response = $this-&gt;createJsonapiResponse($top_level_data, $request, 201, []);
    $response-&gt;addCacheableDependency($cacheability);

    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function getRouteResourceTypes(Route $route, string $route_name): array {
    $fields = [
      'drupalUid' =&gt; new ResourceTypeAttribute('drupalUid'),
      'status' =&gt; new ResourceTypeAttribute('status'),
    ];
    $resource_type = new ResourceType('current_user', 'current_user', NULL, FALSE, TRUE, TRUE, FALSE, $fields);
    // @todo Add role entities as a relatable resource type.
    $resource_type-&gt;setRelatableResourceTypes([]);
    return [$resource_type];
  }

}