chore(operator): O.9 verification + roll follow-ups into NEXT-STEPS

- Add _verify-token.get.ts to both operator and portal — decodes the
  access token stored in the nuxt-oidc-auth session and echoes iss/aud/
  sub/groups. Used to confirm operator tokens carry aud=dezky-operator
  and portal tokens carry aud=dezky-portal. Listed in NEXT-STEPS.md as
  throwaway, to be removed when proper verification surfaces exist.
- OPERATOR-PLAN.md O.9 marked done with the actual claims captured + the
  Mongo-side verification of attach + suspend flows.
- NEXT-STEPS.md: replaced the "Operator portal — out-of-band track"
  section with a "shipped + follow-ups" version. The 9-item follow-up
  list (impersonation, audit, flags, incidents, support, partner
  portal, env switcher, on-call, workspace impersonation) is now the
  authoritative roadmap, not buried inside OPERATOR-PLAN.md.
This commit is contained in:
Ronni Baslund
2026-05-24 08:47:56 +02:00
parent c71e782dc0
commit 19e1a4fca3
4 changed files with 148 additions and 22 deletions
@@ -0,0 +1,31 @@
// Throwaway verification endpoint mirroring the operator one. Decodes the
// portal access token from the nuxt-oidc-auth session and echoes the claims
// that matter (iss/aud/sub/groups/exp). Useful for confirming that signing
// in here yields aud=dezky-portal, distinct from the operator's dezky-operator.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
function decodeJwtClaims(token: string): Record<string, unknown> {
const parts = token.split('.')
if (parts.length < 2) throw new Error('Not a JWT')
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
}
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'No session' })
const claims = decodeJwtClaims(accessToken)
return {
iss: claims.iss,
aud: claims.aud,
sub: claims.sub,
email: claims.email,
groups: claims.groups,
exp: claims.exp,
iat: claims.iat,
}
})