# UPrint — Serveur (PHP 8 + MySQL)

*Powered by Ket-Up sarl — https://www.ket-up.com*

API REST minimaliste, sans framework (PHP ≥ 8.1, PDO MySQL), déployable sur n'importe quel
hébergement Apache/nginx + php-fpm + MySQL 8 / MariaDB 10.4+.

Elle implémente les deux mécanismes du cahier des charges :

* **Bibliothèque descendante** — les postes ChequeApp vérifient un manifeste et téléchargent
  les templates officiels (nouveaux ou mis à jour) ;
* **Bibliothèque ascendante** — les banques partagent volontairement un template, qui entre
  dans une **file de validation**, puis est **publié** comme version officielle (v1, v2, …).

Le serveur ne reçoit et ne stocke **jamais** de données de prêt, de client, de compte ni
d'historique d'impression : uniquement des templates (image de référence du chèque, géométrie
et typographie des champs, banque, pays, version). Tout payload de soumission contenant des
clés métier (`loanInfo`, `rows`, `clientName`, `accountNumber`…) est rejeté (HTTP 422).

## Installation

```bash
mysql -u root -p < sql/schema.sql            # bibliothèque de templates
mysql -u root -p chequeapp < sql/licensing.sql   # comptes, commandes, licences, activations
cp config/config.example.php config/config.php
# éditer config/config.php : DSN/identifiants MySQL, clé admin, dossier de stockage
chmod 750 storage && chown www-data:www-data storage
```

* Racine web = `server/public` **uniquement** (`.htaccess` fourni pour Apache,
  `nginx.conf.example` pour nginx). `src/`, `config/` et `storage/` ne doivent pas être servis.
* Les soumissions embarquent l'image du chèque en base64 : prévoyez
  `post_max_size` / `client_max_body_size` ≥ 40 Mo.

Test rapide en local : `php -S 127.0.0.1:8085 -t public` puis `curl http://127.0.0.1:8085/api/health`.

## Authentification

| En-tête        | Rôle    | Obligatoire |
|----------------|---------|-------------|
| `X-Api-Key`    | client  | Non tant qu'aucune clé client n'est configurée (config ou table `api_keys`) |
| `X-Admin-Key`  | admin   | Oui pour `/review`, `/publish` et la liste des soumissions |

Les clés sont comparées par hash sha256 à la table `api_keys`, puis aux valeurs de secours
de `config.php`. Pour créer une clé admin en base :

```sql
INSERT INTO api_keys (label, key_hash, role) VALUES ('equipe-validation', SHA2('ma-cle-secrete', 256), 'admin');
```

## Endpoints

| Méthode | Route | Accès | Description |
|---------|-------|-------|-------------|
| GET  | `/api/health` | public | État du service |
| GET  | `/api/library/manifest` | client | `{ version, templates: [{ id, bankName, country, version, updatedAt }] }` |
| GET  | `/api/library/templates/{id}[?version=N]` | client | `CheckTemplate` complet, image incluse dans `background.imageBase64` |
| POST | `/api/template-submissions` | client | Soumet un `TemplateSubmissionPayload` → `TemplateSubmissionRecord` (201) |
| GET  | `/api/template-submissions/{id}` | client | Statut d'une soumission |
| GET  | `/api/template-submissions?status=submitted` | admin | Liste (200 max) pour l'équipe de validation |
| POST | `/api/template-submissions/{id}/review` | admin | `{ "decision": "inReview" \| "changesRequested" \| "rejected", "note": "…" }` |
| POST | `/api/template-submissions/{id}/publish` | admin | `{ "templateId"?: "banque-x-cm", "name"?: "Chèque standard" }` → crée la version N+1 |

Statuts d'une soumission : `submitted → inReview → changesRequested | rejected | published`.

### Publication et versionnement

`/publish` transforme la soumission en template officiel :

1. l'id officiel est `templateId` s'il est fourni (pour publier une nouvelle version d'un
   template existant), sinon un slug `banque-pays` ;
2. `templates.current_version` est incrémenté et une ligne **immuable** est ajoutée dans
   `template_versions` (JSON du template + image copiée dans `storage/templates/{id}/vN.png`) ;
3. les dimensions pixel de l'image sont mesurées côté serveur (`getimagesize`), jamais
   reprises telles quelles du client ;
4. les postes voient la nouvelle version au prochain « Vérifier les mises à jour ».

## Contrat avec le client desktop

Les formes JSON sont celles de `src/shared/types/sync.ts` et `src/shared/types/template.ts`
de l'application. Le client :

* appelle `GET /api/library/manifest`, compare les versions avec ses templates système locaux,
  puis `GET /api/library/templates/{id}` pour chaque id nouveau/mis à jour et écrit l'image
  décodée dans son dossier local (`main/updates/updateService.ts`) ;
* envoie `POST /api/template-submissions` depuis la boîte « Partager ce template »
  (`main/updates/shareService.ts`), après aperçu et confirmation explicite de l'opérateur.

## Exemple de cycle complet

```bash
# soumission (client)
curl -X POST http://HOST/api/template-submissions -H 'Content-Type: application/json' --data-binary @payload.json
# revue puis publication (admin)
curl -X POST http://HOST/api/template-submissions/ID/review  -H 'X-Admin-Key: …' -H 'Content-Type: application/json' -d '{"decision":"inReview","note":"Scan conforme"}'
curl -X POST http://HOST/api/template-submissions/ID/publish -H 'X-Admin-Key: …' -H 'Content-Type: application/json' -d '{"name":"Chèque standard"}'
# distribution (client)
curl http://HOST/api/library/manifest
curl http://HOST/api/library/templates/banque-demo-cameroun
```


---

## Licences et activation des postes

### Mise en place

```bash
mysql -u root -p chequeapp < sql/licensing.sql
```

Six tables : `accounts`, `orders`, `invoices`, `licences`, `licence_activations`, `licence_events`.

Le token n'est **jamais stocké en clair** — seulement son SHA-256 et un préfixe d'affichage.
Une fuite de la base ne livre donc aucune licence utilisable, mais implique aussi qu'un token
perdu ne peut pas être retrouvé : il faut en émettre un nouveau.

### Routes appelées par les postes

| Méthode | Route                   | Rôle |
|---------|-------------------------|------|
| POST    | `/api/licence/activate` | Active ce poste, consomme un siège |
| POST    | `/api/licence/validate` | Revalidation périodique |
| POST    | `/api/licence/release`  | Libère le siège de ce poste |

Aucune clé d'API : **le token porte lui-même le droit**. En exiger une obligerait à
distribuer un second secret à chaque installation.

Un refus répond `200` avec `{"ok":false,"reason":"…"}` plutôt qu'un code d'erreur HTTP :
l'application doit distinguer « token expiré » de « serveur injoignable », et un 4xx brouille
cette distinction dès qu'un portail captif s'intercale. Raisons possibles : `unknown_token`,
`expired`, `revoked`, `seats_exhausted`, `not_activated`.

### Administration en ligne de commande

Le backoffice web n'existe pas encore ; `bin/licence.php` permet de vendre et de dépanner
dès maintenant, et restera l'outil de secours ensuite.

```bash
php bin/licence.php account:create client@banque.cm motdepasse "Banque X"
php bin/licence.php issue client@banque.cm 3 2 "Agence centrale"   # 3 postes, 2 ans
php bin/licence.php list [email]                                    # licences et postes consommés
php bin/licence.php seats CQ-7SKHG-BB                               # postes activés d'une licence
php bin/licence.php release CQ-7SKHG-BB <machineId>                 # libérer un poste à distance
php bin/licence.php revoke CQ-7SKHG-BB
php bin/licence.php stats                                           # indicateurs
```

Tarif de référence : **300 000 XAF par poste et par an** (`Licensing::UNIT_PRICE_CENTS`).
Il est figé dans la commande au moment de l'achat, pour qu'un changement de tarif ne
réécrive jamais l'historique.

### Postes hors ligne

L'activation exige le serveur ; **l'usage non**. Le poste conserve la dernière réponse et
s'y fie jusqu'à la date d'expiration, avec 3 jours de grâce. Une revalidation qui échoue
faute de réseau ne retire jamais la licence : seul un refus explicite du serveur l'efface.

Conséquence à connaître : révoquer une licence ne bloque les postes qu'à leur **prochaine
revalidation**, pas immédiatement.

### Ce qui reste à construire

Les pages web — inscription, achat, espace client, tableau de bord admin — et l'intégration
du paiement Fapshi (`initiate-pay`, webhook `x-wh-secret`). Les tables `orders` et `invoices`
sont déjà prévues pour.


---

## Déploiement de l'application web

L'API et le backoffice partagent **un seul point d'entrée** (`public/index.php`) : il n'y a
qu'une chose à déployer. `/api/*` répond en JSON, tout le reste rend des pages HTML.

### Prérequis

PHP ≥ 8.1 (`pdo_mysql`, `session`), MySQL 8 ou MariaDB 10.4+, Apache avec `mod_rewrite` ou
nginx + php-fpm. Aucune dépendance à installer, aucune étape de construction.

### Mise en place

```bash
# 1. déposer le code hors de la racine web
rsync -a server/ deploy@serveur:/var/www/uprint/server/

# 2. base de données
mysql -u root -p -e "CREATE DATABASE uprint CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p uprint < sql/schema.sql       # bibliothèque de templates
mysql -u root -p uprint < sql/licensing.sql    # comptes, commandes, licences

# 3. configuration
cp config/config.example.php config/config.php
chmod 600 config/config.php                    # contient les identifiants MySQL

# 4. droits
chown -R www-data:www-data storage
chmod 750 storage
```

### Serveur web

**La racine doit être `server/public`, jamais `server/`.** Sinon `config/config.php` devient
téléchargeable avec vos identifiants MySQL et votre clé admin.

* nginx : voir `nginx.conf.example` (HTTPS, en-têtes, un seul `.php` exécutable).
* Apache : `public/.htaccess` est fourni ; `mod_rewrite` doit être actif et
  `AllowOverride All` autorisé sur le répertoire.

### HTTPS obligatoire

Les tokens d'activation circulent dans le corps des requêtes, et le cookie de session n'est
marqué `Secure` que sur une connexion chiffrée. En HTTP, tout passe en clair.

```bash
certbot --nginx -d licences.ket-up.com
```

### Sessions

Le backoffice utilise les sessions PHP. Le répertoire `session.save_path` doit être
accessible en écriture par php-fpm — c'est le cas par défaut sur la plupart des
distributions. Derrière un répartiteur de charge à plusieurs frontaux, prévoyez un stockage
de sessions partagé (Redis, ou `session.save_handler = memcached`), sinon un utilisateur
sera déconnecté à chaque changement de frontal.

### Premier compte administrateur

Le rôle ne s'attribue pas depuis l'interface — c'est délibéré : personne ne doit pouvoir
s'élever en s'inscrivant.

```bash
php bin/licence.php account:create admin@ket-up.com "mot-de-passe-long"
mysql -u root -p uprint -e "UPDATE accounts SET role='admin' WHERE email='admin@ket-up.com';"
```

### Vérification

```bash
curl https://licences.ket-up.com/api/health
# {"status":"ok","product":"UPrint","poweredBy":"Ket-Up sarl",…}
```

Puis dans un navigateur : `/` doit afficher la page d'accueil, `/connexion` le formulaire.
Un 404 sur `/connexion` alors que `/api/health` répond signifie que la réécriture ne couvre
pas les pages web — vérifiez `mod_rewrite` ou le bloc `try_files`.

### Relier les postes

Dans UPrint : **Paramètres → Bibliothèque de templates → Adresse du serveur**, saisir
`https://licences.ket-up.com`. C'est cette adresse qu'utilisent l'activation et la
revalidation des licences.

### Avant d'ouvrir au public

Le paiement Fapshi **n'est pas branché** : à ce stade, toute commande est immédiatement
marquée payée et le token émis. N'exposez pas le backoffice sur Internet tant que
l'intégration n'est pas faite, sinon n'importe qui s'émet des licences gratuitement.

Pour une mise en service anticipée, restreignez l'accès au backoffice (VPN, ou
`allow`/`deny` nginx sur vos adresses) et émettez les licences avec `bin/licence.php`.
