From ce95a02012fade36b1ad26b9874e0ab434982178 Mon Sep 17 00:00:00 2001 From: Etienne Delvarre Date: Thu, 28 May 2026 13:39:47 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20TdB=20RdB=20fris?= =?UTF-8?q?e=20narrative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Express.js + NocoDB + SortableJS drag-and-drop Design V2 (Playfair Display + Lora, palette chaude) Auth cookie, proxy NocoDB, édition en modal Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 7 + package.json | 13 ++ public/index.html | 454 ++++++++++++++++++++++++++++++++++++++++++++++ public/login.html | 44 +++++ server.js | 157 ++++++++++++++++ 5 files changed, 675 insertions(+) create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 public/index.html create mode 100644 public/login.html create mode 100644 server.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0613cf9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --production +COPY . . +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..a60f212 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "tdb-rdb", + "version": "1.0.0", + "description": "Tableau de bord RdB — frise narrative collaborative", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.21.0", + "cookie-parser": "^1.4.7" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..25c73af --- /dev/null +++ b/public/index.html @@ -0,0 +1,454 @@ + + + + + +Tableau de bord — RdB + + + + + +
+ +
+
+
Frise
+
+ +
E
+
M
+
+
+
+
Chargement...
+
+
+
+ + + + + + + diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..1dbec78 --- /dev/null +++ b/public/login.html @@ -0,0 +1,44 @@ + + + + + +Tableau de bord — Connexion + + + + + + + + diff --git a/server.js b/server.js new file mode 100644 index 0000000..b541aca --- /dev/null +++ b/server.js @@ -0,0 +1,157 @@ +const express = require('express'); +const cookieParser = require('cookie-parser'); +const crypto = require('crypto'); +const path = require('path'); + +const app = express(); +app.use(express.json()); +app.use(cookieParser()); + +const PORT = process.env.PORT || 3000; +const PASSWORD = process.env.TDB_PASSWORD || 'rdb2026'; +const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'); +const NOCODB_URL = process.env.NOCODB_URL || 'https://nocodb.hub.delvarre.net'; +const NOCODB_TOKEN = process.env.NOCODB_TOKEN || ''; +const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID || ''; +const NOCODB_TABLE_ID = process.env.NOCODB_TABLE_ID || ''; + +// Auth +function makeToken(ts) { + return crypto.createHmac('sha256', SESSION_SECRET).update(`tdb-${ts}`).digest('hex'); +} + +function isAuth(req) { + const ts = req.cookies?.tdb_ts; + const tok = req.cookies?.tdb_tok; + if (!ts || !tok) return false; + if (Date.now() - Number(ts) > 7 * 24 * 3600 * 1000) return false; + return tok === makeToken(ts); +} + +// Login +app.post('/api/login', (req, res) => { + if (req.body?.password !== PASSWORD) return res.status(401).json({ error: 'Mot de passe incorrect' }); + const ts = String(Date.now()); + const tok = makeToken(ts); + const opts = { httpOnly: true, sameSite: 'lax', maxAge: 7 * 24 * 3600 * 1000, secure: false }; + res.cookie('tdb_ts', ts, opts); + res.cookie('tdb_tok', tok, opts); + res.json({ ok: true }); +}); + +// Auth middleware for API +function requireAuth(req, res, next) { + if (!isAuth(req)) return res.status(401).json({ error: 'Non authentifié' }); + next(); +} + +// Proxy NocoDB — list records +app.get('/api/records', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}?limit=200&sort=-Ordre`; + const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } }); + const data = await r.json(); + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Proxy NocoDB — create record +app.post('/api/records', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}`; + const r = await fetch(url, { + method: 'POST', + headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' }, + body: JSON.stringify(req.body), + }); + const data = await r.json(); + res.status(r.status).json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Proxy NocoDB — update record +app.patch('/api/records/:id', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`; + const r = await fetch(url, { + method: 'PATCH', + headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' }, + body: JSON.stringify(req.body), + }); + const data = await r.json(); + res.status(r.status).json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Proxy NocoDB — delete record +app.delete('/api/records/:id', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`; + const r = await fetch(url, { + method: 'DELETE', + headers: { 'xc-token': NOCODB_TOKEN }, + }); + res.status(r.status).json({ ok: true }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Proxy NocoDB — get column options (for dynamic selects) +app.get('/api/columns', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v2/meta/tables/${NOCODB_TABLE_ID}`; + const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } }); + const table = await r.json(); + const selectCols = {}; + for (const col of table.columns || []) { + if (col.uidt === 'SingleSelect' || col.uidt === 'MultiSelect') { + selectCols[col.title] = { + type: col.uidt, + options: (col.colOptions?.options || []).map(o => o.title), + }; + } + } + res.json(selectCols); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Bulk update order (for drag-and-drop) +app.post('/api/reorder', requireAuth, async (req, res) => { + try { + const updates = req.body; // [{id, Acte, Ordre}, ...] + for (const u of updates) { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${u.id}`; + await fetch(url, { + method: 'PATCH', + headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' }, + body: JSON.stringify({ Acte: u.Acte, Ordre: u.Ordre }), + }); + } + res.json({ ok: true }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Static files +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA fallback +app.get('*', (req, res) => { + if (isAuth(req)) { + res.sendFile(path.join(__dirname, 'public', 'index.html')); + } else { + res.sendFile(path.join(__dirname, 'public', 'login.html')); + } +}); + +app.listen(PORT, () => console.log(`TdB RdB — port ${PORT}`));