## 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.
75 lines
3.0 KiB
PHP
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);
|
|
}
|
|
});
|
|
}
|
|
}
|