diff --git a/.devcontainer/caddy/Caddyfile b/.devcontainer/caddy/Caddyfile new file mode 100644 index 00000000..f4c0d703 --- /dev/null +++ b/.devcontainer/caddy/Caddyfile @@ -0,0 +1,108 @@ +ontrack.dev { + header { + Content-Security-Policy "default-src https: 'unsafe-inline' 'unsafe-eval' blob: data: ws:; worker-src 'self' blob:" + } + + tls internal + + # Student submission downloads retain their existing record-based URL. Caddy + # asks Rails to authenticate and authorise the request, then serves only the + # relative path returned by that trusted internal endpoint. + @submissionDownload { + method GET HEAD + path_regexp submissionDownload ^/api/projects/[0-9]+/task_def_id/[0-9]+/(?:submission|submission_files)$ + } + @portfolioDownload { + method GET HEAD + path_regexp portfolioDownload ^/api/submission/unit/[0-9]+/portfolio$ + } + @unitContentFile { + method GET HEAD + path_regexp unitContentFile ^/api/units/[0-9]+/content/sites/[0-9]+/files(?:/.*)?$ + } + route { + handle @unitContentFile { + route { + request_header -X-OnTrack-File + request_header -X-OnTrack-Content-Disposition + request_header -X-OnTrack-Content-Type + request_header -X-OnTrack-Content-Site-Id + + forward_auth formatif-dev-container:3000 { + uri /api/internal/downloads/unit-content + header_up X-OnTrack-Download-Auth {$DOWNLOAD_AUTH_SECRET} + copy_headers X-OnTrack-File X-OnTrack-Content-Disposition X-OnTrack-Content-Type X-OnTrack-Content-Site-Id + } + + rewrite * /{http.request.header.X-OnTrack-File} + root * /srv/student-work + header Content-Disposition "{http.request.header.X-OnTrack-Content-Disposition}" + header Content-Type "{http.request.header.X-OnTrack-Content-Type}" + header X-Content-Site-Id "{http.request.header.X-OnTrack-Content-Site-Id}" + header Cache-Control "private, no-cache" + file_server + } + } + + handle @submissionDownload { + route { + # Never allow a client to seed the internal routing headers. + request_header -X-OnTrack-File + request_header -X-OnTrack-Content-Disposition + request_header -X-OnTrack-Content-Type + + forward_auth formatif-dev-container:3000 { + uri /api/internal/downloads/submission + header_up X-OnTrack-Download-Auth {$DOWNLOAD_AUTH_SECRET} + copy_headers X-OnTrack-File X-OnTrack-Content-Disposition X-OnTrack-Content-Type + } + + rewrite * /{http.request.header.X-OnTrack-File} + root * /srv/student-work + header Content-Disposition "{http.request.header.X-OnTrack-Content-Disposition}" + header Content-Type "{http.request.header.X-OnTrack-Content-Type}" + file_server + } + } + + handle @portfolioDownload { + route { + # The browser cannot select the filesystem path; only Rails can + # return these headers after authenticating and authorising the unit. + request_header -X-OnTrack-File + request_header -X-OnTrack-Content-Disposition + request_header -X-OnTrack-Content-Type + + forward_auth formatif-dev-container:3000 { + uri /api/internal/downloads/portfolio + header_up X-OnTrack-Download-Auth {$DOWNLOAD_AUTH_SECRET} + copy_headers X-OnTrack-File X-OnTrack-Content-Disposition X-OnTrack-Content-Type + } + + rewrite * /{http.request.header.X-OnTrack-File} + root * /srv/student-work + header Content-Disposition "{http.request.header.X-OnTrack-Content-Disposition}" + header Content-Type "{http.request.header.X-OnTrack-Content-Type}" + header Cache-Control "private, no-store" + file_server + } + } + + # Rails exposes its health check at /health. Keep the public URL aligned + # with the production /api namespace. + handle /api/health { + rewrite * /health + reverse_proxy formatif-dev-container:3000 + } + + # Preserve the /api prefix when forwarding application requests to Rails. + handle /api/* { + reverse_proxy formatif-dev-container:3000 + } + + # Angular's development server also uses this proxy for its HMR WebSocket. + handle { + reverse_proxy formatif-dev-container:4200 + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 8cb54a01..380f71c7 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -25,7 +25,7 @@ services: # mounts are relative to the first file in the list, which is a level up. - ..:/workspace:cached - formatif_tmp_compose:/workspace/tmp - - formatif_student-work_compose:/student-work + - ../data/student-work:/student-work - jplag_results:/jplag/results - ws_node_modules:/workspace/node_modules - web_node_modules:/workspace/doubtfire-web/node_modules @@ -48,10 +48,34 @@ services: environment: LATEX_CONTAINER_NAME: 1-formatif-texlive-container LATEX_BUILD_PATH: /texlive/shell/latex_build.sh + DF_INSTITUTION_HOST: https://ontrack.dev + DF_COOKIE_DOMAIN: ontrack.dev + DF_CADDY_DOWNLOAD_AUTH_SECRET: local-development-only-caddy-download-secret depends_on: - dev-db - gotenberg-volumes + # Production-like local HTTPS entry point. Student work is mounted read-only + # and is only served after Rails authorises the protected content route. + caddy: + image: caddy:latest + container_name: ontrack-caddy + restart: unless-stopped + environment: + DOWNLOAD_AUTH_SECRET: local-development-only-caddy-download-secret + ports: + - "80:80" + - "443:443" + networks: + - frontnet + depends_on: + - formatif-dev-container + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + - ../data/student-work:/srv/student-work:ro + dev-db: container_name: doubtfire-dev-db image: mariadb:10 @@ -85,7 +109,7 @@ services: # context: ../ # dockerfile: texlive.Dockerfile volumes: - - formatif_student-work_compose:/student-work + - ../data/student-work:/student-work - ../doubtfire-api/tmp/rails-latex:/workdir/texlive-latex - ../doubtfire-api/public/assets/images:/workspace/doubtfire-api/public/assets/images - ../doubtfire-api/test_files:/workspace/doubtfire-api/test_files @@ -101,7 +125,7 @@ services: # context: ../ # dockerfile: jplag.Dockerfile volumes: - - formatif_student-work_compose:/student-work + - ../data/student-work:/student-work - jplag_results:/jplag/results - ../doubtfire-api/tmp/jplag:/tmp/jplag command: /bin/sh -c "sleep infinity" @@ -143,3 +167,5 @@ volumes: codex_home: claude_home: vscode_server_home: + caddy_data: + caddy_config: