# Creatia Button

Client area addon that lets a customer provision themselves a Creatia account.

## An address that already has a Creatia login

When the client is not yet bound to an organisation but their billing address already signs
in to one, the button links that organisation to the client — adding the identity key = the
client ID alongside whatever it already holds — and shows `already.tpl`. Mercury applies the
same rule when an order arrives in this state, so whichever runs first, the customer ends up
in the organisation they sign in to rather than refused or given a second one.

It happens only from a verified address (below). That proves the client controls the login,
not that they own the organisation: a team member's own billing account links to the
organisation they were invited into. That is deliberate. Each link is written to the
client's activity log with the organisation's subtenant next to this install's, so a
cross-brand link is visible.

GSL install only (subtenant `gsl`). On a brand install an existing login still goes to an
administrator: a brand's dedi right (`dedi-go-dedicated`, `dedi-streamline`) promotes every
read-only user in the organisation, so a read-only colleague who linked their own billing
account and bought a server would promote them all. Nothing granted on GSL does that.

A client already bound to one organisation whose address signs in to another is still sent
to an administrator (`already-unlinked.tpl`) — linking would leave the identity held by two.

## Email verification, and when it applies

Verification is required only where the module would add to something that **already
exists** — an organisation already bound to this client that is missing its user, or one the
client's address already signs in to, which gets linked to the client.

It is deliberately **not** required for a first-time provision. There, the module creates a
new organisation and sends the invitation to the address in question, so claiming an
address you do not control gains you nothing — the invitation goes to its real owner.
Requiring it would break the ordinary flow of signing up and ordering in one sitting,
before the verification mail has even been opened.

Where it does apply, `Controller::addressStatus()` checks two things against `tblusers`:

1. `email_verified_at` is set on the client's owner user.
2. That user's address matches the client's address.

The second is not redundant. Verification is tracked on the WHMCS **user**, but the Creatia
account is created against the **client** address, and the two can diverge.

The two failures are shown differently, because they need different things from the
customer. An unverified address is self service (`verify.tpl`). A mismatch is not, so
`verify-mismatch.tpl` shows both addresses and sends them to support.

### Size the lockout before deploying

Anyone whose address predates verification being enabled is turned away **only on the
resume and link paths**, not on a first provision. That is a much smaller population than
it used to be, but worth knowing:

```sql
SELECT COUNT(DISTINCT uc.client_id) AS locked_out
FROM tblusers u
JOIN tblusers_clients uc ON uc.auth_user_id = u.id AND uc.owner = 1
JOIN tblhosting h ON h.userid = uc.client_id AND h.domainstatus = 'Active'
WHERE u.email_verified_at IS NULL;
```

And the divergence case, which is the less obvious half of the gate:

```sql
SELECT COUNT(DISTINCT uc.client_id) AS mismatched
FROM tblusers_clients uc
JOIN tblusers u ON u.id = uc.auth_user_id
JOIN tblclients c ON c.id = uc.client_id
JOIN tblhosting h ON h.userid = uc.client_id AND h.domainstatus = 'Active'
WHERE uc.owner = 1
  AND LOWER(TRIM(u.email)) <> LOWER(TRIM(c.email));
```

The two groups need different remediation, and re-verification does **not** clear the
second one — a mismatched client stays blocked however many times they verify.

- **Unverified**: trigger a verification email. Self service from there.
- **Mismatched**: someone has to decide which address is right, per client, and align
  the two. Then verification applies to the address that will actually be used.

Both need doing before this goes live, or their first experience of the module is a
wall.

## No HMAC

An earlier version collected an HMAC secret and sent an `X-Signature` header. The
internal endpoints this module calls authenticate on the API token alone and never
verified it, so it was removed rather than left looking like protection. The
`creatia_hmac_secret` row may still exist in `tbladdonmodules`; it is unread.

Signing is worth revisiting, but not as it stood: it covered only the payload, with
no method, path, timestamp or nonce, so a captured signature was replayable against
a different endpoint.

## The `gsl` external identity

This module writes `tblclients.id` under the identity key. So does the Mercury
provisioning module, and so does the network side, which reads the value as a CXID. One
namespace, one meaning.

## Deploying

Copy to `modules/addons/creatia_button/`. `Controller.php` and `PhpCreatia.php` must go
**together** — the constructor dropped from four arguments to three when HMAC was removed.
`lib/Eligibility.php` and the `verify`, `verify-mismatch` and `already-unlinked` templates
are new files; without `Eligibility.php` the navbar item silently disappears and the module
page fatals.

Check the settings rows exist before deploying, because a missing `any_product` row now
qualifies nobody where the previous code qualified everybody, and it fails silently:

```sql
SELECT setting, value FROM tbladdonmodules WHERE module = 'creatia_button';
```

`any_product` must be `0` or `1`, and if it is `1`, `target_server_id` must be a real
server ID. The `creatia_hmac_secret` row is now unread and can be deleted.

## Eligibility

`Eligibility::clientQualifies()` is the single rule, used by both the navbar hook and
the controller. When they disagreed, hiding the menu item enforced nothing, because
`index.php?m=creatia_button&action=setup` was reachable directly.

An unconfigured module qualifies nobody. The settings rows only exist once an
administrator has saved the addon configuration, and until then there is no way to
tell whether "any service" was intended.
