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.
This commit is contained in:
+150
@@ -2579,6 +2579,156 @@ paths:
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/account/deletion:
|
||||
get:
|
||||
tags:
|
||||
- Security
|
||||
summary: Describe account deletion requirements
|
||||
description: Returns the authenticated customer or chauffeur deletion state, required confirmation phrase, and categories retained for legal obligations.
|
||||
operationId: getAccountDeletion
|
||||
responses:
|
||||
'200':
|
||||
description: Account deletion requirements retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- principal_type
|
||||
- status
|
||||
- confirmation_phrase
|
||||
- password_required
|
||||
- two_factor_required
|
||||
- access_effect
|
||||
- retained_data_categories
|
||||
- privacy_policy_version
|
||||
properties:
|
||||
principal_type:
|
||||
type: string
|
||||
enum: [customer, subuser]
|
||||
status:
|
||||
type: string
|
||||
enum: [available, requested, processing, failed, manual_review, completed]
|
||||
confirmation_phrase:
|
||||
type: string
|
||||
enum: [SLET MIN KONTO]
|
||||
password_required:
|
||||
type: boolean
|
||||
description: False for authenticated passkey-only accounts that have no password.
|
||||
two_factor_required:
|
||||
type: boolean
|
||||
access_effect:
|
||||
type: string
|
||||
retained_data_categories:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [invoices_payments_accounting, orders_wash_history, security_audit_logs, legal_obligations, customer_reference, driver_reference]
|
||||
privacy_policy_version:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
requested_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
post:
|
||||
tags:
|
||||
- Security
|
||||
summary: Request deletion of the authenticated account
|
||||
description: Reauthenticates the principal, records an auditable deletion request, and revokes access immediately. A background worker subsequently anonymizes personal account fields while preserving legally required history.
|
||||
operationId: requestAccountDeletion
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- confirmation
|
||||
- acknowledge_legal_retention
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: Required when password_required is true; omit for passkey-only accounts.
|
||||
passkey_challenge_token:
|
||||
type: string
|
||||
description: Required for passwordless accounts; issued only by the deletion-specific challenge endpoint.
|
||||
passkey_credential:
|
||||
type: object
|
||||
description: Fresh WebAuthn assertion bound to passkey_challenge_token and the authenticated principal.
|
||||
two_factor_code:
|
||||
type: string
|
||||
description: Required when two-factor authentication is enabled.
|
||||
confirmation:
|
||||
type: string
|
||||
enum: [SLET MIN KONTO]
|
||||
acknowledge_legal_retention:
|
||||
type: boolean
|
||||
enum: [true]
|
||||
responses:
|
||||
'202':
|
||||
description: Deletion request accepted and account access revoked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- request_id
|
||||
- status
|
||||
- requested_at
|
||||
- access_revoked
|
||||
- retained_data_categories
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
format: uuid
|
||||
status:
|
||||
type: string
|
||||
enum: [requested, processing, failed, manual_review, completed]
|
||||
requested_at:
|
||||
type: string
|
||||
format: date-time
|
||||
access_revoked:
|
||||
type: boolean
|
||||
enum: [true]
|
||||
retained_data_categories:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [invoices_payments_accounting, orders_wash_history, security_audit_logs, legal_obligations, customer_reference, driver_reference]
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'429':
|
||||
description: Too many deletion confirmation attempts
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/account/deletion/passkey/challenge:
|
||||
post:
|
||||
tags: [Security]
|
||||
summary: Create a deletion-specific WebAuthn challenge
|
||||
description: Creates a short-lived, single-use challenge bound to the authenticated passwordless principal. A normal sign-in assertion cannot authorize deletion.
|
||||
operationId: createAccountDeletionPasskeyChallenge
|
||||
responses:
|
||||
'200':
|
||||
description: Deletion-specific challenge created
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/auth/2fa/setup:
|
||||
post:
|
||||
tags:
|
||||
|
||||
Reference in New Issue
Block a user