From 6ab88749a7ae3456f3b63bf381df76acb7e7c151 Mon Sep 17 00:00:00 2001 From: Bryan Brooks Date: Wed, 25 Feb 2026 10:27:08 -0600 Subject: [PATCH] fix: Express 5 req.params type safety, restore CI pipeline Add String() wrappers on all req.params usages to satisfy Express 5's string | string[] union type. Restore .gitea/workflows/deploy.yml that was accidentally removed during FOSS sanitization. --- .gitea/workflows/deploy.yml | 132 ++++++++++++++++++++++++++ gateway/src/routes/admin-dashboard.ts | 25 ++--- gateway/src/routes/admin.ts | 81 +++++++++------- gateway/src/routes/email.ts | 2 +- gateway/src/routes/passkey.ts | 4 +- 5 files changed, 194 insertions(+), 50 deletions(-) create mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..8952c6d --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,132 @@ +name: Deploy ATAuth to DO +# CI/CD: Gitea Actions โ†’ DigitalOcean managed k8s + +on: + push: + branches: [main] + paths-ignore: + - '*.md' + - 'docs/**' + +env: + REGISTRY: registry.digitalocean.com/ghostmesh-registry + IMAGE_NAME: atauth + DEPLOYMENT: atauth + NAMESPACE: atauth + +jobs: + test: + name: Test Gateway + runs-on: ubuntu-latest + defaults: + run: + working-directory: gateway + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Run tests + run: npm run test:run + + - name: Notify test failure + if: failure() + run: | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + FIRST_LINE=$(echo "${{ github.event.head_commit.message }}" | head -1) + MSG=$(jq -n --arg body "๐Ÿงช ${{ github.repository }}: tests FAILED @ ${SHORT_SHA} + โ”” ${FIRST_LINE}" '{msgtype: "m.text", body: $body}') + curl -sf -X PUT \ + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "$MSG" || true + + build-and-deploy: + name: Build, Push, Deploy + runs-on: host + needs: [test] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set short SHA + id: sha + run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT" + + - name: Notify build started + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) + MSG=$(jq -n --arg body "๐Ÿ”จ ${{ github.repository }}: build started @ ${{ steps.sha.outputs.short }} + โ”œ Commit: $FIRST_LINE + โ”” Triggered by: ${{ github.actor }}" '{msgtype: "m.text", body: $body}') + curl -sf -X PUT \ + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "$MSG" || true + + - name: Login to DO Container Registry + run: echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin + + - name: Build Docker image + run: | + docker build \ + --platform linux/amd64 \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ + gateway/ + + - name: Push Docker image + run: | + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Deploy to DO Kubernetes + run: | + echo "${{ secrets.DO_KUBECONFIG }}" | base64 -d > /tmp/kubeconfig + KUBECONFIG=/tmp/kubeconfig kubectl set image deployment/${{ env.DEPLOYMENT }} \ + ${{ env.DEPLOYMENT }}=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} \ + -n ${{ env.NAMESPACE }} + KUBECONFIG=/tmp/kubeconfig kubectl rollout status deployment/${{ env.DEPLOYMENT }} -n ${{ env.NAMESPACE }} --timeout=120s + rm -f /tmp/kubeconfig + + - name: Notify build result + if: always() + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + if [ "${{ job.status }}" = "success" ]; then + EMOJI="โœ…"; STATUS="deployed" + else + EMOJI="โŒ"; STATUS="FAILED" + fi + FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) + MSG=$(jq -n --arg body "${EMOJI} ${{ github.repository }}: ${STATUS} @ ${{ steps.sha.outputs.short }} + โ”” $FIRST_LINE" '{msgtype: "m.text", body: $body}') + curl -sf -X PUT \ + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "$MSG" || true + + - name: Clean up Docker images + if: always() + run: | + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} 2>/dev/null || true + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 2>/dev/null || true diff --git a/gateway/src/routes/admin-dashboard.ts b/gateway/src/routes/admin-dashboard.ts index ce41d10..5679063 100644 --- a/gateway/src/routes/admin-dashboard.ts +++ b/gateway/src/routes/admin-dashboard.ts @@ -212,7 +212,7 @@ export function createAdminDashboardRoutes( router.post('/origins/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - db.removeProxyAllowedOrigin(parseInt(req.params.id, 10)); + db.removeProxyAllowedOrigin(parseInt(String(req.params.id), 10)); res.redirect('/admin/dashboard/origins?msg=Origin+deleted'); }); @@ -347,7 +347,7 @@ export function createAdminDashboardRoutes( router.post('/access/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - db.deleteProxyAccessRule(parseInt(req.params.id, 10)); + db.deleteProxyAccessRule(parseInt(String(req.params.id), 10)); res.redirect('/admin/dashboard/access?msg=Rule+deleted'); }); @@ -408,7 +408,7 @@ export function createAdminDashboardRoutes( router.post('/sessions/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - db.deleteProxySession(req.params.id); + db.deleteProxySession(String(req.params.id)); res.redirect('/admin/dashboard/sessions?msg=Session+revoked'); }); @@ -700,7 +700,7 @@ export function createAdminDashboardRoutes( // --- Edit Client --- router.get('/clients/:id/edit', (req: Request, res: Response) => { - const client = db.getOIDCClient(req.params.id); + const client = db.getOIDCClient(String(req.params.id)); if (!client) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); } @@ -798,7 +798,7 @@ export function createAdminDashboardRoutes( router.post('/clients/:id/edit', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - const clientId = req.params.id; + const clientId = String(req.params.id); const existing = db.getOIDCClient(clientId); if (!existing) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); @@ -838,7 +838,7 @@ export function createAdminDashboardRoutes( router.post('/clients/:id/rotate-secret', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - const clientId = req.params.id; + const clientId = String(req.params.id); const existing = db.getOIDCClient(clientId); if (!existing) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); @@ -857,7 +857,7 @@ export function createAdminDashboardRoutes( router.post('/clients/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - db.deleteApp(req.params.id); + db.deleteApp(String(req.params.id)); res.redirect('/admin/dashboard/clients?msg=Client+deleted'); }); @@ -886,7 +886,7 @@ export function createAdminDashboardRoutes( }); router.get('/clients/wizard/:preset', (req: Request, res: Response) => { - const presetData = getPresetByKey(req.params.preset); + const presetData = getPresetByKey(String(req.params.preset)); if (!presetData) { return res.status(404).setHeader('Content-Type', 'text/html; charset=utf-8').send( layout('Not Found', '

Preset not found

Back to wizard

') @@ -979,7 +979,8 @@ export function createAdminDashboardRoutes( router.post('/clients/wizard/:preset', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; - const presetData = getPresetByKey(req.params.preset); + const presetKey = String(req.params.preset); + const presetData = getPresetByKey(presetKey); if (!presetData) { return res.redirect('/admin/dashboard/clients/wizard'); } @@ -990,15 +991,15 @@ export function createAdminDashboardRoutes( const require_pkce = req.body.require_pkce === 'on'; if (!id || !name || !domain) { - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } if (!/^[a-z0-9][a-z0-9_-]*$/.test(id) || id.length > 64) { - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } const existing = db.getOIDCClient(id); if (existing) { - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } // Build redirect URIs from template diff --git a/gateway/src/routes/admin.ts b/gateway/src/routes/admin.ts index 3b6039e..ddcf47c 100644 --- a/gateway/src/routes/admin.ts +++ b/gateway/src/routes/admin.ts @@ -188,9 +188,10 @@ export function createAdminRoutes( * Get application configuration (without secret) */ router.get('/apps/:id', requireAdmin, async (req: Request, res: Response) => { - const app = db.getApp(req.params.id); + const id = String(req.params.id); + const app = db.getApp(id); if (!app) { - throw httpError.notFound('app_not_found', `Application '${req.params.id}' not found`); + throw httpError.notFound('app_not_found', `Application '${id}' not found`); } res.json({ @@ -206,15 +207,16 @@ export function createAdminRoutes( * Update application configuration */ router.put('/apps/:id', requireAdmin, async (req: Request, res: Response) => { - const existing = db.getApp(req.params.id); + const id = String(req.params.id); + const existing = db.getApp(id); if (!existing) { - throw httpError.notFound('app_not_found', `Application '${req.params.id}' not found`); + throw httpError.notFound('app_not_found', `Application '${id}' not found`); } const { name, token_ttl_seconds, callback_url, rotate_secret } = req.body; const updated = { - id: req.params.id, + id, name: name || existing.name, hmac_secret: rotate_secret ? generateHmacSecret() : existing.hmac_secret, token_ttl_seconds: token_ttl_seconds || existing.token_ttl_seconds, @@ -224,9 +226,9 @@ export function createAdminRoutes( db.upsertApp(updated); if (rotate_secret) { - db.logAuditEvent('app.secret_rotate', 'admin', req.params.id, 'HMAC secret rotated', clientIp(req)); + db.logAuditEvent('app.secret_rotate', 'admin', id, 'HMAC secret rotated', clientIp(req)); } else { - db.logAuditEvent('app.update', 'admin', req.params.id, `Updated app config`, clientIp(req)); + db.logAuditEvent('app.update', 'admin', id, `Updated app config`, clientIp(req)); } const response: Record = { @@ -360,9 +362,10 @@ export function createAdminRoutes( * Get OIDC client details */ router.get('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { - const client = db.getOIDCClient(req.params.id); + const id = String(req.params.id); + const client = db.getOIDCClient(id); if (!client) { - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); } res.json({ @@ -386,9 +389,10 @@ export function createAdminRoutes( * Update OIDC client */ router.put('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { - const existing = db.getOIDCClient(req.params.id); + const id = String(req.params.id); + const existing = db.getOIDCClient(id); if (!existing) { - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); } const { @@ -403,7 +407,7 @@ export function createAdminRoutes( refresh_token_ttl_seconds, } = req.body; - db.updateOIDCClient(req.params.id, { + db.updateOIDCClient(id, { redirect_uris: redirect_uris ?? existing.redirect_uris, grant_types: grant_types ?? existing.grant_types, allowed_scopes: allowed_scopes ?? existing.allowed_scopes, @@ -416,16 +420,16 @@ export function createAdminRoutes( // Update app name if provided if (name) { - const app = db.getApp(req.params.id); + const app = db.getApp(id); if (app) { db.upsertApp({ ...app, name }); } } - db.logAuditEvent('oidc_client.update', 'admin', req.params.id, 'Updated OIDC client config', clientIp(req)); + db.logAuditEvent('oidc_client.update', 'admin', id, 'Updated OIDC client config', clientIp(req)); res.json({ - id: req.params.id, + id, message: 'OIDC client updated', }); }); @@ -435,13 +439,14 @@ export function createAdminRoutes( * Delete OIDC client */ router.delete('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { - const existing = db.getOIDCClient(req.params.id); + const id = String(req.params.id); + const existing = db.getOIDCClient(id); if (!existing) { - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); } - db.deleteApp(req.params.id); - db.logAuditEvent('oidc_client.delete', 'admin', req.params.id, `Deleted OIDC client "${existing.name}"`, clientIp(req)); + db.deleteApp(id); + db.logAuditEvent('oidc_client.delete', 'admin', id, `Deleted OIDC client "${existing.name}"`, clientIp(req)); res.json({ message: 'OIDC client deleted', @@ -453,19 +458,20 @@ export function createAdminRoutes( * Rotate OIDC client secret */ router.post('/oidc/clients/:id/rotate-secret', requireAdmin, async (req: Request, res: Response) => { - const existing = db.getOIDCClient(req.params.id); + const id = String(req.params.id); + const existing = db.getOIDCClient(id); if (!existing) { - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); } const clientSecret = crypto.randomBytes(32).toString('hex'); const clientSecretHash = crypto.createHash('sha256').update(clientSecret).digest('hex'); - db.updateOIDCClientSecret(req.params.id, clientSecretHash); - db.logAuditEvent('oidc_client.secret_rotate', 'admin', req.params.id, 'Client secret rotated', clientIp(req)); + db.updateOIDCClientSecret(id, clientSecretHash); + db.logAuditEvent('oidc_client.secret_rotate', 'admin', id, 'Client secret rotated', clientIp(req)); res.json({ - id: req.params.id, + id, client_secret: clientSecret, message: 'Client secret rotated. Update your application configuration!', }); @@ -504,8 +510,9 @@ export function createAdminRoutes( * Revoke a specific session */ router.delete('/sessions/:id', requireAdmin, async (req: Request, res: Response) => { - db.deleteSession(req.params.id); - db.logAuditEvent('session.revoke', 'admin', req.params.id, 'Session revoked', clientIp(req)); + const id = String(req.params.id); + db.deleteSession(id); + db.logAuditEvent('session.revoke', 'admin', id, 'Session revoked', clientIp(req)); res.json({ message: 'Session revoked' }); }); @@ -575,7 +582,7 @@ export function createAdminRoutes( * Get user details including MFA status and passkeys */ router.get('/users/:did', requireAdmin, async (req: Request, res: Response) => { - const { did } = req.params; + const did = String(req.params.did); const passkeys = passkeyService?.listPasskeys(did) || []; const mfaStatus = mfaService?.getMFAStatus(did, passkeys.length) || { @@ -610,7 +617,7 @@ export function createAdminRoutes( * Reset MFA for a user (admin override) */ router.delete('/users/:did/mfa', requireAdmin, async (req: Request, res: Response) => { - const { did } = req.params; + const did = String(req.params.did); if (mfaService) { mfaService.disableTOTP(did); @@ -628,7 +635,8 @@ export function createAdminRoutes( * Delete a passkey for a user (admin override) */ router.delete('/users/:did/passkeys/:passkeyId', requireAdmin, async (req: Request, res: Response) => { - const { did, passkeyId } = req.params; + const did = String(req.params.did); + const passkeyId = String(req.params.passkeyId); if (passkeyService) { const success = passkeyService.deletePasskey(did, passkeyId); @@ -699,8 +707,9 @@ export function createAdminRoutes( * Remove an allowed origin */ router.delete('/proxy/origins/:id', requireAdmin, async (req: Request, res: Response) => { - db.removeProxyAllowedOrigin(parseInt(req.params.id, 10)); - db.logAuditEvent('proxy.origin_remove', 'admin', req.params.id, 'Removed proxy origin', clientIp(req)); + const id = String(req.params.id); + db.removeProxyAllowedOrigin(parseInt(id, 10)); + db.logAuditEvent('proxy.origin_remove', 'admin', id, 'Removed proxy origin', clientIp(req)); res.json({ message: 'Origin removed' }); }); @@ -722,8 +731,9 @@ export function createAdminRoutes( * Revoke a proxy session */ router.delete('/proxy/sessions/:id', requireAdmin, async (req: Request, res: Response) => { - db.deleteProxySession(req.params.id); - db.logAuditEvent('proxy.session_revoke', 'admin', req.params.id, 'Proxy session revoked', clientIp(req)); + const id = String(req.params.id); + db.deleteProxySession(id); + db.logAuditEvent('proxy.session_revoke', 'admin', id, 'Proxy session revoked', clientIp(req)); res.json({ message: 'Proxy session revoked' }); }); @@ -806,8 +816,9 @@ export function createAdminRoutes( * Delete an access rule. */ router.delete('/proxy/access/:id', requireAdmin, async (req: Request, res: Response) => { - db.deleteProxyAccessRule(parseInt(req.params.id, 10)); - db.logAuditEvent('proxy.access_rule_delete', 'admin', req.params.id, 'Deleted access rule', clientIp(req)); + const id = String(req.params.id); + db.deleteProxyAccessRule(parseInt(id, 10)); + db.logAuditEvent('proxy.access_rule_delete', 'admin', id, 'Deleted access rule', clientIp(req)); res.json({ message: 'Access rule deleted' }); }); diff --git a/gateway/src/routes/email.ts b/gateway/src/routes/email.ts index 507d17e..97cd826 100644 --- a/gateway/src/routes/email.ts +++ b/gateway/src/routes/email.ts @@ -128,7 +128,7 @@ export function createEmailRouter( throw new HttpError(401, 'unauthorized', 'Authentication required'); } - const { email } = req.params; + const email = String(req.params.email); if (!email || !isValidEmail(email)) { throw new HttpError(400, 'invalid_request', 'Invalid email address'); diff --git a/gateway/src/routes/passkey.ts b/gateway/src/routes/passkey.ts index 963a07f..b8a7f9d 100644 --- a/gateway/src/routes/passkey.ts +++ b/gateway/src/routes/passkey.ts @@ -196,7 +196,7 @@ export function createPasskeyRouter( throw new HttpError(401, 'unauthorized', 'Authentication required'); } - const { id } = req.params; + const id = String(req.params.id); const { name } = req.body as { name: string }; if (!name) { @@ -227,7 +227,7 @@ export function createPasskeyRouter( throw new HttpError(401, 'unauthorized', 'Authentication required'); } - const { id } = req.params; + const id = String(req.params.id); // Prevent deleting last passkey if MFA is required // (This is a safety check - can be configured based on policy) -- knotmirror 0.0.1