diff --git a/.dockerignore b/.dockerignore index a6d701c..2e6e575 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,6 @@ content/*.pdf *.md !README.md content/crowd +docker/auth +docker/drops +content/drops diff --git a/.gitignore b/.gitignore index c30672a..7f8b5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,8 @@ content/*.mov # previz source renders (web versions in public/previz) content/crowd/ + +# /drops/ login and clips: server only, never committed (see DEPLOY.md) +docker/auth/ +docker/drops/ +content/drops/ diff --git a/DEPLOY.md b/DEPLOY.md index b21607b..37e3560 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -112,6 +112,49 @@ The workflow is already in the repo (`.gitea/workflows/deploy.yml`). In Gitea, o Test it: push any commit to `main` and watch **Actions** in the repo. The job should run `git pull` and `docker compose up -d --build`, then the site updates. +### 8. Private shot previews at /drops/ + +`/drops/` is a view-only screening page for vendors and clients. nginx asks for a login before +it serves the page or any clip. The login file and the clips stay on the server only, in +`docker/auth/` and `docker/drops/`, and are never committed. The page reads +`docker/drops/manifest.json` to know which clips to show. + +**Share a drop.** On your PC, convert the clips to web MP4 without re-encoding, then copy them up: + +```bash +ffmpeg -i SHOT.mov -map 0:v -map 0:a? -c copy -movflags +faststart -map_metadata -1 SHOT.mp4 +scp SHOT.mp4 manifest.json @178.104.56.62:/opt/ttvfx_web/docker/drops/ +``` + +`manifest.json` looks like this (`src` is the file name in `docker/drops/`): + +```json +{ "title": "DH2 shot previews", "note": "For review only.", + "files": [{ "name": "DH2_VFX_027_210_REF", "src": "DH2_VFX_027_210_REF.mp4" }] } +``` + +Then set a password on the server. Use a new password for each vendor and drop: + +```bash +cd /opt/ttvfx_web +mkdir -p docker/auth +printf "vendor:$(openssl passwd -apr1)\n" > docker/auth/drops.htpasswd # prompts for the password +docker compose up -d --force-recreate # first time only, so the folders get mounted +``` + +Send `https://twotalesvfx.com/drops/`, the username `vendor` and the password through two +different channels (for example, the link by email and the password by WhatsApp). + +**Take it down.** No deploy is needed. Delete the clips and the login, and the page returns +"taken down" or 500 straight away: + +```bash +rm /opt/ttvfx_web/docker/drops/* /opt/ttvfx_web/docker/auth/drops.htpasswd +``` + +If the login file is missing, `/drops/` returns 500, so it is never left open. Nothing in +`/drops/` is cached by browsers or Cloudflare. + --- ## Day to day diff --git a/app/drops/page.tsx b/app/drops/page.tsx new file mode 100644 index 0000000..cc40eb5 --- /dev/null +++ b/app/drops/page.tsx @@ -0,0 +1,28 @@ +import type { Metadata, Viewport } from "next"; +import Fox from "@/components/Fox"; +import DropViewer from "@/components/drops/DropViewer"; +import { site } from "@/content/site"; + +// Private screening room for vendors and clients. nginx puts the whole of /drops/ behind a +// login, and the clips plus their list (manifest.json) live on the server only, never in git +// (see DEPLOY.md). Not linked from the site, kept out of search and the sitemap. +export const metadata: Metadata = { + title: "Shot previews", + robots: { index: false, follow: false }, +}; + +export const viewport: Viewport = { + themeColor: "#0a0a0b", +}; + +export default function DropsPage() { + return ( +
+
+ + {site.name} · Private +
+ +
+ ); +} diff --git a/components/drops/DropViewer.tsx b/components/drops/DropViewer.tsx new file mode 100644 index 0000000..0b7d672 --- /dev/null +++ b/components/drops/DropViewer.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// Served by nginx from the server's docker/drops/ folder, behind the same login as the page. +const base = "/drops/files/"; + +type Drop = { + title: string; + note?: string; + files: { name: string; src: string }[]; +}; + +export default function DropViewer() { + const [drop, setDrop] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + fetch(`${base}manifest.json`, { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : Promise.reject(r.status))) + .then(setDrop) + .catch(() => setFailed(true)); + }, []); + + if (failed) { + return ( +
+

Nothing here

+

+ These previews have been taken down. Ask your Two Tales contact if you still need them. +

+
+ ); + } + + if (!drop) { + return

Loading…

; + } + + return ( + <> +
+

Confidential · view only

+

+ {drop.title} +

+ {drop.note &&

{drop.note}

} +
+ +
    + {drop.files.map((f, i) => ( +
  1. +
    + {String(i + 1).padStart(2, "0")} + + {f.name} +
    + +
  2. + ))} +
+ + ); +} + +// Plays inline with no download, cast or picture-in-picture options and no right-click menu. +// This only stops casual saving: anyone who can watch can still screen-record. +function Player({ src, label }: { src: string; label: string }) { + return ( +
e.preventDefault()}> +
+ ); +} diff --git a/docker-compose.yml b/docker-compose.yml index b14e638..0d69493 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,11 @@ services: restart: unless-stopped + # /drops/ login and clips (gitignored; created on the server, see DEPLOY.md) + volumes: + - ./docker/auth:/etc/nginx/auth:ro + - ./docker/drops:/srv/drops:ro + networks: - npm_default diff --git a/docker/nginx.conf b/docker/nginx.conf index b1df78a..39f5fd9 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -30,6 +30,22 @@ server { try_files $uri $uri/ $uri.html =404; } + # Private shot previews: login required for the page and the clips (see DEPLOY.md). + # The login file and the clips live on the server only. If the login file is missing, + # nginx answers 500, so /drops/ is never left open. ^~ beats the media regex above, + # so nothing in here is cached by the browser or Cloudflare. + location ^~ /drops/ { + auth_basic "Two Tales VFX"; + auth_basic_user_file /etc/nginx/auth/drops.htpasswd; + add_header Cache-Control "private, no-store"; + add_header X-Robots-Tag "noindex, nofollow"; + try_files $uri $uri/ $uri.html =404; + + location ^~ /drops/files/ { + alias /srv/drops/; + } + } + # Offline worker for /set must always be revalidated so updates roll out location = /set/sw.js { add_header Cache-Control "no-cache";