Put the feedback and tickets admin areas behind the shared identity

feedback.auth.ts and tickets.auth.ts each become one binding to
requireAppAccess. Everything downstream was already written against
requireAdminAuth and res.locals.admin, and both still mean what they
meant, so no router or service changed. What changed is the policy: an
activated @nachklang.art account is no longer sufficient, an explicit
per-app permission is.

Three things followed from that and are not obvious from the diff:

- APP_ORIGINS gets a production default. It feeds better-auth's
  trustedOrigins, and this is the first time the tickets and feedback
  origins matter there - before, the only browser origin that ever
  reached /admin/auth was the admin app itself. An origin missing from
  that list fails in a way that is easy to misread: sign-in works, the
  app works, and only sign-out returns an origin error.

- Nothing reads X-Session-* any more; these two files were the last
  readers, and the calendar module passes its session in query
  parameters. The headers stay in the CORS allowedHeaders only so a
  browser still running a pre-cutover bundle gets a clean 401 rather
  than a preflight failure, and can come out once both frontends are
  deployed.

- 40 admin operations documented a required X-Session-Id/X-Session-Key
  in swagger. They now declare the AdminSessionCookie scheme the admin
  module already defined, and each documents a 403 next to its 401.

The integration assertions flip as their own comment predicted: one
admin cookie opens both /feedback/admin/me and /tickets/admin/me, a
user holding only feedback gets 200 and 403 respectively, and a legacy
header session gets 401. The unit test that covered the old header
authenticator is replaced by one asserting each module is bound to its
own app and that neither consults the calendar users service.

163 unit tests and 43 integration tests green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 14:10:42 +02:00
parent bf7be65b03
commit 2405625f99
19 changed files with 439 additions and 308 deletions
+21 -2
View File
@@ -73,8 +73,27 @@ const parseList = (value: string | undefined, fallback: string[]): string[] => {
return parsed.length > 0 ? parsed : fallback;
};
// The apps whose frontends may talk to /admin/* with credentials.
export const APP_ORIGINS = parseList(process.env.APP_ORIGINS, []).map(origin => origin.replace(/\/$/, ''));
/**
* The apps whose frontends may talk to /admin/* with credentials.
*
* They feed better-auth's `trustedOrigins`, which is what lets the tickets and
* feedback admin areas call /admin/auth/sign-out from their own origin. That
* became load-bearing with the step 4 cutover: before it, the only browser
* origin that ever reached /admin/auth was the admin app itself.
*
* Hence the production default rather than an empty list. An origin missing
* here fails in a way that is easy to misread - sign-in works, the app works,
* and only sign-out returns an origin error - so the two frontends we know
* about are named here and APP_ORIGINS overrides them for a staging host.
* Dev adds the localhost ports separately (see admin.auth.ts).
*/
const DEFAULT_APP_ORIGINS = [
'https://tickets.nachklang.art',
'https://feedback.nachklang.art'
];
export const APP_ORIGINS = parseList(process.env.APP_ORIGINS, DEFAULT_APP_ORIGINS)
.map(origin => origin.replace(/\/$/, ''));
// Kept in sync by construction rather than by three separate lists: the admin
// app itself always counts, and dev adds the local ports.