Files
api/services/nginx/app/routes/accountDeletionRoute.php
T
Jeppe B 0060fb45ca Add in-app account deletion (#319)
## Summary
- Add self-service deletion for the authenticated customer or subuser
identity only.
- Preserve shared customer grants, reset keys, bookings, order bookings,
vehicles, invoices, and legally required history.
- Require password/TOTP or a fresh deletion-specific, five-minute,
single-use WebAuthn assertion.
- Reject support impersonation and expired legacy plain-session tokens.
- Use durable database throttling, transactional request processing, a
durable outbox, and terminal `manual_review` state.
- Keep API and worker default-off behind separate
`account_deletion.api_enabled` and `account_deletion.worker_enabled`
module-config flags.

## Safe rollout
1. Keep both flags disabled.
2. Run `php scripts/account-deletion-schema.php check`.
3. If needed, run `php scripts/account-deletion-schema.php apply --yes`,
then rerun `check` until `ready:true`.
4. Deploy the frontend companion PR while the API remains disabled.
5. Enable `api_enabled` for a controlled canary; verify password and
passwordless request flows plus immediate authentication revocation.
6. Inspect queued request/outbox state, then enable `worker_enabled`.
7. Verify anonymization, preserved tenant/history data, outbox delivery,
retries, and manual-review behavior before broad rollout.

## Verification
- Account deletion unit tests: 2 passed, 43 assertions.
- PHP lint, both OpenAPI YAML parses, runtime-DDL scan,
destructive-scope scan, and `git diff --check` passed.
- Full API/unit/integration evidence is required from exact-head CI;
local Docker is unavailable and shared-vendor tests were explicitly
discarded.

## Security notes
- Schema mutation is CLI-only; web and cron paths perform read-only
readiness checks.
- Runtime behavior fails closed when schema/config/throttle/delivery
prerequisites are unavailable.
2026-07-22 19:22:17 +02:00

75 lines
3.0 KiB
PHP

<?php
namespace routes;
use classes\account_deletion_http_exception;
use classes\account_deletion_service;
use Throwable;
use traits\route_t;
class accountDeletionRoute
{
use route_t;
public function run(): void
{
$this->get('/account/deletion', function () {
global $response;
try {
if (!account_deletion_service::apiEnabled()) {
$response->error('Account deletion is unavailable', 404);
return;
}
$service = new account_deletion_service();
$principal = $service->currentPrincipal();
$response->success($service->state($principal));
} catch (account_deletion_http_exception $exception) {
$response->error($exception->getMessage(), $exception->status);
} catch (Throwable $throwable) {
$response->error('Unable to load account deletion status', 500);
}
});
$this->post('/account/deletion', function () {
global $response;
try {
if (!account_deletion_service::apiEnabled()) {
$response->error('Account deletion is unavailable', 404);
return;
}
$service = new account_deletion_service();
$principal = $service->currentPrincipal();
$payload = $service->request(
$principal,
$this->getParametersAsArray(),
isset($_SERVER['REMOTE_ADDR']) ? (string)$_SERVER['REMOTE_ADDR'] : null,
isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : null,
);
$response->success($payload, 202);
} catch (account_deletion_http_exception $exception) {
$response->error($exception->getMessage(), $exception->status);
} catch (Throwable $throwable) {
error_log('[account-deletion] Request failed: ' . $throwable->getMessage());
$response->error('Unable to request account deletion', 500);
}
});
$this->post('/account/deletion/passkey/challenge', function () {
global $response;
try {
if (!account_deletion_service::apiEnabled()) {
$response->error('Account deletion is unavailable', 404);
return;
}
$service = new account_deletion_service();
$response->success($service->passkeyChallenge($service->currentPrincipal()));
} catch (account_deletion_http_exception $exception) {
$response->error($exception->getMessage(), $exception->status);
} catch (Throwable $throwable) {
error_log('[account-deletion] Passkey challenge failed: ' . $throwable->getMessage());
$response->error('Unable to create deletion passkey challenge', 500);
}
});
}
}