diff --git a/local-instance/README.md b/local-instance/README.md new file mode 100644 index 0000000..a1b5824 --- /dev/null +++ b/local-instance/README.md @@ -0,0 +1,204 @@ +# A local Schulcloud + +A Docker Compose stack that runs a real Schulcloud instance on this machine, +modelled on **schulcloud-thueringen.de** — the instance this MCP server reads. + +It exists so that the parts of the product we can only observe from a student's +account can also be *produced*: log in as the teacher, grade a submission, then +look at it through `list_submissions` and see whether what we render matches +what the student is shown. + +> Everything here is a throwaway development instance. The credentials are the +> upstream development defaults, published in the upstream repositories. Bind +> nothing beyond `127.0.0.1`. + +## Quick start + +```bash +cd local-instance +docker compose up -d # ~5 min on a cold cache +./scripts/seed.sh # loads the demo school; takes another minute or two +``` + +Then open and sign in as +`demo-schueler@schul-cloud.org` / `schulcloud` (Fritz Schmidt, a student with +graded submissions). `seed.sh` prints the full account list on completion. + +> The demo password differs by account — an upstream quirk, not ours. The two +> `demo-*` accounts use `schulcloud`, the built-in `admin`/`lehrer` and the +> teacher `klara.fall` use `Schulcloud1!`, and the `*.qa` accounts use +> `Schulcloud1qa!`. + +To add the external tools (Etherpad, H5P, tldraw, Collabora): + +```bash +docker compose --profile tools up -d +``` + +## Why the images are the interesting part + +`quay.io/schulcloudverbund/*` are the **same images the live instance runs**, +built for the `thr` theme, and they are public. So this is not a rebuild of +`main` that may have drifted — it is the deployed artefact, pinned to the +version the live instance reports: + +```console +$ curl -s https://schulcloud-thueringen.de/version +{"client": {"version": "33.40.0"}, "nuxt-client": {"version": "33.40.1"}, + "server": {"version": "33.40.2"}, "dof_app_deploy": "33.40.2"} +``` + +Hence `SC_VERSION` defaults to `33.40`. To pin something else: + +```bash +SC_VERSION=33.27 docker compose up -d +``` + +`CLAUDE.md` says *live behaviour beats upstream source* — the clones in +`vendor/` track `main` and may be ahead of what is deployed. Running the +deployed images is how that rule is honoured here rather than worked around. + +## What is faithful, and what is not + +Faithful, and load-bearing for what we test: + +| | | +|---|---| +| **Images** | the live instance's own, `thr` theme, tag `33.40` | +| **Feature flags** | `env/api.env` is a replay of `GET /api/v3/config/public` from the live instance, not a hand-picked set | +| **Instance identity** | `env/shared.env` mirrors `dof_app_deploy/ansible/group_vars/thr/instance_cfg.yml` | +| **URL routing** | `proxy/nginx.conf` is generated from the deployment's own ingress table (see below) | +| **Session model** | Valkey in `single` mode, so the JWT whitelist expires sessions exactly as production does | +| **Seed data** | the upstream demo school, including graded and ungraded submissions | + +Deliberately different: + +- **No external OAuth / Schulportal login.** Excluded by request, and it is the + one part that cannot be stood up locally. Local login is username + password + against the seeded accounts. +- **No BigBlueButton.** `FEATURE_VIDEOCONFERENCE_ENABLED` is off; upstream it is + on. BBB is a separate product of its own scale. +- **`FEATURE_CONSENT_NECESSARY=false`**, or every seeded user hits a consent + wall before reaching any content. +- **No LDAP / TSP sync, no Nextcloud, no calendar service.** +- **Antivirus off by default** — see the `av` profile. + +Every one of these is marked in the env files at the line it affects. + +## Layout + +``` +docker-compose.yml the stack; profiles: (default) | tools | av | preview +env/ one file per service, all values local-only +proxy/nginx.conf GENERATED — the single origin, see scripts/gen-proxy-conf.py +scripts/seed.sh loads the demo school via the management app +scripts/minio-init.sh creates the S3 buckets each service expects +etherpad/APIKEY.txt fixed Etherpad API key, matching env/api.env +``` + +### Ports + +| Port | | +|---|---| +| **4400** | **the instance** — everything a browser touches | +| 3030 | server API, direct | +| 3333 | management app (seeding only, not part of a running instance) | +| 3100 / 4000 | legacy client / SPA, direct | +| 4444 | file-storage | +| 9900 / 9901 | MinIO S3 API / console (`miniouser` / `miniouser`) | +| 9980 | Collabora (`tools` profile) | +| 27019 | MongoDB | +| 6381 | Valkey | +| 15673 | RabbitMQ management | + +Everything binds to `127.0.0.1`. Port numbers are shifted off their upstream +defaults where those commonly collide (Mongo, Valkey, RabbitMQ, MinIO) — note +`docker-compose.override.yml` in the repo root already uses 8080 and 55432. + +## The proxy is generated, not written + +The live instance is one origin whose paths are split across the legacy client, +the SPA, and several APIs. That split is not cosmetic: `/rooms/courses-list` is +the SPA, `/courses/:id` is the legacy client, and `/api/v3/file/` is a +different service from `/api/v3/`. Get it wrong and you are testing a different +application from the one students use. + +The rules live in `dof_app_deploy/ansible/group_vars/all/x_ingress.yml` (46 of +them) plus a per-path ingress inside each service repo. Transcribing that by +hand invites drift, so `scripts/gen-proxy-conf.py` reads the deployment repo and +emits `proxy/nginx.conf`: + +```bash +python3 scripts/gen-proxy-conf.py > proxy/nginx.conf +docker compose restart proxy +``` + +It needs the upstream clones in `../vendor` (gitignored — see the root README), +which is why the output is committed. + +Two details in there are worth keeping: + +- Every `proxy_pass` goes through a **variable plus a `resolver`**, so names are + resolved per request. With a literal upstream, nginx refuses to start + whenever an optional profile is down — which is the normal case. +- **Etherpad gets the deployment's own rewrite rules**, not a plain + `proxy_pass`. It is mounted under a prefix it knows nothing about; without the + rewrites a pad loads and then silently never syncs. + +## Seeding + +`scripts/seed.sh` does what the real deployment's init job does: it asks the +management app to load `backup/setup/*.json`, which ships **inside the server +image**. It also registers MinIO in the `storageproviders` collection, which has +no seed data on purpose (it holds credentials) and without which legacy file +uploads fail. + +Re-running it is safe; collections are replaced, not appended to. + +### What the demo data already contains + +The seed includes 50 tasks and 24 submissions covering the grading states that +are otherwise hard to obtain — including the two this server renders: + +| | | +|---|---| +| `graded: true`, `grade: null`, `gradeComment` set | graded by feedback alone | +| `graded: true`, `grade: 100`, `gradeComment` set | a percentage **and** feedback | +| `graded: false`, `submitted: true` | handed in, not yet looked at | + +The second row matters: no submission in the real account has ever had a +numeric grade, so `formatGradeState`'s percentage branch had never been seen +against real data. Here it can be. + +## Profiles + +| Profile | Services | Cost | +|---|---|---| +| *(default)* | Mongo, Valkey, RabbitMQ, MinIO, api, management, board-collaboration, admin-api, file-storage, client, nuxt, proxy | ~4 GB images | +| `tools` | Etherpad, H5P editor + static files + library install, tldraw server + worker, Collabora | ~4 GB more, Collabora is the bulk | +| `av` | ClamAV + Clammit | ~1.5 GB resident for the signature database | +| `preview` | file-preview generator (thumbnails) | small | + +`av` is opt-in for a reason beyond size: with `ENABLE_FILE_SECURITY_CHECK=true` +but no scanner reachable, every upload stays at +`securityCheck.status=pending` and can never be downloaded. Turn the flag in +`env/file-storage.env` on **together with** the profile, or neither. + +## Troubleshooting + +**Everything 502s.** The proxy is up before the apps are. `docker compose logs +-f api` — the server waits for Mongo to become primary. + +**Mongo never becomes healthy.** It runs as a single-node replica set because +the migration runner opens transactions, which Mongo refuses on a standalone. +The healthcheck initiates the set on first boot; give it ~30 s. + +**Login succeeds, then immediately bounces back.** The `jwt` cookie is being +dropped. Check `COOKIE__SECURE=false` in `env/client.env` — the proxy speaks +plain HTTP locally. + +**A file uploads but will not download.** See the `av` note above. + +**H5P element stays empty.** `docker compose --profile tools run --rm +h5p-libraries` and watch it finish; the editor has nothing to offer until the +content types are in the bucket. diff --git a/local-instance/docker-compose.yml b/local-instance/docker-compose.yml new file mode 100644 index 0000000..2b8b1b0 --- /dev/null +++ b/local-instance/docker-compose.yml @@ -0,0 +1,267 @@ +# A local Schulcloud, as close to schulcloud-thueringen.de as it can be made +# without its external identity provider. +# +# The application images are the *same* images the real instance runs +# (quay.io/schulcloudverbund, thr theme, tag 33.40 — see README.md), so the +# behaviour under test is the deployed behaviour, not a rebuild of main. +# +# docker compose up -d core: login, courses, boards, files +# docker compose --profile tools up -d + etherpad, h5p, tldraw, collabora +# docker compose --profile av up -d + virus scanning of uploads +# +# Everything here is a throwaway dev instance: the credentials are the upstream +# development defaults and are published in the upstream repositories. Do not +# expose any of it beyond localhost. + +x-sc-version: &sc-version "${SC_VERSION:-33.40}" + +x-server-image: &server-image + image: quay.io/schulcloudverbund/schulcloud-server:${SC_VERSION:-33.40} + env_file: [env/shared.env, env/jwt.env, env/api.env] + depends_on: + mongo: {condition: service_healthy} + valkey: {condition: service_started} + rabbitmq: {condition: service_healthy} + restart: unless-stopped + +services: + # ---------------------------------------------------------------- infra --- + mongo: + image: docker.io/mongo:7 + # Single-node replica set rather than a bare mongod: the server's migration + # runner opens transactions, which mongo refuses outside a replica set. + command: ["--replSet", "rs0", "--bind_ip_all"] + volumes: + - mongo-data:/data/db + ports: ["127.0.0.1:27019:27017"] + healthcheck: + # Initiates the replica set on first start and reports healthy once the + # node is actually primary, which is what every other service waits for. + test: >- + mongosh --quiet --eval ' + try { rs.status() } catch (e) { rs.initiate({_id:"rs0",members:[{_id:0,host:"mongo:27017"}]}) } + quit(db.hello().isWritablePrimary ? 0 : 1)' + interval: 5s + timeout: 10s + retries: 30 + start_period: 10s + restart: unless-stopped + + valkey: + # The JWT whitelist. Sessions die when their key expires, exactly as in + # production — this is the piece that makes local session testing honest. + image: docker.io/valkey/valkey:8-alpine + ports: ["127.0.0.1:6381:6379"] + restart: unless-stopped + + rabbitmq: + image: docker.io/rabbitmq:4-management-alpine + ports: ["127.0.0.1:15673:15672"] + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 10s + retries: 20 + start_period: 20s + restart: unless-stopped + + minio: + # Stands in for the S3 provider the real instance uses. Buckets are created + # by minio-init below. + image: quay.io/minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: miniouser + MINIO_ROOT_PASSWORD: miniouser + volumes: + - minio-data:/data + ports: + - "127.0.0.1:9900:9000" # S3 API + - "127.0.0.1:9901:9001" # console (miniouser / miniouser) + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 30 + restart: unless-stopped + + minio-init: + image: quay.io/minio/mc:latest + depends_on: + minio: {condition: service_healthy} + entrypoint: ["/bin/sh", "/init.sh"] + volumes: + - ./scripts/minio-init.sh:/init.sh:ro + restart: "no" + + # ------------------------------------------------------- schulcloud api --- + api: + <<: *server-image + container_name: sc-api + command: ["dist/apps/server/apps/server.app"] + ports: ["127.0.0.1:3030:3030"] + + management: + # Not part of the running instance — it exposes the seeding and migration + # endpoints that the real deployment's init job calls, and nothing else. + <<: *server-image + command: ["dist/apps/server/apps/management.app"] + # Port and base path are hardcoded to 3333 and /api in management.app.ts; + # PORT is not read here. + ports: ["127.0.0.1:3333:3333"] + + board-collaboration: + # The websocket behind column boards. Without it a board renders once and + # then never updates. + <<: *server-image + command: ["dist/apps/server/apps/board-collaboration.app"] + environment: + PORT: "4450" + + admin-api: + <<: *server-image + command: ["dist/apps/server/apps/admin-api-server.app"] + environment: + PORT: "4030" + + file-storage: + image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40} + env_file: [env/shared.env, env/jwt.env, env/file-storage.env] + depends_on: + mongo: {condition: service_healthy} + rabbitmq: {condition: service_healthy} + minio: {condition: service_healthy} + ports: ["127.0.0.1:4444:4444"] + restart: unless-stopped + + file-preview: + # Generates thumbnails via ImageMagick, driven off RabbitMQ. Optional: with + # it absent, files still upload and download, they just have no preview. + image: quay.io/schulcloudverbund/file-storage:file-preview-${SC_VERSION:-33.40} + profiles: ["preview"] + env_file: [env/shared.env, env/jwt.env, env/file-storage.env] + depends_on: + rabbitmq: {condition: service_healthy} + minio: {condition: service_healthy} + restart: unless-stopped + + # -------------------------------------------------------------- clients --- + client: + # The legacy UI. Still owns "/" and much of the course view. + image: quay.io/schulcloudverbund/schulcloud-client-thr:${SC_VERSION:-33.40} + env_file: [env/shared.env, env/jwt.env, env/client.env] + depends_on: [api] + ports: ["127.0.0.1:3100:3100"] + restart: unless-stopped + + nuxt: + # The Vue SPA, built for the thr theme. The image is an nginx that + # templates env vars into its config at start. + image: quay.io/schulcloudverbund/schulcloud-frontend-thr:${SC_VERSION:-33.40} + env_file: [env/nuxt.env] + ports: ["127.0.0.1:4000:4000"] + restart: unless-stopped + + proxy: + # The single origin. Everything a browser touches goes through here, so the + # app sees one host the way it does in production. + image: docker.io/nginx:1.29-alpine + volumes: + - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro + ports: ["127.0.0.1:4400:4400"] + depends_on: [api, client, nuxt] + restart: unless-stopped + + # -------------------------------------------------- external tools ------- + etherpad: + # The collaborative text editor element on column boards. + image: docker.io/etherpad/etherpad:3.3.3 + profiles: ["tools"] + env_file: [env/etherpad.env] + volumes: + - ./etherpad/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt:ro + depends_on: + mongo: {condition: service_healthy} + restart: unless-stopped + + h5p-editor: + image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40} + profiles: ["tools"] + command: ["dist/apps/h5p-editor.app"] + env_file: [env/shared.env, env/jwt.env, env/h5p.env] + environment: + PORT: "4448" + depends_on: + mongo: {condition: service_healthy} + minio: {condition: service_healthy} + restart: unless-stopped + + h5p-staticfiles: + image: quay.io/schulcloudverbund/h5p-server:static-files-${SC_VERSION:-33.40} + profiles: ["tools"] + restart: unless-stopped + + h5p-libraries: + # One-shot: installs the H5P content types listed in env/h5p.env into the + # library bucket. Exits when done; re-run it after changing that list. + image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40} + profiles: ["tools"] + command: ["dist/apps/h5p-library-management.app"] + env_file: [env/shared.env, env/jwt.env, env/h5p.env] + depends_on: + mongo: {condition: service_healthy} + minio-init: {condition: service_completed_successfully} + restart: "no" + + tldraw-server: + # The whiteboard element. + image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40} + profiles: ["tools"] + command: ["dist/apps/tldraw-server.app.js"] + env_file: [env/shared.env, env/jwt.env, env/tldraw.env] + depends_on: [valkey, minio] + restart: unless-stopped + + tldraw-worker: + image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40} + profiles: ["tools"] + command: ["dist/apps/tldraw-worker.app.js"] + env_file: [env/shared.env, env/jwt.env, env/tldraw.env] + depends_on: [valkey, minio] + restart: unless-stopped + + collabora: + # Office document editing. Reached by the browser directly on :9980, the + # way the real deployment puts it on its own hostname. + image: docker.io/collabora/code:latest + profiles: ["tools"] + environment: + extra_params: --o:ssl.enable=false --o:ssl.termination=false + domain: ".*" + aliasgroup1: "http://localhost:4400" + ports: ["127.0.0.1:9980:9980"] + cap_add: ["MKNOD"] + restart: unless-stopped + + # ------------------------------------------------------------ antivirus --- + clamav: + # ~1.5 GB resident once the signature database loads, hence its own profile. + image: docker.io/clamav/clamav:1.5.3 + profiles: ["av"] + volumes: + - clamav-db:/var/lib/clamav + restart: unless-stopped + + clammit: + image: ghcr.io/dbildungsplattform/clammit:0.9.1 + profiles: ["av"] + environment: + CLAMMIT_CLAMD_URL: tcp://clamav:3310 + CLAMMIT_LISTEN: 0.0.0.0:8438 + depends_on: [clamav] + restart: unless-stopped + +volumes: + mongo-data: + minio-data: + clamav-db: diff --git a/local-instance/env/api.env b/local-instance/env/api.env new file mode 100644 index 0000000..fd9b055 --- /dev/null +++ b/local-instance/env/api.env @@ -0,0 +1,95 @@ +# The schulcloud-server API app (apps/server.app). +# +# The FEATURE_* block below is not hand-picked: it is a replay of +# GET /api/v3/config/public from the live schulcloud-thueringen.de, so the local +# instance exposes the same surface to students that the real one does. +# Regenerate with ./scripts/sync-feature-flags.sh. + +PORT=3030 + +# Internal service addresses (compose DNS), not the browser-facing origin. +API_HOST=http://api:3030/api +FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444 +LICENSE_SUMMARY_URL=http://api:3030/api/licenses/summary +ROOM_MEMBER_INFO_URL=http://api:3030/api/rooms/member-info + +ALERT_STATUS_URL=https://status.schulcloud-thueringen.de/ +CALENDAR_SERVICE_ENABLED=false +NEST_LOG_LEVEL=info + +# Etherpad (see ../etherpad/APIKEY.txt) +ETHERPAD__URI=http://etherpad:9001/api/1 +ETHERPAD__PAD_URI=http://localhost:4400/etherpad/p +ETHERPAD__API_KEY=381d67e6347d235ac9446da3ea10a82efd6f8ae09fa2e90efeda80f82feeb4fd + +# tldraw + admin API +TLDRAW__WEBSOCKET_URL=ws://localhost:4400/tldraw-server +TLDRAW_ADMIN_API_CLIENT__BASE_URL=http://tldraw-server:3349 +TLDRAW_ADMIN_API_CLIENT__API_KEY=tldraw-admin-key +ADMIN_API__ALLOWED_API_KEYS=thisisasupersecureapikeythatisabsolutelysave + +# Teacher/student visibility, as configured for thr. +TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE=false +TEACHER_STUDENT_VISIBILITY__IS_ENABLED_BY_DEFAULT=true +TEACHER_STUDENT_VISIBILITY__IS_VISIBLE=false + +# --- feature flags, mirrored from the live instance ---------------------- +FEATURE_ADMINISTRATE_ROOMS_ENABLED=true +FEATURE_AI_TUTOR_ENABLED=false +FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED=false +FEATURE_BOARD_LAYOUT_ENABLED=true +FEATURE_BOARD_READERS_CAN_EDIT_TOGGLE=true +FEATURE_COLUMN_BOARD_COLLABORATIVE_TEXT_EDITOR_ENABLED=true +FEATURE_COLUMN_BOARD_COLLABORA_ENABLED=true +FEATURE_COLUMN_BOARD_ENABLED=true +FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED=true +FEATURE_COLUMN_BOARD_FILE_FOLDER_ENABLED=true +FEATURE_COLUMN_BOARD_H5P_ENABLED=true +FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED=true +FEATURE_COLUMN_BOARD_SHARE=true +FEATURE_COLUMN_BOARD_SOCKET_ENABLED=true +# local override (live: true) — needs infrastructure we do not run +FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED=false +FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED=false +FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED=false +FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE=1073741824 +# local override (live: true) — otherwise every seeded user hits a consent wall +FEATURE_CONSENT_NECESSARY=false +FEATURE_COPY_SERVICE_ENABLED=true +FEATURE_COURSE_SHARE=true +FEATURE_CTL_TOOLS_COPY_ENABLED=true +FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION=false +FEATURE_EXTERNAL_PERSON_REGISTRATION_ENABLED=false +FEATURE_EXTERNAL_SYSTEM_LOGOUT_ENABLED=false +FEATURE_FWU_CONTENT_ENABLED=false +FEATURE_LESSON_SHARE=true +FEATURE_LOGIN_LINK_ENABLED=false +FEATURE_MEDIA_SHELF_ENABLED=true +FEATURE_NOTIFICATIONS_ENABLED=false +FEATURE_PREFERRED_CTL_TOOLS_ENABLED=true +FEATURE_ROOM_ADD_EXTERNAL_PERSONS_ENABLED=false +FEATURE_ROOM_COPY_ENABLED=true +FEATURE_ROOM_LINK_INVITATION_EXTERNAL_PERSONS_ENABLED=false +FEATURE_ROOM_REGISTER_EXTERNAL_PERSONS_ENABLED=false +FEATURE_ROOM_SHARE=true +FEATURE_SCHOOL_POLICY_ENABLED_NEW=true +FEATURE_SCHOOL_TERMS_OF_USE_ENABLED=true +FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED=false +FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED=false +FEATURE_SHOW_MIGRATION_WIZARD=false +FEATURE_SHOW_OUTDATED_USERS=false +FEATURE_TASK_SHARE=true +FEATURE_TEAMS_ENABLED=true +FEATURE_TEAM_CREATE_ROOM_ENABLED=true +FEATURE_TLDRAW_ENABLED=true +FEATURE_USER_LOGIN_MIGRATION_ENABLED=false +FEATURE_USER_MIGRATION_ENABLED=false +# local override (live: true) — needs infrastructure we do not run +FEATURE_VIDEOCONFERENCE_ENABLED=false +FEATURE_VIDIS_MEDIA_ACTIVATIONS_ENABLED=false + +# --- required-but-unused endpoints --------------------------------------- +# The config classes validate these as present strings even when the feature +# is off, so they get a placeholder rather than a real service. Hydra is the +# OAuth2 provider behind external tool launches; we do not run it. +HYDRA_URI=http://hydra.invalid:4444 diff --git a/local-instance/env/client.env b/local-instance/env/client.env new file mode 100644 index 0000000..39d03b5 --- /dev/null +++ b/local-instance/env/client.env @@ -0,0 +1,31 @@ +# schulcloud-client, the legacy Express/Handlebars UI. Still serves "/" and +# large parts of the course view, so it is not optional. + +PORT=3100 +# HOST is what the client puts into redirects, so it must be the browser-facing +# origin (the proxy), not this container's own address. +HOST=http://localhost:4400 +API_HOST=http://api:3030/api +PUBLIC_BACKEND_URL=http://localhost:4400/api + +FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444 + +ETHERPAD__PAD_URI=http://localhost:4400/etherpad/p +ETHERPAD__PAD_PATH=/etherpad/p +ETHERPAD__DOMAIN=localhost +FEATURE_ETHERPAD_ENABLED=true + +SESSION_VALKEY__MODE=single +SESSION_VALKEY__URI=redis://valkey:6379 +SESSION_COOKIE_SAME_SITE=lax + +# The proxy terminates plain HTTP locally; without this the client marks the +# jwt cookie Secure and the browser silently drops it. +COOKIE__SECURE=false +COOKIE__SAME_SITE=lax +COOKIE__HTTP_ONLY=false + +# Signs the session cookie. Local-only value; the app refuses to start without it. +COOKIE_SECRET=local-instance-cookie-secret-not-a-real-secret + +LOG_LEVEL=info diff --git a/local-instance/env/etherpad.env b/local-instance/env/etherpad.env new file mode 100644 index 0000000..88a489e --- /dev/null +++ b/local-instance/env/etherpad.env @@ -0,0 +1,11 @@ +# Etherpad, the collaborative text editor element on column boards. +# Settings taken from docs/topics/etherpad/Local setup.md. + +REQUIRE_SESSION=true +PAD_OPTIONS_SHOW_CHAT=true +DISABLE_IP_LOGGING=true +DEFAULT_PAD_TEXT=Schreib etwas! +DB_TYPE=mongodb +DB_URL=mongodb://mongo:27017/etherpad +AUTHENTICATION_METHOD=apikey +TRUST_PROXY=true diff --git a/local-instance/env/file-storage.env b/local-instance/env/file-storage.env new file mode 100644 index 0000000..78f7653 --- /dev/null +++ b/local-instance/env/file-storage.env @@ -0,0 +1,30 @@ +# file-storage service (repo hpi-schul-cloud/file-storage), the /api/v3/file/* API. + +FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444 +FILE_STORAGE_SERVICE_URL=http://file-storage:4444 +AUTHORIZATION_API_URL=http://api:3030/api/v3 + +FILES_STORAGE_S3_ENDPOINT=http://minio:9000/ +FILES_STORAGE_S3_BUCKET=schulcloud +FILES_STORAGE_S3_REGION=eu-central-1 +FILES_STORAGE_S3_ACCESS_KEY_ID=miniouser +FILES_STORAGE_S3_SECRET_ACCESS_KEY=miniouser + +# Antivirus. Off by default: ClamAV wants ~1.5 GB of RAM for its signature DB. +# Turn it on together with the `av` profile — with the scanner absent but the +# check enabled, every upload stays stuck in securityCheck.status=pending and +# can never be downloaded. +ENABLE_FILE_SECURITY_CHECK=false +ANTIVIRUS_SERVICE_HOSTNAME=clamav +ANTIVIRUS_SERVICE_PORT=3310 +FILES_STORAGE_USE_STREAM_TO_ANTIVIRUS=false + +PREVIEW_PRODUCER_INCOMING_REQUEST_TIMEOUT=10000 + +# Collabora must be reachable by the *browser*, so this one is a host URL. +COLLABORA_ONLINE_URL=http://localhost:9980 +WOPI_URL=http://localhost:4400/api/v3/wopi/files +WOPI_POST_MESSAGE_ORIGIN=http://localhost:4400 +FEATURE_COLUMN_BOARD_COLLABORA_ENABLED=true + +LOGGER_LOG_LEVEL=info diff --git a/local-instance/env/h5p.env b/local-instance/env/h5p.env new file mode 100644 index 0000000..d55f6b5 --- /dev/null +++ b/local-instance/env/h5p.env @@ -0,0 +1,18 @@ +# h5p-server: the h5p-editor app plus the one-shot library-management job. + +H5P_EDITOR__S3_ENDPOINT=http://minio:9000 +H5P_EDITOR__S3_REGION=eu-central-1 +H5P_EDITOR__S3_ACCESS_KEY_ID=miniouser +H5P_EDITOR__S3_SECRET_ACCESS_KEY=miniouser +H5P_EDITOR__S3_BUCKET_CONTENT=h5p-content-bucket +H5P_EDITOR__S3_BUCKET_LIBRARIES=h5p-library-bucket +H5P_EDITOR__LIBRARIES_S3_ACCESS_KEY_ID=miniouser +H5P_EDITOR__LIBRARIES_S3_SECRET_ACCESS_KEY=miniouser + +# A short list keeps the one-off library install to a couple of minutes; the +# upstream default installs ~40 libraries and takes far longer. +H5P_EDITOR__LIBRARY_LIST=H5P.ArithmeticQuiz,H5P.Chart,H5P.MultiChoice,H5P.Blanks + +API_HOST=http://api:3030/api +CORE_INCOMING_REQUEST_TIMEOUT_MS=8000 +LOGGER_LOG_LEVEL=info diff --git a/local-instance/env/jwt.env b/local-instance/env/jwt.env new file mode 100644 index 0000000..63da19e --- /dev/null +++ b/local-instance/env/jwt.env @@ -0,0 +1,5 @@ +# Development RSA keypair, copied verbatim from schulcloud-server/.env.development +# (upstream, public, committed in their repo). It exists so every local service +# validates the same tokens. NEVER use these keys anywhere reachable from outside. +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n" +JWT_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA0/oW2sIZWvVt0AEgQ8PS80/udJzfWXu6t2QWjUcQA2THGvDSXXMH\n6YMMY2czyBgf6L7hHV/9p1Trfpe7YgxYhOoGsxhXG1keAYQ4+mdveaUAa3uiACdE\nodsB0OFjVUdgOHCyUIXFfhSsp2p2tmZeFi/bE2v/05kYO+ExgQuzUDbB8bCr1sc7\ngMS/2dC2iE/BVw/I0F14oZkZn0fshojg4qoaLbLVKB7Iw53IXF2878zXp81Jdnnv\nHdwVbGWqoII6sHZFQs8ob5S/WGMl4QnBHN98x0KmORUFyTv5kK4cdcC8LJ1HpoVW\nNC6js84iF9yFRhYXY2RHqh7BwaZZ4XZym/MetTdQTBDaSvhXe0A3WdahNG+DGrie\nhd6doWk98Adb49InaodH64ZRkurxiX61GEtzjMRq9EfGS5R/IfcWyPQbiir6ymKX\nfOUtywRjcm3FZzmT7j3c0UHzQVEH0NBfTMj+QKz5NILNP230j0DcjNImDbHHcVH1\nquSb6e0WXjKANTkf4gaTOw7jdQDFw0Ou3aEmwPg+Xk1cwCwSHOOmPSSssZwgjpzG\nodPO3vsMGfRYTwcGbzgdQFFj0qTmvgnM5MHtEy8qCyvM4OsAPnE0zQWn48p7PVdJ\nm6j0H/1BYgVw1KxecIVk/HryoTOkgS9lhLu8iEIyrpAlWascIK7Uw58CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n" diff --git a/local-instance/env/nuxt.env b/local-instance/env/nuxt.env new file mode 100644 index 0000000..a5bb517 --- /dev/null +++ b/local-instance/env/nuxt.env @@ -0,0 +1,10 @@ +# schulcloud-frontend (nuxt-client). The image is an nginx serving the built +# SPA; these values are substituted into its config template at container start +# (see nuxt-client/config/docker/nginx.conf.template). + +PUBLIC_BACKEND_URL=http://localhost:4400/api +LEGACY_CLIENT_URL=http://client:3100 +COLLABORA_OFFICE_URL=http://localhost:9980 +LICENSE_SUMMARY_URL_FOR_CSP=http://localhost:4400 +H5P_SCRIPT_SRC_URLS=http://localhost:4400 +H5P_IMG_SRC_URLS=http://localhost:4400 diff --git a/local-instance/env/shared.env b/local-instance/env/shared.env new file mode 100644 index 0000000..6b64229 --- /dev/null +++ b/local-instance/env/shared.env @@ -0,0 +1,46 @@ +# Shared by every Schulcloud app in the stack. +# Everything in this directory is local-only and deliberately non-secret. + +NODE_ENV=production +NO_COLOR=true +TZ=Europe/Berlin + +# --- identity of this instance ------------------------------------------- +# Mirrors dof_app_deploy/ansible/group_vars/thr/instance_cfg.yml so the local +# instance looks and behaves like schulcloud-thueringen.de. +SC_THEME=thr +SC_SHORTNAME=thr +SC_TITLE=Thüringer Schulcloud +SC_PRODUCTNAME=Thüringer Schulcloud +SC_NAV_TITLE=Thüringer Schulcloud +SC_CONTACT_EMAIL=schulcloud-support@thillm.de +ACCESSIBILITY_REPORT_EMAIL=institut@thillm.de + +# The single origin the browser talks to (the nginx in ./proxy). +SC_DOMAIN=localhost:4400 +HOST=http://localhost:4400 +# The API stamps SC_DOMAIN into every JWT as both issuer and audience. The +# satellite services (file-storage, h5p) validate iss/aud against JWT_DOMAIN, +# which defaults to a bare "localhost" — so without this they reject every +# token the API issued and the homework page's file lookups 401. Keep the two +# in lockstep. +JWT_DOMAIN=localhost:4400 +PUBLIC_BACKEND_URL=http://localhost:4400/api +CTL_TOOLS_BACKEND_URL=http://localhost:4400/api + +# --- infrastructure ------------------------------------------------------ +DB_URL=mongodb://mongo:27017/schulcloud +DB_ENSURE_INDEXES=true +RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 + +# The JWT whitelist. `single` reproduces production: every authenticated +# request re-sets a Valkey key with a JWT_TIMEOUT_SECONDS TTL, and losing that +# key logs the session out. `in-memory` would hide that behaviour entirely, +# which is exactly the behaviour this instance exists to test. +SESSION_VALKEY__MODE=single +SESSION_VALKEY__URI=redis://valkey:6379 +JWT_TIMEOUT_SECONDS=7200 +JWT_SHOW_TIMEOUT_WARNING_SECONDS=3600 + +AES_KEY=randomStringWithAtLeast16Chars; +S3_KEY=abcdefghijklmnop diff --git a/local-instance/env/tldraw.env b/local-instance/env/tldraw.env new file mode 100644 index 0000000..f801717 --- /dev/null +++ b/local-instance/env/tldraw.env @@ -0,0 +1,17 @@ +# tldraw-server: the whiteboard element on column boards. Server + worker. + +REDIS_URL=redis://valkey:6379 +AUTHORIZATION_API_HOST=http://api:3030 + +S3_ACCESS_KEY=miniouser +S3_SECRET_KEY=miniouser +S3_BUCKET=ydocs +S3_ENDPOINT=minio +S3_PORT=9000 +S3_SSL=false + +FEATURE_TLDRAW_ENABLED=true +TLDRAW_WEBSOCKET_URL=ws://localhost:4400/tldraw-server +X_API_ALLOWED_KEYS=tldraw-admin-key +NOT_AUTHENTICATED_REDIRECT_URL=http://localhost:4400/login +LOGGER_LOG_LEVEL=info diff --git a/local-instance/etherpad/APIKEY.txt b/local-instance/etherpad/APIKEY.txt new file mode 100644 index 0000000..6d76661 --- /dev/null +++ b/local-instance/etherpad/APIKEY.txt @@ -0,0 +1 @@ +381d67e6347d235ac9446da3ea10a82efd6f8ae09fa2e90efeda80f82feeb4fd \ No newline at end of file diff --git a/local-instance/proxy/nginx.conf b/local-instance/proxy/nginx.conf new file mode 100644 index 0000000..22ce5eb --- /dev/null +++ b/local-instance/proxy/nginx.conf @@ -0,0 +1,585 @@ +# GENERATED by scripts/gen-proxy-conf.py — do not edit by hand. +# +# One origin in front of the whole stack, the way the real instance is fronted +# by its Kubernetes ingress. The path split between the legacy client and the +# new SPA is not cosmetic: get it wrong and you are testing a different +# application from the one the students use. + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 4400; + server_name localhost; + + # Docker's embedded DNS. Every proxy_pass below goes through a variable so + # that names resolve per request rather than at startup — otherwise this + # container refuses to boot whenever an optional profile (tools, av) is + # down, which is the normal case. + resolver 127.0.0.11 valid=10s ipv6=off; + + # Course files and H5P uploads are large; the ingress allows the same. + client_max_body_size 2600m; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + + # version-aggregator-svc upstream; /serverversion and /nuxtversion are the + # real per-app endpoints and are routed below. + location = /version { + default_type application/json; + return 200 '{"local-instance":true,"see":["/serverversion","/nuxtversion"]}'; + } + + # --- service-owned ingresses and websockets --- + + location /api/v3/file/ { + set $up_api_v3_file file-storage:4444; + proxy_pass http://$up_api_v3_file; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/v3/wopi/ { + set $up_api_v3_wopi file-storage:4444; + proxy_pass http://$up_api_v3_wopi; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/v3/h5p-editor/h5pstatics/ { + set $up_api_v3_h5p_editor_h5pstatics h5p-staticfiles:8080; + rewrite ^/api/v3/h5p-editor/h5pstatics/(.*)$ /h5pstatics/$1 break; + proxy_pass http://$up_api_v3_h5p_editor_h5pstatics; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/v3/h5p-editor/ { + set $up_api_v3_h5p_editor h5p-editor:4448; + proxy_pass http://$up_api_v3_h5p_editor; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/v3/ { + set $up_api_v3 api:3030; + proxy_pass http://$up_api_v3; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /admin/api/v1 { + set $up_admin_api_v1 admin-api:4030; + proxy_pass http://$up_admin_api_v1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /board-collaboration { + set $up_board_collaboration board-collaboration:4450; + proxy_pass http://$up_board_collaboration; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location /tldraw-server { + set $up_tldraw_server tldraw-server:3345; + proxy_pass http://$up_tldraw_server; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location /api/tldraw { + set $up_api_tldraw tldraw-server:3345; + proxy_pass http://$up_api_tldraw; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # --- from dof_app_deploy x_ingress.yml --- + + # default + location / { + set $up_root client:3100; + proxy_pass http://$up_root; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # themes + location /favicon.png { + set $up_favicon_png nuxt:4000; + proxy_pass http://$up_favicon_png; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # bbb_presentation_pdf + location /bbb-presentation.pdf { + set $up_bbb_presentation_pdf nuxt:4000; + proxy_pass http://$up_bbb_presentation_pdf; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # runtime + location /runtime.config.json { + set $up_runtime_config_json nuxt:4000; + proxy_pass http://$up_runtime_config_json; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # metrics + location /metrics { + return 404; + } + + # login + location /login { + set $up_login client:3100; + proxy_pass http://$up_login; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # error + location /error { + set $up_error nuxt:4000; + proxy_pass http://$up_error; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # imprint + location /imprint { + set $up_imprint nuxt:4000; + proxy_pass http://$up_imprint; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # mint-ec + location /mint-ec { + set $up_mint_ec nuxt:4000; + proxy_pass http://$up_mint_ec; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # news + location /news { + set $up_news nuxt:4000; + proxy_pass http://$up_news; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # tasks + location /tasks { + set $up_tasks nuxt:4000; + proxy_pass http://$up_tasks; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # nuxtversion + location /nuxtversion { + set $up_nuxtversion nuxt:4000; + proxy_pass http://$up_nuxtversion; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # content + location /content { + set $up_content nuxt:4000; + proxy_pass http://$up_content; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # _nuxt + location /_nuxt { + set $up__nuxt nuxt:4000; + proxy_pass http://$up__nuxt; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_ldap + location /administration/ldap { + set $up_administration_ldap nuxt:4000; + proxy_pass http://$up_administration_ldap; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_migration + location /administration/migration { + set $up_administration_migration nuxt:4000; + proxy_pass http://$up_administration_migration; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_school-settings + location /administration/school-settings { + set $up_administration_school_settings nuxt:4000; + proxy_pass http://$up_administration_school_settings; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_students + location /administration/students { + set $up_administration_students nuxt:4000; + proxy_pass http://$up_administration_students; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_teachers + location /administration/teachers { + set $up_administration_teachers nuxt:4000; + proxy_pass http://$up_administration_teachers; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_rooms + location /administration/rooms/manage { + set $up_administration_rooms_manage nuxt:4000; + proxy_pass http://$up_administration_rooms_manage; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_groups_classes + location /administration/groups/classes { + set $up_administration_groups_classes nuxt:4000; + proxy_pass http://$up_administration_groups_classes; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # boards + location /boards { + set $up_boards nuxt:4000; + proxy_pass http://$up_boards; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # administration_rooms_new + location /administration/rooms/new { + set $up_administration_rooms_new nuxt:4000; + proxy_pass http://$up_administration_rooms_new; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # rooms-overview + location /rooms-overview { + set $up_rooms_overview nuxt:4000; + proxy_pass http://$up_rooms_overview; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # dashboard + location /dashboard { + set $up_dashboard nuxt:4000; + proxy_pass http://$up_dashboard; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # rooms + location /rooms { + set $up_rooms nuxt:4000; + proxy_pass http://$up_rooms; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # folder + location /folder { + set $up_folder nuxt:4000; + proxy_pass http://$up_folder; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # collabora + location /collabora { + set $up_collabora nuxt:4000; + proxy_pass http://$up_collabora; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # h5p-player + location /h5p/player { + set $up_h5p_player nuxt:4000; + proxy_pass http://$up_h5p_player; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # h5p-editor + location /h5p/editor { + set $up_h5p_editor nuxt:4000; + proxy_pass http://$up_h5p_editor; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # migration + location /migration { + set $up_migration nuxt:4000; + proxy_pass http://$up_migration; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # tools_context + location /tools/context { + set $up_tools_context nuxt:4000; + proxy_pass http://$up_tools_context; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # media_shelf + location /media-shelf { + set $up_media_shelf nuxt:4000; + proxy_pass http://$up_media_shelf; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # licenses + location /licenses { + set $up_licenses nuxt:4000; + proxy_pass http://$up_licenses; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # registration-external-members + location /registration-external-members { + set $up_registration_external_members nuxt:4000; + proxy_pass http://$up_registration_external_members; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # security + location /system/security { + set $up_system_security nuxt:4000; + proxy_pass http://$up_system_security; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api + location /api { + return 404; + } + + # api_v1_roster + location /api/v1/roster { + return 404; + } + + # api_v1_consentVersions + location /api/v1/consentVersions { + set $up_api_v1_consentVersions api:3030; + proxy_pass http://$up_api_v1_consentVersions; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api_v1_ldap-config + location /api/v1/ldap-config { + set $up_api_v1_ldap_config api:3030; + proxy_pass http://$up_api_v1_ldap_config; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api_version + location /serverversion { + set $up_serverversion api:3030; + proxy_pass http://$up_serverversion; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api_v1_courses + location /api/v1/courses { + set $up_api_v1_courses api:3030; + proxy_pass http://$up_api_v1_courses; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api_v1_users + location /api/v1/users { + set $up_api_v1_users api:3030; + proxy_pass http://$up_api_v1_users; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # api_v1_classes + location /api/v1/classes { + set $up_api_v1_classes api:3030; + proxy_pass http://$up_api_v1_classes; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ^~ /etherpad/admin { return 404; } + location ^~ /etherpad/stats { return 404; } + + location /etherpad/socket.io { + set $up_etherpad etherpad:9001; + rewrite /etherpad/socket.io/(.*) /socket.io/$1 break; + proxy_pass http://$up_etherpad; + proxy_redirect / /etherpad/; + proxy_set_header Host $http_host; + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location /etherpad { + set $up_etherpad etherpad:9001; + rewrite ^/etherpad$ /etherpad/ permanent; + rewrite /etherpad/(.*) /$1 break; + proxy_pass http://$up_etherpad; + proxy_pass_header Server; + proxy_redirect / /etherpad/; + proxy_set_header Host $host; + proxy_buffering off; + } + + # version_aggregator + location /version { + return 404; + } +} diff --git a/local-instance/scripts/gen-proxy-conf.py b/local-instance/scripts/gen-proxy-conf.py new file mode 100644 index 0000000..1544a2a --- /dev/null +++ b/local-instance/scripts/gen-proxy-conf.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Regenerate proxy/nginx.conf from the real deployment's ingress table. + +The live instance is a Kubernetes ingress that splits one origin across the +legacy client, the new SPA and several APIs. Which path goes where is not +documented in prose — it is the table in +dof_app_deploy/ansible/group_vars/all/x_ingress.yml plus a per-path ingress in +each service repo. Transcribing 46 rules by hand invites exactly the drift that +would make this instance lie about the real one, so we generate them. + +Usage: python3 scripts/gen-proxy-conf.py > proxy/nginx.conf +Needs the upstream clones in ../vendor (see README.md). +""" +import pathlib +import sys + +try: + import yaml +except ImportError: + sys.exit('needs PyYAML: pip install pyyaml') + +VENDOR = pathlib.Path(__file__).resolve().parents[2] / 'vendor' +INGRESS = VENDOR / 'dof_app_deploy/ansible/group_vars/all/x_ingress.yml' + +# Kubernetes service name -> compose upstream. None means the real deployment +# deliberately 404s that path. +UPSTREAM = { + 'client-svc': 'client:3100', + 'nuxtclient-svc': 'nuxt:4000', + 'api-svc': 'api:3030', + 'default-backend-404-svc': None, + 'version-aggregator-svc': None, # replaced by our own /version below + None: None, +} + +# Routes that live in the individual service repos' own ingress templates +# rather than the shared table, plus the two websocket endpoints. +# (path, upstream, websocket, rewrite-or-None) +EXTRA = [ + ('/api/v3/file/', 'file-storage:4444', False, None), + ('/api/v3/wopi/', 'file-storage:4444', False, None), + ('/api/v3/h5p-editor/h5pstatics/', 'h5p-staticfiles:8080', False, + '^/api/v3/h5p-editor/h5pstatics/(.*)$ /h5pstatics/$1'), + ('/api/v3/h5p-editor/', 'h5p-editor:4448', False, None), + ('/api/v3/', 'api:3030', False, None), + ('/admin/api/v1', 'admin-api:4030', False, None), + ('/board-collaboration', 'board-collaboration:4450', True, None), + ('/tldraw-server', 'tldraw-server:3345', True, None), + ('/api/tldraw', 'tldraw-server:3345', False, None), +] + +PREAMBLE = '''# GENERATED by scripts/gen-proxy-conf.py — do not edit by hand. +# +# One origin in front of the whole stack, the way the real instance is fronted +# by its Kubernetes ingress. The path split between the legacy client and the +# new SPA is not cosmetic: get it wrong and you are testing a different +# application from the one the students use. + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 4400; + server_name localhost; + + # Docker's embedded DNS. Every proxy_pass below goes through a variable so + # that names resolve per request rather than at startup — otherwise this + # container refuses to boot whenever an optional profile (tools, av) is + # down, which is the normal case. + resolver 127.0.0.11 valid=10s ipv6=off; + + # Course files and H5P uploads are large; the ingress allows the same. + client_max_body_size 2600m; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + + # version-aggregator-svc upstream; /serverversion and /nuxtversion are the + # real per-app endpoints and are routed below. + location = /version { + default_type application/json; + return 200 '{"local-instance":true,"see":["/serverversion","/nuxtversion"]}'; + } +''' + +COMMON = ''' proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +''' + +WS = ''' proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; +''' + +GONE = ''' + location {path} {{ + return 404; + }} +''' + +# Etherpad is mounted under a prefix it knows nothing about, so the deployment +# runs a dedicated nginx in front of it that rewrites the prefix away and +# proxies socket.io separately. Copied from +# dof_app_deploy/ansible/roles/dof_etherpad/templates/nginx-configmap-files.yml.j2 +# — a plain proxy_pass gets you a pad that loads and then never syncs. +ETHERPAD = ''' + location ^~ /etherpad/admin { return 404; } + location ^~ /etherpad/stats { return 404; } + + location /etherpad/socket.io { + set $up_etherpad etherpad:9001; + rewrite /etherpad/socket.io/(.*) /socket.io/$1 break; + proxy_pass http://$up_etherpad; + proxy_redirect / /etherpad/; + proxy_set_header Host $http_host; + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location /etherpad { + set $up_etherpad etherpad:9001; + rewrite ^/etherpad$ /etherpad/ permanent; + rewrite /etherpad/(.*) /$1 break; + proxy_pass http://$up_etherpad; + proxy_pass_header Server; + proxy_redirect / /etherpad/; + proxy_set_header Host $host; + proxy_buffering off; + } +''' + + +def block(path: str, upstream: str, ws: bool, rewrite: str | None, var: str) -> str: + out = [f'\n\tlocation {path} {{\n', f'\t\tset ${var} {upstream};\n'] + if rewrite: + out.append(f'\t\trewrite {rewrite} break;\n') + out.append(f'\t\tproxy_pass http://${var};\n') + out.append(COMMON) + if ws: + out.append(WS) + out.append('\t}\n') + return ''.join(out) + + +def varname(path: str) -> str: + safe = ''.join(c if c.isalnum() else '_' for c in path.strip('/')) or 'root' + return f'up_{safe}' + + +def main() -> None: + table = yaml.safe_load(INGRESS.read_text())['default_ingress'] + + seen: set[str] = set() + out = [PREAMBLE] + + out.append('\n\t# --- service-owned ingresses and websockets ---\n') + for path, upstream, ws, rewrite in EXTRA: + seen.add(path) + out.append(block(path, upstream, ws, rewrite, varname(path))) + + out.append('\n\t# --- from dof_app_deploy x_ingress.yml ---\n') + for name, entry in table.items(): + path = entry.get('path') + if path is None or path in seen: + continue + seen.add(path) + if path == '/etherpad': + out.append(ETHERPAD) + continue + upstream = UPSTREAM.get(entry.get('serviceName'), 'MISSING') + if upstream == 'MISSING': + sys.exit(f'unknown serviceName for {name}: {entry.get("serviceName")}') + out.append(f'\n\t# {name}') + out.append(GONE.format(path=path) if upstream is None + else block(path, upstream, False, None, varname(path))) + + out.append('}\n') + sys.stdout.write(''.join(out)) + + +if __name__ == '__main__': + main() diff --git a/local-instance/scripts/minio-init.sh b/local-instance/scripts/minio-init.sh new file mode 100755 index 0000000..a3b74d6 --- /dev/null +++ b/local-instance/scripts/minio-init.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Creates the buckets each service expects. MinIO does not create them on +# demand: file-storage reports a generic 500 on upload if its bucket is +# missing, and the h5p library job fails halfway through. +set -eu + +mc alias set local http://minio:9000 miniouser miniouser + +for bucket in \ + schulcloud ` # files-storage (the /api/v3/file API)` \ + h5p-content-bucket ` # h5p-editor content` \ + h5p-library-bucket ` # h5p content types` \ + ydocs ` # tldraw whiteboard documents` \ + fwu-content # FWU media, unused but cheap to create +do + mc mb --ignore-existing "local/$bucket" +done + +mc ls local diff --git a/local-instance/scripts/seed.sh b/local-instance/scripts/seed.sh new file mode 100755 index 0000000..62038b3 --- /dev/null +++ b/local-instance/scripts/seed.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Seed the local instance with the upstream demo school. +# +# This is what the real deployment's init job does (dof_app_deploy +# .../schulcloud-server-init/templates/configmap_file_init.yml.j2): it asks the +# management app to load backup/setup/*.json, which ships inside the server +# image. Safe to re-run — collections are replaced, not appended to. +set -euo pipefail + +cd "$(dirname "$0")/.." + +MGMT=http://127.0.0.1:3333/api/management/database +COMPOSE=(docker compose) + +echo "==> waiting for the management app" +for _ in $(seq 1 60); do + if curl -fsS -o /dev/null "$MGMT/../../docs" 2>/dev/null || curl -fsS -o /dev/null -X POST "$MGMT/sync-indexes" 2>/dev/null; then + break + fi + sleep 5 +done + +echo "==> seeding collections (this takes a minute or two)" +curl -fsS --retry 30 --retry-all-errors --retry-delay 10 \ + -X POST "$MGMT/seed?with-indexes=true" >/dev/null +echo " done" + +# The legacy file service (course/topic attachments, as opposed to the newer +# /api/v3/file API) reads its S3 credentials from a storageproviders document +# rather than from the environment, and there is deliberately no seed data for +# it. Without this, legacy uploads fail with a provider-not-found error. +echo "==> registering MinIO as the legacy storage provider" +S3_KEY=$(grep -E '^S3_KEY=' env/shared.env | cut -d= -f2-) +SECRET=$(curl -fsS -X POST "$MGMT/encrypt-plain-text" \ + -H 'Content-Type: application/json' \ + -d "$(printf '{"plainText":"miniouser","key":"%s"}' "$S3_KEY")") + +"${COMPOSE[@]}" exec -T mongo mongosh schulcloud --quiet --eval " + const id = ObjectId('62949a4003839b6162aa566b'); + db.storageproviders.replaceOne({ _id: id }, { + _id: id, isShared: true, region: 'eu-central-1', type: 'S3', + endpointUrl: 'http://minio:9000', + accessKeyId: 'miniouser', + secretAccessKey: '$SECRET', + maxBuckets: 150, freeBuckets: 138, + createdAt: new Date(), updatedAt: new Date(), __v: 0, + }, { upsert: true }); + const r = db.schools.updateMany({}, { \$set: { storageProvider: id } }); + print(' schools linked to the provider: ' + r.modifiedCount); +" + +cat <<'ACCOUNTS' + +==> ready — http://localhost:4400 + + Seeded accounts (the demo password differs by account — upstream quirk): + + demo-schueler@schul-cloud.org student Fritz Schmidt schulcloud + ^ has graded submissions, incl. a feedback-only and a 100% one + demo-lehrer@schul-cloud.org teacher Erika Meier schulcloud + klara.fall@schul-cloud.org teacher Klara Fall Schulcloud1! + ^ owns Fritz's graded Biologie submissions + lehrer@schul-cloud.org teacher Cord Carl Schulcloud1! + admin@schul-cloud.org admin Thorsten Test Schulcloud1! + *.qa@schul-cloud.org various Schulcloud1qa! + + Sign in as the teacher to grade, as the student to see what grading looks + like from the side our MCP server reads. +ACCOUNTS