{"record":{"id":"a88f3913687a71a1","repo":"passbolt/passbolt_api","slug":"user-account-recovery-settings-cannot-be-edited","errorCode":null,"errorMessage":"User account recovery settings cannot be edited.","messagePattern":"User account recovery settings cannot be edited\\.","errorType":"http","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryUserSettings/AccountRecoveryUserSettingsSetService.php","lineNumber":96,"sourceCode":"            ->fetchTable('Passbolt/AccountRecovery.AccountRecoveryUserSettings');\n        $this->AccountRecoveryPrivateKeys = $this\n            ->fetchTable('Passbolt/AccountRecovery.AccountRecoveryPrivateKeys');\n        $this->AccountRecoveryPrivateKeyPasswords = $this\n            ->fetchTable('Passbolt/AccountRecovery.AccountRecoveryPrivateKeyPasswords');\n        $this->uac = $uac;\n    }\n\n    /**\n     * @param array $data Payload\n     * @return \\Passbolt\\AccountRecovery\\Model\\Entity\\AccountRecoveryUserSetting\n     */\n    public function set(array $data): AccountRecoveryUserSetting\n    {\n        // Ensure user can only enroll once\n        // It's not possible for a user to enroll and de-enroll or enroll and re-enroll\n        $currentSettings = (new AccountRecoveryUserSettingsGetService())->get($this->uac->getId());\n        if (isset($currentSettings) && $currentSettings->isApproved()) {\n            throw new BadRequestException(__('User account recovery settings cannot be edited.'));\n        }\n\n        $setting = $this->patchEntity($data);\n        $this->AccountRecoveryUserSettings->saveOrFail($setting);\n\n        return $setting;\n    }\n\n    /**\n     * @param array $data Payload\n     * @return \\Passbolt\\AccountRecovery\\Model\\Entity\\AccountRecoveryUserSetting\n     */\n    public function patchEntity(array $data): AccountRecoveryUserSetting\n    {\n        $this->data = $data;\n        $this->organizationPolicy = (new AccountRecoveryOrganizationPolicyGetService())->getOrFail();\n        $status = $data['status'] ?? '';\n        $setting = $this->validateAccountRecoveryUserSetting($status);","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryUserSettings/AccountRecoveryUserSettingsSetService.php#L78-L114","documentation":"Thrown in AccountRecoveryUserSettingsSetService::set() when the user's existing account recovery setting is already 'approved'. Policy allows a user to enroll only once: re-enrollment, de-enrollment or any edit of an approved setting is rejected with a BadRequestException.","triggerScenarios":"PATCH/POST to the account recovery user settings endpoint with status=approved when the current setting is already approved; replaying an enrollment request; a client re-submitting settings after a successful enroll.","commonSituations":"Double-submission of the enrollment form (network retry, double click); clients not fetching current settings before saving; tests reusing a fixture user that is already enrolled.","solutions":["Fetch current settings first and skip the save if already approved","Return the existing setting idempotently for repeated approved submissions","For a genuine policy change (de-enroll/re-enroll), an administrator must reset the setting per organization policy","Fix client-side double submission (disable submit, dedupe retries)"],"exampleFix":"// before\n$service->set($data); // throws if already approved\n// after\n$current = (new AccountRecoveryUserSettingsGetService())->get($userId);\nif (!$current || !$current->isApproved()) {\n    $service->set($data);\n}","handlingStrategy":"validation","validationCode":"const current = await getUserSetting(userId); if (current && current.status === 'approved') return current; // skip save","typeGuard":"const canEdit = (s) => s == null || s.status !== 'approved';","tryCatchPattern":"try { await setSettings(data); } catch (e) { if (e.status === 400 && /cannot be edited/.test(e.message)) { /* reload and show current state */ } }","preventionTips":["Fetch current settings before every save","Make saves idempotent by short-circuiting when already approved","Debounce/dedupe form submissions"],"tags":["bad-request","account-recovery","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T09:17:21.228Z"}