refactor(split): make the app self-hosting ready

Remove the transitional website auth, admin hooks, SEO and hosted analytics together, then align the app routes, runtime configuration, tests and dependencies. These changes form one atomic cleanup because the filtered app must switch its identity and runtime surfaces as a unit.
This commit is contained in:
itsrubberduck
2026-07-27 19:17:27 +02:00
parent 3ed3865ebb
commit c8c2365c4d
29 changed files with 123 additions and 2038 deletions

View File

@@ -1,27 +1,7 @@
import { defineEventHandler } from 'h3'
import { clearRefreshTokenCookie, getUserFromEvent } from '../../utils/auth'
import { getAuthMode } from '../../utils/authMode'
import { clearAppSession } from '../../utils/session'
export default defineEventHandler(async (event) => {
// Always drop the app's own session, whatever else happens below.
clearAppSession(event)
// AUTH_MODE=open has no session to end — the local identity is the instance.
if (getAuthMode() === 'open') {
return { success: true }
}
const user = await getUserFromEvent(event)
// PHASE 1 (app repo): drop this branch. Bumping tokenVersion invalidates the
// website's outstanding access tokens; an app session is ended by clearing
// the cookie above. `tokenVersion` only exists on website User documents.
if (user && typeof (user as any).tokenVersion === 'number') {
;(user as any).tokenVersion += 1
await user.save().catch(() => null)
}
clearRefreshTokenCookie(event)
return { success: true }
})

View File

@@ -8,8 +8,5 @@ export default defineEventHandler(async (event) => {
name: user.name,
role: user.role,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
invitationCodesIssued: user.invitationCodesIssued,
}
})

View File

@@ -1,8 +1,7 @@
import { defineEventHandler } from 'h3'
import { createError, defineEventHandler } from 'h3'
import { AppUser } from '../../models/AppUser'
import { getAuthMode, getLocalAppUser } from '../../utils/authMode'
import { createAppAccessToken, issueAppSession, readAppSession } from '../../utils/session'
import { rotateRefreshToken } from '../../utils/auth'
/**
* Hands the client a fresh bearer token for whatever session it already has.
@@ -11,8 +10,6 @@ import { rotateRefreshToken } from '../../utils/auth'
* "session" means is the server's job:
* open → the single local identity, no credentials involved
* sso → the app's own session cookie, minted at the SSO exchange
* (transitional) → the website's refresh cookie, for users logged in before
* the split. That last branch disappears with the website half of auth.ts.
*/
export default defineEventHandler(async (event) => {
if (getAuthMode() === 'open') {
@@ -31,6 +28,5 @@ export default defineEventHandler(async (event) => {
}
}
// PHASE 1 (app repo): delete — there is no website refresh cookie there.
return await rotateRefreshToken(event)
throw createError({ statusCode: 401, statusMessage: 'No app session present' })
})

View File

@@ -10,7 +10,7 @@ interface ExchangeResponse {
}
/**
* Consumer half of the SSO handoff (see Phase 0.2 of the split plan).
* Consumer half of the optional SSO handoff.
*
* The browser only ever carries a one-time code. It is redeemed here,
* server-to-server against the issuer and authenticated with SERVICE_SECRET, so

View File

@@ -60,7 +60,6 @@ export default defineEventHandler(async (event) => {
return { success: true, id: String(report._id), code }
}
const adminUrl = `${process.env.APP_URL || 'https://app.opensquawk.de'}/admin`
const sourceLabel = SOURCE_LABELS[source]
const stateInfo = body?.pmState?.currentStateId
? `State: ${body.pmState.currentStateId} (Flow: ${body.pmState.flowSlug || '—'})`
@@ -75,7 +74,7 @@ export default defineEventHandler(async (event) => {
<p><strong>Fehlerbeschreibung/Featurewunsch:</strong><br>${comment.replace(/\n/g, '<br>')}</p>
<p><strong>Nenne den Fehlercode ${code} beim Commit.</strong></p>
${stateInfo ? `<p><strong>${stateInfo}</strong></p>` : ''}
<p><a href="${adminUrl}">Im Admin-Panel ansehen →</a></p>`,
`,
text: `Bug Report von ${contact}
Bereich: ${sourceLabel}
@@ -84,8 +83,7 @@ ${comment}
Nenne den Fehlercode ${code} beim Commit.
${stateInfo}
Admin: ${adminUrl}`,
`,
}).catch(() => {})
return { success: true, id: String(report._id), code }

View File

@@ -1,11 +1,11 @@
import { createError } from 'h3'
import { getDevBypassUser, issueAuthTokens } from '../../utils/auth'
import { getDevBypassUser } from '../../utils/auth'
import { mirrorAppUser } from '../../utils/authMode'
import { createAppAccessToken, issueAppSession } from '../../utils/session'
/**
* Local-dev-only auto-login: issues real session tokens for a fixed,
* entirely in-memory test user — no database involved — so an agent (or a
* developer) can reach any require-auth-gated page on localhost without an
* invitation code, even when the dev database itself is unreachable.
* Local-dev-only auto-login: persists a fixed app identity and issues an app
* session so an agent or developer can exercise AUTH_MODE=sso locally.
*
* Hard-disabled outside development. This must never be reachable from a
* deployed environment — the NODE_ENV check below is the entire security
@@ -19,12 +19,23 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 404, statusMessage: 'Not found' })
}
const user = getDevBypassUser()
const tokens = await issueAuthTokens(event, user)
const identity = getDevBypassUser()
const user = await mirrorAppUser({
subject: identity.ssoSubject,
email: identity.email,
name: identity.name,
role: identity.role,
}, { force: true })
if (!user) {
throw createError({ statusCode: 500, statusMessage: 'Could not persist dev identity' })
}
issueAppSession(event, user)
return {
success: true,
accessToken: tokens.accessToken,
accessToken: createAppAccessToken(user),
user: {
id: String(user._id),
email: user.email,