From a3ac0551a229099a99fb177436a1ad6f351f32be Mon Sep 17 00:00:00 2001 From: autmoate Date: Mon, 27 Apr 2026 10:37:04 +0200 Subject: [PATCH 1/5] added dockerfile Made this for a docker setup to host this wonderful app on ScaDS.AI Living Lab Launchpad / Digital Living Lab. --- .dockerignore | 3 +++ Dockerfile | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a21f178 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..744d770 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json /app/ +RUN npm ci + +COPY . /app + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] From cd36bcf209949babf5dbbd40d69c9d871c047483 Mon Sep 17 00:00:00 2001 From: autmoate Date: Thu, 21 May 2026 10:26:17 +0200 Subject: [PATCH 2/5] Harden inputs and switch pond to production static serving --- Dockerfile | 10 ++++-- README.md | 10 ++++++ nginx/default.conf | 17 +++++++++ src/components/LilyCreator.vue | 64 ++++++++++++++++++++++++++++++++-- src/locales/de.ts | 6 ++++ src/locales/en.ts | 6 ++++ src/style.css | 13 +++++++ 7 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 nginx/default.conf diff --git a/Dockerfile b/Dockerfile index 744d770..619a22c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS builder WORKDIR /app @@ -6,7 +6,13 @@ COPY package.json package-lock.json /app/ RUN npm ci COPY . /app +RUN npm run build + +FROM nginx:1.27-alpine + +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 6592f74..cfa6e1a 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,19 @@ Then open the Vite dev server URL (usually `http://localhost:5173`). - Embeddings are computed client-side via `@huggingface/transformers` using `Xenova/clip-vit-base-patch32`. - PCA is computed client-side with `ml-pca`. +- Input hardening: + - text input is capped at 280 characters, + - image input only accepts `jpg/jpeg/png`, + - image file size is limited to 3 MB. - This is a prototype UI: no backend, no persistence (refresh clears lilies), no auth. - Water background uses a tileable image: `public/water_tileable.jpg`. +## Deployment Notes + +- The production Docker image builds static assets (`npm run build`) and serves them via Nginx. +- Security headers (including CSP, frame restrictions, and content type hardening) are set in `nginx/default.conf`. +- Inputs stay browser-local: uploaded images are represented as object URLs and are not sent to a backend by this app. + ## Project Structure - `src/App.vue` main state + orchestration (create/grow/plant/select) diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..0e85942 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,17 @@ +server { + listen 5173; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data:; worker-src 'self' blob:; connect-src 'self' https://huggingface.co https://cdn-lfs.huggingface.co; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/components/LilyCreator.vue b/src/components/LilyCreator.vue index 97bc7ba..a9a05e0 100644 --- a/src/components/LilyCreator.vue +++ b/src/components/LilyCreator.vue @@ -43,10 +43,14 @@ v-if="mode === 'text'" v-model="textInput" :placeholder="t('creator.placeholder')" + :maxlength="TEXT_MAX_CHARS" rows="6" > +

{{ t('creator.textLimit', { count: textLength, max: TEXT_MAX_CHARS }) }}

- + +

{{ t('creator.imageLimit', { maxSizeMb: IMAGE_MAX_SIZE_MB, formats: ALLOWED_IMAGE_EXTENSIONS_LABEL }) }}

+
@@ -117,7 +121,7 @@ diff --git a/src/locales/de.ts b/src/locales/de.ts index 8718530..8c337c2 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -23,9 +23,15 @@ export default { imagePreviewAlt: 'Vorschau', helperStart: 'Zum Start wähle', helperOr: 'oder', + textLimit: '{count}/{max} Zeichen', + imageLimit: 'Erlaubt: {formats} bis {maxSizeMb} MB', growButton: 'Wachsen lassen! 🪷', growingButton: 'Wächst ... 🪷', plantButton: 'Einpflanzen! 🪷', + errors: { + invalidImageType: 'Ungültiges Bildformat. Bitte nutze {formats}.', + imageTooLarge: 'Das Bild ist zu groß. Maximale Dateigröße: {maxSizeMb} MB.', + }, idle: { title: 'Lass aus deinen Daten eine Seerose wachsen', condensedMeaning: 'Liste aus Zahlen, die die Bedeutung deiner Daten beinhaltet', diff --git a/src/locales/en.ts b/src/locales/en.ts index c97f444..19ee777 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -23,9 +23,15 @@ export default { imagePreviewAlt: 'Preview', helperStart: 'To begin, choose', helperOr: 'or', + textLimit: '{count}/{max} characters', + imageLimit: 'Allowed: {formats} up to {maxSizeMb} MB', growButton: 'Grow it! 🪷', growingButton: 'Growing... 🪷', plantButton: 'Plant it! 🪷', + errors: { + invalidImageType: 'Invalid image format. Please use {formats}.', + imageTooLarge: 'Image is too large. Maximum file size is {maxSizeMb} MB.', + }, idle: { title: 'Grow a waterlily from your data', condensedMeaning: '"condensed meaning"', diff --git a/src/style.css b/src/style.css index ae89417..97e5dc9 100644 --- a/src/style.css +++ b/src/style.css @@ -1082,6 +1082,19 @@ button { margin-bottom: 12px; } +.input-meta { + margin: 2px 0 10px; + font-size: 0.84rem; + color: rgba(10, 45, 65, 0.72); +} + +.input-error { + margin: 0 0 12px; + color: #8a1f1f; + font-weight: 600; + font-size: 0.88rem; +} + .image-input .preview { display: grid; place-items: center; From 47aea912864e5994a3a5d0d680f2b082122099f5 Mon Sep 17 00:00:00 2001 From: autmoate Date: Thu, 21 May 2026 10:59:06 +0200 Subject: [PATCH 3/5] Fix CSP for client-side embedding and worker/network requests --- nginx/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/default.conf b/nginx/default.conf index 0e85942..e8ad77b 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -9,7 +9,7 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data:; worker-src 'self' blob:; connect-src 'self' https://huggingface.co https://cdn-lfs.huggingface.co; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data: https:; worker-src 'self' blob:; connect-src 'self' blob: data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; location / { try_files $uri $uri/ /index.html; From 758d30ac438e05f53c668c8bee690bc259ef7468 Mon Sep 17 00:00:00 2001 From: autmoate Date: Thu, 21 May 2026 11:23:17 +0200 Subject: [PATCH 4/5] Fix planting error --- nginx/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/default.conf b/nginx/default.conf index e8ad77b..8ae87ac 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -9,7 +9,7 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data: https:; worker-src 'self' blob:; connect-src 'self' blob: data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data: https:; worker-src 'self' blob:; connect-src 'self' blob: data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; location / { try_files $uri $uri/ /index.html; From bd67c6dc7e58fa66e190dc2e3b5563f76cc0630e Mon Sep 17 00:00:00 2001 From: autmoate Date: Thu, 21 May 2026 11:53:05 +0200 Subject: [PATCH 5/5] Fix planting error --- nginx/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/default.conf b/nginx/default.conf index 8ae87ac..95692c1 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -9,7 +9,7 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data: https:; worker-src 'self' blob:; connect-src 'self' blob: data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' blob: https:; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https:; font-src 'self' data: https:; worker-src 'self' blob: https:; connect-src 'self' blob: data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always; location / { try_files $uri $uri/ /index.html;