No description
Find a file
Thomas Radosh 8e09fd4e0b
Some checks failed
Build and Push to Azure Container Registry / quality-gates (push) Has been cancelled
Build and Push to Azure Container Registry / build-and-push (push) Has been cancelled
split the client into it's own file
2026-03-13 15:10:51 +00:00
.forgejo/workflows add tests to the code and CI/CD pipeline 2026-03-13 08:56:39 +00:00
.githooks add version control automation 2026-03-09 16:58:17 +00:00
backend split the client into it's own file 2026-03-13 15:10:51 +00:00
frontend split the client into it's own file 2026-03-13 15:10:51 +00:00
scripts add version control automation 2026-03-09 16:58:17 +00:00
.dockerignore Upload files to "/" 2026-02-23 13:10:50 +00:00
.env.example move slack token into the ui 2026-03-13 15:02:41 +00:00
.gitignore add features 2026-02-24 15:36:38 +00:00
docker-compose.yml security improvements and lock down the images 2026-03-09 18:13:06 +00:00
Dockerfile update docker build 2026-03-13 08:40:37 +00:00
package.json split the client into it's own file 2026-03-13 15:10:51 +00:00
README.md move slack token into the ui 2026-03-13 15:02:41 +00:00

Deployflow Resource Managment

Resource & Project Management for IT Consultancies — with Microsoft Entra ID authentication and automatic engineer sync.

CI Quality Gates


Quick Start

1. Register the app in Azure Portal

  1. Go to portal.azure.comMicrosoft Entra IDApp registrationsNew registration
  2. Fill in:
    • Name: Resource Manager
    • Supported account types: Accounts in this organizational directory only
  • Redirect URI: Webhttp://your-host:3006/auth/callback (default docker-compose.yml mapping)
  1. Click Register — note the Application (client) ID and Directory (tenant) ID
  2. Certificates & secretsNew client secret → copy the value immediately

2. Grant API permissions (needed for engineer sync)

In your app registration:

  1. API permissionsAdd a permissionMicrosoft GraphApplication permissions
  2. Add: User.Read.All (to list all users in your org)
  3. Click Grant admin consent for [your org]

User.Read.All (Application permission) is required for the daily background sync. The delegated User.Read permission is used for the login flow.

3. Configure your environment

cp .env.example .env
nano .env
ENTRA_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ENTRA_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ENTRA_CLIENT_SECRET=your~secret~value
APP_URL=http://your-docker-host:3006
REDIS_URL=redis://redis:6379
SYNC_LOCK_TTL_SECONDS=3600
SESSION_SECRET=<output of: openssl rand -hex 32>
ENGINEER_DOMAIN=yourdomain.com     # ← your actual domain
GLOBAL_ADMIN_EMAILS=you@yourdomain.com,admin2@yourdomain.com
GLOBAL_ADMIN_GROUP_IDS=00000000-0000-0000-0000-000000000000
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxx
SENDGRID_FROM_EMAIL=noreply@yourdomain.com
SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxxxxxxxxxxxxxx  # optional fallback, can be set in Manage Alerts UI
RAG_TEAMS_WEBHOOK_URL=https://....webhook.office.com/....   # optional legacy fallback, can be set in Manage Alerts UI
ALERT_WEBHOOK_TIMEOUT_MS=8000

Alert provider credentials (SendGrid + Slack token + Teams fallback webhooks) can be managed in the portal via Manage Alerts and are restricted to global admins.

⚠️ Never commit .env to git. It's excluded by .dockerignore.

4. Build and run

docker compose up -d --build

# Watch logs
docker compose logs -f

Visit http://your-host:3006 — redirects to Microsoft login automatically.


Features

Engineer Sync from Entra ID

  • All users with @yourdomain.com UPNs are automatically synced
  • Job titles are pulled from their Entra ID profiles
  • Sync runs automatically every day at 02:00
  • RAG staleness reconciliation runs automatically every day at 02:15
  • Manual sync available from the Engineers tab
  • Configure the domain via ENGINEER_DOMAIN env var
  • Manual sync is lock-protected so only one sync can run at a time across startup/cron/manual triggers

Account Manager Management

  • Add, edit, and remove Account Managers via the Manage AMs button
  • Removing an AM unassigns (but does not delete) their projects

Delivery Manager Management

  • Add and remove Delivery Managers via the Manage DMs button
  • Removing a DM unassigns (but does not delete) their projects

Alert Channel Management

  • Global admins can manage RAG alert destinations via Manage Alerts
  • Slack uses Slack App channels (requires a Slack bot token configured in Manage Alerts or via env fallback)
  • Teams uses webhook destinations
  • Slack destination input expects a channel ID (for example C0123ABCD), not a channel name

Slack App Setup (Required for Slack Alerts)

  1. In Slack, create an app at api.slack.com/apps (From scratch).
  2. Under OAuth & PermissionsBot Token Scopes, add:
  • chat:write (required)
  • chat:write.public (optional; useful for posting to public channels without explicit invite)
  1. Install the app to your workspace and copy the Bot User OAuth Token (xoxb-...).
    • Do not use xapp-... (App-Level Token) or xoxp-... (User Token) for SLACK_BOT_TOKEN.
  2. Configure the Slack bot token in DeployFlow (Manage AlertsProvider Credentials).
  • Optional fallback: set SLACK_BOT_TOKEN in .env.
  1. Invite the bot to each target channel (recommended for both public and private channels):
  • In channel: /invite @YourAppName
  1. In DeployFlow, open Manage Alerts and add a destination of type Slack App Channel using the channel ID.

How To Find The Slack Channel ID

  1. In Slack desktop/web, open PreferencesAdvanced and enable Developer mode.
  2. Right-click the target channel in the sidebar.
  3. Select Copy channel ID.
  4. Paste that value into DeployFlow Manage Alerts.

Slack Admin Approval Notes

  • Some workspaces require admin approval before custom apps can be installed.
  • Admins may need to approve requested scopes (chat:write, optional chat:write.public).
  • If alerts fail with not_in_channel, invite the bot to the channel.
  • If alerts fail with channel_not_found, verify the channel ID and workspace context.

Permission Tiers

  • Global Admin (GLOBAL_ADMIN_EMAILS and/or GLOBAL_ADMIN_GROUP_IDS) can manage AM/DM roles, create/delete projects, manage clients, run sync, and manage engineers.
  • AM/DM users can view all projects, but can edit only projects where they are the assigned AM or DM. On other projects they see project/client data, AM/DM, RAG, and allocated engineers, without meeting notes.
  • Resources/Engineers can view only their assigned projects, cannot view AM/DM meeting notes, and can see AM/DM, RAG, and allocated engineers on those projects.

For group-based global admin, configure your Entra app registration to include group claims in ID tokens:

  • Azure Portal → App registrations → your app → Token configuration → Add groups claim.
  • Use the Entra Group Object ID values in GLOBAL_ADMIN_GROUP_IDS.
  • If users are in many groups, Entra can emit group overage claims; now falls back to Microsoft Graph transitive group lookup during login.
  • For Graph group lookup fallback, grant Microsoft Graph Application permission GroupMember.Read.All (or Directory.Read.All) with admin consent.
  • /api/me includes diagnostics: admin_source, groups_overage, group_count, admin_group_matches, group_lookup_attempted, and group_lookup_error.

Project Features

  • New projects default to Amber RAG until AM meeting data is captured
  • Projects with stale AM meeting dates automatically downgrade Green → Amber after 14+ days, then Amber → Red when still stale
  • Group by Client toggle to view all projects organised by client
  • Filter by RAG status, Account Manager, and Client

Port remapping

# docker-compose.yml
ports:
  - "8080:3001"   # Accessible at :8080

Also update APP_URL in .env and the Azure Redirect URI.


Backup & Restore

# Backup
docker cp image:/data/database.db ./database-backup.db

# Restore
docker cp ./database-backup.db image:/data/database.db
docker compose restart

Updates

docker compose down
docker compose up -d --build
# Volume data is preserved

Post-Deploy Smoke Checklist

Run these checks after each deployment to catch upstream/container regressions early:

  1. Verify containers are healthy.
docker compose ps
  1. Verify backend health endpoint from inside the host/network.
curl -fsS http://127.0.0.1:3006/health

Expected result: ok

  1. Verify OpenResty/Nginx can reach upstream backend.
# Replace with your public URL
curl -I https://your-domain.example/

Expected result: HTTP 200 (or redirect to /auth/login), not 502.

  1. Check recent backend logs for startup/module/env failures.
docker compose logs --tail=200 resource_managment_tool
  1. Check OpenResty logs for upstream connect errors (server-specific path).
# Typical paths; use whichever exists on your server
sudo tail -n 200 /var/log/nginx/error.log
sudo tail -n 200 /usr/local/openresty/nginx/logs/error.log

If you see connect() failed (111: Connection refused) while connecting to upstream, the backend process is down or not listening on expected port.

Tests

Run all tests from repo root:

npm test

Run backend route registration tests only:

npm --prefix backend test

Run frontend meeting utility tests only:

npm --prefix frontend test

CI Pipeline

Forgejo workflow: .forgejo/workflows/build-and-push-acr.yml

Pipeline behavior:

  • On every push (all branches), quality-gates runs:
    • backend dependency install (npm --prefix backend ci)
    • frontend dependency install (npm --prefix frontend ci)
    • backend tests (npm --prefix backend test)
    • frontend tests (npm --prefix frontend test)
    • frontend build (npm --prefix frontend run build)
    • backend syntax check (node --check backend/server.js)
  • On main and master only, build-and-push runs after quality-gates passes:
    • docker build + push
    • Trivy vulnerability scan gate
    • SBOM generation
    • optional Cosign signing + attestation

This ensures failed tests/builds block image publication.

Package and Version Numbering

  • Root package: deployflow-rm-tool in package.json (monorepo package metadata).
  • Backend package: deployflow-backend in backend/package.json.
  • Frontend package: deployflow-frontend in frontend/package.json.
  • Current version source for CI image tags: root package.json version (fallback to backend/package.json).
  • Every commit can auto-bump patch version via a repository pre-commit hook.

CI publishes these Docker tags on each build:

  • <short-sha>
  • latest
  • v<package-version>
  • <package-version>-build.<run-number>

Example for version 1.2.0 and run 45:

  • v1.2.0
  • 1.2.0-build.45

To bump versions:

# Patch bump (1.2.0 -> 1.2.1)
npm run version:patch

# Minor bump (1.2.0 -> 1.3.0)
npm run version:minor

# Major bump (1.2.0 -> 2.0.0)
npm run version:major

Enable repository-managed hooks once per clone:

npm run hooks:install

Manual minor/major flow without an extra auto-patch bump:

  1. Run npm run version:minor or npm run version:major.
  2. Commit normally; the pre-commit hook detects staged version files and skips auto patch bump.

Optional override to skip auto patch for any commit:

  • PowerShell: $env:SKIP_AUTO_PATCH='1'; git commit -m "..."
  • Bash: SKIP_AUTO_PATCH=1 git commit -m "..."

CI Image Security Gates

The Forgejo image pipeline now includes:

  • Vulnerability scan gate (Trivy) failing on HIGH/CRITICAL findings.
  • CycloneDX SBOM generation (Syft).
  • Optional image signing and SBOM attestation (Cosign).

Optional Forgejo secrets for signing:

  • COSIGN_PRIVATE_KEY
  • COSIGN_PASSWORD

API Reference

All /api/* endpoints require an authenticated session.

Auth routes

Method Path Description
GET /auth/login Start Microsoft Entra ID login flow
GET /auth/callback OIDC callback endpoint
GET /auth/logout Logout local session and Entra session
GET /auth/not-allowed Access denied page

REST API routes

Method Path Access Description
GET /api/me Authenticated user Signed-in user profile + role diagnostics
GET /api/projects Authenticated user List visible projects (scoped by role)
POST /api/projects Global admin Create project
PUT /api/projects/:id Global admin, assigned AM/DM Update project (managers are restricted to meeting/resource fields)
PATCH /api/projects/:id/status Global admin Archive/unarchive project
DELETE /api/projects/:id Global admin Delete project
GET /api/engineers Authenticated user List engineers (scoped by role)
PUT /api/engineers/:id Global admin Update engineer profile/status
DELETE /api/engineers/:id Global admin Delete engineer (and detach assignments)
GET /api/account-managers Authenticated user List account managers
PUT /api/account-managers/:id Global admin Promote engineer to account manager
DELETE /api/account-managers/:id Global admin Remove account manager role
GET /api/delivery-managers Authenticated user List delivery managers
PUT /api/delivery-managers/:id Global admin Promote engineer to delivery manager
DELETE /api/delivery-managers/:id Global admin Remove delivery manager role
GET /api/clients Authenticated user List clients (scoped by role)
POST /api/clients Global admin Create client
PATCH /api/clients/:id/status Global admin Archive/unarchive client
GET /api/sync/status Authenticated user Last sync run and sync config summary
POST /api/sync/run Global admin Trigger manual Entra sync
GET /api/alerts/config Global admin Read alert provider credentials (SendGrid/Slack/Teams fallback)
PATCH /api/alerts/config Global admin Update alert provider credentials
GET /api/alerts/destinations Global admin List configured alert destinations
POST /api/alerts/destinations Global admin Add alert destination
PATCH /api/alerts/destinations/:id Global admin Enable/disable or relabel destination
DELETE /api/alerts/destinations/:id Global admin Delete destination
POST /api/alerts/destinations/:id/test Global admin Send test notification to destination
GET /api/revenue/account-managers Global admin Revenue totals grouped by AM and currency

/api/me includes admin/group diagnostics fields such as admin_source, groups_overage, group_count, admin_group_matches, group_lookup_attempted, and group_lookup_error.

Tech Stack

  • Backend runtime: Node.js 24 LTS (Docker node:24-alpine) + Express 5 (CommonJS)
  • Frontend: React 19 + Vite 7 + @vitejs/plugin-react
  • Authentication: Microsoft Entra ID OIDC via @azure/msal-node (authorization code flow)
  • Authorization model: Global admin, manager (AM/DM), resource-scoped API responses
  • Data layer: SQLite with libsql (WAL enabled)
  • Session persistence: express-session + Redis (connect-redis + redis)
  • Security middleware: helmet + cors
  • Background jobs: node-cron (daily engineer sync at 02:00)
  • Microsoft Graph integration: user sync + transitive group lookup fallback
  • RAG notifications: SendGrid email + Slack App channels + Teams destinations
  • Containerization: Multi-stage Docker build + Docker Compose + health checks