diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 906401e51..e27484631 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,11 @@ updates: reviewers: - canihavesomecoffee - thealphadollar +- package-ecosystem: npm + directory: "/web" + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + minor-and-patch: + update-types: ["minor", "patch"] diff --git a/.github/workflows/sp-deployment-pipeline.yml b/.github/workflows/sp-deployment-pipeline.yml index 39bf36aca..df04668f1 100644 --- a/.github/workflows/sp-deployment-pipeline.yml +++ b/.github/workflows/sp-deployment-pipeline.yml @@ -91,6 +91,38 @@ jobs: sudo systemctl reload platform fi + # The console is a static build, so it is compiled here and only the + # output is shipped; the VM needs no Node toolchain. This runs after + # the code deploy so the target directory exists. + - name: Build web console + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Compile console + working-directory: web + run: | + npm ci --ignore-scripts + npm run build:app + + - name: Ship console + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: ${{ vars.PLATFORM_DOMAIN }} + username: ${{ vars.SSH_USER }} + key: ${{ secrets.SSH_KEY_PRIVATE }} + port: 22 + # Lands at /web/app/, which is what the nginx + # root expects: the directory name has to match the /app/ URL. + source: "web/dist/**" + target: ${{ env.INSTALL_FOLDER }}/web/app + strip_components: 2 + overwrite: true + - name: Verify deployment id: health_check uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100644 index 000000000..1e9970ca3 --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,34 @@ +name: Web console + +# Only runs when the console changes; the Python checks in main.yml are +# unaffected by anything under web/. +on: + push: + branches: [master] + paths: ['web/**', '.github/workflows/web.yml'] + pull_request: + paths: ['web/**', '.github/workflows/web.yml'] + +defaults: + run: + working-directory: web + +jobs: + web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + # --ignore-scripts: nothing here needs a lifecycle script, and not + # running them stops a compromised dependency executing during + # install. + - run: npm ci --ignore-scripts + - run: npm run lint + # The local binary, not npx, which would fetch on demand. + - run: ./node_modules/.bin/tsc -b + - run: npm run build:app + - run: npm audit --audit-level=high diff --git a/install/install.sh b/install/install.sh index d44cce1fc..6a8a904f5 100644 --- a/install/install.sh +++ b/install/install.sh @@ -240,6 +240,9 @@ GITHUB_CLIENT_KEY = '${github_client_secret_key}' INSTALL_FOLDER = '${root_dir}' SAMPLE_REPOSITORY = '${sample_repository}' SESSION_COOKIE_PATH = '/' +# Where the web console is served. Recovery emails link here instead of the +# classic pages when it is set; leave empty to keep the old behaviour. +CONSOLE_URL = 'https://${config_server_name}/app' FTP_PORT = $ftp_port MAX_CONTENT_LENGTH = $max_content_length MIN_PWD_LEN = $min_pwd_len diff --git a/install/nginx.conf b/install/nginx.conf index 8ee0a8b37..21381b764 100644 --- a/install/nginx.conf +++ b/install/nginx.conf @@ -33,12 +33,36 @@ server { ssl_prefer_server_ciphers on; ssl_ecdh_curve secp384r1; + # Samples are media files and the API accepts up to MAX_CONTENT_LENGTH + # (512 MB). Without this nginx applies its 1 MB default and rejects an + # upload with its own 413 before the application ever sees it. + client_max_body_size 512m; + location ^~ /static/ { # Serve static files with Nginx include /etc/nginx/mime.types; root NGINX_DIR; } + # Web console: a static build served straight off disk. Everything it + # talks to is under /api on this same host, so no CORS is involved. + # + # The directory is named to match the URL so plain root resolution + # applies. "alias" with try_files appends the whole URI to the alias + # rather than the remainder, which silently breaks the SPA fallback. + location ^~ /app/ { + include /etc/nginx/mime.types; + root NGINX_DIR/web; + try_files $uri $uri/ /app/index.html; + } + + location ^~ /app/assets/ { + include /etc/nginx/mime.types; + root NGINX_DIR/web; + expires 30d; # hashed filenames + add_header Cache-Control "public, immutable"; + } + location / { try_files $uri @proxy_to_app; } diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..819f8de3f --- /dev/null +++ b/web/README.md @@ -0,0 +1,42 @@ +# Web console + +The platform's browser client. A Vite + React single-page app that talks to +the `mod_api` blueprint at `/api/v1` and nothing else — no server of its own, +no session cookie, no template rendering. + +## Working on it + +```sh +npm install +npm run dev # http://localhost:5173, API proxied to the backend +``` + +`npm run lint` and `npx tsc -b` are what CI checks. + +## Building for the platform + +```sh +npm run build:app # static dist/, based at /app/ +``` + +`--base=/app/` matters: the console is served from a path on the platform's own +domain, not from a root. Nginx maps `/app/` at that build output — see the +`location ^~ /app/` block in `install/nginx.conf`. + +Being on the same origin as the API is deliberate. It means no CORS, no second +certificate, and no third party in the path when someone signs in. + +## Deployment + +The console is built by `sp-deployment-pipeline.yml` on the runner and only +the build output is copied to the VM, so the server needs no Node toolchain. +It lands in `web/app/` — the directory name matches the `/app/` URL so nginx +can serve it with a plain `root`. Nothing here is served by Flask. + +Set `CONSOLE_URL` in `config.py` so password-reset emails link to the console +rather than the classic pages. `install.sh` writes it for new installs. + +--- + +Built by Pulkit Chauhan ([@pulk17](https://github.com/pulk17)) during Google +Summer of Code 2026.