| .forgejo/workflows | ||
| .githooks | ||
| backend | ||
| frontend | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package.json | ||
| README.md | ||
Deployflow Resource Managment
Resource & Project Management for IT Consultancies — with Microsoft Entra ID authentication and automatic engineer sync.
Quick Start
1. Register the app in Azure Portal
- Go to portal.azure.com → Microsoft Entra ID → App registrations → New registration
- Fill in:
- Name:
Resource Manager - Supported account types: Accounts in this organizational directory only
- Name:
- Redirect URI:
Web→http://your-host:3006/auth/callback(defaultdocker-compose.ymlmapping)
- Click Register — note the Application (client) ID and Directory (tenant) ID
- Certificates & secrets → New client secret → copy the value immediately
2. Grant API permissions (needed for engineer sync)
In your app registration:
- API permissions → Add a permission → Microsoft Graph → Application permissions
- Add:
User.Read.All(to list all users in your org) - Click Grant admin consent for [your org] ✓
User.Read.All(Application permission) is required for the daily background sync. The delegatedUser.Readpermission 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
.envto 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.comUPNs 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_DOMAINenv 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)
- In Slack, create an app at
api.slack.com/apps(From scratch). - Under OAuth & Permissions → Bot Token Scopes, add:
chat:write(required)chat:write.public(optional; useful for posting to public channels without explicit invite)
- Install the app to your workspace and copy the Bot User OAuth Token (
xoxb-...).- Do not use
xapp-...(App-Level Token) orxoxp-...(User Token) forSLACK_BOT_TOKEN.
- Do not use
- Configure the Slack bot token in DeployFlow (Manage Alerts → Provider Credentials).
- Optional fallback: set
SLACK_BOT_TOKENin.env.
- Invite the bot to each target channel (recommended for both public and private channels):
- In channel:
/invite @YourAppName
- 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
- In Slack desktop/web, open Preferences → Advanced and enable Developer mode.
- Right-click the target channel in the sidebar.
- Select Copy channel ID.
- 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, optionalchat: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_EMAILSand/orGLOBAL_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(orDirectory.Read.All) with admin consent. /api/meincludes diagnostics:admin_source,groups_overage,group_count,admin_group_matches,group_lookup_attempted, andgroup_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:
- Verify containers are healthy.
docker compose ps
- Verify backend health endpoint from inside the host/network.
curl -fsS http://127.0.0.1:3006/health
Expected result: ok
- 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.
- Check recent backend logs for startup/module/env failures.
docker compose logs --tail=200 resource_managment_tool
- 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-gatesruns:- 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)
- backend dependency install (
- On
mainandmasteronly,build-and-pushruns afterquality-gatespasses:- 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-toolinpackage.json(monorepo package metadata). - Backend package:
deployflow-backendinbackend/package.json. - Frontend package:
deployflow-frontendinfrontend/package.json. - Current version source for CI image tags: root
package.jsonversion(fallback tobackend/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>latestv<package-version><package-version>-build.<run-number>
Example for version 1.2.0 and run 45:
v1.2.01.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:
- Run
npm run version:minorornpm run version:major. - 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/CRITICALfindings. - CycloneDX SBOM generation (Syft).
- Optional image signing and SBOM attestation (Cosign).
Optional Forgejo secrets for signing:
COSIGN_PRIVATE_KEYCOSIGN_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