Skip to content

Feat/working branch - #159

Open
simonvanlierde wants to merge 1350 commits into
mainfrom
feat/working-branch
Open

Feat/working branch#159
simonvanlierde wants to merge 1350 commits into
mainfrom
feat/working-branch

Conversation

@simonvanlierde

Copy link
Copy Markdown
Contributor

feat/working-branch → main: security hardening, MFA, brand refresh, and repo restructure

TL;DR

A ~2.5-month working branch (728 commits) intended as a single squash-merge. It hardens the auth/account-security stack, adds TOTP MFA with recovery codes, removes dead data-model surface (organizations, newsletter), rebuilds the public homepage around live stats, rebrands toward R9lab / ReLab, and restructures the two frontend subrepos.

Reading the diff: the raw stat (1507 files, +115k/−170k) is misleading. Most of it is two directory renames (frontend-webwww, frontend-appapp), regenerated files (api.generated.ts, OpenAPI schema), and brand assets. The real logic change surface is a fraction of that — the themes below are where it lives.

Why squash

The branch grew organically over ~2.5 months, so its intermediate history is noisy. Squashing collapses it into one clean, coherent commit on main. The changes were checked against main across multiple angles — correctness, security, test coverage, performance, and over-engineering — before landing.


Highlights by area

🔐 Auth & account security (largest area)

The auth module was substantially reworked — service layer split into focused units (lifecycle, login_flow, mfa_service, session_flow, token_store, email/*, oauth/*) and hardened end to end.

  • TOTP MFA — enrollment + challenge flow, MFA account management in the profile, and MFA-completion enforcement on OAuth login handoffs.
  • Recovery codes — accept recovery codes for MFA login and for MFA disable; replaced the old email-based MFA reset; hardened code lifecycle.
  • OAuth link/unlink — step-up re-auth required to unlink a social login (password prompt in-app), provider-bound OAuth state JWTs, email notifications on link/unlink and welcome mail for social signups.
  • Non-enumerable registration — registration no longer reveals whether an email exists, and auto-login after signup was removed to keep the flow non-enumerable.
  • Session/token lifecycle — cookie names single-sourced, sessions revoked before account deletion, refresh tokens keyed by fingerprint, closed token-lifecycle gaps, caches bypassed for credentialed/no-store responses.
  • Passwords — common-password blocklist (3000 entries) via a dedicated checker; password-hashing service extracted.
  • Audit logging — authentication, session, authorization-denial, and rate-limit events logged with sanitization.

🗄️ Data model & migrations

17 new migrations. Notable removals and reshapes:

  • Removed organizations and newsletter subscribers (dead/unused surface).
  • Split product roles; normalized rows before the role CHECK constraint.
  • Circularity columns → JSONB; removed product dismantling notes/times.
  • Upload quota: per-user quota ledger + upload size metadata, enforced on product uploads.
  • Email canonicalization (email_canonical), has_usable_password, MFA/recovery tables.
  • Ops: autovacuum tuning for high-churn tables, FK indexes on material↔product links.

📁 File storage hardening

Paginated media lists, real zip-size enforcement, malware scanning extended to device thumbnails, quota owner keying fixed, sensitive-key rejection in local storage, JSON-recursion guard, dotted filenames accepted, uploaded filenames validated against MIME type.

🌐 Public site (www, formerly frontend-web)

Homepage rebuilt: new Hero, SiteFooter, privacy page, and a StatsPanel fed by new public stats API (/stats/totals, /stats/categories, /stats/series) — monthly activity chart with per-category part counts. New brand/token CSS, theme handling, 404 page, and a Vitest suite for the new components.

📷 RPi camera plugin

WebSocket relay bounded against unresponsive devices, camera-flapping-to-offline fixed, livestream/recording lifecycle corrected, device key kept on the LAN, response ownership verified, device-assertion lifetime capped, circuit breaker made Redis-only and atomic.

📱 App (React Native, formerly frontend-app)

MFA challenge screen + pending-login routing, OAuth callback via URL fragment, new-product drafts can set type/material before first save, "already live" stream dialog, single-flight auth/MFA submits, gallery no longer silently deletes unrenderable images, static-background refactor (parallax scaffolding removed), plus a large sweep of review-driven bug fixes.

🎨 Brand → R9lab / ReLab

R9lab logo/wordmark/flask marks wired into www, docs, and app UIs; email templates rebranded with hosted wordmark; README wordmark. Asset generators live in assets/logo-src/.

Copy pass across the app UI, docs, and www: unified sign-in/sign-out terminology, sentence-case titles and buttons, plainer error and empty-state messages, and less boilerplate — with tests and e2e specs updated to match.

Note: this is the incremental brand pass. A full code/name rename (siteMeta, package names, app name) is deliberately deferred.

🏗️ Infra / CI / deploy

  • Opt-in restic backups and Google/GitHub OAuth; secret inventory split into required/optional; startup warning when secrets still hold deploy placeholders.
  • ClamAV service added to the deploy stack (topology documented).
  • CI: required-jobs gate derived from needs, OpenAPI/app-codegen freshness gates, JUnit → Codecov Test Analytics, per-PR RN a11y lint, Playwright browser caching.
  • Supply-chain: reject Python deps newer than 3 days; hardened HTTP client for auth integrations; DB/Redis TLS hardening; trusted-proxy CIDR validation; rightmost X-Forwarded-For.
  • OpenSSF Scorecard badge; Cloudflare edge rules for the public stats endpoint.

🧭 Repo restructure

  • frontend-webwww
  • frontend-appapp

Renames (with history preserved) touch devcontainers, CI, docs, and configs — the bulk of the file count.


Verification

Each subrepo's loop (just fixjust checkjust test) was run as changes landed. Beyond the test suites, the diff was reviewed across correctness, security, test-coverage, performance, and simplification angles. New migrations include a downgrade/upgrade round-trip test, and new logic (stats, auth flows, storage, MFA) ships with regression tests.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread backend/Dockerfile.backups Fixed
Comment thread app/src/services/api/auth/authUser.ts Fixed
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Comment thread app/scripts/redact_api.mjs Fixed
Comment thread app/src/features/auth/useOAuthLogin.ts Fixed
Comment thread docs/e2e/api-reference.spec.ts Fixed
Comment thread app/src/services/api/auth/authUser.ts Fixed

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread backend/app/core/lifecycle.py Fixed
Comment thread backend/app/api/plugins/rpi_cam/websocket/connection_manager.py Fixed
Comment thread backend/app/api/plugins/rpi_cam/websocket/connection_manager.py Fixed
Comment thread backend/app/api/auth/services/email/providers.py Fixed
Comment thread backend/app/api/auth/services/email/providers.py Fixed
Comment thread backend/app/api/auth/services/email/providers.py Fixed
Comment thread backend/Dockerfile.backups Fixed
simonvanlierde added a commit that referenced this pull request Jul 12, 2026
- guard the getUser slot by sequence instead of promise identity (CodeQL missing-await)
- read api.generated.ts directly instead of existsSync-then-read (CodeQL TOCTOU)
- drop redundant location.hash guard and document server-side OAuth enforcement (CodeQL bypass FP)
- anchor the Scalar request-blocking regex in docs e2e (CodeQL regex anchor)
- catch Exception instead of BaseException in shutdown steps (code-quality)
- add stamp-freshness HEALTHCHECK to the backups image (Trivy DS-0026)
- use docstring bodies for protocol stubs instead of bare ellipses (code-quality)
- remove unused Union import from alembic migration (code-quality)
simonvanlierde added a commit that referenced this pull request Jul 12, 2026
- break auth-services<->core import cycle: inline redis bool coercion in blocklist_store
- break auth models<->user_database cycle: move get_user_db/get_auth_async_session to user_manager
- rpi-cam ws auth: hash camera rate-limit bucket via rate_limit_bucket_key like all other callers
- rpi-cam models: replace non-exhaustive match with dict lookup in to_http_error
- connection_manager: sanitize camera_id in logs; pairing: drop unused _STATUS_WAITING
- support_services/test_upload_security: replace del-parameter idiom (noqa B027 / pass)
- test_email_providers: match provider URLs with startswith instead of substring
- codeql config: enable AlertSuppression packs; exclude py/unused-import (ruff owns it) and informational py/cyclic-import
- suppress verified false positives inline (token fingerprint sha256, secret-name logging, masked email logging)
Comment thread backend/app/api/auth/services/email/service.py Dismissed
Comment thread backend/app/api/auth/services/email/service.py Dismissed
Comment thread backend/app/api/auth/services/rate_limiter.py Fixed
Comment thread backend/app/api/auth/services/token_store.py Dismissed
Comment thread backend/app/core/secrets.py Dismissed
Comment thread scripts/env_policy.py Dismissed
Comment thread scripts/env_policy.py Dismissed
Comment thread backend/app/api/auth/services/blocklist_store.py Dismissed
Comment thread backend/tests/unit/plugins/rpi_cam/conftest.py Dismissed
Comment thread backend/alembic/env.py Dismissed
Comment thread backend/app/api/auth/services/oauth/login.py Dismissed
simonvanlierde added a commit that referenced this pull request Jul 14, 2026
- guard the getUser slot by sequence instead of promise identity (CodeQL missing-await)
- read api.generated.ts directly instead of existsSync-then-read (CodeQL TOCTOU)
- drop redundant location.hash guard and document server-side OAuth enforcement (CodeQL bypass FP)
- anchor the Scalar request-blocking regex in docs e2e (CodeQL regex anchor)
- catch Exception instead of BaseException in shutdown steps (code-quality)
- add stamp-freshness HEALTHCHECK to the backups image (Trivy DS-0026)
- use docstring bodies for protocol stubs instead of bare ellipses (code-quality)
- remove unused Union import from alembic migration (code-quality)
simonvanlierde added a commit that referenced this pull request Jul 14, 2026
- break auth-services<->core import cycle: inline redis bool coercion in blocklist_store
- break auth models<->user_database cycle: move get_user_db/get_auth_async_session to user_manager
- rpi-cam ws auth: hash camera rate-limit bucket via rate_limit_bucket_key like all other callers
- rpi-cam models: replace non-exhaustive match with dict lookup in to_http_error
- connection_manager: sanitize camera_id in logs; pairing: drop unused _STATUS_WAITING
- support_services/test_upload_security: replace del-parameter idiom (noqa B027 / pass)
- test_email_providers: match provider URLs with startswith instead of substring
- codeql config: enable AlertSuppression packs; exclude py/unused-import (ruff owns it) and informational py/cyclic-import
- suppress verified false positives inline (token fingerprint sha256, secret-name logging, masked email logging)
Comment thread backend/tests/unit/core/test_image_processing.py Fixed
Comment thread backend/tests/unit/core/test_image_processing.py Fixed
- drop unused AppTokens.spacing and constants.spacing.{lg,xl,xxl}
- drop unused breakpoints.desktop and radius.{md,lg} aliases
- migrate radius.sm call sites to radius.control, drop the sm alias
- stop exporting SEMANTIC_COLORS, lightTheme, and darkTheme from the
  public theme barrel; import lightTheme/darkTheme from theme/themes
  directly in the tests that need theme instances
- Extend ErrorState with title/actionLabel/iconColor props
- Rewrite ProductPageErrorState on top of ErrorState, dropping the
  themeColors prop in favor of useAppTheme() internally
- Update ProductDetailScreen's caller to drop themeColors
- Delete dead centerState/centerText/subtleCenterText styles from
  States.tsx (slowLoadingContainer kept, still used for positioning)

LivePreview.tsx's PreviewErrorOverlay is left as-is: it's an
absolute-positioned overlay inside a fixed aspect-[4/3] card
(in-card pattern), not the full-height centered pattern ErrorState
covers.
…components out of base

- merge SectionNav's component code into SectionNavLayout as a non-exported
  helper and delete SectionNav.tsx, its sole importer
- move SVGCube, LocalizedFloatInput, ZoomableImage, and SpecFacts (with their
  co-located tests) into product/ alongside their single consumers
- Convert all 32 `variant="plain"` AppText call sites to body/label/data
  per the default mapping, with reasoned deviations:
  - profile avatar/username/joined text, dialog helper/toast/error text,
    tooltip modal, form field labels, card titles, and metadata rows take
    `body` — none are chip/pill/badge interiors, so the default rung applies
    even where callers already pin their own fontSize
  - HeaderRightPill, Chip, ActiveStreamBanner's product-name pill,
    ImagePlaceholder's caption, InfoTooltip's tight single-line floating
    bubble, LocalizedFloatInput's unit suffix, and ProductTags/ProductCard's
    compact metadata take `label` — tight pill/chip/badge-style layouts
  - OtpInput's digit cells and ProductTags/ProductCard's numeric readouts
    take `data` — numbers/counts per the default mapping
- Remove `'plain'` from AppText's Variant union, the
  `variant === 'plain' ? undefined :` branch, and the migration comment;
  drop the AppText test's plain-variant case (data variant already covers
  the tabular-nums path)
- Strip ui/text.tsx's unused cva variant table (no call site passed a
  variant) down to a single base class string; TextClassContext stays,
  still consumed by ui/button, ui/badge, ui/toggle, ui/toggle-group
- watchdog checks every stack service, all three timers, and empty RELAB_PING_* URLs,
  with time-bounded docker calls and compose stderr kept out of container ids
- run_scheduled.sh traps TERM so a killed job still posts its failure ping
- restore-check runs at 06:00, clear of the backup prune, with a deterministic
  container name the unit reaps on timeout
- backup unit retries are bounded (3 per day) instead of forever
- installer escapes sed replacements and warns when ping URLs are left empty
- deploy env reader strips quotes and comments like the compose reader does
- test the traversal escape at the path a real escape would write
… key

- install guide: replace the deleted backup-unit recipe with timers-install
- drop the backups profile from every up example; backup-run seeds the first snapshot
- document TELEMETRY_EDGE_KEY next to the token everywhere the token is set
- cutover: host-level checks for log rotation and telemetry leaving the host
- DEPLOY-PROD: daemon.json log-opts fallback, what the watchdog now checks, and the
  rule that its local checks stay until the central alerts exist
- security: the log-sanitization guarantee stops at the backend's own logs
- remove MONITORING-DESIGN.md and MONITORING-HANDOVER.md; the architecture is
  CMLPlatform/monitoring ADR 0002 and the open work is its docs/HANDOVER.md
- the one Relab-side constraint, keeping the local watchdog checks until the central
  alerts exist, already lives in DEPLOY-PROD.md
- add create_password_user to the auth integration shared helpers and use it
  at the 19 sites that hashed a password inline
- add build_mfa_user for the 13 MFA flow tests that stubbed the same
  user/user_manager pair
- parametrize the seven assertion-claim rejection tests behind one fixture,
  and pin each case to its own rejection message; three of them previously
  asserted no message at all
- camerasQuery factory for 18 useCamerasQuery stubs
- renderAssociations helper per describe in the OAuth associations tests
- expectAlert helper for six of the login alert assertions
- renderMulti/renderSingle for the filter modal prop lists
- one shared twoImages fixture in the gallery lightbox tests
- hoist the no-op delete-mutation mock into beforeEach
- hlsOpts factory for the web HLS helper options
- read RELAB_PING_* from the unit environment when the seeded 0600 file is
  unreadable; systemd already loaded it as root, so a filled-in URL no longer
  alerts as empty forever
- seed /etc/relab/relab.env owned by the deploy user and fix ownership on
  re-install, which also lets the post-install emptiness warning actually run
- keep compose stderr out of the config --services capture so a warning cannot
  word-split into phantom "not running" services
- factor the service-state and ping-URL checks into reducers and cover them in
  test_ops.sh, including the unreadable-file regression
- treat a whitespace-only ping URL as empty, matching the install-time check
- remove a leftover relab-backup container before backup-run so a host crash
  cannot wedge the deterministic name
- strip quotes before the rclone: prefix test in the offsite-remote warning
- give the watchdog timer Persistent=true like its two siblings
- run Alloy with cap_drop ALL plus SYSLOG and DAC_READ_SEARCH and
  no-new-privileges: root keeps its read-only host reads without the default
  set's write-side DAC_OVERRIDE
- give the GPU exporter a read-only root, an empty capability set and
  no-new-privileges
- feed the Cloudflare API token to curl over stdin instead of argv, where
  /proc exposes it to every local user
- scope the edge rate-limit claim to the auth endpoints and name both
  Cloudflare roots; upload and RPi limits are app-layer and fail open
- describe Alloy's socket-proxy wiring and dropped capability set instead of a
  socket mount it never had
- list the egress network's real members in the compose comment
The Alloy overlay, its config and the scheduled-job wrapper are now taken
byte-identical from CMLPlatform/monitoring at v0.2.0. Everything that differs
between projects arrives as an environment variable, so a fix upstream reaches
every project host unchanged.

- rename compose.logging.alloy.yaml and compose.gpu.yaml to
  compose.telemetry.yml and compose.telemetry.gpu.yml
- parameterise the project identity: PROJECT replaces the hardcoded `relab` in
  the Alloy config and the API's resource attributes, committed per environment
  in deploy/env/ because it identifies the project, not the host
- rename the WAF-skip header to X-Telemetry-Key and the dead-man's-switch
  variables to PING_*, both without a compatibility shim; deploy/CUTOVER-PROD.md
  carries the manual order, which for the zone-global Cloudflare rule is deploy
  both hosts first and apply second
- rename the backup-run and backup-restore-smoke recipes to backup and
  restore-check so the vendored wrapper resolves a job to a recipe by name
- run the root env policy check on `up`: the vendored overlay cannot hard-require
  TELEMETRY_EDGE_KEY, and an empty key loses the WAF exemption silently
- record the vendored files and their upstream tag in deploy/README.md
- cover the job-to-recipe resolution in scripts/test_ops.sh, including that every
  job the systemd units invoke exists as a recipe
- reference `expo/types` from the tracked `uniwind-env.d.ts`, so the app
  typecheck resolves the `global.css` side-effect import without the
  gitignored `expo-env.d.ts` that only exists on a developer machine
- set `ENVIRONMENT=testing` on the email-template recipes; `CoreSettings`
  refuses to build without it, so `just check` died in `email-check`
- reformat the deploy docs to what mdformat produces, and tell shellcheck
  the systemd job loop splits on words on purpose
- bump the pinned node, python, uv, and postgres base digests, picking up
  the util-linux fixes behind 39 HIGH findings per image
- drop the unused pip from the backend runtime stages and upgrade openssl
  in the backups image, which the postgres base still ships unpatched
- allowlist the caddy Go binary CVEs with the same expiry-and-statement
  pattern as the existing entries; upstream ships no rebuilt release
- allow the two image-size advisories in dependency review: they cover
  every published version, and metro only ever parses local dev files
- fix two asserts that reopened an image inside the assertion, and
  explain the empty `except FileNotFoundError` in the brand asset sync
GHSA-5p4m-2wfm-xmqj landed after the previous run and dependency review
flagged the transitive js-yaml 4.3.0. Scope the override to the 4.x range;
the 3.15.1 already in the tree carries the fix.
GHSA-vcc3-ghjq-m6fr reaches the app through query-string@7, which every
expo-router release still pins, and the patched release is ESM-only so an
override breaks routing at runtime. Record why each allowed advisory is
unfixable and what would let us drop it.
The six container scans and the three CodeQL analyses are most of the
Security workflow's runtime, and re-running them on every intermediate
push to a draft buys nothing. Skip them while `draft` is true and add
`ready_for_review` to the trigger types so marking a PR ready fills them
in. Schedule, merge queue, dispatch, and pushes to main are untouched.
`filters categories by search query` left waitFor polling for the 300ms
debounce, which cost 5-7s locally against 5-20ms for every sibling test in
the file, and crossed the 15s per-test budget on slower CI hardware.
Advancing the timers directly makes it deterministic and drops it to 8ms.
api and migrator both mount `user_uploads`, and Docker seeds an empty named
volume from each container's image as it starts. Started together by the
orchestration smoke test, the two seedings raced and one container died on
`mkdir .../images: file exists`. Mirror the dependency compose.deploy.yaml
already declares, `required: false` so `compose up api` alone is unchanged.
- merge validate.yml and security.yml into ci.yml; drop per-path change
  detection (all jobs finish in 1-5 min, so filtering saved ~15 free
  runner-minutes at the cost of ~350 lines and skip-logic bugs)
- replace the dynamic smoke/scan/CodeQL matrices with static ones and
  delete detect-changes, check-required-jobs, docker-smoke composites,
  path-filters.yml, and container-images.json
- fold the four dependency-audit jobs into one lockfile-only 'just audit'
  run, with docs advisories downgraded to a warning in the recipe itself
- collapse www/app into a matrixed job, inline the codecov composites into
  publish-reports, and drop redundant build steps the just recipes own
- add 'just policy-check' so the workflow and 'just ci' share one job set
- keep a single ci-result aggregator as the branch-protection context
- scope container scan caches as scan-<service> so they stop thrashing the
  compose smoke caches; skip base-image pulls and drafts on PR scans
- drop merge_group and branch_protection_rule triggers (queue unused)
- PR scans upload SARIF to the Security tab and stay green; the push and
  weekly runs keep exit-code 1 (ported from relab-rpi-cam-plugin#29,
  where a blocking PR gate failed 27 of 71 runs on unfixable base CVEs)
- guard the SARIF upload against fork PRs and their read-only token
…t policy

- bump fast-uri 3.1.5 -> 3.1.7 (four HIGH SSRF/host-confusion advisories)
- bump @xmldom/xmldom past the XML fragment injection advisory
- mirror dependency-review's allow-ghsas into pnpm auditConfig.ignoreGhsas
  so 'just audit' and CI enforce one policy
- drop the docs-audit tolerance again: with the shared allowlist in place,
  a new docs advisory should block like any other
- swap the Expo block from enabled:false to dependencyDashboardApproval,
  so SDK bumps stay visible and vulnerability fixes raise PRs again
- move the block below the automerge rule so automerge:false wins
- match otel.cml-relab.org in the WAF skip rule and its test; the
  monitoring stack renamed the record on 2026-09-05 and the old host
  stopped routing, so the skip no longer matched anything
- invert the env_policy assertions, which required the old hostname and
  explicitly rejected the new one
- update the deploy docs, .env.example and the vendored agent comment
- add `--env` to `env_policy.py check` so placeholder-secret and offsite-rclone warnings report on one environment instead of both
- pass the target env from `deploy_ops.sh stack ENV up`, so `just staging-up` no longer prints prod findings
…e hook

A bare `shfmt -w` (no flags) walks up to the nearest .editorconfig and reindented
every tracked shell script to 2 spaces, fighting the repo hook's `-i 4 -ci -bn`.
Spell those flags as editorconfig keys instead, scoped to *.sh and without
`root = true` so other file types keep inheriting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants