Repository navigation
Move board admin to Azure Static Web Apps with Entra ID sign-in - #21
Closed
selectdimensions wants to merge 13 commits into
Closed
selectdimensions wants to merge 13 commits into
selectdimensions wants to merge 13 commits into
Conversation
…a ID
Phase 2 of the Vercel-to-Azure migration (see the approved plan): ports
the board-admin app's backend from Vercel serverless functions to an
Azure Static Web Apps managed Functions API, and replaces the shared
passphrase with Microsoft Entra ID sign-in restricted to an explicit
board-member allow-list. This is pure code — no Azure resource exists
yet, and Vercel keeps running unaffected throughout.
Deleted (replaced by Azure Static Web Apps' built-in auth, not ported):
- api/auth.js, api/logout.js, api/_lib/session.js — SWA's own
/.auth/login/aad and /.auth/logout replace the custom passphrase
check and HMAC-signed session cookie entirely.
- admin/index.html (passphrase login form) and admin/admin-auth.js.
admin/dashboard.html is renamed to admin/index.html as the single
admin entry point, since there's no separate login page anymore.
- The passphrase rate-limiting functions in api/_lib/rateLimit.js
(checkRateLimit/recordRateLimitFailure/clearRateLimit/getClientIp) —
nothing left to rate-limit locally. throttleGeocodeCall (unrelated
to auth) is kept.
Added:
- staticwebapp.config.json: Entra ID (azureActiveDirectory) custom
auth provider config, route rules requiring the "authenticated" role
on /admin/* and /api/*, and the ported CSP/security headers from
vercel.json. The tenant ID is a placeholder
(REPLACE_WITH_PRAXIUM_TENANT_ID) filled in during the actual Azure
deployment phase.
- api/_lib/clientPrincipal.js: reads the x-ms-client-principal header
SWA injects into authenticated requests, for a defense-in-depth
check inside each function (SWA's route rules already block
unauthenticated requests before they arrive).
- api/host.json, api/package.json (+ lockfile): Azure Functions v4
Node.js project scaffolding. @azure/functions is the one new
runtime dependency this project gains — everything else stays
dependency-free.
Rewritten (business logic unchanged, only the handler wrapper and I/O
calls changed) as api/src/functions/{gist,geocode,publish}.js:
- Vercel's `module.exports = async (req, res) => { res.status(x).end() }`
becomes Azure Functions v4's `app.http(name, { handler })` returning
`{ status, jsonBody, headers }`. Each handler function is also
exported directly (gistHandler/geocodeHandler/publishHandler) so
tests can call it without needing the real Functions runtime.
- req.headers.cookie / req.headers['x-forwarded-for'] access becomes
request.headers.get('...'); readJsonBody(req) becomes
await request.json(); requireSession(req) becomes
getClientPrincipal(request).
- api/_lib/http.js's sendJson/methodNotAllowed rewritten from
res-mutating to return-value style (jsonResponse/methodNotAllowed/
unauthorized) — a simplification, not just a port.
- api/_lib/githubGist.js and api/_lib/categories.js are unchanged —
pure Node, never touched req/res.
- All validation rules, reciprocal-connection logic, cascade-delete,
and optimistic-concurrency (409) behavior in publish.js carries over
byte-for-byte in behavior.
admin/admin.js: every 401 redirect now goes to /.auth/login/aad
instead of the deleted local login page; the logout button links to
/.auth/logout instead of calling a local /api/logout.
test/api.test.js: ported to the new handler shape (a mock Azure
Functions HttpRequest with headers.get()/json(), calling the exported
handlers directly and asserting on their returned response objects).
The passphrase/rate-limit tests are gone (nothing left to test
locally — Entra ID auth is enforced by the platform, outside this
repo); two new tests cover missing/incomplete x-ms-client-principal
returning 401. Every other behavior (session-gating semantics,
validation, reciprocal connections, cascade delete, conflict handling)
is preserved with equivalent assertions. All 13 tests pass locally.
CI (.github/workflows/ci.yml) and package.json's `check`/`test`
scripts updated for the new file layout (api/src/functions/ instead
of api/*.js, no more admin/admin-auth.js, a new api/ npm install +
audit step since it now has its own dependency tree).
Also fixes two small things found while reading these files closely:
removed now-dead .login-body/.login-card CSS from admin/admin.css
(the page that used them is gone), and added api/local.settings.json
to .gitignore (Azure Functions' local-dev secrets file).
Not in this commit (later phases, per the plan): creating any actual
Azure resources, the Entra app registrations, or merging to main.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
Replaces the REPLACE_WITH_PRAXIUM_TENANT_ID placeholder with the real tenant ID now that the staging Entra app registration (Praxium_Map_Admin_Staging) exists. The tenant ID is shared across both the staging and production app registrations (same Entra tenant, different client ID/secret per app) — this one value covers both environments. The client ID/secret themselves are never committed; they're referenced by Application Settings name (AZURE_CLIENT_ID/AZURE_CLIENT_SECRET) and supplied per-environment in Azure during Phase 3/4. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
on-behalf-of: @Azure opensource@microsoft.com
…dler) The staging deployment failed on its first run: Oryx auto-detected Node.js via the root package.json (the Electron desktop packaging manifest, unrelated to the deployed site) and looked for a "build" or "build:azure" npm script that doesn't exist, since this is a plain static HTML/CSS/JS site with no bundler or build step at all. skip_app_build: true tells the deploy action not to attempt a frontend build. This only affects app_location — the api_location (api/) Azure Functions app still gets its normal Oryx build/npm install, so @azure/functions is actually installed and present at runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
Deployment failed again after the skip_app_build fix: "Failed to find a default file in the app artifacts folder (/). Valid default files: index.html,Index.html." Azure's deploy validator requires an index.html to physically exist at the app_location root — this is separate from and in addition to staticwebapp.config.json's own "/" -> "/admin" redirect route rule, which only governs runtime routing, not the build-time artifact check. Added a minimal placeholder: a client-side meta-refresh to /admin, for defense-in-depth in the unlikely case the server-side redirect rule is ever bypassed. In normal operation nobody should actually see this page — the staticwebapp.config.json route rule redirects before this file would be served. Confirmed this doesn't conflict with the GitHub Pages workflow, which builds its own _site/index.html explicitly from organization-network-map.html and never reads any root-level file from the actual repo checkout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
Staging sign-in is looping: Microsoft's login completes, but /.auth/me shows clientPrincipal: null afterward, meaning the token exchange on the callback is failing. We can't see *why* because our own responseOverrides.401 rule immediately redirects any 401 back to /.auth/login/aad, masking whatever real error SWA/Entra is actually returning and turning it into a loop instead of a visible error. Removing this temporarily so the next sign-in attempt surfaces the actual error (likely a clear Microsoft AADSTS error page, or a real 401 response body with detail). Re-add once the root cause is found and fixed — this rule is good production UX, it's only in the way for debugging right now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
…gist Managed Functions logs aren't visible without Application Insights wired up, and gist_fetch_failed alone isn't enough to diagnose why the (already curl-verified) GITHUB_PAT is still failing in Azure. Staging-only; revert once root-caused. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
…ions deploy The Static Web App only deploys api/, so the repo-root categories.json was missing at runtime and readCategories() threw before the Gist fetch ran. The workflow and npm pretest now copy it into api/ (gitignored, never committed) so the single source of truth stays at the repo root. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
…the 401 sign-in redirect The gist_fetch_failed root cause is fixed, so the debug error field is reverted and unauthenticated requests go back to Entra sign-in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
…he revision id The concurrency token is a Git revision SHA, which new Date() cannot parse, so the label read "Invalid Date". This bug already exists on main. The version token is unchanged; the response gains an additive updatedAt field used only for display. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
… main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
… staging->branch environment Drops the separate production resource/workflow and the pull_request preview triggers, since new environments copy Production's settings (including the live GITHUB_PAT). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Clean-checkout validation, expired-session recovery, and null request-body handling have unresolved regressions.
Review effort: Balanced
Findings: 3
Open (3)
What changed in this PR
Migrates the board admin to Azure Static Web Apps and Azure Functions, replacing shared-passphrase authentication with Microsoft Entra ID.
Changes:
- Ports API handlers and tests to Azure Functions v4.
- Adds protected admin routing and branch-based Azure deployment.
- Displays the Gist update timestamp instead of its revision identifier.
| File | Description |
|---|---|
| test/api.test.js | Adapts API tests to Azure requests and principals. |
| staticwebapp.config.json | Configures authentication, routes, and security headers. |
| package.json | Updates validation and stages test categories. |
| index.html | Adds the root admin redirect fallback. |
| api/src/functions/publish.js | Ports publishing to Azure Functions. |
| api/src/functions/gist.js | Adds authenticated Gist retrieval with timestamps. |
| api/src/functions/geocode.js | Ports the geocoding endpoint. |
| api/package.json | Defines API dependencies and runtime requirements. |
| api/package-lock.json | Locks API dependencies. |
| api/logout.js | Removes custom logout. |
| api/host.json | Configures the Azure Functions host. |
| api/gist.js | Removes the Vercel Gist handler. |
| api/auth.js | Removes passphrase authentication. |
| api/_lib/session.js | Removes custom session handling. |
| api/_lib/rateLimit.js | Retains geocoding throttling only. |
| api/_lib/http.js | Returns Azure-compatible responses. |
| api/_lib/githubGist.js | Exposes the Gist update timestamp. |
| api/_lib/clientPrincipal.js | Checks injected authentication principals. |
| api/_lib/categories.js | Reads staged API categories. |
| admin/index.html | Replaces the login page with the dashboard. |
| admin/dashboard.html | Removes the separate dashboard page. |
| admin/admin.js | Updates authentication navigation and timestamp display. |
| admin/admin.css | Removes passphrase-login styles. |
| admin/admin-auth.js | Removes the custom login client. |
| .gitignore | Excludes local secrets and staged categories. |
| .github/workflows/ci.yml | Adds API dependency installation and validation. |
| .github/workflows/azure-static-web-apps-wonderful-field-0cd14c010.yml | Deploys production and staging to Azure. |
Files not reviewed (1)
- api/package-lock.json: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| - name: Install api/ dependencies | ||
| # The Azure Functions API has its own package.json (@azure/functions is | ||
| # its one real runtime dependency), separate from the root project. | ||
| run: cd api && npm ci --ignore-scripts |
| "start": "electron .", | ||
| "check": "node --check map.js && node --check network-data.js && node --check admin/admin.js && node --check admin/admin-auth.js && node --check admin-editor/app.js && node --check electron/main.js && node --check electron/preload.js", | ||
| "check": "node --check map.js && node --check network-data.js && node --check admin/admin.js && node --check admin-editor/app.js && node --check electron/main.js && node --check electron/preload.js && for f in api/_lib/*.js api/src/functions/*.js; do node --check \"$f\" || exit 1; done", | ||
| "pretest": "node -e \"require('fs').copyFileSync('categories.json', 'api/categories.json')\"", |
| } | ||
| ], | ||
| "responseOverrides": { | ||
| "401": { "redirect": "/.auth/login/aad", "statusCode": 302 } |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v
Collaborator
Author
|
Superseded by #22 (same changes, single signed commit). |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
/apifrom Vercel functions to Azure Functions v4 (managed by Azure Static Web Apps); Gist read/publish logic, validation, reciprocal connections, cascade delete and 409 concurrency are unchanged./.auth/login/aad), restricted to assigned board members via "Assignment required" on the Enterprise Application. Custom session/passphrase code is removed.maindeploys to Production,stagingdeploys to a branch environment. No PR-preview environments (they would copy Production's live secrets).updated_at.Vercel is unaffected: it now builds from
vercel-fallbackand stays live as the fallback until decommissioned.Test plan
npm run validatepasses🤖 Generated with Claude Code
https://claude.ai/code/session_018KR2ZSHK8Skmyd1zyTaw7v