Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions docs/api/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ On successful authentication:

```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "9f2c...64 random bytes, hex encoded...",
"refresh_token_ttl": 2592000
}
```

`token` is the JWT to send on every authenticated request. `refresh_token` is an opaque
value that buys a new one when it expires, and `refresh_token_ttl` is its own lifetime in
seconds.

### Using the token

Include the JWT token in the `Authorization` header for authenticated requests:
Expand All @@ -80,6 +86,63 @@ The JWT token contains:

The `type` field indicates whether the user is an Admin or a Customer.

## Refresh tokens

A short-lived JWT keeps the damage of a leaked token small, but it forces the client to
send the credentials again every hour. The refresh token solves that: it is issued
alongside the JWT at login, and exchanged for a fresh pair when the JWT expires.

### Refresh endpoints

```http
POST /api/admin/token/refresh
POST /api/front/token/refresh
Content-Type: application/json

{
"refresh_token": "9f2c..."
}
```

The endpoints also accept a form-encoded body (`refresh_token=9f2c...`). The response has
the same shape as a login response:

```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "1a7e...",
"refresh_token_ttl": 2592000
}
```

### How it behaves

- **Opaque, not a JWT.** The value is 64 random bytes, hex encoded. It carries no
information and cannot be decoded.
- **Single use.** Consuming a refresh token deletes it before the payload is returned, and
the response carries a new one. Replaying the same value fails.
- **Scoped.** A token issued on `/api/admin/login` only works on
`/api/admin/token/refresh`, and a token issued on `/api/front/login` only on
`/api/front/token/refresh`. Presenting one on the other endpoint returns `401`.
- **Stored in the cache pool.** Eviction invalidates the token and forces a new login. In
production, back the pool with a persistent adapter (Redis, filesystem) rather than an
in-memory one.

| Response | Meaning |
| --- | --- |
| `400` | No `refresh_token` in the request body |
| `401` | Unknown, expired, already used, or wrong-scope token |
| `200` | New JWT and new refresh token |

The lifetime is set by `JWT_REFRESH_TOKEN_TTL` (default `2592000`, i.e. 30 days). See
[JWT configuration](#jwt-configuration).

:::tip
Store the refresh token where the access token is not: it is the credential that survives.
On a browser client, prefer a same-site cookie set by your own backend over
`localStorage`.
:::

## Front routes (public)

Public front routes (`/api/front/*`) do not require authentication:
Expand Down Expand Up @@ -118,10 +181,12 @@ JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your-passphrase
# Token lifetime in seconds (read by lexik_jwt_authentication.token_ttl)
JWT_TOKEN_TTL=3600
# Refresh token lifetime in seconds
JWT_REFRESH_TOKEN_TTL=2592000
```

:::note
The bundle configuration (`config/packages/lexik_jwt_authentication.yaml`) maps these variables directly: `secret_key`, `public_key`, `pass_phrase` and `token_ttl: '%env(int:JWT_TOKEN_TTL)%'`. Tokens expire after `JWT_TOKEN_TTL` seconds (default `3600`, i.e. one hour).
The bundle configuration (`config/packages/lexik_jwt_authentication.yaml`) maps these variables directly: `secret_key`, `public_key`, `pass_phrase` and `token_ttl: '%env(int:JWT_TOKEN_TTL)%'`. Tokens expire after `JWT_TOKEN_TTL` seconds (default `3600`, i.e. one hour). `JWT_REFRESH_TOKEN_TTL` is read by Thelia itself, not by the bundle, and drives the [refresh tokens](#refresh-tokens).
:::

## CORS configuration
Expand Down Expand Up @@ -182,6 +247,7 @@ Or for invalid/expired tokens:
2. Configure a token lifetime suited to your use case.
3. Keep the JWT private keys out of version control and restrict access to them.
4. Validate tokens on the server. Never trust client-side validation.
5. Treat the refresh token as the credential that survives: store it apart from the access token, and discard it on logout.

## OpenAPI documentation

Expand Down
48 changes: 48 additions & 0 deletions docs/reference/cli/customer_anonymize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: customer:anonymize
---

## Description
Erase the identifying data of a customer, keeping the accounting record of the orders.

## Usage
```shell
customer:anonymize <email> [options]
```

## Arguments
- `email` Email address of the customer to anonymize.

## Options
- `--force` Do not ask for confirmation.

Deleting an account would take away orders a business is required to keep. This command
erases the identity instead: name, email, password, tokens, address book, cart addresses,
the identity frozen on the order addresses (including SIRET and VAT number), carts,
newsletter subscription, account version history, and the identity copied into the admin
log. The orders keep their reference, invoice number and date, amounts, taxes, coupons and
status history, and stay attached to the now anonymous account.

The whole operation runs in a single Propel transaction: if a module fails, nothing is
written. `customer.anonymized_at` records the date of the first erasure, so running the
command twice does not move the date.

Modules erase their own share by implementing `CustomerPersonalDataProviderInterface`. See
[Personal data](../../security/personal-data.md).

## Examples
Ask for confirmation, then anonymize:
```shell
php Thelia customer:anonymize customer@example.com
```

From a script, without the prompt:
```shell
php Thelia customer:anonymize customer@example.com --force
```

:::caution
This cannot be undone. The account is disabled and its email is replaced by
`anonymous-<id>@anonymous.invalid`, so the person cannot log in again nor be found by
email.
:::
40 changes: 40 additions & 0 deletions docs/reference/cli/customer_export_personal_data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: customer:export-personal-data
---

## Description
Export everything the shop knows about one customer, as JSON.

## Usage
```shell
customer:export-personal-data <email> [options]
```

## Arguments
- `email` Email address of the customer.

## Options
- `--output-file[=OUTPUT-FILE]` Write the archive to this file instead of the standard output.

Core contributes five sections: `customer` (the account), `addresses` (the address book),
`orders` (with their frozen order addresses, products and coupons), `carts`, and
`newsletter`. Every module implementing `CustomerPersonalDataProviderInterface` adds its
own section, under the name it declares.

The same export is available from the back-office, on the customer sheet.

## Examples
Print the export on screen:
```shell
php Thelia customer:export-personal-data customer@example.com
```

Write it to a file:
```shell
php Thelia customer:export-personal-data customer@example.com --output-file=export.json
```

:::tip
The file contains personal data. Deliver it over a channel the person controls, and delete
your local copy once it has been handed over. See [Personal data](../../security/personal-data.md).
:::
30 changes: 25 additions & 5 deletions docs/reference/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ All commands below live in `Thelia\Command\` (core: `core/lib/Thelia/Command/`).

| Command | Description |
| --- | --- |
| `cache:clear` | Invalidate all caches. |
| `thelia:cache:clear` | Invalidate the application, assets, image and document caches. |
| `image-cache:clear` | Empty part or all of the web-space image cache. |
| `hook:clean` | Delete all hooks, then recreate them from the module declarations. |

:::note
`cache:clear` empties the Symfony cache; `image-cache:clear` empties the images generated in the web
space. Clearing one does not clear the other.
`thelia:cache:clear` empties the Thelia caches; `image-cache:clear` empties the images generated in
the web space. Clearing one does not clear the other. Symfony's own `cache:clear` is also available
through `php Thelia`, and empties the Symfony cache only.
:::

### Installation and database
Expand Down Expand Up @@ -88,14 +90,32 @@ space. Clearing one does not clear the other.
| Command | Description |
| --- | --- |
| `thelia:config` | Manage configuration variables. |
| `maintenance:purge` | Purge old data: carts without orders, anonymous carts, and admin logs. |
| [`maintenance:purge`](./maintenance_purge.md) | Purge old data: carts without orders, anonymous carts, admin logs, form firewall records, and the identity of accounts nobody uses anymore. |
| `sale:check-activation` | Check the activation/deactivation dates of sales and apply the required action. |
| `currency:update-rates` | Update currency exchange rates. |
| `thelia:order:rounding-mode` | Show or switch how order line totals are rounded, freezing the orders already placed. |
| `sequence:set` | Set a gapless sequence counter (`order_ref`, `invoice_ref_<year>`, ...) to a given value. |
| `import-export:clean` | Delete the exports and imports whose handler class is no longer available. |

`thelia:order:rounding-mode` writes a pivot in the same run, so the orders already invoiced keep the
amounts they were invoiced with. `sequence:set` is what you reach for when a shop moves to Thelia
with an existing invoice numbering to continue.

:::note
`maintenance:purge` still declares its name with `setName()` in `configure()` rather than the `#[AsCommand]` attribute. Both styles work; new commands should use the attribute.
`maintenance:purge`, `customer:anonymize` and `customer:export-personal-data` still declare their name with `setName()` in `configure()` rather than the `#[AsCommand]` attribute. Both styles work; new commands should use the attribute.
:::

### Customers and personal data

| Command | Description |
| --- | --- |
| [`customer:anonymize`](./customer_anonymize.md) | Erase the identifying data of a customer, keeping the accounting record of the orders. |
| [`customer:export-personal-data`](./customer_export_personal_data.md) | Export everything the shop knows about one customer, as JSON. |

These two commands answer a right of access and a right to erasure. Both are also available from the
back-office, on the customer sheet, and both call the modules that declare personal data. See
[Personal data](../../security/personal-data.md).

### Templates, e-mail, PDF and i18n

| Command | Description |
Expand Down
60 changes: 60 additions & 0 deletions docs/reference/cli/maintenance_purge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: maintenance:purge
---

## Description
Purge old data from the database: carts without orders, anonymous carts, admin logs, form firewall records, and the identity of accounts nobody uses anymore.

## Usage
```shell
maintenance:purge [options]
```

## Options
- `--dry-run` Report what the purge would remove, without touching anything.

Every period is a configuration variable, so a shop sets its own. Read and write them with
`thelia:config`.

| Data | Configuration variable | Default |
| --- | --- | --- |
| Carts without an order | `purification_cart_no_order_days` | 60 days |
| Anonymous carts | `purification_cart_anonymous_days` | 30 days |
| Admin logs | `purification_admin_logs_days` | 180 days |
| Form firewall records | `purification_form_firewall_days` | 1 day |
| Identity of accounts that never ordered | `purification_customer_no_order_days` | `0`, off |
| Identity of accounts whose last order is old | `purification_customer_after_last_order_days` | `0`, off |

Customer retention is off by default, on purpose: erasing an identity cannot be undone,
and the shop is the only one that knows how long it is allowed to keep the data. When a
period is set, the accounts are anonymized through `CUSTOMER_ANONYMIZE`, so modules erase
their share on a scheduled run exactly as on a manual one, and an account already
anonymized is skipped.

The form firewall threshold never goes below the longest waiting period configured, so a
purge cannot hand a blocked IP address a fresh set of attempts.

At the end of its run the command dispatches `TheliaEvents::MAINTENANCE_PURGE`: a module
listens to it, purges its own tables, and appends a line to the report with
`$event->addResult()`.

## Examples
See what would be removed:
```shell
php Thelia maintenance:purge --dry-run
```

Run the purge, typically from a nightly task:
```shell
php Thelia maintenance:purge
```

Set a retention period of two years for accounts that never ordered:
```shell
php Thelia thelia:config set purification_customer_no_order_days 730
```

:::tip
Run `--dry-run` after every change of period, and read the counts before letting the task
run unattended. See [Personal data](../../security/personal-data.md).
:::
Loading
Loading