summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorertopogo <erwin.t.pombett@gmail.com>2026-03-13 00:33:28 +0100
committerertopogo <erwin.t.pombett@gmail.com>2026-03-13 00:33:28 +0100
commitb34873f98052ac5fb4bf6731a25730075796d764 (patch)
tree0b27ef2996894287aaf382b43956d6cf45352e94
Initial commit medias platformHEADmain
-rw-r--r--.env.photoprism-secure.example25
-rw-r--r--DECISIONS.md16
-rw-r--r--INTEGRATIONS.md15
-rw-r--r--MEDIA_ACCESS_API.md78
-rw-r--r--PROJET_CONTEXT.md23
-rw-r--r--VIEWER_BFF.md27
-rw-r--r--WINDOWS_INGESTION.md46
-rw-r--r--compose.photoprism-secure.dev.yml88
-rw-r--r--docs/CADDY_ARAUCARIA.md80
-rw-r--r--docs/CHANGELOG_OPERATIONS.md36
-rw-r--r--docs/CONFIGURATION.md185
-rw-r--r--docs/DEPLOIEMENT_KONENPAN.md158
-rw-r--r--docs/DNSMASQ_MEDIAS.md48
-rw-r--r--docs/INSTALLATION.md106
-rw-r--r--docs/TEMPLATE_INCIDENT.md50
-rw-r--r--docs/TROUBLESHOOTING.md140
-rw-r--r--docs/Untitled1
-rw-r--r--docs/VM_KONENPAN_CREATION.md391
-rw-r--r--sync_windows_to_minio.ps166
-rw-r--r--viewer-bff/.dockerignore2
-rw-r--r--viewer-bff/Dockerfile13
-rw-r--r--viewer-bff/package.json16
-rw-r--r--viewer-bff/public/app.js122
-rw-r--r--viewer-bff/public/index.html43
-rw-r--r--viewer-bff/public/styles.css93
-rw-r--r--viewer-bff/server.js115
26 files changed, 1983 insertions, 0 deletions
diff --git a/.env.photoprism-secure.example b/.env.photoprism-secure.example
new file mode 100644
index 0000000..0d87242
--- /dev/null
+++ b/.env.photoprism-secure.example
@@ -0,0 +1,25 @@
+# IAM (Keycloak externe)
+OIDC_ISSUER=https://kc.arauco.online/realms/chiruca
+OIDC_AUDIENCE=media-access-api
+OIDC_JWKS_URL=https://kc.arauco.online/realms/chiruca/protocol/openid-connect/certs
+OIDC_CLIENT_ID=media-access-api
+OIDC_CLIENT_SECRET=CHANGE_ME_OIDC_CLIENT_SECRET
+
+# RBAC cumulatif
+RBAC_ROLE_PREFIX=media_reader:folder:
+RBAC_ROLE_ALL=media_reader:all
+
+# MinIO
+MINIO_ROOT_USER=minio
+MINIO_ROOT_PASSWORD=CHANGE_ME_MINIO_ROOT_PASSWORD
+S3_BUCKET=medias-private
+S3_REGION=us-east-1
+
+# API d'acces media
+MEDIA_ACCESS_API_IMAGE=ghcr.io/your-org/media-access-api:latest
+PRESIGN_TTL_SECONDS=120
+
+# Viewer BFF
+VIEWER_BFF_PORT=8082
+MEDIA_API_BASE_URL=http://media-access-api:8081
+CORS_ALLOWED_ORIGIN=https://photos.arauco.online
diff --git a/DECISIONS.md b/DECISIONS.md
new file mode 100644
index 0000000..100aa2e
--- /dev/null
+++ b/DECISIONS.md
@@ -0,0 +1,16 @@
+# DECISIONS
+## LDEC-0001
+- Date: 2026-03-07
+- Statut: active
+- Contexte: Les changements d'installation, de configuration et de depannage ne sont pas traces de maniere systematique, ce qui augmente les risques d'erreurs et rallonge le support.
+- Decision: Rendre obligatoire la documentation operationnelle pour toute action d'installation, de configuration ou de troubleshooting. La definition de termine inclut obligatoirement la mise a jour de la documentation associee.
+- Impact: Amelioration de la reproductibilite, baisse du risque operationnel, meilleure autonomie de l'equipe et auditabilite des interventions.
+- Promotion candidate: non
+
+## LDEC-0002
+- Date: 2026-03-08
+- Statut: active
+- Contexte: Le projet doit permettre une visualisation web avec controle d'acces fin par roles Keycloak cumulables, sans exposition directe des objets MinIO.
+- Decision: Imposer une couche `media-access-api` et un `viewer-bff` entre client web et MinIO pour valider les JWT OIDC, evaluer les ACL role->prefixe et delivrer uniquement des URLs pre-signees a TTL court.
+- Impact: Reduction du risque de contournement par URL directe, meilleure tracabilite des acces, architecture plus robuste pour la segregation des droits par dossier.
+- Promotion candidate: non \ No newline at end of file
diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md
new file mode 100644
index 0000000..46bd9a0
--- /dev/null
+++ b/INTEGRATIONS.md
@@ -0,0 +1,15 @@
+# INTEGRATIONS
+## Integrations internes
+- Service/Projet: Plateforme Medias (stockage, acces API, visualisation)
+- Type d'echange: Autorisation JWT/OIDC, controles ACL, URLs pre-signees, metadonnees techniques
+- Contrat: Toute integration interne doit reference sa documentation d'installation, de configuration et de troubleshooting
+- Flux interne cible:
+ - Client web -> Caddy (araucaria) -> viewer-bff (Node/Express)
+ - viewer-bff -> media-access-api (verification droits)
+ - media-access-api -> MinIO (lecture objet autorise via URL pre-signee)
+## Integrations externes
+- Service: Keycloak externe (`kc.arauco.online`), Caddy (edge proxy), services clients consommateurs de medias
+- Auth: OIDC/OAuth2, JWT signe
+- Donnees echangees: Claims d'identite/roles/groupes, decision ACL, liens pre-signes a duree limitee, journaux d'acces
+- Criticite: Elevee (securite et disponibilite)
+- Exigence documentaire: Chaque integration externe doit disposer d'un runbook d'installation, de configuration et de depannage maintenu a jour \ No newline at end of file
diff --git a/MEDIA_ACCESS_API.md b/MEDIA_ACCESS_API.md
new file mode 100644
index 0000000..ec5598e
--- /dev/null
+++ b/MEDIA_ACCESS_API.md
@@ -0,0 +1,78 @@
+# MEDIA_ACCESS_API
+
+## Objectif
+Definir une couche d'autorisation entre viewer-bff et MinIO afin d'appliquer les ACL Keycloak avant chaque lecture media.
+
+## Principes de securite
+- Deny-by-default.
+- Validation JWT obligatoire (issuer, audience, signature, expiration).
+- Evaluation des roles cumulables pour obtenir les prefixes MinIO autorises.
+- Jamais d'acces public direct au bucket prive.
+- Utilisation d'URLs pre-signees courtes (TTL court) pour la lecture.
+
+## Variables d'environnement minimales
+- `OIDC_ISSUER=https://kc.arauco.online/realms/chiruca`
+- `OIDC_AUDIENCE=media-access-api`
+- `OIDC_JWKS_URL=https://kc.arauco.online/realms/chiruca/protocol/openid-connect/certs`
+- `RBAC_ROLE_PREFIX=media_reader:folder:`
+- `RBAC_ROLE_ALL=media_reader:all`
+- `S3_ENDPOINT=http://minio:9000`
+- `S3_BUCKET=medias-private`
+- `S3_FORCE_PATH_STYLE=true`
+- `PRESIGN_TTL_SECONDS=120`
+
+## Contrat API propose
+
+### `GET /health`
+- Reponse: `200` si service operationnel.
+
+### `GET /v1/permissions`
+- Auth: `Bearer <jwt>`
+- Reponse `200`:
+```json
+{
+ "subject": "user-id",
+ "allowAll": false,
+ "allowedPrefixes": [
+ "photos/equipeA/",
+ "photos/projetX/"
+ ]
+}
+```
+
+### `POST /v1/presign`
+- Auth: `Bearer <jwt>`
+- Payload:
+```json
+{
+ "objectKey": "photos/equipeA/2026/03/image-001.jpg"
+}
+```
+- Reponse `200`:
+```json
+{
+ "url": "https://...signature...",
+ "expiresIn": 120
+}
+```
+- Reponses d'erreur:
+ - `401`: token invalide/absent.
+ - `403`: role insuffisant ou `objectKey` hors prefixe autorise.
+ - `404`: objet introuvable.
+
+## Algorithme d'autorisation (reference)
+1. Extraire les roles client depuis le token (`resource_access.media-access-api.roles`).
+2. Si `RBAC_ROLE_ALL` present, autoriser.
+3. Sinon, filtrer les roles commencant par `RBAC_ROLE_PREFIX`.
+4. Convertir chaque role en prefixe MinIO (ex: `media_reader:folder:photos/equipeA` -> `photos/equipeA/`).
+5. Autoriser uniquement si `objectKey` commence par un prefixe calcule.
+
+## Integration viewer-bff
+- Le viewer-bff conserve la navigation et la recherche cote interface web.
+- Les originaux proteges sont servis via `media-access-api` (URL pre-signee).
+- Le bucket MinIO reste prive (pas de policy publique de lecture).
+
+## Journalisation et audit
+- Logger `subject`, `objectKey`, decision (`allow`/`deny`), raison, `requestId`.
+- Ne jamais logger un token complet ni des secrets.
+- Exporter des metriques de refus ACL pour detection d'erreurs de mapping.
diff --git a/PROJET_CONTEXT.md b/PROJET_CONTEXT.md
new file mode 100644
index 0000000..1b1638c
--- /dev/null
+++ b/PROJET_CONTEXT.md
@@ -0,0 +1,23 @@
+# PROJECT_CONTEXT
+## Type de projet
+- Domaine: infra
+- Statut: actif
+## Références globales
+- E:/Dev/System/Araucaria/00_DOCUMENTATION/README.md
+- E:/Dev/System/Araucaria/00_DOCUMENTATION/doc-regles/00_START_HERE.md
+- E:/Dev/System/Pachamama-schemas (si infra/réseau)
+- https://pm.arauco.online/
+## Règles d’application
+- keep: appliquer
+- adapt: appliquer avec justification
+- skip: ignorer sauf besoin explicite
+- doc-obligatoire: toute installation, configuration ou action de troubleshooting doit etre documentee (creation ou mise a jour) avant cloture de la tache
+- definition-of-done: la tache est cloturee uniquement si la checklist documentaire de docs/CHANGELOG_OPERATIONS.md est complete
+## Contexte local
+- Objectif du projet: Mettre en place une plateforme open source de gestion et de controle d'acces aux medias via Keycloak.
+- Périmètre: Deploiement self-hosted de Keycloak, stockage medias, services applicatifs et documentation operationnelle associee.
+- Contraintes:
+ - Securite prioritaire (controle d'acces fin, moindre privilege, audit des acces)
+ - Open source uniquement
+ - Documentation obligatoire pour installation, configuration et troubleshooting
+ - Aucune tache n'est terminee sans preuve de mise a jour documentaire \ No newline at end of file
diff --git a/VIEWER_BFF.md b/VIEWER_BFF.md
new file mode 100644
index 0000000..b5b975e
--- /dev/null
+++ b/VIEWER_BFF.md
@@ -0,0 +1,27 @@
+# VIEWER_BFF
+
+## Objectif
+Fournir une interface web minimale de consultation des medias en s'appuyant sur `media-access-api` pour appliquer les droits Keycloak.
+
+## Endpoints backend
+- `GET /health`: etat du service.
+- `GET /api/me/permissions`: proxy vers `media-access-api /v1/permissions`.
+- `POST /api/media/presign`: proxy vers `media-access-api /v1/presign`.
+
+## Variables
+- `PORT`: port d'ecoute du BFF (defaut `8082`).
+- `MEDIA_API_BASE_URL`: URL interne de `media-access-api`.
+- `CORS_ALLOWED_ORIGIN`: origine frontend autorisee.
+
+## UI POC
+- Saisie d'un token utilisateur.
+- Chargement des permissions effectives.
+- Saisie de cles objets MinIO.
+- Chargement de previsualisations via URLs pre-signees.
+
+## Lancement local
+```bash
+cd viewer-bff
+npm install
+npm start
+```
diff --git a/WINDOWS_INGESTION.md b/WINDOWS_INGESTION.md
new file mode 100644
index 0000000..324984d
--- /dev/null
+++ b/WINDOWS_INGESTION.md
@@ -0,0 +1,46 @@
+# WINDOWS_INGESTION
+
+## Objectif
+Synchroniser des dossiers photos Windows vers MinIO avec une structure de prefixes compatible ACL par roles.
+
+## Convention de structure
+- Source Windows:
+ - `D:\Photos\EquipeA\...`
+ - `D:\Photos\ProjetX\...`
+- Destination MinIO:
+ - `medias-private/photos/EquipeA/...`
+ - `medias-private/photos/ProjetX/...`
+
+Les noms de dossiers deviennent les prefixes de securite (ex: role `media_reader:folder:photos/EquipeA`).
+
+## Prerequis
+- AWS CLI installe sur Windows.
+- Connectivite vers MinIO (`http://<vm>:9000`).
+- Clefs MinIO (`MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD`) ou un compte S3 dedie.
+
+## Commande de sync initiale
+```powershell
+powershell -ExecutionPolicy Bypass -File .\sync_windows_to_minio.ps1 `
+ -SourceRoot "D:\Photos" `
+ -EndpointUrl "http://192.168.99.23:9000" `
+ -Bucket "medias-private" `
+ -AccessKey "minio" `
+ -SecretKey "CHANGE_ME"
+```
+
+## Mode simulation
+```powershell
+powershell -ExecutionPolicy Bypass -File .\sync_windows_to_minio.ps1 `
+ -SourceRoot "D:\Photos" `
+ -EndpointUrl "http://192.168.99.23:9000" `
+ -Bucket "medias-private" `
+ -AccessKey "minio" `
+ -SecretKey "CHANGE_ME" `
+ -WhatIf
+```
+
+## Bonnes pratiques
+- Ne pas utiliser le compte root MinIO en production; creer un compte de sync dedie.
+- Figer une convention de nommage des dossiers avant mise en prod.
+- Eviter les renommages massifs de prefixes pour limiter les remappings de roles.
+- Planifier une sync periodique (Task Scheduler) et journaliser les erreurs.
diff --git a/compose.photoprism-secure.dev.yml b/compose.photoprism-secure.dev.yml
new file mode 100644
index 0000000..6cdfc9a
--- /dev/null
+++ b/compose.photoprism-secure.dev.yml
@@ -0,0 +1,88 @@
+services:
+ minio:
+ image: minio/minio:latest
+ container_name: medias-minio
+ command: server /data --console-address ":9001"
+ environment:
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER}
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ volumes:
+ - minio_data:/data
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ networks:
+ - medias_net
+
+ minio-init:
+ image: minio/mc:latest
+ container_name: medias-minio-init
+ depends_on:
+ minio:
+ condition: service_healthy
+ entrypoint: >
+ /bin/sh -c "
+ mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
+ mc mb -p local/${S3_BUCKET} || true &&
+ mc anonymous set none local/${S3_BUCKET} || true
+ "
+ restart: "no"
+ networks:
+ - medias_net
+
+ media-access-api:
+ image: ${MEDIA_ACCESS_API_IMAGE}
+ container_name: medias-access-api
+ environment:
+ OIDC_ISSUER: ${OIDC_ISSUER}
+ OIDC_AUDIENCE: ${OIDC_AUDIENCE}
+ OIDC_JWKS_URL: ${OIDC_JWKS_URL}
+ OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
+ OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
+ RBAC_ROLE_PREFIX: ${RBAC_ROLE_PREFIX}
+ RBAC_ROLE_ALL: ${RBAC_ROLE_ALL}
+ S3_ENDPOINT: http://minio:9000
+ S3_BUCKET: ${S3_BUCKET}
+ S3_REGION: ${S3_REGION}
+ S3_ACCESS_KEY: ${MINIO_ROOT_USER}
+ S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
+ S3_FORCE_PATH_STYLE: "true"
+ PRESIGN_TTL_SECONDS: ${PRESIGN_TTL_SECONDS}
+ PORT: "8081"
+ depends_on:
+ minio:
+ condition: service_healthy
+ minio-init:
+ condition: service_completed_successfully
+ ports:
+ - "8081:8081"
+ networks:
+ - medias_net
+
+ viewer-bff:
+ build:
+ context: ./viewer-bff
+ dockerfile: Dockerfile
+ container_name: medias-viewer-bff
+ depends_on:
+ - media-access-api
+ environment:
+ PORT: "8082"
+ MEDIA_API_BASE_URL: ${MEDIA_API_BASE_URL}
+ CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN}
+ ports:
+ - "${VIEWER_BFF_PORT:-8082}:8082"
+ networks:
+ - medias_net
+
+networks:
+ medias_net:
+ name: medias_net
+
+volumes:
+ minio_data:
diff --git a/docs/CADDY_ARAUCARIA.md b/docs/CADDY_ARAUCARIA.md
new file mode 100644
index 0000000..975c85b
--- /dev/null
+++ b/docs/CADDY_ARAUCARIA.md
@@ -0,0 +1,80 @@
+# CADDY_ARAUCARIA
+
+## Objectif
+Publier la stack medias en HTTPS via Caddy sur `araucaria`, sans exposition directe des ports applicatifs vers les clients.
+
+## Prerequis
+- Caddy installe et actif sur `araucaria`.
+- `araucaria` peut joindre la VM `konenpan` sur les ports internes:
+ - viewer-bff `8082`
+ - media-access-api `8081`
+ - MinIO API `9000` (si necessaire)
+ - MinIO Console `9001` (admin uniquement)
+
+## Noms DNS recommandes
+- `photos.arauco.online`
+- `media-api.arauco.online`
+- `minio-console.arauco.online`
+- `minio.arauco.online` (optionnel, a limiter)
+
+## Caddyfile (exemple)
+```caddy
+photos.arauco.online {
+ encode zstd gzip
+ reverse_proxy 192.168.99.23:8082
+}
+
+media-api.arauco.online {
+ encode zstd gzip
+ reverse_proxy 192.168.99.23:8081
+}
+
+minio-console.arauco.online {
+ encode zstd gzip
+ reverse_proxy 192.168.99.23:9001
+}
+
+minio.arauco.online {
+ encode zstd gzip
+ reverse_proxy 192.168.99.23:9000
+}
+```
+
+## Application
+```bash
+sudo caddy validate --config /etc/caddy/Caddyfile
+sudo systemctl reload caddy
+sudo systemctl status caddy --no-pager
+```
+
+## Validation
+```bash
+curl -I https://photos.arauco.online
+curl -I https://media-api.arauco.online/health
+curl -I https://minio-console.arauco.online
+```
+
+## Checklist debug rapide
+Utiliser cette sequence en cas de "connexion a echoue" depuis le navigateur.
+
+```bash
+# DNS -> araucaria
+dig +short photos.arauco.online
+
+# TLS/HTTP sur le front Caddy
+curl -vkI https://photos.arauco.online
+
+# Etat et config Caddy
+sudo caddy validate --config /etc/caddy/Caddyfile
+sudo systemctl status caddy --no-pager
+sudo journalctl -u caddy -n 100 --no-pager
+
+# Connectivite backend depuis araucaria
+curl -I http://192.168.99.23:8082/health
+curl -I http://192.168.99.23:8081/health
+```
+
+## Recommandations securite
+- Exposer `minio-console` uniquement aux admins (ACL reseau/VPN/IP allowlist).
+- Ne pas autoriser de lecture anonyme sur le bucket prive.
+- Conserver l'enforcement ACL dans `media-access-api` (deny-by-default + URLs pre-signees).
diff --git a/docs/CHANGELOG_OPERATIONS.md b/docs/CHANGELOG_OPERATIONS.md
new file mode 100644
index 0000000..6cb0d85
--- /dev/null
+++ b/docs/CHANGELOG_OPERATIONS.md
@@ -0,0 +1,36 @@
+# CHANGELOG_OPERATIONS
+
+## Objectif
+Conserver un journal des operations techniques et de la documentation associee.
+
+## Regle de tracabilite
+Chaque action d'installation, configuration ou troubleshooting doit ajouter une entree ci-dessous avec references documentaires.
+
+## Checklist obligatoire de fin de tache
+- Implementation terminee
+- Documentation installation mise a jour (si applicable)
+- Documentation configuration mise a jour (si applicable)
+- Documentation troubleshooting mise a jour (si applicable)
+- Validation technique effectuee
+- Rollback defini ou confirme non necessaire
+- Evidences (logs/tests) referencees
+
+## Entrees
+| Date | Type (install/config/troubleshoot) | Composant | Resume | Docs mises a jour | Validation | Auteur |
+| --- | --- | --- | --- | --- | --- | --- |
+| YYYY-MM-DD | config | service-x | Exemple de changement | CONFIGURATION.md | OK | nom |
+| 2026-03-07 | install | stack medias dev | Installation initiale Keycloak + stockage + API | INSTALLATION.md | OK | equipe-plateforme |
+| 2026-03-07 | config | media-access-api | Activation OIDC + role requis media_reader | CONFIGURATION.md | OK | equipe-plateforme |
+| 2026-03-07 | troubleshoot | media-access-api | Incident 403 resolu par correction mapping groupe-role | TROUBLESHOOTING.md | OK | equipe-plateforme |
+| 2026-03-07 | install | infrastructure VM | Creation VM Konenpan avec volume de donnees 1 TB | VM_KONENPAN_CREATION.md | OK | equipe-plateforme |
+| 2026-03-07 | troubleshoot | processus incidents | Ajout du template incident standard | TEMPLATE_INCIDENT.md | OK | equipe-plateforme |
+| 2026-03-07 | config | VM konenpan | Ajout client NTP chrony pour synchronisation horaire vers araucaria | VM_KONENPAN_CREATION.md, TROUBLESHOOTING.md | OK | equipe-plateforme |
+| 2026-03-08 | config | media-access-api | Modele ACL cumulatif role->prefixe MinIO + OIDC externe chiruca | CONFIGURATION.md, MEDIA_ACCESS_API.md, INTEGRATIONS.md | OK | equipe-plateforme |
+| 2026-03-08 | install | stack photoprism securisee | Ajout compose de reference PhotoPrism+MinIO+API d'acces et exemple env | INSTALLATION.md, compose.photoprism-secure.dev.yml, .env.photoprism-secure.example | OK | equipe-plateforme |
+| 2026-03-08 | install | ingestion Windows | Script de synchronisation Windows vers MinIO avec convention de prefixes ACL | WINDOWS_INGESTION.md, sync_windows_to_minio.ps1 | OK | equipe-plateforme |
+| 2026-03-08 | config | dns local medias | Ajout standard dnsmasq (records A/wildcard) + validation et troubleshooting | docs/DNSMASQ_MEDIAS.md, INSTALLATION.md, CONFIGURATION.md, TROUBLESHOOTING.md | OK | equipe-plateforme |
+| 2026-03-08 | config | edge proxy caddy | Alignement DNS+docs sur Caddy araucaria (HTTPS public, backends konenpan) | docs/CADDY_ARAUCARIA.md, docs/DNSMASQ_MEDIAS.md, INSTALLATION.md, CONFIGURATION.md, INTEGRATIONS.md, TROUBLESHOOTING.md, .env.photoprism-secure.example | OK | equipe-plateforme |
+| 2026-03-08 | troubleshoot | acces web photos | Ajout checklist debug express DNS/Caddy/backend pour `photos.arauco.online` | docs/TROUBLESHOOTING.md, docs/CADDY_ARAUCARIA.md | OK | equipe-plateforme |
+| 2026-03-08 | install | viewer-bff | Ajout backend Node/Express + UI POC pour consultation via `media-access-api` | viewer-bff/, VIEWER_BFF.md, compose.photoprism-secure.dev.yml, .env.photoprism-secure.example, INSTALLATION.md, CONFIGURATION.md, TROUBLESHOOTING.md, INTEGRATIONS.md, CADDY_ARAUCARIA.md, DECISIONS.md, MEDIA_ACCESS_API.md | OK | equipe-plateforme |
+| 2026-03-08 | install | deploiement konenpan | Ajout runbook de transfert/release/rollback pour poser les fichiers sur la VM | docs/DEPLOIEMENT_KONENPAN.md, INSTALLATION.md | OK | equipe-plateforme |
+| 2026-03-08 | install | git relay chillka | Standardisation du flux Antel -> chillka bare repo -> konenpan avec branches `main` + `develop` | docs/DEPLOIEMENT_KONENPAN.md, INSTALLATION.md | OK | equipe-plateforme |
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
new file mode 100644
index 0000000..6d9befa
--- /dev/null
+++ b/docs/CONFIGURATION.md
@@ -0,0 +1,185 @@
+# CONFIGURATION
+
+## Objectif
+Tracer tous les parametres techniques et choix de configuration.
+
+## Perimetre
+- Composant:
+- Environnement:
+- Proprietaire technique:
+
+## Prerequis
+- Installation terminee et validee
+- Acces administrateur au composant
+- Variables d'environnement disponibles
+
+## Parametres de configuration
+| Cle | Valeur cible | Source | Sensible (oui/non) | Commentaire |
+| --- | --- | --- | --- | --- |
+| example.key | example.value | env/file | non | A adapter |
+
+## Procedure de configuration
+1. Sauvegarder l'etat courant.
+2. Appliquer les parametres.
+3. Redemarrer/recharger le composant.
+4. Controler les effets attendus.
+
+## Validation de configuration
+- Parametres appliques et persistants
+- Test fonctionnel reussi
+- Test securite reussi (roles/scopes/groupes)
+
+## Rollback configuration
+- Sauvegarde utilisee:
+- Etapes de retour arriere:
+- Verification apres retour arriere:
+
+## Logs et evidences
+- Logs controles:
+- Captures/preuves:
+- Ticket/incident lie:
+
+## Historique des modifications
+- Date:
+- Auteur:
+- Changement:
+
+## Exemple rempli: configuration OIDC Keycloak pour acces medias
+### Perimetre
+- Composant: media-access-api
+- Environnement: dev
+- Proprietaire technique: equipe plateforme
+
+### Parametres de configuration (exemple)
+| Cle | Valeur cible | Source | Sensible (oui/non) | Commentaire |
+| --- | --- | --- | --- | --- |
+| OIDC_ISSUER | https://auth.local/realms/medias | env | non | URL issuer Keycloak |
+| OIDC_AUDIENCE | media-access-api | env | non | Audience attendue |
+| OIDC_JWKS_CACHE_TTL | 300 | env | non | Cache JWKS en secondes |
+| RBAC_REQUIRED_ROLE | media_reader | env | non | Role minimal lecture |
+| OIDC_CLIENT_SECRET | *** | secret store | oui | Secret client confidentiel |
+
+### Procedure de configuration (exemple)
+1. Sauvegarder l'ancien `.env`.
+2. Mettre a jour les variables OIDC et RBAC.
+3. Redemarrer `media-access-api`.
+4. Verifier le endpoint de sante et un endpoint media protege.
+
+### Validation de configuration (exemple)
+- Token valide accepte (200).
+- Token sans role `media_reader` refuse (403).
+- Token avec mauvais `aud` refuse (401).
+
+### Rollback configuration (exemple)
+- Sauvegarde utilisee: `.env.backup.2026-03-07`
+- Etapes de retour arriere: restaurer backup puis redemarrer service.
+- Verification apres retour arriere: tests 200/401/403 conformes.
+
+## Exemple rempli: ACL cumulatives Keycloak -> MinIO -> viewer BFF
+### Perimetre
+- Composant: media-access-api (enforcement) + viewer-bff (visualisation)
+- Environnement: dev
+- Proprietaire technique: equipe plateforme
+
+### Modele de roles cumulables (source Keycloak)
+- Principe: deny-by-default, puis union des droits autorises par roles.
+- Convention de roles client (client `media-access-api`):
+ - `media_reader:all`
+ - `media_reader:folder:photos/equipeA`
+ - `media_reader:folder:photos/projetX`
+ - `media_reader:folder:photos/partage`
+- Regle d'evaluation:
+ - si `media_reader:all` est present, acces lecture global;
+ - sinon, acces limite aux prefixes declares dans les roles `media_reader:folder:*`;
+ - l'absence de role valide retourne 403.
+
+### Mapping role -> prefixe MinIO (exemple)
+| Role Keycloak | Prefixe autorise | Type d'acces |
+| --- | --- | --- |
+| media_reader:folder:photos/equipeA | photos/equipeA/ | lecture |
+| media_reader:folder:photos/projetX | photos/projetX/ | lecture |
+| media_reader:folder:photos/partage | photos/partage/ | lecture |
+| media_reader:all | photos/ | lecture globale |
+
+### Parametres de configuration (exemple)
+| Cle | Valeur cible | Source | Sensible (oui/non) | Commentaire |
+| --- | --- | --- | --- | --- |
+| OIDC_ISSUER | https://kc.arauco.online/realms/chiruca | env | non | Realm existant |
+| OIDC_AUDIENCE | media-access-api | env | non | Audience attendue |
+| OIDC_CLIENT_ID | media-access-api | env | non | Client OIDC de l'API |
+| OIDC_CLIENT_SECRET | *** | secret store | oui | Secret client confidentiel |
+| RBAC_ROLE_PREFIX | media_reader:folder: | env | non | Prefixe de parsing des roles |
+| RBAC_ROLE_ALL | media_reader:all | env | non | Role de bypass lecture globale |
+| S3_BUCKET | medias-private | env | non | Bucket prive |
+| PRESIGN_TTL_SECONDS | 120 | env | non | Duree de vie URL signee |
+
+### Procedure de configuration (exemple)
+1. Creer/mettre a jour les roles client dans Keycloak (`media-access-api`).
+2. Affecter les roles aux groupes/utilisateurs selon les dossiers autorises.
+3. Aligner les prefixes MinIO avec la convention de roles.
+4. Configurer `media-access-api` avec OIDC + S3 + TTL de signature.
+5. Redemarrer API et front (`viewer-bff`/proxy) puis valider les acces.
+
+### Validation de configuration (exemple)
+- Utilisateur avec 2 roles dossier voit la somme des 2 perimetres.
+- URL signee hors prefixe autorise refusee (403).
+- URL signee expiree refusee (403/401 selon implementation).
+
+### Rollback configuration (exemple)
+- Sauvegarde utilisee: export realm Keycloak + backup `.env`.
+- Etapes de retour arriere: restaurer roles precedents, recharger config API.
+- Verification apres retour arriere: tests ACL et expiration conformes.
+
+## Exemple rempli: configuration viewer-bff (Node/Express)
+### Perimetre
+- Composant: viewer-bff
+- Environnement: dev
+- Proprietaire technique: equipe plateforme
+
+### Parametres de configuration (exemple)
+| Cle | Valeur cible | Source | Sensible (oui/non) | Commentaire |
+| --- | --- | --- | --- | --- |
+| VIEWER_BFF_PORT | 8082 | env | non | Port HTTP du BFF |
+| MEDIA_API_BASE_URL | http://media-access-api:8081 | env | non | URL interne de l'API ACL |
+| CORS_ALLOWED_ORIGIN | https://photos.arauco.online | env | non | Origine frontend autorisee |
+
+### Procedure de configuration (exemple)
+1. Copier `.env.photoprism-secure.example` vers `.env.dev`.
+2. Renseigner `MEDIA_API_BASE_URL` et `CORS_ALLOWED_ORIGIN`.
+3. Lancer `viewer-bff` via `docker compose`.
+4. Verifier `GET /health` et les endpoints `/api/me/permissions`, `/api/media/presign`.
+
+### Validation de configuration (exemple)
+- `curl -I https://photos.arauco.online/health` retourne 200.
+- `GET /api/me/permissions` sans token retourne 401.
+- `POST /api/media/presign` avec token autorise retourne URL signee.
+
+## Exemple rempli: configuration DNS local via dnsmasq (stack medias)
+### Perimetre
+- Composant: dnsmasq (LAN)
+- Environnement: dev
+- Proprietaire technique: equipe plateforme
+
+### Parametres de configuration (exemple)
+| Cle | Valeur cible | Source | Sensible (oui/non) | Commentaire |
+| --- | --- | --- | --- | --- |
+| EDGE_PROXY_HOST | araucaria | infra | non | Point d'entree Caddy |
+| EDGE_PROXY_IP | 192.168.99.10 | infra | non | IP Caddy (exemple) |
+| BACKEND_MEDIA_VM_IP | 192.168.99.23 | infra | non | IP VM konenpan |
+| DNS_RECORD_PHOTOS | photos.arauco.online -> 192.168.99.10 | dnsmasq | non | UI viewer-bff via Caddy |
+| DNS_RECORD_MINIO | minio.arauco.online -> 192.168.99.10 | dnsmasq | non | API S3 via Caddy |
+| DNS_RECORD_MINIO_CONSOLE | minio-console.arauco.online -> 192.168.99.10 | dnsmasq | non | Console MinIO via Caddy |
+| DNS_RECORD_MEDIA_API | media-api.arauco.online -> 192.168.99.10 | dnsmasq | non | API ACL via Caddy |
+
+### Procedure de configuration (exemple)
+1. Creer `/etc/dnsmasq.d/20-medias-araucaria.conf`.
+2. Ajouter les `host-record` de `docs/DNSMASQ_MEDIAS.md` (vers IP `araucaria`).
+3. Configurer les vhosts Caddy de `docs/CADDY_ARAUCARIA.md` (vers IP `konenpan`).
+4. Tester `dnsmasq --test` et `caddy validate`.
+5. Redemarrer `dnsmasq` puis recharger Caddy.
+6. Valider la resolution DNS puis les endpoints HTTPS.
+
+### Validation de configuration (exemple)
+- `dig +short photos.arauco.online` retourne l'IP de `araucaria`.
+- `curl -I https://photos.arauco.online` retourne une reponse HTTP valide.
+- La resolution de `kc.arauco.online` n'est pas surchargee localement.
diff --git a/docs/DEPLOIEMENT_KONENPAN.md b/docs/DEPLOIEMENT_KONENPAN.md
new file mode 100644
index 0000000..4adceb6
--- /dev/null
+++ b/docs/DEPLOIEMENT_KONENPAN.md
@@ -0,0 +1,158 @@
+# DEPLOIEMENT_KONENPAN
+
+## Objectif
+Deployer les fichiers du projet sur `konenpan` de facon propre, reproductible et reversible.
+
+## Strategie recommandee (Git relay)
+- Poste dev (Antel) -> depot bare central sur `chillka`:
+ - `/var/data/git/repositories/medias.git`
+- `konenpan` deploie depuis ce depot Git (clone/pull).
+- Branches officielles:
+ - `main` (stable)
+ - `develop` (integration)
+- Aucune branche `master` exploitee.
+
+## Initialisation du depot central sur chillka
+```bash
+cd /var/data/git/repositories
+git init --bare medias.git
+
+# Forcer HEAD du depot bare sur main (pas master)
+git --git-dir=/var/data/git/repositories/medias.git symbolic-ref HEAD refs/heads/main
+```
+
+## Bootstrap des branches depuis Antel (Windows Git Bash)
+Depuis `e:/Dev/Web-Works/Medias`:
+```bash
+cd /e/Dev/Web-Works/Medias
+git init
+git checkout -b main
+git add .
+git commit -m "Initial commit medias platform"
+
+git remote add origin toshiro@192.168.99.55:/var/data/git/repositories/medias.git
+git push -u origin main
+
+# Creer la branche develop et la publier
+git checkout -b develop
+git push -u origin develop
+```
+
+Verification sur chillka:
+```bash
+git --git-dir=/var/data/git/repositories/medias.git branch -a
+git --git-dir=/var/data/git/repositories/medias.git symbolic-ref HEAD
+```
+
+Resultat attendu:
+- branches: `main`, `develop`
+- HEAD: `refs/heads/main`
+
+## Deploiement depuis konenpan (via Git)
+```bash
+mkdir -p ~/src
+cd ~/src
+git clone toshiro@192.168.99.55:/var/data/git/repositories/medias.git
+cd medias
+
+# Production/validation stable
+git checkout main
+
+# Option integration
+# git checkout develop
+```
+
+Ensuite:
+```bash
+cp .env.photoprism-secure.example .env.dev
+nano .env.dev
+docker compose --env-file .env.dev -f compose.photoprism-secure.dev.yml up -d --build
+```
+
+## Cycle de mise a jour
+Sur Antel:
+```bash
+cd /e/Dev/Web-Works/Medias
+git checkout develop
+git add .
+git commit -m "Update: <changement>"
+git push
+```
+
+Sur konenpan:
+```bash
+cd ~/src/medias
+git checkout develop
+git pull --ff-only
+docker compose --env-file .env.dev -f compose.photoprism-secure.dev.yml up -d --build
+```
+
+## Emplacement recommande
+- Code applicatif: `/opt/medias/releases/<timestamp>`
+- Lien actif: `/opt/medias/current`
+- Secrets env: `/opt/medias/shared/.env.dev` (hors release)
+- Proprietaire: utilisateur d'exploitation (ex: `toshiro`)
+
+Pourquoi:
+- rollback facile via changement du lien `current`
+- separation nette code/secrets
+- evite les edits manuels disperses dans `~/`
+
+## Preparation sur konenpan
+```bash
+sudo mkdir -p /opt/medias/releases /opt/medias/shared
+sudo chown -R toshiro:toshiro /opt/medias
+```
+
+## Transfert depuis Windows (PowerShell)
+Depuis `e:\Dev\Web-Works\Medias`:
+```powershell
+$TS = Get-Date -Format "yyyyMMdd-HHmmss"
+ssh toshiro@192.168.99.23 "mkdir -p /opt/medias/releases/$TS"
+scp -r "e:\Dev\Web-Works\Medias\*" toshiro@192.168.99.23:/opt/medias/releases/$TS/
+```
+
+Option recommandee (si `rsync` dispo) pour transferts incrementaux:
+```bash
+rsync -avz --delete \
+ --exclude ".git" \
+ --exclude ".env*" \
+ e:/Dev/Web-Works/Medias/ \
+ toshiro@192.168.99.23:/opt/medias/releases/<timestamp>/
+```
+
+## Activation de la release
+Sur `konenpan`:
+```bash
+ln -sfn /opt/medias/releases/<timestamp> /opt/medias/current
+cp /opt/medias/current/.env.photoprism-secure.example /opt/medias/shared/.env.dev
+nano /opt/medias/shared/.env.dev
+```
+
+Lancer la stack depuis `current` avec env partage:
+```bash
+cd /opt/medias/current
+docker compose --env-file /opt/medias/shared/.env.dev -f compose.photoprism-secure.dev.yml up -d --build
+docker compose --env-file /opt/medias/shared/.env.dev -f compose.photoprism-secure.dev.yml ps
+```
+
+## Validation post-deploiement
+```bash
+curl -I http://127.0.0.1:8082/health
+curl -I http://127.0.0.1:8081/health
+docker compose --env-file /opt/medias/shared/.env.dev -f compose.photoprism-secure.dev.yml logs --since=15m
+```
+
+## Rollback rapide
+```bash
+ln -sfn /opt/medias/releases/<ancien-timestamp> /opt/medias/current
+cd /opt/medias/current
+docker compose --env-file /opt/medias/shared/.env.dev -f compose.photoprism-secure.dev.yml up -d --build
+```
+
+## Bonnes pratiques
+- Ne jamais transferer de secrets depuis le poste local.
+- Versionner le code, pas `.env.dev`.
+- Garder 2-3 releases precedentes pour rollback.
+- Tracer chaque deploiement dans `docs/CHANGELOG_OPERATIONS.md`.
+- Proteger `main` (pas de push direct en production, privilegier PR depuis `develop`).
diff --git a/docs/DNSMASQ_MEDIAS.md b/docs/DNSMASQ_MEDIAS.md
new file mode 100644
index 0000000..91b5363
--- /dev/null
+++ b/docs/DNSMASQ_MEDIAS.md
@@ -0,0 +1,48 @@
+# DNSMASQ_MEDIAS
+
+## Objectif
+Publier des noms DNS stables pour la stack medias en reseau local, sans ecraser les enregistrements publics existants.
+
+## Recommandation
+- Ne pas surcharger `kc.arauco.online` en local (risque de casser l'OIDC externe).
+- Publier les noms applicatifs vers l'IP de `araucaria` (Caddy), pas directement vers `konenpan`.
+- Utiliser des noms publics de service (`*.arauco.online`) termines par Caddy en HTTPS.
+
+## Exemple de fichier dnsmasq (avec Caddy sur araucaria)
+Fichier recommande sur l'hote DNS:
+- `/etc/dnsmasq.d/20-medias-araucaria.conf`
+
+Contenu:
+```ini
+# Caddy sur araucaria (remplacer par l'IP reelle)
+host-record=photos.arauco.online,192.168.99.10
+host-record=media-api.arauco.online,192.168.99.10
+host-record=minio-console.arauco.online,192.168.99.10
+host-record=minio.arauco.online,192.168.99.10
+```
+
+## Application
+```bash
+sudo dnsmasq --test
+sudo systemctl restart dnsmasq
+sudo systemctl status dnsmasq --no-pager
+```
+
+## Validation
+```bash
+dig +short photos.arauco.online
+dig +short media-api.arauco.online
+dig +short minio-console.arauco.online
+```
+
+Resultat attendu: IP de `araucaria` (Caddy).
+
+## Mapping de ports conseille
+- `https://photos.arauco.online` -> Caddy -> `konenpan:8082` (`viewer-bff`)
+- `https://media-api.arauco.online` -> Caddy -> `konenpan:8081`
+- `https://minio-console.arauco.online` -> Caddy -> `konenpan:9001`
+- `https://minio.arauco.online` -> Caddy -> `konenpan:9000`
+
+## Notes
+- Architecture cible: DNS -> Caddy (araucaria) -> services konenpan.
+- En production, preferer des enregistrements DNS authoritatifs centralises + TLS valide.
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
new file mode 100644
index 0000000..323622d
--- /dev/null
+++ b/docs/INSTALLATION.md
@@ -0,0 +1,106 @@
+# INSTALLATION
+
+## Objectif
+Documenter une installation reproductible de la plateforme medias.
+
+## Documentation liee
+- Creation VM: `docs/VM_KONENPAN_CREATION.md`
+- Troubleshooting: `docs/TROUBLESHOOTING.md`
+- Template incident: `docs/TEMPLATE_INCIDENT.md`
+- Couche acces media: `MEDIA_ACCESS_API.md`
+- Backend viewer: `VIEWER_BFF.md`
+- Ingestion Windows: `WINDOWS_INGESTION.md`
+- DNS local stack medias: `docs/DNSMASQ_MEDIAS.md`
+- Reverse proxy HTTPS: `docs/CADDY_ARAUCARIA.md`
+- Deploiement sur VM: `docs/DEPLOIEMENT_KONENPAN.md`
+
+## Perimetre
+- Services concernes:
+- Version cible:
+- Environnement cible: dev | test | prod
+
+## Prerequis
+- Acces depots et secrets necessaires
+- Docker / Docker Compose installes
+- Certificats/TLS disponibles
+- DNS et ports valides
+
+## Variables et secrets
+- Fichier source des variables:
+- Secrets requis:
+- Regle: ne jamais committer les secrets
+
+## Procedure d'installation
+1. Preparer l'environnement.
+2. Recuperer la configuration necessaire.
+3. Demarrer les services.
+4. Initialiser les composants (DB, realm IAM, etc.).
+5. Verifier l'etat de sante.
+
+## Validation post-installation
+- Services demarres et stables
+- Authentification fonctionnelle
+- Acces medias testes
+- Logs sans erreur bloquante
+
+## Rollback
+- Conditions de rollback:
+- Etapes de rollback:
+- Verification apres rollback:
+
+## Logs et traces
+- Emplacement des logs:
+- Commandes de collecte:
+- Duree de retention:
+
+## Historique des mises a jour
+- Date:
+- Auteur:
+- Resume du changement:
+
+## Exemple rempli: stack medias avec viewer BFF (dev)
+### Perimetre
+- Services concernes: minio, media-access-api, viewer-bff
+- Version cible: keycloak externe (`kc.arauco.online`), minio latest stable, Node.js 20 LTS (viewer-bff)
+- Environnement cible: dev
+
+### Variables et secrets
+- Fichier source des variables: `.env.dev` (non versionne)
+- Secrets requis: `OIDC_CLIENT_SECRET`, `MINIO_ROOT_PASSWORD`
+
+### Procedure d'installation (exemple)
+1. Deployer les fichiers sur `konenpan` via le depot Git relay `chillka` selon `docs/DEPLOIEMENT_KONENPAN.md`.
+2. Copier `.env.photoprism-secure.example` vers `.env.dev` puis renseigner les secrets.
+3. Configurer les enregistrements DNS locaux vers `araucaria` (`docs/DNSMASQ_MEDIAS.md`).
+4. Configurer Caddy sur `araucaria` pour publier les services en HTTPS (`docs/CADDY_ARAUCARIA.md`).
+5. Verifier l'acces OIDC externe (`OIDC_ISSUER` vers `kc.arauco.online/realms/chiruca`).
+6. Demarrer la stack locale avec `compose.photoprism-secure.dev.yml`.
+7. Verifier la creation du bucket prive `medias-private` et l'absence de lecture anonyme.
+8. Publier `viewer-bff` via Caddy (`photos.arauco.online`) et router `media-api.arauco.online` vers `media-access-api`.
+9. Lancer une ingestion initiale des dossiers Windows (`WINDOWS_INGESTION.md`).
+
+Commandes de reference:
+```bash
+cp .env.photoprism-secure.example .env.dev
+docker compose --env-file .env.dev -f compose.photoprism-secure.dev.yml up -d
+docker compose --env-file .env.dev -f compose.photoprism-secure.dev.yml ps
+```
+
+### Validation post-installation (exemple)
+- Login utilisateur test via OIDC OK.
+- `GET /v1/permissions` retourne les prefixes attendus.
+- URL pre-signee valide permet lecture objet autorise.
+- URL directe MinIO sans signature refusee.
+- Utilisateur multi-roles voit la somme des dossiers autorises.
+- Acces HTTPS via Caddy valide (`photos.arauco.online`, `media-api.arauco.online`).
+- `GET https://photos.arauco.online/health` retourne `status=ok`.
+
+### Rollback (exemple)
+- Conditions: echec OIDC global, erreurs 5xx persistantes API, ACL non conformes.
+- Etapes: revenir au compose precedent, restaurer `.env` et mapping roles, redemarrer les services stables.
+- Verification apres rollback: login OIDC + lecture media dossier autorise uniquement.
+
+### Logs et traces (exemple)
+- Emplacement des logs: `docker compose logs`, logs `media-access-api`, logs `viewer-bff`.
+- Commandes de collecte: `docker compose -f compose.photoprism-secure.dev.yml logs --since=30m > logs-install-dev.txt`
+- Duree de retention: 14 jours (dev).
diff --git a/docs/TEMPLATE_INCIDENT.md b/docs/TEMPLATE_INCIDENT.md
new file mode 100644
index 0000000..fc49825
--- /dev/null
+++ b/docs/TEMPLATE_INCIDENT.md
@@ -0,0 +1,50 @@
+# TEMPLATE_INCIDENT
+
+## Resume rapide
+- Date/heure de detection:
+- Environnement: dev | test | prod
+- Service impacte:
+- Gravite: faible | moyenne | elevee | critique
+- Statut: ouvert | en cours | resolu
+
+## Symptome
+- Ce qui est observe:
+- Message d'erreur principal:
+- Portee de l'impact (utilisateurs/systemes):
+
+## Verification initiale
+- Healthcheck service:
+- Disponibilite dependances (DB, IAM, stockage):
+- Verification reseau/DNS:
+- Verification authentification/autorisation:
+
+## Diagnostic
+- Hypothese 1:
+- Hypothese 2:
+- Cause racine retenue:
+
+## Correctif applique
+- Action 1:
+- Action 2:
+- Rollback necessaire: oui | non
+- Detail rollback (si oui):
+
+## Validation
+- Test fonctionnel:
+- Test securite:
+- Resultat final: OK | NOK
+
+## Prevention
+- Action preventive 1:
+- Action preventive 2:
+- Ticket de suivi:
+
+## Pieces de preuve
+- Logs:
+- Captures:
+- Liens dashboard/monitoring:
+
+## Cloture
+- Date/heure:
+- Responsable:
+- Documents mis a jour: TROUBLESHOOTING.md | CHANGELOG_OPERATIONS.md | autre
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
new file mode 100644
index 0000000..58d72a2
--- /dev/null
+++ b/docs/TROUBLESHOOTING.md
@@ -0,0 +1,140 @@
+# TROUBLESHOOTING
+
+## Objectif
+Fournir une procedure de diagnostic et resolution rapide des incidents.
+
+## Symptomes frequents
+- Echec connexion Keycloak
+- Refus d'acces media (403)
+- Erreur token/JWT
+- Indisponibilite stockage media
+- Decalage horaire VM / erreurs de validation temporelle (token, TLS)
+
+## Checklist de diagnostic initial
+1. Identifier l'environnement impacte.
+2. Verifier l'etat des services.
+3. Consulter les logs applicatifs et IAM.
+4. Reproduire le probleme avec un cas minimal.
+
+## Checklist express: `photos.arauco.online` inaccessible
+Executer dans cet ordre pour isoler rapidement le point de blocage.
+
+```bash
+# 1) DNS: le domaine doit pointer vers l'IP de araucaria (Caddy)
+dig +short photos.arauco.online
+
+# 2) Reachability 443 depuis client
+curl -vkI https://photos.arauco.online
+
+# 3) Caddy actif et sans erreur de config sur araucaria
+sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl status caddy --no-pager
+
+# 4) Port 443 ecoute sur araucaria
+sudo ss -lntp | grep :443
+
+# 5) Backend viewer-bff reachable depuis araucaria
+curl -I http://192.168.99.23:8082/health
+```
+
+Interpretation rapide:
+- (1) vide ou mauvaise IP: corriger dnsmasq.
+- (2) timeout/refused: route/firewall/Caddy KO.
+- (3) echec validate: corriger Caddyfile.
+- (4) rien sur 443: Caddy non demarre ou bind incorrect.
+- (5) backend KO: service viewer-bff/route VM a corriger.
+
+## Matrice cause probable -> action
+| Symptom | Cause probable | Verification | Action corrective |
+| --- | --- | --- | --- |
+| 401/403 | Role/groupe manquant | Inspecter claims JWT | Corriger mapping roles/groupes |
+| Timeout | Service indisponible | Healthcheck et reseau | Redemarrer service / corriger reseau |
+| Token invalide | Mauvaise config OIDC | Verifier issuer/audience | Corriger configuration client |
+| 401 sur viewer-bff | Token absent/non transfere | Verifier header Authorization | Rejouer avec Bearer token valide |
+| Heure incoherente | NTP non synchronise | `timedatectl`, `chronyc tracking` | Configurer chrony vers `araucaria` |
+| Nom DNS local non resolu | Record dnsmasq absent/invalide | `dig +short <host>.arauco.online` | Corriger fichier dnsmasq puis restart service |
+| HTTPS KO via domaine | Caddy non charge / vhost invalide | `caddy validate`, `journalctl -u caddy` | Corriger Caddyfile puis reload Caddy |
+
+## Procedure de resolution
+1. Isoler la cause racine.
+2. Appliquer la correction minimale.
+3. Valider la resolution.
+4. Documenter l'incident et la correction.
+
+## Escalade
+- Niveau 1:
+- Niveau 2:
+- Niveau 3:
+
+## Post-mortem court
+- Impact:
+- Cause racine:
+- Correctif:
+- Prevention:
+
+## Template d'incident
+- Utiliser `docs/TEMPLATE_INCIDENT.md` pour chaque incident.
+- Copier le template, le remplir, puis referencer l'entree dans `docs/CHANGELOG_OPERATIONS.md`.
+
+## Historique incidents resolus
+- Date:
+- Incident:
+- Resolution:
+
+## Cas pratique: erreur 403 sur media protege
+### Contexte
+- Date: 2026-03-07
+- Environnement: dev
+- Symptome: utilisateur authentifie, mais acces refuse sur `GET /media/{id}` (403)
+
+### Diagnostic
+1. Token JWT decode: role `media_reader` absent.
+2. Mapping Keycloak verifie: groupe `consultants` non mappe vers role applicatif.
+3. Logs API confirment: `missing required role media_reader`.
+
+### Correction appliquee
+1. Ajouter mapper de groupe dans Keycloak (`consultants` -> `media_reader`).
+2. Forcer reconnexion utilisateur pour emettre un nouveau token.
+3. Rejouer le test d'acces sur le meme media.
+
+### Validation
+- Avant correctif: 403 confirme.
+- Apres correctif: 200 confirme.
+- Aucun impact detecte sur les autres roles.
+
+### Prevention
+- Ajouter test automatique de non-regression RBAC (cas 200/403).
+- Exiger verification des mappers lors de creation de nouveau groupe.
+
+## Cas pratique: DNS local medias non resolu
+### Contexte
+- Symptome: `photos.arauco.online` ne repond pas ou resolve une mauvaise IP.
+
+### Diagnostic
+1. Verifier fichier `/etc/dnsmasq.d/20-medias-araucaria.conf`.
+2. Verifier syntaxe: `sudo dnsmasq --test`.
+3. Verifier service: `sudo systemctl status dnsmasq --no-pager`.
+4. Verifier resolution: `dig +short photos.arauco.online`.
+
+### Correction appliquee
+1. Corriger les records `host-record` vers l'IP de `araucaria` (Caddy).
+2. Redemarrer `dnsmasq`.
+3. Vider cache DNS poste client si necessaire.
+
+### Validation
+- `dig +short photos.arauco.online` retourne l'IP de `araucaria`.
+- Acces URL de service valide.
+
+## Cas pratique: domaine HTTPS indisponible via Caddy
+### Contexte
+- Symptome: `https://photos.arauco.online` indisponible alors que le service local repond.
+
+### Diagnostic
+1. Verifier Caddy: `sudo caddy validate --config /etc/caddy/Caddyfile`.
+2. Verifier etat: `sudo systemctl status caddy --no-pager`.
+3. Verifier logs: `sudo journalctl -u caddy -n 100 --no-pager`.
+4. Verifier backend: `curl -I http://192.168.99.23:8082/health`.
+
+### Correction appliquee
+1. Corriger le bloc de vhost dans Caddyfile.
+2. Recharger: `sudo systemctl reload caddy`.
+3. Rejouer le test HTTPS.
diff --git a/docs/Untitled b/docs/Untitled
new file mode 100644
index 0000000..498098d
--- /dev/null
+++ b/docs/Untitled
@@ -0,0 +1 @@
+gKmen2026 \ No newline at end of file
diff --git a/docs/VM_KONENPAN_CREATION.md b/docs/VM_KONENPAN_CREATION.md
new file mode 100644
index 0000000..6ba9f28
--- /dev/null
+++ b/docs/VM_KONENPAN_CREATION.md
@@ -0,0 +1,391 @@
+# VM_KONENPAN_CREATION
+
+## Objectif
+Documenter la creation complete de la VM `Konenpan` (de A a Z) sur infrastructure KVM/libvirt, avec un volume LVM de `1T`.
+
+## Specification demandee
+- Nom de la VM: `konenpan`
+- Taille stockage: `1T`
+- Hyperviseur: KVM/QEMU via libvirt (`virsh`, `virt-install`)
+- Usage: base d'hebergement de la plateforme medias (IAM, stockage, services applicatifs)
+
+## Parametres cibles
+- RAM: `8192` MB (8 GB, a ajuster)
+- vCPU: `4` (a ajuster)
+- Bridge reseau: `br0`
+- Groupe de volumes LVM: `vgarauco0`
+- Volume LVM: `vgarauco0-konenpan`
+- OS variant: `ubuntu24.04` (Ubuntu Server 24.04.1)
+- ISO: `/var/lib/libvirt/images/ubuntu-24.04.1-live-server-amd64.iso`
+
+## Procedure complete (A a Z)
+
+### 1) Verification des prerequis sur l'hote
+```bash
+# Verifier les commandes necessaires
+command -v virsh
+command -v virt-install
+command -v lvcreate
+
+# Installer les dependances si manquantes (commande issue du projet Araucaria)
+apt-get update && apt-get install -y libvirt-clients libvirt-daemon-system qemu-kvm virtinst
+
+# Verifier le service libvirt
+systemctl status libvirtd
+systemctl enable --now libvirtd
+```
+
+### 2) Verification de l'existant (VMs, reseau, stockage)
+```bash
+# VMs existantes
+virsh list --all
+virsh list --all --name
+
+# Reseaux/bridges
+virsh net-list --all
+brctl show
+ip link show type bridge
+
+# Stockage LVM
+vgs vgarauco0
+lvs vgarauco0 --units g -o lv_name,lv_size
+virsh domblklist --all | grep -E "/dev/mapper" || true
+```
+
+### 3) Creation du volume LVM 1T pour Konenpan
+```bash
+# Creation du LV de 1 To
+lvcreate -L 1T -n konenpan vgarauco0
+
+# Verifications
+lvs vgarauco0 | grep konenpan
+ls -lh /dev/mapper/vgarauco0-konenpan
+```
+
+### 4) Verification de l'ISO Ubuntu
+```bash
+# ISO imposee pour ce runbook
+ISO_UBUNTU="/var/lib/libvirt/images/ubuntu-24.04.1-live-server-amd64.iso"
+echo "ISO selectionnee: $ISO_UBUNTU"
+
+# Verifier acces en lecture
+test -r "$ISO_UBUNTU" && echo "ISO OK" || echo "ISO manquante/inaccessible"
+```
+
+### 5) Verifier que la VM konenpan n'existe pas deja
+```bash
+virsh list --all | grep konenpan || true
+
+# Si necessaire (attention: supprime la definition libvirt, pas le LV)
+# virsh destroy konenpan 2>/dev/null
+# virsh undefine konenpan
+```
+
+### 6) Creation de la VM (nouvelle installation depuis ISO)
+```bash
+virt-install \
+ --name konenpan \
+ --memory 8192 \
+ --vcpus 4 \
+ --os-variant ubuntu24.04 \
+ --disk path=/dev/mapper/vgarauco0-konenpan,bus=virtio \
+ --network bridge=br0,model=virtio \
+ --graphics vnc,listen=0.0.0.0 \
+ --cdrom /var/lib/libvirt/images/ubuntu-24.04.1-live-server-amd64.iso \
+ --noautoconsole
+```
+
+### 6.1) Connexion VNC pour installer le systeme
+```bash
+# Sur l'hote hyperviseur: recuperer l'ecran VNC attribue
+virsh vncdisplay konenpan
+# Exemple de sortie: :0 (port TCP 5900), :1 (5901), etc.
+
+# Option pratique: calculer automatiquement le port VNC
+VNC_DISPLAY=$(virsh vncdisplay konenpan | tr -d ':')
+VNC_PORT=$((5900 + VNC_DISPLAY))
+echo "Display :$VNC_DISPLAY -> Port $VNC_PORT"
+```
+
+```bash
+# Si vous etes connecte directement a l'hote (avec interface graphique)
+vncviewer 127.0.0.1:$VNC_PORT
+```
+
+```bash
+# Si vous etes sur un poste distant: creer un tunnel SSH
+# (a lancer depuis votre poste local)
+ssh -L 5900:127.0.0.1:$VNC_PORT <user>@<hote_hyperviseur>
+
+# Puis, depuis le poste local:
+vncviewer 127.0.0.1:5900
+```
+
+### 7) Alternative: creation en mode import (si OS deja present sur disque)
+```bash
+virt-install \
+ --name konenpan \
+ --memory 8192 \
+ --vcpus 4 \
+ --os-variant ubuntu24.04 \
+ --disk path=/dev/mapper/vgarauco0-konenpan,bus=virtio,cache=none \
+ --network bridge=br0,model=virtio \
+ --graphics vnc,listen=0.0.0.0 \
+ --import \
+ --noautoconsole
+```
+
+### 8) Verification immediate apres creation
+```bash
+virsh list --all | grep konenpan
+virsh dominfo konenpan
+virsh dumpxml konenpan
+virsh domblklist konenpan
+virsh domiflist konenpan
+```
+
+### 9) Demarrage, console et acces VNC
+```bash
+# Demarrer
+virsh start konenpan
+virsh list --all
+virsh domstate konenpan
+
+# Console texte
+virsh console konenpan
+# Quitter la console: Ctrl+]
+
+# Afficher l'ecran VNC attribue
+virsh vncdisplay konenpan
+
+# Option tunnel SSH depuis un poste local
+# ssh -L 5900:127.0.0.1:5900 araucaria
+# puis: vncviewer localhost:5900
+```
+
+### 10) Partitionnement recommande pendant l'installation (ecran VNC)
+Pour `konenpan` (1T), la meilleure approche est de separer le systeme et les donnees avec LVM:
+
+- `root (/)`: `120G` (OS + paquets)
+- `/var`: `200G` (logs, cache, services)
+- `/data`: `reste du disque` (applications/donnees medias)
+- `swap`: pas de partition dediee (utiliser un swapfile apres installation)
+
+#### Procedure dans l'ecran "Storage configuration"
+1. Selectionner `/dev/vda` puis valider pour initialiser le disque (table GPT).
+2. Choisir `Create volume group (LVM)` et creer un VG (exemple: `vg_konenpan`) sur `/dev/vda`.
+3. Creer les volumes logiques (LV):
+ - `lv_root` taille `120G`, format `ext4`, montage `/`
+ - `lv_var` taille `200G`, format `ext4`, montage `/var`
+ - `lv_data` taille `restant` (ou environ `670G`), format `ext4`, montage `/data`
+4. Verifier qu'un point de montage `/` existe (obligatoire pour continuer).
+5. Valider avec `Done`, confirmer l'ecriture des changements, puis poursuivre l'installation Ubuntu.
+
+#### Si le bouton "Done" est grise (cas frequent)
+Cause: l'installateur n'a pas de disque/partition de boot selectionne.
+
+Dans certains ecrans Subiquity, `fat32` n'apparait pas dans la liste des formats.
+Dans ce cas, suivre le mode de boot detecte:
+
+**Cas A - UEFI (recommande)**
+1. Selectionner `/dev/vda` puis `Add GPT partition` de `512M` a `1G`.
+2. Si `fat32` est disponible: format `fat32`, mount `/boot/efi`.
+3. Si `fat32` n'est pas disponible: choisir `Leave unformatted`, mount `/boot/efi` (l'installateur traitera cette partition comme ESP).
+4. Marquer `/dev/vda` comme boot device (`Use as boot device`) si l'option est proposee.
+
+**Cas B - BIOS (legacy)**
+1. Selectionner `/dev/vda` puis `Add GPT partition` de `1M`.
+2. Choisir `Leave unformatted` et affecter le role `bios_grub` (si propose par l'UI).
+3. Ne pas utiliser `/boot/efi` en mode BIOS.
+
+Puis, dans les 2 cas:
+1. Creer le LVM sur le reste du disque (`vg_konenpan` + `lv_root`, `lv_var`, `lv_data`).
+2. Verifier au minimum:
+ - un montage `/` existe
+ - un boot target existe (`/boot/efi` en UEFI, ou `bios_grub` en BIOS)
+3. Revenir sur `Done`: le bouton doit devenir actif.
+
+Remarque: si tout le disque a deja ete consomme par le PV LVM, recreer le schema en reservant d'abord la partition de boot, puis utiliser le reste pour le PV LVM.
+
+#### Cas reel: l'installation est lancee sans selection explicite "boot"
+Si l'installateur a accepte la configuration et a demarre, c'est souvent valide.
+Dans ce cas, faire une verification apres le premier redemarrage:
+
+```bash
+# Dans la VM
+test -d /sys/firmware/efi && echo "Boot UEFI" || echo "Boot BIOS"
+lsblk -f
+df -h
+```
+
+Verifier ensuite la presence de GRUB:
+
+```bash
+# Dans la VM
+sudo grub-install --version
+sudo update-grub
+```
+
+Si la VM ne boote pas apres retrait de l'ISO:
+- remettre temporairement l'ISO,
+- booter en mode rescue/live,
+- chroot sur le systeme installe,
+- reinstaller GRUB (UEFI ou BIOS selon le mode detecte).
+
+#### Option plus simple (si tu veux aller plus vite)
+- Utiliser "Use an entire disk" + LVM automatique (un seul grand `/`).
+- Cette option est rapide, mais moins pratique a long terme pour isoler logs/donnees.
+
+#### Swap (recommande en swapfile, apres installation)
+Oui, ici on ne cree pas de partition swap dediee. Sur Ubuntu Server, un swapfile est plus simple a gerer.
+
+Taille conseillee pour `konenpan` (RAM 8G): `8G` de swap.
+
+```bash
+# Dans la VM, apres installation
+sudo fallocate -l 8G /swapfile
+sudo chmod 600 /swapfile
+sudo mkswap /swapfile
+sudo swapon /swapfile
+
+# Rendre persistant
+echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
+
+# Verifier
+swapon --show
+free -h
+```
+
+### 11) Fin d'installation OS: retirer le CD-ROM puis redemarrer
+```bash
+# Identifier les disques et le lecteur CD-ROM
+virsh domblklist konenpan
+
+# Retirer le CD-ROM persistant (exemple device sda, adapter si besoin)
+virsh detach-disk konenpan sda --persistent
+
+# Verifier puis redemarrer
+virsh domblklist konenpan
+virsh reboot konenpan
+# ou:
+# virsh shutdown konenpan && virsh start konenpan
+```
+
+### 12) Configuration reseau dans la VM (netplan)
+```bash
+# Dans la VM
+ip addr show
+ls /etc/netplan/
+sudoedit /etc/netplan/00-installer-config.yaml
+
+# Appliquer
+sudo netplan try
+sudo netplan apply
+
+# Verifier DNS
+cat /etc/resolv.conf
+nslookup araucaria.local
+nslookup google.com
+```
+
+### 13) Synchronisation de l'heure vers araucaria (NTP client)
+```bash
+# Dans la VM
+sudo apt-get update
+sudo apt-get install -y chrony
+
+# Sauvegarder la configuration par defaut
+sudo cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak.$(date +%F-%H%M%S)
+
+# Declarer araucaria comme source NTP principale
+echo "server araucaria iburst prefer" | sudo tee /etc/chrony/sources.d/araucaria.sources
+
+# Redemarrer et activer le service
+sudo systemctl enable --now chrony
+sudo systemctl restart chrony
+
+# Verifier la synchronisation
+chronyc sources -v
+chronyc tracking
+timedatectl status
+```
+
+Notes:
+- Si `araucaria` n'est pas resolu en DNS, utiliser temporairement son IP dans le fichier source Chrony.
+- Quand le DNS sera finalise, revenir a `server araucaria iburst prefer`.
+
+### 14) Configuration hostname, SSH et mises a jour dans la VM
+```bash
+# Dans la VM
+sudo hostnamectl set-hostname konenpan
+hostnamectl
+
+sudo apt-get update
+sudo apt-get upgrade -y
+sudo apt-get install -y openssh-server qemu-guest-agent
+
+sudo systemctl enable --now ssh
+sudo systemctl enable --now qemu-guest-agent
+```
+
+### 15) Validation finale
+```bash
+# Sur l'hote
+virsh dominfo konenpan
+virsh domblklist konenpan
+virsh domiflist konenpan
+
+# Dans la VM
+hostnamectl
+free -h
+df -h
+ip addr show
+timedatectl status
+chronyc sources -v
+ping -c 3 8.8.8.8
+```
+
+## Checklist de fin
+- [ ] LV `vgarauco0-konenpan` cree en `1T`
+- [ ] VM `konenpan` creee avec `virt-install`
+- [ ] VM demarree (`virsh start konenpan`)
+- [ ] Partitionnement valide (`/`, `/var`, `/data`)
+- [ ] Swap actif (`swapon --show`)
+- [ ] Installation OS terminee et CD-ROM detache
+- [ ] Reseau/DNS operationnels
+- [ ] Synchronisation horaire active vers araucaria (chrony)
+- [ ] SSH operationnel
+- [ ] VM enregistree dans l'inventaire infra
+- [ ] Sauvegarde planifiee
+
+## Rollback (si echec)
+```bash
+# Arreter puis supprimer la definition VM
+virsh destroy konenpan 2>/dev/null || true
+virsh undefine konenpan
+
+# Supprimer le volume LVM (attention: destructif)
+lvremove -y /dev/vgarauco0/konenpan
+
+# Verifier qu'il ne reste rien
+virsh list --all | grep konenpan || true
+lvs vgarauco0 | grep konenpan || true
+```
+
+## Troubleshooting rapide
+- VM non accessible: `virsh dominfo konenpan`, verifier bridge `br0`, firewall, NIC
+- Disque absent: `virsh domblklist konenpan`, `lvs vgarauco0`, `ls -lh /dev/mapper/vgarauco0-konenpan`
+- VNC indisponible: `virsh vncdisplay konenpan`, verifier tunnel SSH
+- DNS KO dans la VM: verifier netplan, `resolv.conf`, `nslookup`
+- Heure non synchronisee: verifier `systemctl status chrony`, `chronyc sources -v`, resolution DNS de `araucaria`
+
+## Sources Araucaria utilisees
+- `E:\Dev\System\Araucaria\01_SKVM\docs\DOCUMENTATION_SKVM.md`
+- `E:\Dev\System\Araucaria\07_VMS\docs\GUIDE_COMPLET_VM_DOCKER.md`
+- `E:\Dev\System\Araucaria\migration_chillka.sh`
+
+## Historique
+- Date: 2026-03-07
+- Auteur: equipe-plateforme
+- Changement: ajout procedure complete de creation VM `konenpan` de A a Z (commandes completes)
+- Changement: ajout de la configuration NTP client (chrony) pour synchronisation vers `araucaria`
diff --git a/sync_windows_to_minio.ps1 b/sync_windows_to_minio.ps1
new file mode 100644
index 0000000..43e0871
--- /dev/null
+++ b/sync_windows_to_minio.ps1
@@ -0,0 +1,66 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$SourceRoot,
+
+ [Parameter(Mandatory = $true)]
+ [string]$EndpointUrl,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Bucket,
+
+ [Parameter(Mandatory = $true)]
+ [string]$AccessKey,
+
+ [Parameter(Mandatory = $true)]
+ [string]$SecretKey,
+
+ [string]$ProfileName = "minio-sync",
+ [string]$PrefixRoot = "photos",
+ [switch]$WhatIf
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+if (-not (Test-Path -Path $SourceRoot)) {
+ throw "Le chemin source '$SourceRoot' est introuvable."
+}
+
+$aws = Get-Command aws -ErrorAction SilentlyContinue
+if (-not $aws) {
+ throw "AWS CLI est requis. Installer AWS CLI puis relancer."
+}
+
+Write-Host "Configuration du profil AWS CLI '$ProfileName'..."
+aws configure set aws_access_key_id $AccessKey --profile $ProfileName | Out-Null
+aws configure set aws_secret_access_key $SecretKey --profile $ProfileName | Out-Null
+aws configure set default.region us-east-1 --profile $ProfileName | Out-Null
+
+$normalizedSource = (Resolve-Path $SourceRoot).Path
+$folders = Get-ChildItem -Path $normalizedSource -Directory
+
+if ($folders.Count -eq 0) {
+ Write-Warning "Aucun sous-dossier detecte. Synchronisation du dossier source complet."
+ $target = "s3://$Bucket/$PrefixRoot/"
+ $cmd = "aws s3 sync `"$normalizedSource`" `"$target`" --endpoint-url `"$EndpointUrl`" --profile `"$ProfileName`" --delete"
+ if ($WhatIf) {
+ Write-Host "[WhatIf] $cmd"
+ } else {
+ Invoke-Expression $cmd
+ }
+ exit 0
+}
+
+foreach ($folder in $folders) {
+ $logicalPrefix = "$PrefixRoot/$($folder.Name)/"
+ $target = "s3://$Bucket/$logicalPrefix"
+ $cmd = "aws s3 sync `"$($folder.FullName)`" `"$target`" --endpoint-url `"$EndpointUrl`" --profile `"$ProfileName`" --delete"
+ if ($WhatIf) {
+ Write-Host "[WhatIf] $cmd"
+ } else {
+ Write-Host "Sync '$($folder.FullName)' -> '$target'"
+ Invoke-Expression $cmd
+ }
+}
+
+Write-Host "Synchronisation terminee."
diff --git a/viewer-bff/.dockerignore b/viewer-bff/.dockerignore
new file mode 100644
index 0000000..3abe3d4
--- /dev/null
+++ b/viewer-bff/.dockerignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
diff --git a/viewer-bff/Dockerfile b/viewer-bff/Dockerfile
new file mode 100644
index 0000000..af28598
--- /dev/null
+++ b/viewer-bff/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY package.json ./
+RUN npm install --omit=dev
+
+COPY . .
+
+ENV PORT=8082
+EXPOSE 8082
+
+CMD ["npm", "start"]
diff --git a/viewer-bff/package.json b/viewer-bff/package.json
new file mode 100644
index 0000000..489d044
--- /dev/null
+++ b/viewer-bff/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "viewer-bff",
+ "version": "0.1.0",
+ "private": true,
+ "description": "BFF Node/Express pour viewer medias securise",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "dependencies": {
+ "express": "^4.21.2"
+ }
+}
diff --git a/viewer-bff/public/app.js b/viewer-bff/public/app.js
new file mode 100644
index 0000000..ecbe3fb
--- /dev/null
+++ b/viewer-bff/public/app.js
@@ -0,0 +1,122 @@
+const tokenInput = document.getElementById("tokenInput");
+const objectKeysInput = document.getElementById("objectKeysInput");
+const permissionsOutput = document.getElementById("permissionsOutput");
+const gallery = document.getElementById("gallery");
+
+const loadPermissionsBtn = document.getElementById("loadPermissionsBtn");
+const buildGalleryBtn = document.getElementById("buildGalleryBtn");
+
+let currentPermissions = null;
+
+function getToken() {
+ return tokenInput.value.trim();
+}
+
+function parseObjectKeys() {
+ return objectKeysInput.value
+ .split("\n")
+ .map((line) => line.trim())
+ .filter(Boolean);
+}
+
+function isAllowedByPermissions(objectKey, permissions) {
+ if (!permissions) return false;
+ if (permissions.allowAll) return true;
+ const prefixes = permissions.allowedPrefixes || [];
+ return prefixes.some((prefix) => objectKey.startsWith(prefix));
+}
+
+async function callJson(url, options = {}) {
+ const token = getToken();
+ const headers = {
+ "Content-Type": "application/json",
+ ...(options.headers || {})
+ };
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+
+ const response = await fetch(url, { ...options, headers });
+ const payload = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(payload.message || `HTTP ${response.status}`);
+ }
+ return payload;
+}
+
+loadPermissionsBtn.addEventListener("click", async () => {
+ try {
+ const perms = await callJson("/api/me/permissions");
+ currentPermissions = perms;
+ permissionsOutput.textContent = JSON.stringify(perms, null, 2);
+ } catch (error) {
+ permissionsOutput.textContent = `Erreur: ${error.message}`;
+ }
+});
+
+buildGalleryBtn.addEventListener("click", async () => {
+ gallery.innerHTML = "";
+ const keys = parseObjectKeys();
+
+ if (!currentPermissions) {
+ permissionsOutput.textContent = "Charger d'abord les permissions.";
+ return;
+ }
+
+ if (!keys.length) {
+ permissionsOutput.textContent = "Ajouter au moins une objectKey.";
+ return;
+ }
+
+ for (const objectKey of keys) {
+ const card = document.createElement("article");
+ card.className = "card";
+
+ const img = document.createElement("img");
+ img.className = "thumb";
+ img.alt = objectKey;
+
+ const keyP = document.createElement("p");
+ keyP.className = "key";
+ keyP.textContent = objectKey;
+
+ const openBtn = document.createElement("button");
+ openBtn.textContent = "Ouvrir";
+ openBtn.disabled = !isAllowedByPermissions(objectKey, currentPermissions);
+
+ openBtn.addEventListener("click", async () => {
+ try {
+ const presign = await callJson("/api/media/presign", {
+ method: "POST",
+ body: JSON.stringify({ objectKey })
+ });
+ const signedUrl = presign.url;
+ img.src = signedUrl;
+ window.open(signedUrl, "_blank", "noopener,noreferrer");
+ } catch (error) {
+ alert(`Presign refuse: ${error.message}`);
+ }
+ });
+
+ if (!openBtn.disabled) {
+ // Previsualisation opportuniste pour les objets autorises.
+ callJson("/api/media/presign", {
+ method: "POST",
+ body: JSON.stringify({ objectKey })
+ })
+ .then((presign) => {
+ img.src = presign.url;
+ })
+ .catch(() => {
+ img.alt = "Previsualisation indisponible";
+ });
+ } else {
+ img.alt = "Acces refuse (roles)";
+ }
+
+ card.appendChild(img);
+ card.appendChild(keyP);
+ card.appendChild(openBtn);
+ gallery.appendChild(card);
+ }
+});
diff --git a/viewer-bff/public/index.html b/viewer-bff/public/index.html
new file mode 100644
index 0000000..8112bfa
--- /dev/null
+++ b/viewer-bff/public/index.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<html lang="fr">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Viewer Medias (POC)</title>
+ <link rel="stylesheet" href="/styles.css" />
+ </head>
+ <body>
+ <main class="container">
+ <h1>Viewer medias securise (POC)</h1>
+
+ <section class="panel">
+ <h2>1) Token utilisateur</h2>
+ <textarea
+ id="tokenInput"
+ placeholder="Coller ici le JWT (Bearer token)"
+ rows="5"
+ ></textarea>
+ <button id="loadPermissionsBtn">Charger permissions</button>
+ <pre id="permissionsOutput">Aucune permission chargee.</pre>
+ </section>
+
+ <section class="panel">
+ <h2>2) Cles objet a visualiser</h2>
+ <p class="hint">
+ Une cle par ligne, exemple: <code>photos/equipeA/2026/03/img001.jpg</code>
+ </p>
+ <textarea id="objectKeysInput" rows="8"></textarea>
+ <div class="actions">
+ <button id="buildGalleryBtn">Construire la galerie</button>
+ </div>
+ </section>
+
+ <section class="panel">
+ <h2>3) Galerie</h2>
+ <div id="gallery" class="gallery"></div>
+ </section>
+ </main>
+
+ <script src="/app.js"></script>
+ </body>
+</html>
diff --git a/viewer-bff/public/styles.css b/viewer-bff/public/styles.css
new file mode 100644
index 0000000..5417318
--- /dev/null
+++ b/viewer-bff/public/styles.css
@@ -0,0 +1,93 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: Arial, sans-serif;
+ background: #0f172a;
+ color: #e2e8f0;
+}
+
+.container {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 24px;
+}
+
+h1 {
+ margin: 0 0 20px;
+}
+
+.panel {
+ background: #111827;
+ border: 1px solid #334155;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+textarea {
+ width: 100%;
+ background: #0b1220;
+ color: #e2e8f0;
+ border: 1px solid #334155;
+ border-radius: 6px;
+ padding: 10px;
+}
+
+button {
+ margin-top: 10px;
+ background: #2563eb;
+ border: none;
+ color: white;
+ border-radius: 6px;
+ padding: 8px 12px;
+ cursor: pointer;
+}
+
+button:hover {
+ background: #1d4ed8;
+}
+
+.hint {
+ margin-top: 0;
+ color: #94a3b8;
+}
+
+#permissionsOutput {
+ white-space: pre-wrap;
+ background: #0b1220;
+ border: 1px solid #334155;
+ border-radius: 6px;
+ padding: 10px;
+ min-height: 50px;
+}
+
+.gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 14px;
+}
+
+.card {
+ background: #0b1220;
+ border: 1px solid #334155;
+ border-radius: 8px;
+ padding: 10px;
+}
+
+.thumb {
+ width: 100%;
+ height: 140px;
+ object-fit: cover;
+ border-radius: 6px;
+ background: #1e293b;
+}
+
+.key {
+ margin: 8px 0;
+ font-size: 12px;
+ word-break: break-all;
+ color: #cbd5e1;
+}
diff --git a/viewer-bff/server.js b/viewer-bff/server.js
new file mode 100644
index 0000000..30ef81a
--- /dev/null
+++ b/viewer-bff/server.js
@@ -0,0 +1,115 @@
+const express = require("express");
+const path = require("path");
+
+const app = express();
+
+const PORT = process.env.PORT || 8082;
+const MEDIA_API_BASE_URL =
+ process.env.MEDIA_API_BASE_URL || "http://media-access-api:8081";
+const CORS_ALLOWED_ORIGIN = process.env.CORS_ALLOWED_ORIGIN || "*";
+
+app.use(express.json({ limit: "1mb" }));
+
+app.use((req, res, next) => {
+ res.setHeader("Access-Control-Allow-Origin", CORS_ALLOWED_ORIGIN);
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
+ res.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type");
+ if (req.method === "OPTIONS") {
+ return res.sendStatus(204);
+ }
+ return next();
+});
+
+app.use(express.static(path.join(__dirname, "public")));
+
+app.get("/health", (_req, res) => {
+ res.json({
+ status: "ok",
+ service: "viewer-bff",
+ mediaApiBaseUrl: MEDIA_API_BASE_URL
+ });
+});
+
+function getBearerToken(req) {
+ const authHeader = req.headers.authorization || "";
+ if (!authHeader.startsWith("Bearer ")) {
+ return null;
+ }
+ return authHeader.slice("Bearer ".length).trim();
+}
+
+async function proxyJson(req, res, targetPath, method = "GET", body) {
+ const token = getBearerToken(req);
+ if (!token) {
+ return res.status(401).json({
+ error: "missing_token",
+ message: "Header Authorization Bearer requis"
+ });
+ }
+
+ try {
+ const response = await fetch(`${MEDIA_API_BASE_URL}${targetPath}`, {
+ method,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ body: body ? JSON.stringify(body) : undefined
+ });
+
+ const text = await response.text();
+ let payload = {};
+ if (text) {
+ try {
+ payload = JSON.parse(text);
+ } catch (_err) {
+ payload = { raw: text };
+ }
+ }
+
+ if (!response.ok) {
+ return res.status(response.status).json({
+ error: "media_api_error",
+ status: response.status,
+ message: payload.message || "Erreur retour media-access-api",
+ details: payload
+ });
+ }
+
+ return res.status(response.status).json(payload);
+ } catch (error) {
+ return res.status(502).json({
+ error: "media_api_unreachable",
+ message: "Impossible de joindre media-access-api",
+ details: error.message
+ });
+ }
+}
+
+app.get("/api/me/permissions", async (req, res) => {
+ return proxyJson(req, res, "/v1/permissions", "GET");
+});
+
+app.post("/api/media/presign", async (req, res) => {
+ const objectKey = req.body?.objectKey;
+ if (!objectKey || typeof objectKey !== "string") {
+ return res.status(400).json({
+ error: "invalid_payload",
+ message: "Champ objectKey requis"
+ });
+ }
+
+ return proxyJson(req, res, "/v1/presign", "POST", { objectKey });
+});
+
+app.get("*", (req, res) => {
+ if (req.path.startsWith("/api/")) {
+ return res.status(404).json({ error: "not_found" });
+ }
+ return res.sendFile(path.join(__dirname, "public", "index.html"));
+});
+
+app.listen(PORT, () => {
+ // eslint-disable-next-line no-console
+ console.log(`viewer-bff listening on port ${PORT}`);
+});