# Architecture technique — Application web PHP

## Vue d'ensemble

Application web PHP sans framework, organisée autour d'un noyau de bibliothèques
(`/lib`) et d'un système de modules extensible. L'interface utilisateur est
rendue côté serveur (pas de SPA) avec du JavaScript progressif.

---

## Structure des répertoires

```
/                         ← racine du projet (non accessible par le web)
├── lib/                  ← bibliothèques métier (autoload)
│   ├── Config.php        ← constantes globales (chemins, rôles, langues…)
│   ├── Database.php      ← singleton SQLite3 natif (PAS de PDO)
│   ├── Session.php       ← gestion de session + flash messages
│   ├── Auth.php          ← authentification (local, OAuth, LDAP) + rôles
│   ├── User.php          ← CRUD utilisateurs + rôles (table user_roles)
│   ├── Module.php        ← registre des modules + nav links + sync rôles
│   ├── I18n.php          ← traductions
│   ├── Settings.php      ← paramètres applicatifs (table BD)
│   ├── Logger.php        ← journalisation (app / debug / trace)
│   ├── Upload.php        ← gestion des fichiers uploadés
│   └── Crypto.php        ← chiffrement at-rest (sodium secretbox)
├── securite/             ← hors web : base SQLite + fichiers sensibles
│   ├── database.sqlite
│   └── master.key        ← clé maître 32 octets (0600) — sauvegarder !
├── log/                  ← journaux (app.log, debug.log, trace.log)
├── mon-appli/            ← racine web (DocumentRoot Apache)
│   ├── bootstrap.php     ← amorçage de chaque page
│   ├── index.php         ← page d'accueil
│   ├── login.php / logout.php / profile.php
│   ├── admin/            ← pages d'administration (users, settings, modules)
│   ├── assets/
│   │   ├── css/          ← feuilles de style (main.css + themes/)
│   │   └── js/           ← main.js
│   ├── i18n/             ← fichiers de traduction (fr/en/de/es/it/pl/ro)
│   ├── includes/         ← fragments partagés (header, footer, sidebar…)
│   ├── modules/          ← modules applicatifs
│   │   ├── oracle/       ← module Oracle
│   │   ├── CMDB/         ← module CMDB (config Oracle CMDB + tables CNP$*)
│   │   ├── CNP/          ← git submodule (repo distinct sw-module-cnp)
│   │   └── developer/    ← module outils développeur
│   └── oracle/           ← front-controller URL propres du module Oracle
└── docs/                 ← documentation technique
```

---

## Bootstrap

Chaque page PHP commence par `require_once 'bootstrap.php'` (ou équivalent
avec chemin). Le bootstrap réalise dans l'ordre :

1. **Config** — chargement de `lib/Config.php` → définition des constantes.
2. **Autoload** — `spl_autoload_register` cherche `lib/{Classe}.php`.
3. **Session** — `Session::start()`.
4. **Base de données** — `Database::getInstance()` (SQLite3 natif, pas de PDO).
   À la première instanciation : création des tables, migrations idempotentes
   (cf. `Database::migrate()`) et seed des données initiales (rôles, settings
   par défaut, méthodes d'auth, admin par défaut avec rôle `app_admin`).
5. **Settings** — paramètres dynamiques depuis la table `settings`.
6. **Logger** — activation des modes debug/trace selon settings.
7. **Auth** — identification de l'utilisateur courant (`$currentUser`).
8. **Modules** — discovery automatique : chaque `modules/*/module.php` déclare
   un tableau de méta-données ; `Module::register()` l'enregistre.
9. **I18n** — détection de la langue (préférence utilisateur > session > défaut).
10. **Thème** — résolution du thème actif (utilisateur > défaut admin).
11. **Commutation langue/thème** — traitement des paramètres `?lang=` et `?theme=`
    (mise à jour BD + redirection propre).

Variables injectées disponibles dans toutes les pages :
`$db`, `$auth`, `$currentUser`, `$moduleRegistry`, `$i18n`, `$settings`,
`$logger`, `$activeTheme`.

---

## Système de modules

Un module est un répertoire `mon-appli/modules/{nom}/` contenant un
descripteur `module.php` qui retourne un tableau de méta-données :

```php
return [
    'name'        => 'cmdb',
    'label'       => 'CMDB',
    'description' => 'Connexion à la base Oracle CMDB et provisioning…',
    'version'     => '0.1.0',

    // Rôles déclarés par le module — synchronisés en table `roles` à
    // chaque bootstrap via Module::register (idempotent, INSERT OR IGNORE).
    // Convention : <module>_admin (config + droits métier) + <module>_user.
    'roles' => [
        'cmdb_admin' => 'CMDB Administrator',
        'cmdb_user'  => 'CMDB User',
    ],

    // Dépendances entre modules : refus d'activation si un module listé
    // n'est pas actif ; refus de désactivation si un autre actif en dépend.
    'depends_on' => ['oracle'],

    // Hook d'activation (chemin de fichier PHP qui retourne une callable
    // signée `function ($db, ?string &$error): bool`). Échec ⇒ activation
    // refusée + flash error + redirection vers config_url si présent.
    'on_enable' => __DIR__ . '/_on_enable.php',

    // Page de configuration ; cible de redirection si on_enable échoue.
    'config_url' => '/modules/CMDB/admin.php',

    'nav_url'   => '/modules/CMDB/',
    'nav_label' => 'CMDB',
    'nav_icon'  => '🗂',
];
```

`Module::register()` insère le module dans la table `modules` (préserve
`active`/`config` existants) et synchronise les rôles déclarés dans la
table `roles`. `Module::getNavLinks($currentUser)` filtre les modules
actifs auxquels l'utilisateur a accès (au moins un rôle `<name>_admin`
ou `<name>_user`, ou `app_admin`).

`Module::syncDescriptor()` rafraîchit `label`/`description` et les
libellés de rôles depuis le descripteur, et ajoute les rôles nouvellement
déclarés. Utilisé **uniquement** lors de la mise à jour d'un module
(`admin/modules.php`), là où `register()` — en `INSERT OR IGNORE` — ne
réécrirait rien. Ne touche jamais `active`, la config, ni `user_roles` :
les rôles disparus du nouveau descripteur restent en base, les supprimer
casserait les affectations utilisateurs (FK `ON DELETE CASCADE`).

### Système de rôles

Modèle multi-rôles : un utilisateur peut avoir 0..N rôles via la table
de jointure `user_roles`. Les rôles vivent en table `roles` ; les modules
sont la source de vérité pour leurs rôles (`<module>_admin`,
`<module>_user`) et les déclarent dans leur descripteur.

| Rôle                | Source                   | Portée                                       |
|---------------------|--------------------------|----------------------------------------------|
| `app_admin`         | seed (système)           | **Super-rôle implicite** — accorde tout      |
| `oracle_admin`      | module oracle            | Configuration Oracle + droits transverses    |
| `oracle_user`       | module oracle            | Consultation Oracle (selon droits oracle_access / oracle_role_perms) |
| `cmdb_admin`        | module cmdb              | Configuration de la connexion CMDB           |
| `cmdb_user`         | module cmdb              | Consommation des données CMDB                |
| `cnp_admin` / `cnp_user` | submodule cnp        | Module CNP                                   |

`app_admin` est marqué `is_system = 1` (non supprimable). Tout check de
rôle (`Auth::hasRole`, `Auth::hasAnyRole`, `Auth::requireRole`,
`Module::canAccess`, `Module::canAccessAsAdmin`) court-circuite vrai si
l'utilisateur a `app_admin`.

### API d'autorisation

| Méthode                                | Résultat                                                 |
|----------------------------------------|----------------------------------------------------------|
| `Auth::hasRole(string)`                | true si le user a ce rôle (ou `app_admin`)              |
| `Auth::hasAnyRole(...string)`          | true si au moins un des rôles (ou `app_admin`)          |
| `Auth::requireRole(...string)`         | exit 403 sinon (inclut `includes/403.php`)              |
| `Module::canAccess(name, user)`        | true si actif **et** user a `<name>_admin`/`_user`      |
| `Module::canAccessAsAdmin(name, user)` | true si user a `<name>_admin` (gating page de config)   |

Constante unique en code : `ROLE_APP_ADMIN`. Les autres rôles sont
référencés par leur chaîne (`'oracle_admin'`, `'cmdb_admin'`…) car ils
sont déclarés dynamiquement par les modules.

### Migration profils → rôles

`Database::migrate()` détecte la présence d'une colonne `users.profile_id`
(ancien modèle mono-profil) :
1. Filet : pour chaque user sans rôle mais avec profil legacy, mappe
   `profiles.name` (`admin`/`oracle_admin`/`cnp_admin`) → rôle équivalent.
2. Recrée `users` sans `profile_id` (pattern table-temporaire SQLite).
3. Drop la table `profiles`.

Idempotente : sur un schéma déjà migré, ne fait rien.

---

## Base de données

SQLite via l'extension native **SQLite3** (pas PDO), fichier
`securite/database.sqlite` (hors DocumentRoot). PRAGMA `foreign_keys = ON`,
`journal_mode = WAL`, `busyTimeout = 5000`.

Helpers de la classe `Database` (singleton) : `query`, `fetchOne`,
`fetchAll`, `fetchScalar`, `execute`, `lastInsertId`. Les paramètres
positionnels (`?`) et nommés (`:name`) sont supportés via `prepare()`.

### Tables centrales

| Table         | Contenu                                                    |
|---------------|------------------------------------------------------------|
| `users`       | Comptes utilisateurs (hash bcrypt) — pas de `profile_id`   |
| `roles`       | Rôles (`name`, `label`, `module`, `is_system`)             |
| `user_roles`  | Jointure user × rôle (CASCADE on user/role delete)         |
| `settings`    | Paires clé/valeur des paramètres applicatifs               |
| `auth_config` | Méthodes d'auth (local/oauth/ldap) + JSON config           |
| `modules`     | État activé/désactivé + JSON config par module             |

### Tables Oracle

| Table                    | Contenu                                                |
|--------------------------|--------------------------------------------------------|
| `oracle_groups`          | Groupes Oracle (hiérarchie groupe/sous-groupe)         |
| `oracle_databases`       | Bases Oracle (connexion + cloud wallet)                |
| `oracle_access`          | Droits directs (user ou groupe AD) sur groupe/sous-grp |
| `oracle_roles`           | Rôles Oracle (distincts des rôles applicatifs)         |
| `oracle_role_perms`      | Permissions d'un rôle Oracle ; `group_id IS NULL` = tous|
| `oracle_role_grants`     | Attribution rôles Oracle → users/groupes AD            |
| `oracle_role_includes`   | Inclusion entre rôles Oracle (héritage, cycle-safe)    |

> Les tables `oracle_role_*` sont **internes au module Oracle** et n'ont
> aucun lien avec la table `roles` applicative. La distinction est :
> `roles` = ce qu'un user *est* dans l'application ; `oracle_roles` =
> bundles de droits sur des bases Oracle.

### Chiffrement at-rest des secrets

Tous les mots de passe stockés dans `database.sqlite` sont chiffrés via
`lib/Crypto` (libsodium `crypto_secretbox` — XSalsa20 + Poly1305 AEAD).

**Champs chiffrés** :
- `oracle_databases.password`
- `modules.config.cmdb.password` (JSON)
- `auth_config.config.ldap.bind_password` (JSON)
- `auth_config.config.oauth.client_secret` (JSON)

**Format** : `enc_v1:` + base64(nonce 24 octets ‖ ciphertext authentifié).

**Clé maître** : `securite/master.key` — 32 octets random, auto-générée à
la première utilisation, mode 0600. Couverte par `.gitignore` via le
dossier `securite/`. `Crypto::encrypt()` est idempotent (skip si déjà
chiffré) ; `Crypto::decrypt()` renvoie `null` en cas d'échec d'auth.

**Migration** : `Database::migrateEncryptSecrets()` — exécutée à chaque
bootstrap, parcourt les 4 emplacements et chiffre les valeurs en clair
restantes. Sûr à exécuter en boucle (idempotent).

**Modèle de menace couvert** : vol du seul fichier `database.sqlite`
(backup volé, sauvegarde mal protégée) → secrets illisibles sans
`master.key`. **Modèle non couvert** : compromission complète du
filesystem → l'attaquant a les deux fichiers et peut tout déchiffrer.
Pour aller plus loin il faudrait un HSM ou un KMS externe.

> ⚠ **Backup** : `master.key` doit être sauvegardé en même temps que
> `database.sqlite`. Le perdre rend les passwords irrécupérables.

### Tables CMDB côté Oracle

Le module CMDB ne crée **aucune table SQLite** ; il provisionne les
tables `CNP$*` dans la base Oracle CMDB (cf. `_cmdb_schema.php`,
hook `on_enable`). Le test de connexion + création est bloquant à
l'activation : pas de connexion ⇒ pas d'activation.

---

## Activation des modules

Page `mon-appli/admin/modules.php`. Chaque carte propose **Activer** /
**Désactiver** (CSRF). Logique :

1. **Activation** :
   - vérifie `depends_on` (modules listés doivent être actifs) ;
   - exécute `on_enable($db, &$err)` si déclaré — toute exception ou
     retour `false` annule l'activation et redirige vers `config_url` ;
   - `Module::setActive(name, true)` si tout passe.
2. **Désactivation** :
   - refus si un autre module actif a `depends_on` qui inclut celui-ci ;
   - flash *warning* listant le nombre d'utilisateurs ayant encore un
     rôle `<name>_admin` (les rôles ne sont **pas** révoqués
     automatiquement — la désactivation ne fait que cacher le module).

> Cas CMDB — l'activation passe par `mon-appli/modules/CMDB/admin.php`
> avec l'action `save_and_activate` : la page admin sauvegarde la config
> formulaire **puis** déclenche le hook ; en cas d'échec elle affiche
> l'erreur sans activer. C'est le seul module qui contourne le bouton
> *Activer* de la grille (la connexion CMDB doit être renseignée avant).

---

## Modules

La documentation technique de chaque module est dans son repertoire :

- **Module Oracle** : [modules/oracle/docs/architecture-technique.md](../mon-appli/modules/oracle/docs/architecture-technique.md)
- **Module CMDB** : [modules/CMDB/docs/architecture-technique.md](../mon-appli/modules/CMDB/docs/architecture-technique.md)
- **Module CNP** : [modules/CNP/docs/architecture-technique.md](../mon-appli/modules/CNP/docs/architecture-technique.md)

---
## Internationalisation (i18n)

Classe `I18n` (singleton). Fichiers de traduction : `mon-appli/i18n/{lang}.php`,
retournent un tableau PHP `['clé' => 'traduction']`.

Fonction globale `t('clé')` — retourne la traduction ou la clé si absente.

Langues supportées (constante `LANGUAGES`) : `fr`, `en`, `de`, `es`, `it`, `pl`, `ro`.

Résolution de langue (par priorité décroissante) :
1. Préférence enregistrée de l'utilisateur connecté.
2. Valeur en session (commutation en cours de navigation).
3. Langue par défaut (`DEFAULT_LANGUAGE` = `en`).

Le changement de langue via `?lang=xx` met à jour la session et le profil
utilisateur, puis redirige sans le paramètre GET pour éviter une double application.

---

## Thèmes CSS

Quatre thèmes : `os` (système), `light`, `dark`, `colorblind`.
Chaque thème est une feuille CSS séparée `assets/css/themes/{theme}.css`
chargée dynamiquement. La feuille `main.css` contient les styles communs.

Prévisualisation au survol des liens de thème : `main.js` modifie
`data-theme` et le `href` de `#theme-css` à la volée.

Cache busting : paramètre `?v=N` sur `main.css`.


---

## Déploiement

Deux instances de l'application tournent en parallèle :

| Instance     | Port  | Chemin                                      |
|--------------|-------|---------------------------------------------|
| Développement| 8180  | `C:\Users\jolyo\sw\sw\mon-appli`            |
| Production   | 8081  | `C:\Users\jolyo\ppowa-64S\my-app\mon-appli` |

Workflow recommandé : **single-source-of-truth via git**. On édite dans
le clone de dev, on `commit`/`push`, puis le clone Apache fait
`git pull --recurse-submodules --ff-only`. Ne pas éditer directement les
fichiers servis par Apache (cf. README §« Modèle deux instances »).
Les deux instances partagent la même base de code mais ont des bases
SQLite distinctes.

---

## Journalisation

Trois niveaux, activables indépendamment via les settings :

| Niveau  | Fichier          | Contenu                                   |
|---------|------------------|-------------------------------------------|
| app     | `log/app.log`    | Erreurs applicatives                      |
| debug   | `log/debug.log`  | Informations de débogage                  |
| trace   | `log/trace.log`  | Requêtes HTTP, requêtes SQL, appels OCI8  |

Les modes debug/trace peuvent être activés par l'administrateur dans
`/admin/settings.php` sans redéploiement.
