From b34873f98052ac5fb4bf6731a25730075796d764 Mon Sep 17 00:00:00 2001 From: ertopogo Date: Fri, 13 Mar 2026 00:33:28 +0100 Subject: Initial commit medias platform --- viewer-bff/.dockerignore | 2 + viewer-bff/Dockerfile | 13 +++++ viewer-bff/package.json | 16 ++++++ viewer-bff/public/app.js | 122 +++++++++++++++++++++++++++++++++++++++++++ viewer-bff/public/index.html | 43 +++++++++++++++ viewer-bff/public/styles.css | 93 +++++++++++++++++++++++++++++++++ viewer-bff/server.js | 115 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 404 insertions(+) create mode 100644 viewer-bff/.dockerignore create mode 100644 viewer-bff/Dockerfile create mode 100644 viewer-bff/package.json create mode 100644 viewer-bff/public/app.js create mode 100644 viewer-bff/public/index.html create mode 100644 viewer-bff/public/styles.css create mode 100644 viewer-bff/server.js (limited to 'viewer-bff') 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 @@ + + + + + + Viewer Medias (POC) + + + +
+

Viewer medias securise (POC)

+ +
+

1) Token utilisateur

+ + +
Aucune permission chargee.
+
+ +
+

2) Cles objet a visualiser

+

+ Une cle par ligne, exemple: photos/equipeA/2026/03/img001.jpg +

+ +
+ +
+
+ +
+

3) Galerie

+ +
+
+ + + + 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}`); +}); -- cgit v1.2.3