# User actions JSON migration

`app/operators/library/ajax/user_actions.php` no longer returns executable JavaScript.
All six caller pages use `userAction()` in `pages_common.js`; SACK is no longer
loaded by these pages. Other SACK consumers are outside this change.

## Request contract

- An explicit `action` is required. Missing, array-valued or unknown actions return
  HTTP 400 and never fall back to enabling users.
- `userEnable`, `userDisable`, `refillSessionTime`, `refillSessionTraffic` and
  `userMail` require a form-encoded POST body containing `action`, `username`
  (or repeated `username[]`) and `csrf_token`. Existing `dalo_check_csrf_token()`
  validates the token generated by the page's form helpers.
- `checkDisabled` uses GET, with `action=checkDisabled` and `username` or
  `username[]`. As before, it checks the first user; the response includes a
  boolean `disabled`. No mutation is performed.
- Usernames are deduplicated and SQL-escaped separately from display values;
  percent signs, quotes, ampersands and Unicode survive transport.
- ACL mappings are unchanged: mutations require `mng_edit`, status reads require
  `mng_search`. The existing authentication redirect and empty ACL-denial HTTP 403
  are preserved and explicitly handled by the client.
- Action results are JSON objects with `success`, plain-text `message`, `level`
  and nullable `disabled`. Messages are rendered with `textContent`, not HTML.

External clients using the former query-string flags (`userEnable=true`, etc.)
or `divContainer` must adopt this contract. POST parameters in the URL are not
accepted as mutation parameters. There is no database schema migration.

## Mutation safety and billing scope

The client permits one in-flight user action per page, disables relevant controls,
and restores their original state after completion. It never automatically
retries or cancels a mutation. An unconfirmed response tells the operator to check
records before attempting the action again. This is UI duplicate-click protection,
not a server-side idempotency key or a cross-operator lock.

Accounting resets, billing-plan selection, history entries, refill costs, taxes
and invoice creation retain their existing business rules. `userInvoiceAdd()`
accepts an optional DB-error callback so its separate connection can use the
existing `db_open.php` hook and return a JSON failure instead of printing HTML.
Invoice and invoice-item inserts share a transaction, so an item or commit failure
rolls back the whole invoice. Earlier accounting and billing-history operations use
separate connections, so the endpoint still reports that some changes may have been
applied rather than presenting an uncertain outcome as success.

Mail content and recipient queries are preserved. The result aggregates successful
and failed sends rather than reporting only the last recipient; no recipients is
not reported as success.

## Validation

```sh
node --test tests/*.test.cjs
python3 tests/user_actions_http.py
```

The Docker test uses `mariadb:11.8` and the built application image
`lirantal/daloradius` (override with `USER_ACTIONS_WEB_IMAGE`). It creates disposable
containers on an **internal network**, a fresh database from the repository's
MariaDB schemas and temporary authenticated operator sessions. It never reads
live configuration or credentials. PHP sessions, ACL, CSRF, PEAR DB, MariaDB and
PHPMailer are real; the operators/users/plans are test fixtures. SMTP is captured
on the web container's loopback interface, with no external mail delivery.
Containers and temporary data are removed in `finally`.

Covered: individual/bulk actions, empty and malformed selections, special-character
usernames, already-disabled users, invalid methods/actions/CSRF, permission denial,
paid/free/no-plan refills, accounting isolation, billing-history and invoice/tax
records, individual/bulk/mixed-failure mail, injected DB failures at group/history/
invoice/item stages, and usable CSRF fields rendered by all six PHP caller pages.
Node tests use DOM/fetch doubles for double-click protection, failure rendering,
no retries, exact parameter encoding and restoration of disabled controls.
These tests do not claim real browser, NAS or RADIUS protocol validation.
