Skip to content

Public Portal

The public portal enables external users to interact with Floh without direct access to the firewalled admin interface. It provides a minimal, user-facing experience for accepting invitations, completing tasks, uploading documents, and managing approvals.

Architecture

                              ┌────── Firewall ──────┐
                              │                       │
 ┌──────────────┐   ┌──────────────┐   ┌──────────────────────┐   ┌──────────┐
 │  Portal SPA  │──▶│  Portal BFF  │──▶│  Floh Internal       │──▶│   DB     │
 │  (Angular)   │   │  (Fastify)   │   │  Server (Fastify)    │   │  Redis   │
 │  port 7073   │   │  port 7071   │   │  port 7070           │   │  SMTP    │
 └──────────────┘   └──────────────┘   └──────────────────────┘   └──────────┘
      Public             Public                Private

The portal consists of two public-facing services:

  1. Portal BFF (packages/portal-bff) — a stateless reverse proxy (HTTPS in local dev when PORTAL_LISTEN_TLS=true, HTTP otherwise; TLS at the edge in production)
  2. Portal SPA (packages/portal-web) — a minimal Angular frontend

Both sit outside the organization's firewall. The internal Floh server, database, Redis, and OIDC provider remain inside the firewall, accessible only to the BFF.

Portal BFF

The Backend-for-Frontend is a lightweight Fastify application that acts as a pure HTTP proxy. It has no direct connections to the database, Redis, or OIDC provider, minimizing the attack surface of the publicly exposed service.

Route Whitelisting

Only a predefined set of user-facing API routes are forwarded to the internal server. All other requests receive a 404 Not Found response.

Category Routes
Auth GET /api/auth/config, GET /api/auth/login, GET /api/auth/callback, POST /api/auth/logout, GET /api/auth/me, GET /api/auth/session, POST /api/auth/session/extend
Invitations GET /api/invitations/verify/:token, POST /api/invitations/respond/:token, GET /api/invitations/pending
Tasks GET /api/tasks, GET /api/tasks/:id, POST /api/tasks/:id/complete, POST /api/tasks/:id/respond
Task Lifecycle POST /api/tasks/:id/claim, POST /api/tasks/:id/hold, POST /api/tasks/:id/unhold (LSA-8721 / LSA-8722). assign / unassign remain admin-only on the upstream.
Task Comments GET /api/tasks/:id/comments, POST /api/tasks/:id/comments, PATCH /api/tasks/:id/comments/:commentId, DELETE /api/tasks/:id/comments/:commentId (LSA-8723).
Approvals GET /api/approvals, GET /api/approvals/:id, POST /api/approvals/:id/decide
Documents GET /api/documents, GET /api/documents/:id, GET /api/documents/:id/download, POST /api/documents/upload
Document Templates GET /api/document-templates/:id, GET /api/document-templates/:id/download
Runs (read-only) GET /api/runs/:id
Request Catalog GET /api/request-catalog, POST /api/request-catalog/:id/submit
Health GET /api/health

Administrative routes (workflows, users, roles, connectors, audit, reports, config-transfer, etc.) are blocked.

Scope Enforcement

The BFF strips the scope query parameter from /api/tasks and /api/approvals requests. This ensures portal users always see only their own assignments, even if a client attempts to request scope=all.

Header Injection

On every proxied request, the BFF injects:

  • X-Portal-Origin — set to PORTAL_FRONTEND_URL, so the internal server knows to redirect back to the portal after authentication
  • X-Forwarded-For — the original client IP address

Security Features

Feature Implementation
Rate limiting @fastify/rate-limit — configurable max requests per window
Security headers @fastify/helmet with strict Content Security Policy
CORS Restricted to PORTAL_FRONTEND_URL only
Body size limits Configurable via MAX_UPLOAD_SIZE (default 10 MB)

Configuration

Variable Default Description
PORTAL_PORT 7071 BFF listen port
PORTAL_HOST 0.0.0.0 BFF listen host
FLOH_INTERNAL_URL https://localhost:7070 Internal Floh server URL (use https:// when the API serves TLS; http:// only for HTTP-only dev)
PORTAL_FRONTEND_URL https://localhost:7073 Portal SPA URL (for CORS, header injection); use http:// only for HTTP-only dev
MAX_UPLOAD_SIZE 10485760 Maximum request body size in bytes
RATE_LIMIT_MAX 100 Max requests per rate limit window
RATE_LIMIT_WINDOW_MS 60000 Rate limit window in milliseconds
LOG_LEVEL info Log level
PORTAL_LISTEN_TLS (unset) When true or 1, BFF serves HTTPS on PORTAL_PORT using TLS_CERT_FILE / TLS_KEY_FILE (same paths as the API). pnpm dev:portal:https sets this in the shell.

Portal SPA

The portal SPA is a stripped-down Angular application derived from the main Floh frontend. It includes only the components and routes external users need.

Routes

Path Component Auth Required Description
/welcome WelcomeComponent No Landing page with login button
/dashboard PortalDashboardComponent Yes Pending invitations, tasks, approvals
/tasks TaskInboxComponent Yes Task inbox (tasks and approvals)
/requests/catalog RequestCatalogComponent Yes Browse and submit workflow requests
/invitations/respond InvitationRespondComponent No* Accept or decline invitations
/auth/callback AuthCallbackComponent No OIDC callback handler

*Invitations require authentication to respond, but the page itself loads without auth to verify the token and prompt login.

Removed Features

Compared to the main Floh frontend, the portal SPA does not include:

  • Sidebar navigation
  • Workflow designer / definition management
  • User / role / organization management
  • Connector management
  • Audit log viewer
  • Reports / analytics
  • Admin panel
  • Permission override controls
  • Project and workflow set filters

Request Catalog

The Request Catalog allows portal users to browse published workflows and submit requests. Administrators configure which workflows appear in the catalog from the admin UI (catalog publishing toggle, icon, description, and tags on the workflow detail page).

  • Users browse published workflows displayed as cards with icons, descriptions, category tags, and searchable content
  • Filtering is available by category, free-text search, and tag selection
  • Administrators can restrict submission to members of specific groups via the "Submission Restrictions" setting on the catalog publishing card; restricted entries are hidden from non-members
  • Submitting a request opens a dynamic form built from the workflow's variable definitions (excluding secret variables)
  • On submission, a workflow run is started via POST /api/request-catalog/:id/submit (requires only authentication, no specific permission) and a success confirmation is shown
  • When no real catalog entries exist, example entries are shown to give users a sense of the UI

The Request Catalog link appears in the portal topbar alongside Dashboard and Tasks.

Draft Preview (workflow author testing)

To shorten the develop-and-test loop for workflow authors, draft workflows that have catalogPublished turned on are also visible in the request catalog — but only to users who hold the workflow:publish permission (admin and resource_manager by default). Other portal users continue to see only active catalog entries; the draft is filtered out of GET /api/request-catalog and POST /api/request-catalog/:id/submit returns 404 rather than disclosing that the draft exists.

This lets authors validate the real portal submission UX (the rendered form, group restrictions, step-up auth) without flipping the workflow to active for the entire org. Submissions of a draft from the portal create real workflow runs with all configured side effects (notifications, approvals, connector calls), so authors should treat them like a regular run when their workflow makes external changes.

In the admin workflow editor, the Catalog Publishing card is now also available while a workflow is in draft status. Toggle Published to Catalog on a draft to expose it in the portal preview; the card shows a Draft preview tag so authors can see that the entry is not yet visible to the wider org. The toggle remains disabled for deprecated workflows.

Dashboard

The portal dashboard displays three cards:

  1. Pending Invitations — invitations awaiting the user's response, with a "Respond" link
  2. Active Tasks — the user's assigned tasks (up to 5), with a link to the full task inbox
  3. Pending Approvals — approvals awaiting the user's decision (up to 5)

Internal Server Changes

The portal requires two small changes to the internal Floh server:

Portal-Aware Auth Redirects

The resolveRedirectBase function in packages/server/src/modules/auth/routes.ts checks for the X-Portal-Origin header on incoming requests. If the header value matches one of the configured ALLOWED_PORTAL_ORIGINS, the server redirects to the portal frontend instead of the admin frontend.

This affects:

  • OIDC callback — redirects to {portalUrl}/auth/callback after login
  • Account deleted — redirects to {portalUrl}/auth/callback?error=account_deleted
  • Logout — uses the portal URL as the post_logout_redirect_uri
  • Cookie security — sets the Secure flag based on the portal URL scheme

When PORTAL_FRONTEND_URL is configured, invitation emails link to the portal instead of the admin frontend. This is controlled by:

const baseUrl = this.config.portalFrontendUrl || this.config.frontendUrl;

in packages/server/src/modules/notifications/service.ts.

Internal Server Configuration

Variable Default Description
ALLOWED_PORTAL_ORIGINS (empty) Comma-separated list of allowed portal frontend URLs
PORTAL_FRONTEND_URL (empty) Portal frontend URL for invitation email links

Development

Starting the Portal

Preferred (HTTPS): configure TLS on the API (TLS_CERT_FILE, TLS_KEY_FILE, NODE_EXTRA_CA_CERTS) and use the HTTPS dev scripts.

# Start infrastructure (if not already running)
docker compose -f docker/docker-compose.yml up -d postgres redis mailhog

# Run migrations
pnpm migrate:latest

# Terminal 1 — API + admin UI (or `pnpm dev:server` if you only need the API)
pnpm dev:https

# Terminal 2 — portal BFF + portal SPA (HTTPS on :7071 and :7073; script sets PORTAL_LISTEN_TLS)
pnpm dev:portal:https

HTTP-only alternative:

pnpm dev:server &
pnpm dev:portal

Or start the main stack with pnpm dev, then pnpm dev:portal in another terminal.

Environment Setup

Add these variables to your .env file (HTTPS preferred; match schemes to your dev scripts):

# Portal BFF
PORTAL_PORT=7071
# Optional in .env: pnpm dev:portal:https already exports PORTAL_LISTEN_TLS=true
# PORTAL_LISTEN_TLS=true
FLOH_INTERNAL_URL=https://localhost:7070
PORTAL_FRONTEND_URL=https://localhost:7073

# Internal server (portal support)
ALLOWED_PORTAL_ORIGINS=https://localhost:7073

For HTTP-only local dev, use http:// in all three URLs above, omit PORTAL_LISTEN_TLS, and run pnpm dev:portal.

Running Tests

pnpm test:portal-bff    # Portal BFF tests (vitest)
pnpm test:portal-web    # Portal frontend tests (jest)

Docker Deployment

Building

docker compose -f docker/docker-compose.portal.yml build

Running

# Start everything (main + portal)
docker compose -f docker/docker-compose.yml -f docker/docker-compose.portal.yml up -d

Or run only the portal stack (assumes the internal server is already running):

docker compose -f docker/docker-compose.portal.yml up -d

Docker Services

Service Image Port Description
portal-bff Dockerfile.portal-bff 7071 Stateless proxy
portal-web Dockerfile.portal-web 7073 Angular SPA via nginx

Network Topology

┌─────────── Public Network ───────────┐
│  portal-web ──▶ portal-bff           │
└──────────────────┬───────────────────┘
┌──────────────────▼───────────────────┐
│  portal-bff ──▶ server               │
│         Internal Network             │
│  server ──▶ postgres, redis          │
└──────────────────────────────────────┘

The portal-bff and portal-web services are on the public network. The BFF also joins the internal network to reach the server. The server, database, and Redis are on the internal network only, never exposed publicly.

Security Considerations

  1. Minimal attack surface — the BFF has no database, Redis, or OIDC connections; it only forwards whitelisted HTTP requests
  2. Route whitelisting — only user-facing endpoints are accessible; admin APIs return 404
  3. Scope enforcementscope=all is stripped, preventing privilege escalation
  4. Origin validation — the internal server validates X-Portal-Origin against an allowlist before using it for redirects
  5. Rate limiting — the BFF limits request rates to prevent abuse
  6. CORS — strict origin policy locked to the portal frontend URL
  7. CSP — Content Security Policy via helmet restricts script/style/image sources
  8. Cookie securitySecure flag is set based on the target URL scheme
  9. No secrets in the BFF — the BFF only needs the internal server URL and portal frontend URL; no database credentials, session secrets, or OIDC secrets
  10. Step-up authentication — sensitive workflow steps (consent steps with requireStepUpAuth, catalog entries with catalogRequireStepUpAuth) trigger an OIDC re-auth with acr_values=mod-mf. The portal SPA's stepUpInterceptor watches for 401 { code: "MFA_OR_AAL_2_REQUIRED" } responses and redirects through /api/auth/login?acr_values=mod-mf&return_to=<currentPath> so the user resumes the original action after MFA. See Security › Step-Up Authentication.