From 309c5380d281428fcd83ea310b865fe1f0173fc7 Mon Sep 17 00:00:00 2001 From: Pato Perpetua Date: Sun, 23 Aug 2026 03:52:06 +1000 Subject: [PATCH] feat: MKT-7 point Decap at cms-oauth-kit Point Decap /admin at shared cms-oauth-kit (https://auth.singletonsd.com) and remove the in-repo Azure Function, Bicep, deploy scripts, and workflow. Closes #7 --- .github/workflows/deploy-decap-oauth.yml | 118 --------------- .github/workflows/release.yml | 5 - AGENTS.md | 9 +- HANDOFF.md | 18 +-- README.md | 3 +- SETUP.md | 13 +- apps/marketing-oauth/.gitignore | 4 - apps/marketing-oauth/host.json | 20 --- .../local.settings.json.example | 12 -- apps/marketing-oauth/package.json | 22 --- apps/marketing-oauth/src/functions/auth.ts | 32 ---- .../marketing-oauth/src/functions/callback.ts | 52 ------- apps/marketing-oauth/src/functions/health.ts | 11 -- apps/marketing-oauth/src/github-oauth.spec.ts | 25 --- apps/marketing-oauth/src/github-oauth.ts | 58 ------- apps/marketing-oauth/src/index.ts | 4 - apps/marketing-oauth/src/login-script.spec.ts | 64 -------- apps/marketing-oauth/src/login-script.ts | 112 -------------- apps/marketing-oauth/tsconfig.json | 15 -- apps/marketing/public/admin/config.yml | 4 +- apps/marketing/src/lib/site.test.mjs | 15 ++ docs/marketing-astro-decap.md | 40 +++-- docs/pr-pipelines.md | 3 +- infra/decap-oauth.bicep | 143 ------------------ infra/marketing.bicep | 28 +--- pnpm-lock.yaml | 28 ---- scripts/bootstrap-decap-oauth.ps1 | 86 ----------- scripts/deploy-decap-oauth.ps1 | 80 ---------- scripts/release-changed.mjs | 3 - 29 files changed, 62 insertions(+), 965 deletions(-) delete mode 100644 .github/workflows/deploy-decap-oauth.yml delete mode 100644 apps/marketing-oauth/.gitignore delete mode 100644 apps/marketing-oauth/host.json delete mode 100644 apps/marketing-oauth/local.settings.json.example delete mode 100644 apps/marketing-oauth/package.json delete mode 100644 apps/marketing-oauth/src/functions/auth.ts delete mode 100644 apps/marketing-oauth/src/functions/callback.ts delete mode 100644 apps/marketing-oauth/src/functions/health.ts delete mode 100644 apps/marketing-oauth/src/github-oauth.spec.ts delete mode 100644 apps/marketing-oauth/src/github-oauth.ts delete mode 100644 apps/marketing-oauth/src/index.ts delete mode 100644 apps/marketing-oauth/src/login-script.spec.ts delete mode 100644 apps/marketing-oauth/src/login-script.ts delete mode 100644 apps/marketing-oauth/tsconfig.json delete mode 100644 infra/decap-oauth.bicep delete mode 100644 scripts/bootstrap-decap-oauth.ps1 delete mode 100644 scripts/deploy-decap-oauth.ps1 diff --git a/.github/workflows/deploy-decap-oauth.yml b/.github/workflows/deploy-decap-oauth.yml deleted file mode 100644 index dbf46f3..0000000 --- a/.github/workflows/deploy-decap-oauth.yml +++ /dev/null @@ -1,118 +0,0 @@ -# Decap GitHub OAuth proxy — Azure Functions on ssd-mkt-plan-prod-ae. -name: Deploy Decap OAuth (Function) - -on: - push: - branches: [main] - paths: - - 'apps/marketing-oauth/**' - - 'infra/decap-oauth.bicep' - - 'scripts/deploy-decap-oauth.ps1' - - '.github/workflows/deploy-decap-oauth.yml' - workflow_dispatch: - -concurrency: - group: deploy-decap-oauth-production - cancel-in-progress: false - -env: - AZURE_RESOURCE_GROUP: rg-ssd-marketing - AZURE_FUNCTIONAPP_NAME: ssd-mkt-decap-oauth-prod-ae - AZURE_KEY_VAULT: ssd-global-kv-prod-ae - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - name: Build + Decap OAuth Function - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 9.15.0 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install - run: pnpm install --frozen-lockfile - - - name: Test marketing-oauth - run: pnpm --filter @singleton-sd/marketing-oauth run test - - - name: Check OIDC + client id - id: cfg - run: | - set -euo pipefail - if [ -z "${{ vars.AZURE_CLIENT_ID }}" ] || [ -z "${{ vars.AZURE_TENANT_ID }}" ] || [ -z "${{ vars.AZURE_SUBSCRIPTION_ID }}" ]; then - echo "configured=false" >> "$GITHUB_OUTPUT" - echo "OIDC Variables not set — skipping Decap OAuth deploy." - elif [ -z "${{ vars.DECAP_OAUTH_CLIENT_ID }}" ]; then - echo "configured=false" >> "$GITHUB_OUTPUT" - echo "DECAP_OAUTH_CLIENT_ID Variable not set." - else - echo "configured=true" >> "$GITHUB_OUTPUT" - fi - - - name: Azure login (OIDC) - if: steps.cfg.outputs.configured == 'true' - uses: azure/login@v2 - with: - client-id: ${{ vars.AZURE_CLIENT_ID }} - tenant-id: ${{ vars.AZURE_TENANT_ID }} - subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - - name: Assert KV secret exists - if: steps.cfg.outputs.configured == 'true' - run: | - set -euo pipefail - az keyvault secret show \ - --vault-name "$AZURE_KEY_VAULT" \ - --name github-decap-oauth-client-secret \ - --query name -o tsv >/dev/null - - - name: Resolve SWA hostname for ORIGINS - id: origins - if: steps.cfg.outputs.configured == 'true' - run: | - set -euo pipefail - host=$(az staticwebapp show -g rg-ssd-marketing -n ssd-mkt-prod-ae --query defaultHostname -o tsv) - prefix=$(echo "$host" | cut -d. -f1) - echo "value=singletonsd.com,www.singletonsd.com,${prefix}*.azurestaticapps.net,localhost:4321" >> "$GITHUB_OUTPUT" - - - name: Deploy Function App infra - if: steps.cfg.outputs.configured == 'true' - run: | - set -euo pipefail - az deployment group create \ - --resource-group "$AZURE_RESOURCE_GROUP" \ - --template-file infra/decap-oauth.bicep \ - --parameters oauthClientId="${{ vars.DECAP_OAUTH_CLIENT_ID }}" \ - origins="${{ steps.origins.outputs.value }}" \ - --name "decap-oauth-${GITHUB_RUN_ID}" - - - name: Stage zip - if: steps.cfg.outputs.configured == 'true' - working-directory: apps/marketing-oauth - run: | - set -euo pipefail - pnpm run build - STAGE=$(mktemp -d) - cp host.json package.json "$STAGE/" - cp -r dist "$STAGE/dist" - (cd "$STAGE" && npm install --omit=dev --package-lock=false) - (cd "$STAGE" && zip -r "$GITHUB_WORKSPACE/decap-oauth.zip" .) - - - name: Zip deploy Function App - if: steps.cfg.outputs.configured == 'true' - run: | - set -euo pipefail - az functionapp deployment source config-zip \ - --resource-group "$AZURE_RESOURCE_GROUP" \ - --name "$AZURE_FUNCTIONAPP_NAME" \ - --src decap-oauth.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 880c393..7f8bb0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,11 +92,6 @@ jobs: gh workflow run deploy-marketing.yml --ref main dispatch=true fi - if [ "$f" = 'apps/marketing-oauth/package.json' ]; then - echo "Dispatching deploy-decap-oauth.yml @ main ($HEAD_SHA)" - gh workflow run deploy-decap-oauth.yml --ref main - dispatch=true - fi done if [ "$dispatch" = false ]; then echo "No marketing package.json in release commit — skipping deploys." diff --git a/AGENTS.md b/AGENTS.md index 3108200..a96641d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ ClickUp tickets must include `[repo=singleton-sd/marketing]`. | --- | --- | | Site | Astro SSG (`apps/marketing`) | | CMS | Decap at `/admin` (GitHub backend) | -| OAuth | Azure Function `ssd-mkt-decap-oauth-prod-ae` | +| OAuth | Shared `cms-oauth-kit` at `https://auth.singletonsd.com` | | Host | Azure SWA Free `ssd-mkt-prod-ae` (`www.singletonsd.com`) | | Secrets | Shared KV `ssd-global-kv-prod-ae` in `rg-ssd-global` | | Assets CDN | `assets.singletonsd.com` (favicons/OG — link only, no binaries in repo) | @@ -43,7 +43,6 @@ detached/behind). Do not rely on a dirty or stale worktree. pnpm install pnpm dev pnpm --filter @singleton-sd/marketing build -pnpm --filter @singleton-sd/marketing-oauth test pnpm release # dry-run path-aware package bumps pnpm release:ci # bump, CHANGELOG, commit, tag, push (CI only) pnpm changelog:test # client-facing changelog unit tests @@ -64,4 +63,8 @@ pnpm changelog:check # MD ↔ JSON projection drift check Branch names: `feature/MKT--slug`, `hotfix/MKT--slug`, `release/vX.Y.Z`, or `main`. -Package tags: `@singleton-sd/marketing@x.y.z`, `@singleton-sd/marketing-oauth@x.y.z`. +Package tags: `@singleton-sd/marketing@x.y.z`. + +Decap `/admin` login uses `singleton-sd/cms-oauth-kit` (`https://auth.singletonsd.com`). +Shared `ORIGINS` already covers `*.singletonsd.com`, apex, and `localhost:4321`. +Add origins or deploy OAuth only in that repo — not here. diff --git a/HANDOFF.md b/HANDOFF.md index aa2fc3b..56ae540 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -7,13 +7,14 @@ Parent ClickUp: https://app.clickup.com/t/86d3zhkz4 ## Done -- Repo `singleton-sd/marketing` with Astro + Decap + OAuth Function code +- Repo `singleton-sd/marketing` with Astro + Decap - Content retargeted to Singleton Software Development + Discovery Call CTA - `rg-ssd-global` / `ssd-global-kv-prod-ae` (shared KV) -- `rg-ssd-marketing` / SWA `ssd-mkt-prod-ae` + plan `ssd-mkt-plan-prod-ae` +- `rg-ssd-marketing` / SWA `ssd-mkt-prod-ae` - GHA OIDC app `ssd-mkt-gha-oidc-prod` + Variables + ID-form federated credentials - Production SWA deploy via Actions succeeding -- Workflows: `deploy-marketing.yml`, `preview-marketing.yml`, `deploy-decap-oauth.yml` +- Workflows: `deploy-marketing.yml`, `preview-marketing.yml` +- Decap `/admin` OAuth: shared `cms-oauth-kit` at `https://auth.singletonsd.com` ## Blocked on you (interactive) @@ -33,15 +34,12 @@ ClickUp: https://app.clickup.com/t/86d3zhkzt ### 2. GitHub OAuth App (Decap `/admin`) -Create OAuth App (UI only): https://github.com/settings/developers +Shared org app (do not create a second callback on this repo): +- App: [Singleton SD CMS OAuth](https://github.com/settings/applications/3783537) - Homepage URL: `https://singletonsd.com` -- Authorization callback URL: `https://ssd-mkt-decap-oauth-prod-ae.azurewebsites.net/callback` +- Authorization callback URL: `https://auth.singletonsd.com/callback` -```powershell -powershell -File ./scripts/bootstrap-decap-oauth.ps1 -ClientId '' -ClientSecret '' -# Saves to global KV: client-id, client-secret, and app-config JSON (name/homepage/callback) -powershell -File ./scripts/deploy-decap-oauth.ps1 -OauthClientId '' -``` +OAuth Function code, Bicep, and deploy live in [`singleton-sd/cms-oauth-kit`](https://github.com/singleton-sd/cms-oauth-kit). This site’s `config.yml` `base_url` is `https://auth.singletonsd.com`. ClickUp: https://app.clickup.com/t/86d3zhkzr diff --git a/README.md b/README.md index 310a447..f3987b8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Public landing site for **Singleton Software Development** at [singletonsd.com]( - Astro SSG + Tailwind + `@singleton-sd/tokens` (GitLab npm, `--ssd-*`) - Decap CMS at `/admin` (GitHub backend) -- Azure Static Web Apps Free + Decap OAuth Azure Function +- Azure Static Web Apps Free +- Shared Decap OAuth: [`cms-oauth-kit`](https://github.com/singleton-sd/cms-oauth-kit) at `https://auth.singletonsd.com` - Shared Key Vault in `rg-ssd-global` ## Quick start diff --git a/SETUP.md b/SETUP.md index 3933474..225a68f 100644 --- a/SETUP.md +++ b/SETUP.md @@ -5,7 +5,7 @@ - Node 20+ / pnpm 9.15 - Azure CLI logged into subscription `01c0bb8b-3770-4765-979a-cb13ae7e3dd2` - AWS CLI (Route53 for `singletonsd.com`) -- GitHub OAuth App for Decap (see `docs/marketing-astro-decap.md`) +- Decap `/admin` login uses shared `cms-oauth-kit` (see `docs/marketing-astro-decap.md`) ## Local @@ -15,7 +15,7 @@ pnpm install pnpm dev # http://localhost:4321 -# Admin: http://localhost:4321/admin (needs OAuth Function + ORIGINS including localhost:4321) +# Admin: http://localhost:4321/admin (shared OAuth at https://auth.singletonsd.com; ORIGINS includes localhost:4321) ``` ## Azure resources @@ -26,17 +26,17 @@ pnpm dev | Global Key Vault | `ssd-global-kv-prod-ae` | | Marketing RG | `rg-ssd-marketing` | | SWA | `ssd-mkt-prod-ae` | -| Decap OAuth Function | `ssd-mkt-decap-oauth-prod-ae` | +| Decap OAuth | `https://auth.singletonsd.com` (`singleton-sd/cms-oauth-kit` in `rg-ssd-global`) | ### Secrets in global KV (tagged `repo=singleton-sd/marketing`) - `swa-marketing-deployment-token` -- `github-decap-oauth-client-secret` + +`github-decap-oauth-client-secret` stays in the same vault for `cms-oauth-kit`. Do not manage OAuth client id/secret or `ORIGINS` from this repo. ### GitHub Variables (IDs only) - `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID` -- `DECAP_OAUTH_CLIENT_ID` Entra app `ssd-mkt-gha-oidc-prod` federated subjects must include both classic and ID-form claims (GitHub may emit either): @@ -51,8 +51,7 @@ Entra app `ssd-mkt-gha-oidc-prod` federated subjects must include both classic a az account set --subscription 01c0bb8b-3770-4765-979a-cb13ae7e3dd2 az deployment group create -g rg-ssd-global -f infra/global.bicep az deployment group create -g rg-ssd-marketing -f infra/marketing.bicep -# After OAuth App + KV secret: -pwsh ./scripts/deploy-decap-oauth.ps1 -OauthClientId '' +# OAuth Function lives in singleton-sd/cms-oauth-kit (not this repo). pwsh ./scripts/deploy-swa-from-kv.ps1 -ConfigPath ./infra/custom-domains.marketing.json -DeployName marketing ``` diff --git a/apps/marketing-oauth/.gitignore b/apps/marketing-oauth/.gitignore deleted file mode 100644 index bdf0557..0000000 --- a/apps/marketing-oauth/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -local.settings.json -dist/ -node_modules/ -.azure/ diff --git a/apps/marketing-oauth/host.json b/apps/marketing-oauth/host.json deleted file mode 100644 index 1c14b6a..0000000 --- a/apps/marketing-oauth/host.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - }, - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.*, 5.0.0)" - }, - "extensions": { - "http": { - "routePrefix": "" - } - } -} diff --git a/apps/marketing-oauth/local.settings.json.example b/apps/marketing-oauth/local.settings.json.example deleted file mode 100644 index 1289002..0000000 --- a/apps/marketing-oauth/local.settings.json.example +++ /dev/null @@ -1,12 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "node", - "OAUTH_CLIENT_ID": "replace-with-github-oauth-app-client-id", - "OAUTH_CLIENT_SECRET": "replace-with-github-oauth-app-client-secret", - "REDIRECT_URL": "http://localhost:7071/callback", - "SCOPES": "repo,user", - "ORIGINS": "plattform-kit.poc.singletonsd.com,purple-field-05048bf00*.azurestaticapps.net,localhost:4321,localhost:3000" - } -} diff --git a/apps/marketing-oauth/package.json b/apps/marketing-oauth/package.json deleted file mode 100644 index 2fb9152..0000000 --- a/apps/marketing-oauth/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@singleton-sd/marketing-oauth", - "version": "0.2.0", - "private": true, - "main": "dist/src/index.js", - "scripts": { - "build": "tsc -p tsconfig.json", - "lint": "echo \"lint:marketing-oauth — covered by root tooling\"", - "test": "pnpm build && node --test dist/src/login-script.spec.js dist/src/github-oauth.spec.js", - "start": "func start" - }, - "dependencies": { - "@azure/functions": "^4.6.0" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - }, - "engines": { - "node": ">=20" - } -} diff --git a/apps/marketing-oauth/src/functions/auth.ts b/apps/marketing-oauth/src/functions/auth.ts deleted file mode 100644 index f576bae..0000000 --- a/apps/marketing-oauth/src/functions/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; -import { buildGithubAuthorizeUrl, requireEnv } from '../github-oauth'; - -export async function authHandler( - _request: HttpRequest, - context: InvocationContext, -): Promise { - try { - const location = buildGithubAuthorizeUrl({ - clientId: requireEnv('OAUTH_CLIENT_ID'), - redirectUri: requireEnv('REDIRECT_URL'), - scope: process.env.SCOPES?.trim() || 'repo,user', - }); - return { - status: 302, - headers: { Location: location }, - }; - } catch (error) { - context.error('auth failed', error); - return { - status: 500, - jsonBody: { error: error instanceof Error ? error.message : 'auth failed' }, - }; - } -} - -app.http('auth', { - methods: ['GET'], - authLevel: 'anonymous', - route: 'auth', - handler: authHandler, -}); diff --git a/apps/marketing-oauth/src/functions/callback.ts b/apps/marketing-oauth/src/functions/callback.ts deleted file mode 100644 index 26074bd..0000000 --- a/apps/marketing-oauth/src/functions/callback.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; -import { exchangeGithubCode, requireEnv } from '../github-oauth'; -import { buildLoginScript, parseOrigins } from '../login-script'; - -export async function callbackHandler( - request: HttpRequest, - context: InvocationContext, -): Promise { - try { - const code = request.query.get('code'); - const origins = parseOrigins(process.env.ORIGINS); - - if (!code) { - const html = buildLoginScript('github', 'error', { error: 'missing code' }, origins); - return { status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: html }; - } - - const result = await exchangeGithubCode({ - clientId: requireEnv('OAUTH_CLIENT_ID'), - clientSecret: requireEnv('OAUTH_CLIENT_SECRET'), - code, - redirectUri: requireEnv('REDIRECT_URL'), - }); - - if ('error' in result) { - context.error('token exchange failed', result.error); - const html = buildLoginScript('github', 'error', { error: result.error }, origins); - return { status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: html }; - } - - const html = buildLoginScript( - 'github', - 'success', - { token: result.accessToken, provider: 'github' }, - origins, - ); - return { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }, body: html }; - } catch (error) { - context.error('callback failed', error); - return { - status: 500, - jsonBody: { error: error instanceof Error ? error.message : 'callback failed' }, - }; - } -} - -app.http('callback', { - methods: ['GET'], - authLevel: 'anonymous', - route: 'callback', - handler: callbackHandler, -}); diff --git a/apps/marketing-oauth/src/functions/health.ts b/apps/marketing-oauth/src/functions/health.ts deleted file mode 100644 index f0d072a..0000000 --- a/apps/marketing-oauth/src/functions/health.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { app, HttpResponseInit } from '@azure/functions'; - -app.http('health', { - methods: ['GET'], - authLevel: 'anonymous', - route: 'health', - handler: async (): Promise => ({ - status: 200, - jsonBody: { status: 'ok', service: 'decap-oauth' }, - }), -}); diff --git a/apps/marketing-oauth/src/github-oauth.spec.ts b/apps/marketing-oauth/src/github-oauth.spec.ts deleted file mode 100644 index c902f2a..0000000 --- a/apps/marketing-oauth/src/github-oauth.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { buildGithubAuthorizeUrl } from './github-oauth'; - -describe('buildGithubAuthorizeUrl', () => { - it('points at GitHub authorize with client and redirect', () => { - const url = new URL( - buildGithubAuthorizeUrl({ - clientId: 'abc123', - redirectUri: 'https://ssd-pocpk-decap-oauth-dev-ae.azurewebsites.net/callback', - scope: 'repo,user', - state: 'fixed-state', - }), - ); - assert.equal(url.origin, 'https://github.com'); - assert.equal(url.pathname, '/login/oauth/authorize'); - assert.equal(url.searchParams.get('client_id'), 'abc123'); - assert.equal( - url.searchParams.get('redirect_uri'), - 'https://ssd-pocpk-decap-oauth-dev-ae.azurewebsites.net/callback', - ); - assert.equal(url.searchParams.get('scope'), 'repo,user'); - assert.equal(url.searchParams.get('state'), 'fixed-state'); - }); -}); diff --git a/apps/marketing-oauth/src/github-oauth.ts b/apps/marketing-oauth/src/github-oauth.ts deleted file mode 100644 index 38144cd..0000000 --- a/apps/marketing-oauth/src/github-oauth.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { randomBytes } from 'node:crypto'; - -export function requireEnv(name: string): string { - const value = process.env[name]?.trim(); - if (!value) { - throw new Error(`Missing required env var: ${name}`); - } - return value; -} - -export function buildGithubAuthorizeUrl(input: { - clientId: string; - redirectUri: string; - scope: string; - state?: string; -}): string { - const url = new URL('https://github.com/login/oauth/authorize'); - url.searchParams.set('client_id', input.clientId); - url.searchParams.set('redirect_uri', input.redirectUri); - url.searchParams.set('scope', input.scope); - url.searchParams.set('state', input.state ?? randomBytes(16).toString('hex')); - return url.toString(); -} - -export async function exchangeGithubCode(input: { - clientId: string; - clientSecret: string; - code: string; - redirectUri: string; -}): Promise<{ accessToken: string } | { error: string }> { - const response = await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - client_id: input.clientId, - client_secret: input.clientSecret, - code: input.code, - redirect_uri: input.redirectUri, - }), - }); - - const body = (await response.json()) as { - access_token?: string; - error?: string; - error_description?: string; - }; - - if (!response.ok || !body.access_token) { - return { - error: body.error_description || body.error || `token exchange failed (${response.status})`, - }; - } - - return { accessToken: body.access_token }; -} diff --git a/apps/marketing-oauth/src/index.ts b/apps/marketing-oauth/src/index.ts deleted file mode 100644 index c621f62..0000000 --- a/apps/marketing-oauth/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Azure Functions v4 programming model entry — side-effect imports register routes. -import './functions/auth'; -import './functions/callback'; -import './functions/health'; diff --git a/apps/marketing-oauth/src/login-script.spec.ts b/apps/marketing-oauth/src/login-script.spec.ts deleted file mode 100644 index 0543dda..0000000 --- a/apps/marketing-oauth/src/login-script.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { buildLoginScript, isAllowedOauthHostname, parseOrigins } from './login-script'; - -describe('parseOrigins', () => { - it('splits and trims hostnames', () => { - assert.deepEqual(parseOrigins('a.example.com, localhost:4321 '), [ - 'a.example.com', - 'localhost:4321', - ]); - }); - - it('rejects empty ORIGINS', () => { - assert.throws(() => parseOrigins(''), /ORIGINS/); - assert.throws(() => parseOrigins(undefined), /ORIGINS/); - }); -}); - -describe('isAllowedOauthHostname', () => { - const marketingSwa = 'purple-field-05048bf00*.azurestaticapps.net'; - const allowlist = ['plattform-kit.poc.singletonsd.com', marketingSwa, 'localhost:4321']; - - it('allows exact custom-domain and localhost hosts', () => { - assert.equal(isAllowedOauthHostname('plattform-kit.poc.singletonsd.com', allowlist), true); - assert.equal(isAllowedOauthHostname('localhost:4321', allowlist), true); - }); - - it('allows marketing SWA default and PR preview hosts', () => { - assert.equal( - isAllowedOauthHostname('purple-field-05048bf00.7.azurestaticapps.net', allowlist), - true, - ); - assert.equal( - isAllowedOauthHostname('purple-field-05048bf00-91.eastasia.7.azurestaticapps.net', allowlist), - true, - ); - }); - - it('rejects other SWA instances and open multi-tenant wildcards', () => { - assert.equal( - isAllowedOauthHostname('kind-rock-0f409fe00-57.eastasia.7.azurestaticapps.net', allowlist), - false, - ); - assert.equal( - isAllowedOauthHostname('attacker.7.azurestaticapps.net', ['*.azurestaticapps.net']), - false, - ); - }); -}); - -describe('buildLoginScript', () => { - it('embeds authorization success payload for Decap handshake', () => { - const html = buildLoginScript('github', 'success', { token: 'gho_test', provider: 'github' }, [ - 'plattform-kit.poc.singletonsd.com', - 'purple-field-05048bf00*.azurestaticapps.net', - ]); - assert.match(html, /authorization:github:success:/); - assert.match(html, /gho_test/); - assert.match(html, /authorizing:github/); - assert.match(html, /plattform-kit\.poc\.singletonsd\.com/); - assert.match(html, /purple-field-05048bf00\*\.azurestaticapps\.net/); - assert.match(html, /swaMarker/); - }); -}); diff --git a/apps/marketing-oauth/src/login-script.ts b/apps/marketing-oauth/src/login-script.ts deleted file mode 100644 index 4b4a602..0000000 --- a/apps/marketing-oauth/src/login-script.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Decap / Netlify CMS OAuth handshake HTML (postMessage to opener). - * @see https://github.com/vencax/netlify-cms-github-oauth-provider - */ - -export type OauthResultMessage = 'success' | 'error'; - -const AZURE_SWA_ROOT = 'azurestaticapps.net'; -const SWA_INSTANCE_SUFFIX = `*.${AZURE_SWA_ROOT}`; - -export function parseOrigins(raw: string | undefined): string[] { - if (!raw?.trim()) { - throw new Error('ORIGINS must be a comma-separated list of allowed hostnames'); - } - return raw - .split(',') - .map((o) => o.trim()) - .filter(Boolean); -} - -/** - * Allow exact hostnames, generic `*` globs, and marketing SWA instance prefixes - * (`purple-field-05048bf00*.azurestaticapps.net`) for default + PR preview hosts. - * Do not use open `*.azurestaticapps.net` (any Azure customer’s SWA). - */ -export function isAllowedOauthHostname(host: string, origins: readonly string[]): boolean { - for (const entry of origins) { - if (entry === host) { - return true; - } - - if (entry.endsWith(SWA_INSTANCE_SUFFIX)) { - const swaName = entry.slice(0, -SWA_INSTANCE_SUFFIX.length); - if (!swaName || swaName.includes('*')) { - continue; - } - if (!host.endsWith(`.${AZURE_SWA_ROOT}`)) { - continue; - } - if (host.startsWith(`${swaName}.`) || host.startsWith(`${swaName}-`)) { - return true; - } - continue; - } - - if (entry.includes('*')) { - const regex = new RegExp(`^${entry.replace(/\./g, '\\.').replace(/\*/g, '[\\w_.-]+')}$`); - if (regex.test(host)) { - return true; - } - } - } - return false; -} - -/** Browser-side mirror of `isAllowedOauthHostname` (no TypeScript in the popup). */ -function buildContainsRuntime(): string { - return `function contains(arr, elem) { - for (var i = 0; i < arr.length; i++) { - var entry = arr[i]; - if (entry === elem) return true; - var swaMarker = '*.azurestaticapps.net'; - if (entry.length >= swaMarker.length && entry.slice(-swaMarker.length) === swaMarker) { - var swaName = entry.slice(0, -swaMarker.length); - if (!swaName || swaName.indexOf('*') >= 0) continue; - if (elem.slice(-'.azurestaticapps.net'.length) !== '.azurestaticapps.net') continue; - if (elem.indexOf(swaName + '.') === 0 || elem.indexOf(swaName + '-') === 0) return true; - continue; - } - if (entry.indexOf('*') >= 0) { - var regex = new RegExp('^' + entry.replace(/\\./g, '\\\\.').replace(/\\*/g, '[\\\\w_.-]+') + '$'); - if (elem.match(regex) !== null) return true; - } - } - return false; - }`; -} - -export function buildLoginScript( - oauthProvider: string, - message: OauthResultMessage, - content: Record | string, - origins: string[], -): string { - const contentJson = typeof content === 'string' ? content : JSON.stringify(content); - return ` - -Decap OAuth - - -

Completing GitHub login…

- -`; -} diff --git a/apps/marketing-oauth/tsconfig.json b/apps/marketing-oauth/tsconfig.json deleted file mode 100644 index 0028a6a..0000000 --- a/apps/marketing-oauth/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "commonjs", - "moduleResolution": "node", - "outDir": "dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": false, - "sourceMap": true - }, - "include": ["src/**/*.ts"] -} diff --git a/apps/marketing/public/admin/config.yml b/apps/marketing/public/admin/config.yml index 6821636..52bdc70 100644 --- a/apps/marketing/public/admin/config.yml +++ b/apps/marketing/public/admin/config.yml @@ -1,10 +1,10 @@ # Decap CMS — git-backed editor for company marketing Markdown. -# Auth: GitHub backend via Azure Function OAuth proxy. +# Auth: shared cms-oauth-kit (https://auth.singletonsd.com). Do not add origins here. backend: name: github repo: singleton-sd/marketing branch: main - base_url: https://ssd-mkt-decap-oauth-prod-ae.azurewebsites.net + base_url: https://auth.singletonsd.com auth_endpoint: auth publish_mode: simple diff --git a/apps/marketing/src/lib/site.test.mjs b/apps/marketing/src/lib/site.test.mjs index 0764f63..3a8c7ea 100644 --- a/apps/marketing/src/lib/site.test.mjs +++ b/apps/marketing/src/lib/site.test.mjs @@ -1,5 +1,8 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import test from 'node:test'; +import { fileURLToPath } from 'node:url'; import { resolveOgImageUrl } from './resolve-og-image-url.mjs'; const FALLBACK = 'https://assets.singletonsd.com/og-image/light/og-default.jpg'; @@ -29,3 +32,15 @@ test('resolveOgImageUrl resolves relative paths against site', () => { test('resolveOgImageUrl returns relative path when site is missing', () => { assert.equal(resolveOgImageUrl('/uploads/og.jpg', undefined, FALLBACK), '/uploads/og.jpg'); }); + +test('Decap GitHub backend uses shared cms-oauth-kit', () => { + const yaml = readFileSync( + join(dirname(fileURLToPath(import.meta.url)), '../../public/admin/config.yml'), + 'utf8', + ); + assert.match(yaml, /^ repo: singleton-sd\/marketing$/m); + assert.match(yaml, /^ base_url: https:\/\/auth\.singletonsd\.com$/m); + assert.match(yaml, /^ auth_endpoint: auth$/m); + assert.doesNotMatch(yaml, /azurewebsites\.net/); + assert.doesNotMatch(yaml, /ssd-mkt-decap-oauth/); +}); diff --git a/docs/marketing-astro-decap.md b/docs/marketing-astro-decap.md index 82a37a1..99dd946 100644 --- a/docs/marketing-astro-decap.md +++ b/docs/marketing-astro-decap.md @@ -12,35 +12,33 @@ Locked stack for `singletonsd.com` (Azure SWA Free `ssd-mkt-prod-ae`). | Styling | Tailwind 3 + Singleton SD design tokens | | Content | Markdown under `apps/marketing/src/content/pages/` | | CMS | Decap at `/admin` | -| OAuth | Function `ssd-mkt-decap-oauth-prod-ae` | +| OAuth | Shared `cms-oauth-kit` at `https://auth.singletonsd.com` | | Secrets | Global KV `ssd-global-kv-prod-ae` | -## Decap bootstrap +## Decap `/admin` auth -1. GitHub → OAuth Apps → New: - - Application name: `Singleton SD Marketing Decap` - - Homepage: `https://singletonsd.com` - - Callback: `https://ssd-mkt-decap-oauth-prod-ae.azurewebsites.net/callback` -2. Store id + secret + form metadata in global KV (and GitHub Variable): +GitHub backend via the org OAuth proxy. Consumer `config.yml` (already set): -```powershell -powershell -File ./scripts/bootstrap-decap-oauth.ps1 -ClientId '' -ClientSecret '' +```yaml +backend: + name: github + repo: singleton-sd/marketing + branch: main + base_url: https://auth.singletonsd.com + auth_endpoint: auth ``` -| KV secret | Purpose | +| Item | Value | | --- | --- | -| `github-decap-oauth-client-id` | Client id (also `DECAP_OAUTH_CLIENT_ID` Variable) | -| `github-decap-oauth-client-secret` | Client secret (Function App Key Vault ref) | -| `github-decap-oauth-app-config` | JSON with name / homepage / callback for reuse | +| Shared repo | [`singleton-sd/cms-oauth-kit`](https://github.com/singleton-sd/cms-oauth-kit) | +| Public origin | `https://auth.singletonsd.com` | +| Callback | `https://auth.singletonsd.com/callback` | +| GitHub OAuth App | [Singleton SD CMS OAuth](https://github.com/settings/applications/3783537) | -All tagged `project=marketing`, `repo=singleton-sd/marketing`, `component=decap-oauth`. +Shared `ORIGINS` already includes `*.singletonsd.com`, apex `singletonsd.com`, and `localhost:4321`. Do **not** maintain a second origin list here. Do **not** open `/admin` on a raw `*.azurestaticapps.net` host. -3. Deploy Function: `powershell -File ./scripts/deploy-decap-oauth.ps1 -OauthClientId ''` +Add origins, rotate the OAuth App, or deploy the Function only in `cms-oauth-kit`. This repo must not recreate `apps/marketing-oauth` or `infra/decap-oauth.bicep`. -Re-read the form values later: +KV `github-decap-oauth-client-secret` is owned by the shared Function. Do not delete it from this cutover. -```powershell -az keyvault secret show --vault-name ssd-global-kv-prod-ae --name github-decap-oauth-app-config --query value -o tsv -``` - -`ORIGINS` must include `singletonsd.com`, `www.singletonsd.com`, the SWA default/preview prefix pattern, and `localhost:4321`. +Local: `http://localhost:4321/admin` (production `base_url`; `ORIGINS` already allows that host). diff --git a/docs/pr-pipelines.md b/docs/pr-pipelines.md index b6e045b..2d84f74 100644 --- a/docs/pr-pipelines.md +++ b/docs/pr-pipelines.md @@ -4,6 +4,7 @@ | --- | --- | --- | | `deploy-marketing.yml` | push `main` | Production SWA upload | | `preview-marketing.yml` | pull_request | SWA Free preview env + PR comment | -| `deploy-decap-oauth.yml` | push `main` (oauth paths) | Function infra + zip | + +Decap OAuth deploys in [`singleton-sd/cms-oauth-kit`](https://github.com/singleton-sd/cms-oauth-kit), not this repository. OIDC Variables required: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`. Deploy token from `ssd-global-kv-prod-ae` / `swa-marketing-deployment-token`. diff --git a/infra/decap-oauth.bicep b/infra/decap-oauth.bicep deleted file mode 100644 index 8c55204..0000000 --- a/infra/decap-oauth.bicep +++ /dev/null @@ -1,143 +0,0 @@ -// Decap CMS GitHub OAuth proxy — Azure Functions on marketing Linux plan. -// Secrets: OAUTH_CLIENT_SECRET from global Key Vault only. -// CAF: ssd-mkt-decap-oauth-prod-ae - -@description('Azure region') -param location string = resourceGroup().location - -@description('Function App name (CAF)') -param functionAppName string = 'ssd-mkt-decap-oauth-prod-ae' - -@description('Storage account for Functions (3-24 lowercase alphanumeric)') -param storageAccountName string = 'ssdmktstoauth' - -@description('Existing Linux App Service Plan name') -param planName string = 'ssd-mkt-plan-prod-ae' - -@description('Global Key Vault name (rg-ssd-global)') -param keyVaultName string = 'ssd-global-kv-prod-ae' - -@description('Resource group of the global Key Vault') -param keyVaultResourceGroup string = 'rg-ssd-global' - -@description('GitHub OAuth App client id (non-secret)') -param oauthClientId string - -@description('Comma-separated Decap opener hostnames (no scheme)') -param origins string = 'singletonsd.com,www.singletonsd.com,localhost:4321' - -@description('KV secret name for GitHub OAuth client secret') -param oauthClientSecretName string = 'github-decap-oauth-client-secret' - -var roleKeyVaultSecretsUser = '4633458b-17de-408a-b874-0445c86b69e6' -var redirectUrl = 'https://${functionAppName}.azurewebsites.net/callback' - -var marketingTags = { - org: 'singleton-sd' - project: 'marketing' - repo: 'singleton-sd/marketing' - env: 'prod' - component: 'decap-oauth' - 'managed-by': 'bicep' -} - -resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = { - name: storageAccountName - location: location - tags: marketingTags - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - properties: { - allowBlobPublicAccess: false - minimumTlsVersion: 'TLS1_2' - supportsHttpsTrafficOnly: true - } -} - -resource plan 'Microsoft.Web/serverfarms@2023-12-01' existing = { - name: planName -} - -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { - name: keyVaultName - scope: resourceGroup(keyVaultResourceGroup) -} - -resource functionApp 'Microsoft.Web/sites@2023-12-01' = { - name: functionAppName - location: location - tags: marketingTags - kind: 'functionapp,linux' - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: plan.id - httpsOnly: true - siteConfig: { - linuxFxVersion: 'Node|24' - ftpsState: 'Disabled' - minTlsVersion: '1.2' - appSettings: [ - { - name: 'AzureWebJobsStorage' - value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storage.listKeys().keys[0].value}' - } - { - name: 'FUNCTIONS_EXTENSION_VERSION' - value: '~4' - } - { - name: 'FUNCTIONS_WORKER_RUNTIME' - value: 'node' - } - { - name: 'AzureWebJobsFeatureFlags' - value: 'EnableWorkerIndexing' - } - { - name: 'WEBSITE_NODE_DEFAULT_VERSION' - value: '~24' - } - { - name: 'OAUTH_CLIENT_ID' - value: oauthClientId - } - { - name: 'OAUTH_CLIENT_SECRET' - value: '@Microsoft.KeyVault(SecretUri=${keyVault.properties.vaultUri}secrets/${oauthClientSecretName}/)' - } - { - name: 'REDIRECT_URL' - value: redirectUrl - } - { - name: 'SCOPES' - value: 'repo,user' - } - { - name: 'ORIGINS' - value: origins - } - ] - } - } -} - -resource kvFunctionSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(keyVault.id, functionApp.id, roleKeyVaultSecretsUser) - scope: keyVault - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleKeyVaultSecretsUser) - principalId: functionApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -output functionAppName string = functionApp.name -output functionAppHostname string = functionApp.properties.defaultHostName -output functionAppPrincipalId string = functionApp.identity.principalId -output redirectUrl string = redirectUrl -output baseUrl string = 'https://${functionApp.properties.defaultHostName}' diff --git a/infra/marketing.bicep b/infra/marketing.bicep index 3d1c2c5..8536b23 100644 --- a/infra/marketing.bicep +++ b/infra/marketing.bicep @@ -1,8 +1,6 @@ -// Company marketing Static Web App + shared Linux plan for Decap OAuth Function. +// Company marketing Static Web App. // Deploy into rg-ssd-marketing. Secrets live in rg-ssd-global / ssd-global-kv-prod-ae. - -@description('Azure region for App Service plan / Function') -param location string = resourceGroup().location +// Decap OAuth is the shared cms-oauth-kit service (auth.singletonsd.com) — no plan here. @description('Static Web Apps region (Free SKU is region-limited; eastasia works)') param swaLocation string = 'eastasia' @@ -10,13 +8,6 @@ param swaLocation string = 'eastasia' @description('CAF marketing Static Web App name') param marketingSwaName string = 'ssd-mkt-prod-ae' -@description('App Service Plan name for Decap OAuth Function') -param planName string = 'ssd-mkt-plan-prod-ae' - -@description('App Service Plan SKU') -@allowed(['B1', 'Y1']) -param appServiceSku string = 'B1' - @allowed(['Free', 'Standard']) param staticWebAppSku string = 'Free' @@ -28,19 +19,6 @@ var marketingTags = { 'managed-by': 'bicep' } -resource appPlan 'Microsoft.Web/serverfarms@2023-12-01' = { - name: planName - location: location - tags: union(marketingTags, { component: 'appservice-plan' }) - sku: { - name: appServiceSku - } - kind: 'linux' - properties: { - reserved: true - } -} - resource marketingStaticWebApp 'Microsoft.Web/staticSites@2022-09-01' = { name: marketingSwaName location: swaLocation @@ -57,5 +35,3 @@ resource marketingStaticWebApp 'Microsoft.Web/staticSites@2022-09-01' = { output marketingStaticWebAppName string = marketingStaticWebApp.name output marketingStaticWebAppHostname string = marketingStaticWebApp.properties.defaultHostname -output planNameOut string = appPlan.name -output planId string = appPlan.id diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02f25e3..10b5541 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,19 +67,6 @@ importers: specifier: ^8.4.49 version: 8.5.26 - apps/marketing-oauth: - dependencies: - '@azure/functions': - specifier: ^4.6.0 - version: 4.16.2 - devDependencies: - '@types/node': - specifier: ^22.10.2 - version: 22.20.1 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - packages: '@alloc/quick-lru@5.2.0': @@ -133,14 +120,6 @@ packages: '@astrojs/yaml2ts@0.2.4': resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==} - '@azure/functions-extensions-base@0.3.0': - resolution: {integrity: sha512-Cux0hLu5ZXlC/Kb+yvJVhRLIdkfFwui2HeT5oGZL00r/GCUUkhGTzRfZUjRN4Bq729mPv3okPucz2z7SMQLStA==} - engines: {node: '>=18.0'} - - '@azure/functions@4.16.2': - resolution: {integrity: sha512-6uq0Z7e3njy8fHpgCVIJWDtbkZz+BYXc6L8fQjisf8+hPdnauM5yZ70j9YC8xas6zvL1dFA+EfLuZcNYyuWLTg==} - engines: {node: '>=20.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -3632,13 +3611,6 @@ snapshots: dependencies: yaml: 2.9.0 - '@azure/functions-extensions-base@0.3.0': {} - - '@azure/functions@4.16.2': - dependencies: - '@azure/functions-extensions-base': 0.3.0 - cookie: 0.7.2 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 diff --git a/scripts/bootstrap-decap-oauth.ps1 b/scripts/bootstrap-decap-oauth.ps1 deleted file mode 100644 index 38b0c0c..0000000 --- a/scripts/bootstrap-decap-oauth.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -<# -.SYNOPSIS - After creating the GitHub OAuth App, store id/secret/config in global KV - and set the GitHub Actions Variable. - -.DESCRIPTION - Writes three Key Vault secrets (tagged for marketing): - - | Secret | Contents | - | --- | --- | - | github-decap-oauth-client-id | OAuth App client id | - | github-decap-oauth-client-secret | OAuth App client secret | - | github-decap-oauth-app-config | JSON: name, homepageUrl, callbackUrl, description | - - Re-read later with: - az keyvault secret show --vault-name ssd-global-kv-prod-ae --name github-decap-oauth-app-config --query value -o tsv - -.EXAMPLE - powershell -File ./scripts/bootstrap-decap-oauth.ps1 -ClientId 'Ov23li...' -ClientSecret '...' -#> -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)][string] $ClientId, - [Parameter(Mandatory = $true)][string] $ClientSecret, - - [string] $VaultName = 'ssd-global-kv-prod-ae', - [string] $Repo = 'singleton-sd/marketing', - - [string] $ApplicationName = 'Singleton SD Marketing Decap', - [string] $HomepageUrl = 'https://singletonsd.com', - [string] $CallbackUrl = 'https://ssd-mkt-decap-oauth-prod-ae.azurewebsites.net/callback', - [string] $Description = 'Decap CMS login for company marketing' -) - -$ErrorActionPreference = 'Stop' - -$tags = @( - 'org=singleton-sd', - 'project=marketing', - "repo=$Repo", - 'env=prod', - 'component=decap-oauth', - 'managed-by=cli' -) - -function Set-KvSecret { - param([string]$Name, [string]$Value) - az keyvault secret set ` - --vault-name $VaultName ` - --name $Name ` - --value $Value ` - --tags $tags ` - --query name -o tsv | Out-Host -} - -$appConfig = @{ - applicationName = $ApplicationName - homepageUrl = $HomepageUrl - callbackUrl = $CallbackUrl - description = $Description - clientIdSecret = 'github-decap-oauth-client-id' - clientSecretSecret = 'github-decap-oauth-client-secret' - functionApp = 'ssd-mkt-decap-oauth-prod-ae' - createdAt = (Get-Date).ToUniversalTime().ToString('o') -} | ConvertTo-Json -Compress - -Write-Host "Writing OAuth secrets to $VaultName ..." -Set-KvSecret -Name 'github-decap-oauth-client-id' -Value $ClientId -Set-KvSecret -Name 'github-decap-oauth-client-secret' -Value $ClientSecret -Set-KvSecret -Name 'github-decap-oauth-app-config' -Value $appConfig - -Write-Host "Setting GitHub Variable DECAP_OAUTH_CLIENT_ID on $Repo ..." -gh variable set DECAP_OAUTH_CLIENT_ID --body $ClientId --repo $Repo - -Write-Host "" -Write-Host "KV secrets saved:" -Write-Host " github-decap-oauth-client-id" -Write-Host " github-decap-oauth-client-secret" -Write-Host " github-decap-oauth-app-config" -Write-Host "" -Write-Host "OAuth App form values (also in app-config JSON):" -Write-Host " Application name: $ApplicationName" -Write-Host " Homepage URL: $HomepageUrl" -Write-Host " Callback URL: $CallbackUrl" -Write-Host "" -Write-Host "Next: powershell -File ./scripts/deploy-decap-oauth.ps1 -OauthClientId $ClientId" diff --git a/scripts/deploy-decap-oauth.ps1 b/scripts/deploy-decap-oauth.ps1 deleted file mode 100644 index 277e3cd..0000000 --- a/scripts/deploy-decap-oauth.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -<# -.SYNOPSIS - Deploy Decap GitHub OAuth Azure Function (infra + zip). -#> -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string] $OauthClientId, - - [string] $ResourceGroup = 'rg-ssd-marketing', - [string] $FunctionAppName = 'ssd-mkt-decap-oauth-prod-ae', - [string] $Origins = '', - [switch] $SkipInfra, - [switch] $SkipZip -) - -$ErrorActionPreference = 'Stop' -$root = Resolve-Path (Join-Path $PSScriptRoot '..') -$appDir = Join-Path $root 'apps/marketing-oauth' - -Add-Type -AssemblyName System.IO.Compression.FileSystem - -Push-Location $root -try { - if ([string]::IsNullOrWhiteSpace($Origins)) { - $hostName = az staticwebapp show -g $ResourceGroup -n ssd-mkt-prod-ae --query defaultHostname -o tsv - $prefix = ($hostName -split '\.')[0] - $Origins = "singletonsd.com,www.singletonsd.com,${prefix}*.azurestaticapps.net,localhost:4321" - } - - if (-not $SkipInfra) { - Write-Host "Deploying Bicep infra/decap-oauth.bicep ..." - az deployment group create ` - --resource-group $ResourceGroup ` - --template-file (Join-Path $root 'infra/decap-oauth.bicep') ` - --parameters oauthClientId=$OauthClientId origins=$Origins ` - --name "decap-oauth-$(Get-Date -Format 'yyyyMMddHHmmss')" | Out-Host - } - - if ($SkipZip) { - return - } - - Write-Host "Building @singleton-sd/marketing-oauth ..." - pnpm --filter @singleton-sd/marketing-oauth... install - pnpm --filter @singleton-sd/marketing-oauth run build - - $stage = Join-Path $env:TEMP "decap-oauth-stage-$(Get-Random)" - New-Item -ItemType Directory -Path $stage | Out-Null - try { - Copy-Item (Join-Path $appDir 'host.json') $stage - Copy-Item (Join-Path $appDir 'package.json') $stage - Copy-Item (Join-Path $appDir 'dist') (Join-Path $stage 'dist') -Recurse - - Push-Location $stage - try { - npm install --omit=dev --package-lock=false | Out-Host - $zipPath = Join-Path $env:TEMP 'decap-oauth-deploy.zip' - if (Test-Path $zipPath) { Remove-Item $zipPath -Force } - [System.IO.Compression.ZipFile]::CreateFromDirectory($stage, $zipPath) - } - finally { - Pop-Location - } - - Write-Host "Zip deploying to $FunctionAppName ..." - az functionapp deployment source config-zip ` - --resource-group $ResourceGroup ` - --name $FunctionAppName ` - --src $zipPath | Out-Host - - Write-Host "Done. base_url=https://$FunctionAppName.azurewebsites.net" - } - finally { - Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue - } -} -finally { - Pop-Location -} diff --git a/scripts/release-changed.mjs b/scripts/release-changed.mjs index 14e8d62..0b7b5ee 100644 --- a/scripts/release-changed.mjs +++ b/scripts/release-changed.mjs @@ -151,9 +151,6 @@ function watchPathsFor(pkg) { if (pkg.name === '@singleton-sd/marketing') { paths.push('infra/marketing.bicep', 'infra/global.bicep'); } - if (pkg.name === '@singleton-sd/marketing-oauth') { - paths.push('infra/decap-oauth.bicep', 'scripts/deploy-decap-oauth.ps1'); - } return paths; }