mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
refactor(auth): move auth guards to the Node layer; drop next-auth dependency
The edge middleware decoded the session via next-auth getToken, which only resolves legacy cookies — not the thin hub civ-token (no embedded user; edge can't hit Redis/DB). Move the session-based guards to the Node layer and remove next-auth entirely. - /moderator + /testing page guards and the preview-deploy gate now run in _app getInitialProps with the resolved session (preview Flipt check extracted to server/auth/preview-access.ts). Catches direct/SSR loads; non-mods can't client-nav there and the tRPC procedures are the real data gate. - Strip getToken from middleware/index.ts; drop user/useSession from the Middleware type; route-guards keeps only the sessionless /api/testing gate. - Delete preview-auth.middleware + next-auth.d.ts; repoint middleware.trpc's ExtendedUser -> first-party SessionUser (a superset). Drop next-auth + @next-auth/prisma-adapter — next-auth is fully removed. - Preview e2e minters mint the legacy JWE via jose (mirrors @civitai/auth's legacy-cookie decoder) instead of next-auth/jwt. Needs a QA pass before release (auth-critical, not runtime-tested). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-3
@@ -85,8 +85,8 @@
|
||||
"@aws-sdk/lib-storage": "^3.490.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.490.0",
|
||||
"@axiomhq/axiom-node": "^0.12.0",
|
||||
"@civitai/auth": "workspace:*",
|
||||
"@civitai/app-sdk": "^0.6.0",
|
||||
"@civitai/auth": "workspace:*",
|
||||
"@civitai/blocks-react": "^0.4.0",
|
||||
"@civitai/client": "0.2.0-beta.71",
|
||||
"@civitai/cybertipline-tools": "^0.1.0",
|
||||
@@ -116,7 +116,6 @@
|
||||
"@marsidev/react-turnstile": "^1.0.1",
|
||||
"@meilisearch/instant-meilisearch": "0.13.5",
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@next/bundle-analyzer": "^16.2.7",
|
||||
"@next/third-parties": "^15.0.3",
|
||||
"@node-oauth/oauth2-server": "^5.3.0",
|
||||
@@ -221,7 +220,6 @@
|
||||
"motion": "^11.11.17",
|
||||
"msgpackr": "^1.10.2",
|
||||
"next": "^16.2.7",
|
||||
"next-auth": "^4.24.5",
|
||||
"nodemailer": "^6.8.0",
|
||||
"obscenity": "^0.4.5",
|
||||
"openai": "^4.73.0",
|
||||
|
||||
Generated
+5
-63
@@ -118,9 +118,6 @@ importers:
|
||||
'@microsoft/signalr':
|
||||
specifier: ^7.0.10
|
||||
version: 7.0.14(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.11(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(nodemailer@6.10.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
'@next/bundle-analyzer':
|
||||
specifier: ^16.2.7
|
||||
version: 16.2.7(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
@@ -433,9 +430,6 @@ importers:
|
||||
next:
|
||||
specifier: ^16.2.7
|
||||
version: 16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0)
|
||||
next-auth:
|
||||
specifier: ^4.24.5
|
||||
version: 4.24.11(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(nodemailer@6.10.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nodemailer:
|
||||
specifier: ^6.8.0
|
||||
version: 6.10.1
|
||||
@@ -715,13 +709,13 @@ importers:
|
||||
version: 5.62.0(eslint@8.57.1)(typescript@5.9.2)
|
||||
'@vitest/browser':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
'@vitest/browser-playwright':
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)
|
||||
version: 4.0.18(@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
@@ -2632,12 +2626,6 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next-auth/prisma-adapter@1.0.7':
|
||||
resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==}
|
||||
peerDependencies:
|
||||
'@prisma/client': '>=2.26.0 || >=3'
|
||||
next-auth: ^4
|
||||
|
||||
'@next/bundle-analyzer@16.2.7':
|
||||
resolution: {integrity: sha512-Lh52e3gpnJQ8ZwBNsp54g/Czg1oqbx7bdZ9mEqHfI0VbtNMHCpneYNzKj6C+oW1JIeyjpmRSRGnhGOmi0SdNow==}
|
||||
|
||||
@@ -13205,11 +13193,6 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.11(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(nodemailer@6.10.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))':
|
||||
dependencies:
|
||||
'@prisma/client': 6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2)
|
||||
next-auth: 4.24.11(next@16.2.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(nodemailer@6.10.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
'@next/bundle-analyzer@16.2.7(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
@@ -15793,19 +15776,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/browser-playwright@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
'@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))
|
||||
playwright: 1.57.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.9)(@vitest/browser-playwright@4.0.18)(happy-dom@20.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(jiti@2.5.1)(jsdom@27.4.0(@noble/hashes@1.8.0)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser-playwright@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
@@ -15818,7 +15788,6 @@ snapshots:
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/browser-playwright@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.3))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
@@ -15834,23 +15803,6 @@ snapshots:
|
||||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))
|
||||
'@vitest/utils': 4.0.18
|
||||
magic-string: 0.30.21
|
||||
pixelmatch: 7.1.0
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.9)(@vitest/browser-playwright@4.0.18)(happy-dom@20.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(jiti@2.5.1)(jsdom@27.4.0(@noble/hashes@1.8.0)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
ws: 8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))
|
||||
@@ -15867,7 +15819,6 @@ snapshots:
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
@@ -15887,7 +15838,7 @@ snapshots:
|
||||
- vite
|
||||
optional: true
|
||||
|
||||
'@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)':
|
||||
'@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.0.18
|
||||
@@ -15901,7 +15852,7 @@ snapshots:
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.9)(@vitest/browser-playwright@4.0.18)(happy-dom@20.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(jiti@2.5.1)(jsdom@27.4.0(@noble/hashes@1.8.0)(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
'@vitest/browser': 4.0.18(bufferutil@4.0.9)(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.18)
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
dependencies:
|
||||
@@ -15912,15 +15863,6 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(vite@6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
msw: 2.12.10(@types/node@20.19.9)(typescript@5.9.2)
|
||||
vite: 6.4.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
|
||||
'@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.9)(typescript@5.9.2))(vite@7.3.1(@types/node@20.19.9)(jiti@2.5.1)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.48.0)(tsx@4.20.3)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
|
||||
+28
-1
@@ -39,7 +39,7 @@ import { ToursProvider } from '~/components/Tours/ToursProvider';
|
||||
import { TrackPageView } from '~/components/TrackView/TrackPageView';
|
||||
import { UpdateRequiredWatcher } from '~/components/UpdateRequiredWatcher/UpdateRequiredWatcher';
|
||||
import { env } from '~/env/client';
|
||||
import { isDev, isProd } from '~/env/other';
|
||||
import { isDev, isPreview, isProd } from '~/env/other';
|
||||
import { civitaiTokenCookieName } from '~/libs/auth';
|
||||
import { ActivityReportingProvider } from '~/providers/ActivityReportingProvider';
|
||||
import { AppProvider } from '~/providers/AppProvider';
|
||||
@@ -498,6 +498,33 @@ MyApp.getInitialProps = async (appContext: AppContext) => {
|
||||
hasAuthCookie = false;
|
||||
}
|
||||
|
||||
// Auth route guards, moved off the edge middleware (the thin hub civ-token can't resolve the full user in the
|
||||
// edge runtime — see preview-access.ts / route-guards). Runs here in Node with the resolved session, catching
|
||||
// direct/SSR loads. A non-mod can't client-nav to these (the nav isn't shown to them) and the tRPC procedures
|
||||
// are the real data gate, so the client-nav gap is harmless.
|
||||
const guardPath = (request.url ?? '').split('?')[0];
|
||||
const isModerator = !!session?.user?.isModerator;
|
||||
let guardTo: string | undefined;
|
||||
if (
|
||||
(guardPath.startsWith('/moderator') && !isModerator) ||
|
||||
(guardPath.startsWith('/testing') && isProd && !isModerator)
|
||||
) {
|
||||
// Login can't grant the missing permission (it would loop back here), so authed-but-unauthorized → home.
|
||||
guardTo = session?.user ? '/' : `/login?returnUrl=${encodeURIComponent(guardPath)}`;
|
||||
} else if (isPreview && !guardPath.startsWith('/login') && guardPath !== '/preview-restricted') {
|
||||
// Preview deploys gate every page behind login + the moderator/testers allowlist.
|
||||
if (!session?.user) {
|
||||
guardTo = `/login?returnUrl=${encodeURIComponent(guardPath)}`;
|
||||
} else if (!isModerator) {
|
||||
const { checkPreviewAccess } = await import('~/server/auth/preview-access');
|
||||
if (!(await checkPreviewAccess(session.user))) guardTo = '/preview-restricted';
|
||||
}
|
||||
}
|
||||
if (guardTo && appContext.ctx.res) {
|
||||
appContext.ctx.res.writeHead(302, { Location: guardTo });
|
||||
appContext.ctx.res.end();
|
||||
}
|
||||
|
||||
return {
|
||||
pageProps: {
|
||||
...pageProps,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getServerAuthSession } from '~/server/auth/get-server-auth-session';
|
||||
|
||||
// next-auth/react's SessionProvider + useSession (and the ~317 useCurrentUser sites) poll this endpoint. It
|
||||
// shadows the old [...nextauth] catch-all for exactly /api/auth/session and returns the hub-resolved session
|
||||
// (civ-token, or a legacy civitai-token via the jose decoder) in next-auth's `{ user, expires }` shape, so the
|
||||
// client half is unchanged until the first-party provider replaces next-auth/react.
|
||||
// The first-party SessionProvider + useSession (and the ~317 useCurrentUser sites) poll this endpoint. It returns
|
||||
// the hub-resolved session (civ-token, or a legacy civitai-token via the jose decoder) in the `{ user, expires }`
|
||||
// shape the client expects. (Replaced the old next-auth [...nextauth] /api/auth/session route.)
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
if (!session?.user) {
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
// First-party replacement for next-auth/react's `SessionProvider` + `useSession` (cutover phase 4). The session
|
||||
// is the hub's thin civ-token; the rich user is resolved server-side and exposed by GET /api/auth/session in
|
||||
// next-auth's `{ user, expires }` shape — so this keeps the exact `useSession()` contract the ~13 call sites
|
||||
// already use (`{ data, status, update }`), no per-site rewrites. NB: `Session` is still the next-auth type;
|
||||
// phase 5 swaps it for a first-party type app-wide. signIn/signOut/getProviders remain on next-auth/react until
|
||||
// phase 5 (they're coupled to the next-auth server endpoints).
|
||||
// First-party replacement for the old next-auth/react `SessionProvider` + `useSession`. The session is the hub's
|
||||
// thin civ-token; the rich user is resolved server-side and exposed by GET /api/auth/session in the `{ user,
|
||||
// expires }` shape — keeping the exact `useSession()` contract (`{ data, status, update }`) the call sites use.
|
||||
// next-auth is fully removed: login is hub-driven (popup / full-page redirect), sign-out hits /api/auth/logout,
|
||||
// and `Session` is the first-party type from ~/types/session.
|
||||
|
||||
type SessionStatus = 'authenticated' | 'unauthenticated' | 'loading';
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { SessionUser } from '~/types/session';
|
||||
|
||||
// Preview-site access check (preview deploys only) — extracted from the former `preview-auth` edge middleware,
|
||||
// now that auth runs in the Node layer (_app getInitialProps). A user gets in if they're a moderator OR enabled
|
||||
// in the Flipt `preview-site-access` segment. Plain fetch + a per-pod TTL cache; no Node-only deps.
|
||||
|
||||
const FLIPT_URL = process.env.FLIPT_URL;
|
||||
const FLIPT_CACHE_TTL_MS = 60_000;
|
||||
const FLIPT_TIMEOUT_MS = 2_000;
|
||||
const fliptCache = new Map<string, { enabled: boolean | null; expiresAt: number }>();
|
||||
|
||||
type FliptBooleanResponse = { enabled?: boolean };
|
||||
|
||||
async function evaluateFliptFlag(
|
||||
flagKey: string,
|
||||
entityId: string,
|
||||
context: Record<string, string>
|
||||
): Promise<boolean | null> {
|
||||
if (!FLIPT_URL) return null;
|
||||
|
||||
const cacheKey = `${flagKey}:${entityId}`;
|
||||
const now = Date.now();
|
||||
const cached = fliptCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > now) return cached.enabled;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FLIPT_URL}/evaluate/v1/boolean`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ namespaceKey: 'default', flagKey, entityId, context }),
|
||||
signal: AbortSignal.timeout(FLIPT_TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as FliptBooleanResponse;
|
||||
const enabled = data.enabled ?? null;
|
||||
fliptCache.set(cacheKey, { enabled, expiresAt: now + FLIPT_CACHE_TTL_MS });
|
||||
return enabled;
|
||||
} catch {
|
||||
// Includes timeout (AbortError) and network errors — fall through to deny.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** True if the user may access a preview deploy: moderators always; otherwise the Flipt testers segment. */
|
||||
export async function checkPreviewAccess(user: SessionUser): Promise<boolean> {
|
||||
if (user.isModerator) return true;
|
||||
const hasAccess = await evaluateFliptFlag('preview-site-access', String(user.id), {
|
||||
userId: String(user.id),
|
||||
isModerator: String(!!user.isModerator),
|
||||
tier: user.tier ?? 'free',
|
||||
isLoggedIn: 'true',
|
||||
});
|
||||
return !!hasAccess;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { UserPreferencesInput } from '~/server/schema/base.schema';
|
||||
import { getAllHiddenForUser } from '~/server/services/user-preferences.service';
|
||||
import { middleware } from '~/server/trpc';
|
||||
import { getRequestDomainColor } from '~/server/utils/server-domain';
|
||||
import type { ExtendedUser } from '~/types/next-auth';
|
||||
import type { SessionUser } from '~/types/session';
|
||||
import { withSpan } from '~/server/utils/otel-helpers';
|
||||
import { hashifyObject, slugit } from '~/utils/string-helpers';
|
||||
import { booleanString } from '~/utils/zod-helpers';
|
||||
@@ -106,7 +106,7 @@ export function cacheIt<TInput extends object>({
|
||||
export type RateLimit = {
|
||||
limit: number;
|
||||
period: number; // seconds
|
||||
userReq?: (user: ExtendedUser) => boolean;
|
||||
userReq?: (user: SessionUser) => boolean;
|
||||
errorMessage?: string;
|
||||
};
|
||||
export type RateLimitOptions = {
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import type { SessionUser } from '~/types/session';
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { civitaiTokenCookieName } from '~/libs/auth';
|
||||
import { apiCacheMiddleware } from '~/server/middleware/api-cache.middleware';
|
||||
import { apiRegionBlockMiddleware } from '~/server/middleware/api-region-block.middleware';
|
||||
import { botDetectionMiddleware } from '~/server/middleware/bot-detection.middleware';
|
||||
import type { Middleware } from '~/server/middleware/middleware-utils';
|
||||
import { regionBlockMiddleware } from '~/server/middleware/region-block.middleware';
|
||||
import { regionRestrictionMiddleware } from '~/server/middleware/region-restriction.middleware';
|
||||
import { previewAuthMiddleware } from '~/server/middleware/preview-auth.middleware';
|
||||
import { routeGuardsMiddleware } from '~/server/middleware/route-guards.middleware';
|
||||
|
||||
// NOTE: order matters! Preview auth first, then region blocking, then restriction redirect.
|
||||
// botDetectionMiddleware is last because it returns `NextResponse.next({ request })`
|
||||
// to inject a request header for downstream handlers — the runner below merges any
|
||||
// response-header-only passthroughs (like apiCacheMiddleware's Cache-Control) into
|
||||
// NOTE: order matters! Region blocking, then restriction redirect. botDetectionMiddleware is last because it
|
||||
// returns `NextResponse.next({ request })` to inject a request header for downstream handlers — the runner
|
||||
// below merges any response-header-only passthroughs (like apiCacheMiddleware's Cache-Control) into
|
||||
// botDetection's terminal-with-request-mods response so both effects apply.
|
||||
// (Session-based guards — /moderator, /testing, preview-auth — moved to _app getInitialProps: the edge runtime
|
||||
// can't resolve the thin hub civ-token to a full user.)
|
||||
const middlewares: Middleware[] = [
|
||||
previewAuthMiddleware,
|
||||
regionBlockMiddleware,
|
||||
regionRestrictionMiddleware,
|
||||
apiRegionBlockMiddleware,
|
||||
@@ -53,8 +49,6 @@ export async function runMiddlewares(
|
||||
request: NextRequest,
|
||||
middlewareList: Middleware[] = middlewares
|
||||
) {
|
||||
let user: SessionUser | null = null;
|
||||
let hasToken = true;
|
||||
const redirect = (to: string) => NextResponse.redirect(new URL(to, request.url));
|
||||
|
||||
// Response headers from passthrough middlewares (e.g. apiCacheMiddleware's
|
||||
@@ -63,19 +57,9 @@ export async function runMiddlewares(
|
||||
|
||||
for (const middleware of middlewareList) {
|
||||
if (middleware.shouldRun && !middleware.shouldRun(request)) continue;
|
||||
if (middleware.useSession && !user && hasToken) {
|
||||
const token = await getToken({
|
||||
req: request,
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
cookieName: civitaiTokenCookieName,
|
||||
});
|
||||
if (!token) hasToken = false;
|
||||
user = token?.user as SessionUser;
|
||||
}
|
||||
|
||||
const response = await middleware.handler({
|
||||
request,
|
||||
user,
|
||||
redirect,
|
||||
});
|
||||
if (!response) continue;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { SessionUser } from '~/types/session';
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export type Middleware = {
|
||||
matcher: string[];
|
||||
useSession?: boolean;
|
||||
shouldRun?: (request: NextRequest) => boolean;
|
||||
handler: (ctx: {
|
||||
request: NextRequest;
|
||||
user: SessionUser | null;
|
||||
redirect: (to: string) => NextResponse;
|
||||
}) => Promise<NextResponse | void>;
|
||||
};
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { createMiddleware } from '~/server/middleware/middleware-utils';
|
||||
import { isPreview } from '~/env/other';
|
||||
|
||||
// Read once at module init — env vars don't change at runtime.
|
||||
const FLIPT_URL = process.env.FLIPT_URL;
|
||||
|
||||
// Edge-runtime-safe in-memory TTL cache. Lives for the isolate's lifetime;
|
||||
// different regions/instances have independent caches, which is fine — even
|
||||
// partial hit-rate eliminates most of the per-request Flipt round-trips on
|
||||
// preview deploys. Can't use ~/server/utils/ttl-cache because that pulls in
|
||||
// prom-client (Node-only).
|
||||
const FLIPT_CACHE_TTL_MS = 60_000;
|
||||
const FLIPT_TIMEOUT_MS = 2_000;
|
||||
const fliptCache = new Map<string, { enabled: boolean | null; expiresAt: number }>();
|
||||
|
||||
type FliptBooleanResponse = { enabled?: boolean };
|
||||
|
||||
// Evaluate a Flipt boolean flag via HTTP API (Edge-compatible, no WASM SDK).
|
||||
async function evaluateFliptFlag(
|
||||
flagKey: string,
|
||||
entityId: string,
|
||||
context: Record<string, string>
|
||||
): Promise<boolean | null> {
|
||||
if (!FLIPT_URL) return null;
|
||||
|
||||
const cacheKey = `${flagKey}:${entityId}`;
|
||||
const now = Date.now();
|
||||
const cached = fliptCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > now) return cached.enabled;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FLIPT_URL}/evaluate/v1/boolean`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
namespaceKey: 'default',
|
||||
flagKey,
|
||||
entityId,
|
||||
context,
|
||||
}),
|
||||
signal: AbortSignal.timeout(FLIPT_TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as FliptBooleanResponse;
|
||||
const enabled = data.enabled ?? null;
|
||||
fliptCache.set(cacheKey, { enabled, expiresAt: now + FLIPT_CACHE_TTL_MS });
|
||||
return enabled;
|
||||
} catch {
|
||||
// Includes timeout (AbortError) and network errors — fall through to deny.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const previewAuthMiddleware = createMiddleware({
|
||||
matcher: ['/:path*'],
|
||||
useSession: true,
|
||||
shouldRun: (request) => {
|
||||
if (!isPreview) return false;
|
||||
|
||||
const { pathname } = request.nextUrl;
|
||||
// Don't block auth/login routes or static assets
|
||||
if (pathname.startsWith('/api/auth')) return false;
|
||||
if (pathname.startsWith('/login')) return false;
|
||||
if (pathname === '/preview-restricted') return false;
|
||||
if (pathname.startsWith('/_next')) return false;
|
||||
if (pathname.startsWith('/favicon')) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
handler: async ({ user, redirect, request }) => {
|
||||
if (!user) {
|
||||
return redirect(`/login?returnUrl=${request.nextUrl.pathname}`);
|
||||
}
|
||||
|
||||
// Moderators always have access
|
||||
if (user.isModerator) return;
|
||||
|
||||
// Check Flipt testers segment via HTTP API (Edge-compatible)
|
||||
const hasAccess = await evaluateFliptFlag('preview-site-access', String(user.id), {
|
||||
userId: String(user.id),
|
||||
isModerator: String(!!user.isModerator),
|
||||
tier: user.tier ?? 'free',
|
||||
isLoggedIn: 'true',
|
||||
});
|
||||
if (hasAccess) return;
|
||||
|
||||
return redirect('/preview-restricted');
|
||||
},
|
||||
});
|
||||
@@ -1,18 +1,12 @@
|
||||
import type { SessionUser } from '~/types/session';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { createMiddleware } from '~/server/middleware/middleware-utils';
|
||||
import { pathToRegexp } from 'path-to-regexp';
|
||||
import { isProd } from '~/env/other';
|
||||
|
||||
// The session-based PAGE guards (/moderator, /testing) moved to _app getInitialProps — the edge runtime can't
|
||||
// resolve the thin hub civ-token to a full user. What's left here is the sessionless /api/testing gate (those
|
||||
// debug endpoints are non-prod only), so no getToken is needed.
|
||||
const routeGuards: RouteGuard[] = [];
|
||||
addRouteGuard({
|
||||
matcher: ['/moderator/:path*'],
|
||||
canAccess: ({ user }) => user?.isModerator,
|
||||
});
|
||||
addRouteGuard({
|
||||
matcher: ['/testing/:path*'],
|
||||
canAccess: ({ user }) => !isProd || user?.isModerator,
|
||||
});
|
||||
addRouteGuard({
|
||||
matcher: ['/api/testing/:path*'],
|
||||
canAccess: () => !isProd,
|
||||
@@ -22,7 +16,7 @@ addRouteGuard({
|
||||
type RouteGuard = {
|
||||
matcher: string[];
|
||||
isMatch: (pathname: string) => boolean;
|
||||
canAccess: (ctx: { request: NextRequest; user: SessionUser | null }) => boolean | undefined;
|
||||
canAccess: (ctx: { request: NextRequest }) => boolean | undefined;
|
||||
redirect?: string;
|
||||
};
|
||||
function addRouteGuard(routeGuard: Omit<RouteGuard, 'isMatch'>) {
|
||||
@@ -36,21 +30,13 @@ function addRouteGuard(routeGuard: Omit<RouteGuard, 'isMatch'>) {
|
||||
}
|
||||
export const routeGuardsMiddleware = createMiddleware({
|
||||
matcher: routeGuards.flatMap((routeGuard) => routeGuard.matcher),
|
||||
useSession: true,
|
||||
handler: async ({ user, request, redirect }) => {
|
||||
handler: async ({ request, redirect }) => {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
for (const routeGuard of routeGuards) {
|
||||
if (!routeGuard.isMatch(pathname)) continue;
|
||||
if (routeGuard.canAccess({ user, request })) continue;
|
||||
|
||||
// Can't access. Sending an already-authenticated user to /login can't grant the
|
||||
// missing permission, and login redirects a logged-in user straight back to
|
||||
// returnUrl → infinite /moderator/... ⇄ /login loop. So only unauthenticated
|
||||
// users go to login; logged-in-but-unauthorized users go home (or the guard's
|
||||
// explicit redirect, if set).
|
||||
if (routeGuard.redirect) return redirect(routeGuard.redirect);
|
||||
return redirect(user ? '/' : `/login?returnUrl=${pathname}`);
|
||||
if (routeGuard.canAccess({ request })) continue;
|
||||
return redirect(routeGuard.redirect ?? '/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getSanitizedStringSchema } from '~/server/schema/utils.schema';
|
||||
export const commentRateLimits: RateLimit[] = [
|
||||
{ limit: 10, period: CacheTTL.hour },
|
||||
{ limit: 4 * 10, period: CacheTTL.day },
|
||||
{ limit: 60, period: CacheTTL.hour, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 8 * 60, period: CacheTTL.day, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 60, period: CacheTTL.hour, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
{ limit: 8 * 60, period: CacheTTL.day, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
];
|
||||
|
||||
export type GetAllCommentsSchema = z.infer<typeof getAllCommentsSchema>;
|
||||
|
||||
@@ -13,10 +13,10 @@ export const reactionRateLimits: RateLimit[] = [
|
||||
// 24 hour limit - prevent systematic abuse while allowing heavy usage
|
||||
{ limit: 5000, period: CacheTTL.day },
|
||||
// Higher limits for users with good reputation scores (≥1000 total score)
|
||||
{ limit: 100, period: CacheTTL.xs, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 500, period: CacheTTL.md, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 1500, period: CacheTTL.hour, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 8000, period: CacheTTL.day, userReq: (user) => user.meta?.scores?.total >= 1000 },
|
||||
{ limit: 100, period: CacheTTL.xs, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
{ limit: 500, period: CacheTTL.md, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
{ limit: 1500, period: CacheTTL.hour, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
{ limit: 8000, period: CacheTTL.day, userReq: (user) => (user.meta?.scores?.total ?? 0) >= 1000},
|
||||
];
|
||||
|
||||
export const reactableEntities: readonly [string, ...string[]] = [
|
||||
|
||||
Vendored
-67
@@ -1,67 +0,0 @@
|
||||
import type { DefaultSession, DefaultUser } from 'next-auth';
|
||||
import type { UserTier, UserSubscriptionsByBuzzType } from '~/server/schema/user.schema';
|
||||
import type { User as PrismaUser } from '~/shared/utils/prisma/enums';
|
||||
import type { getUserBanDetails } from '~/utils/user-helpers';
|
||||
|
||||
interface ExtendedUser {
|
||||
id: number;
|
||||
showNsfw: boolean;
|
||||
blurNsfw: boolean; // client only
|
||||
browsingLevel: number;
|
||||
onboarding: number;
|
||||
username?: string;
|
||||
image?: string;
|
||||
email?: string;
|
||||
emailVerified?: Date;
|
||||
createdAt?: Date;
|
||||
isModerator?: boolean;
|
||||
customerId?: string; // could be fetched
|
||||
paddleCustomerId?: string; // could be fetched
|
||||
subscriptionId?: string; // could be fetched - deprecated, kept for backward compatibility
|
||||
tier?: UserTier; // Highest tier across all subscriptions
|
||||
muted?: boolean;
|
||||
mutedAt?: Date;
|
||||
bannedAt?: Date;
|
||||
autoplayGifs?: boolean; // client only - could be cookie setting
|
||||
permissions?: string[];
|
||||
filePreferences?: UserFilePreferences;
|
||||
leaderboardShowcase?: string; // client only
|
||||
referral?: { id: number }; // client only
|
||||
memberInBadState?: boolean;
|
||||
meta?: UserMeta;
|
||||
allowAds?: boolean;
|
||||
banDetails?: ReturnType<typeof getUserBanDetails>;
|
||||
redBrowsingLevel?: number;
|
||||
deletedAt?: Date;
|
||||
// Multi-subscription support per buzzType
|
||||
subscriptions?: UserSubscriptionsByBuzzType;
|
||||
// TODO.briant - clean up user session data
|
||||
/*
|
||||
remove `emailVerified`, update user account page to make call to get current user data
|
||||
*/
|
||||
}
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface User extends ExtendedUser, Omit<DefaultUser, 'id'> {
|
||||
id: PrismaUser['id'];
|
||||
}
|
||||
|
||||
interface SessionUser extends ExtendedUser, DefaultSession['user'] {}
|
||||
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
user?: ExtendedUser & DefaultSession['user'];
|
||||
error?: string;
|
||||
needsCookieRefresh?: boolean;
|
||||
/** Moderator impersonation (F): the moderator's id when this session is impersonating someone. */
|
||||
impersonatedBy?: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface TokenUser {
|
||||
id: number;
|
||||
username: string;
|
||||
isModerator?: boolean;
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import type {
|
||||
} from '~/server/schema/user.schema';
|
||||
import type { getUserBanDetails } from '~/utils/user-helpers';
|
||||
|
||||
// First-party session types (NextAuth cutover phase 5) — replaces the `declare module 'next-auth'` augmentation
|
||||
// in src/types/next-auth.d.ts. The app imports `Session`/`SessionUser` from here instead of from 'next-auth', so
|
||||
// the type no longer depends on the next-auth package. `UserFilePreferences` is an ambient global (global.d.ts).
|
||||
// First-party session types — the app imports `Session`/`SessionUser` from here (not from 'next-auth'), so the
|
||||
// types don't depend on the next-auth package (now fully removed). `UserFilePreferences` is an ambient global
|
||||
// (global.d.ts).
|
||||
|
||||
/** The rich user attached to a session (was next-auth's augmented `SessionUser`/`ExtendedUser`). */
|
||||
export interface SessionUser {
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { encode } = require('next-auth/jwt');
|
||||
const { EncryptJWT } = require('jose');
|
||||
const { hkdfSync } = require('node:crypto');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const SECRET = process.env.NEXTAUTH_SECRET;
|
||||
@@ -33,6 +34,11 @@ const BASE_URL = process.env.BASE_URL;
|
||||
const COOKIE_NAME = '__Secure-civitai-token'; // libs/auth.ts — https => __Secure- prefix
|
||||
const MAX_AGE_S = 30 * 24 * 60 * 60;
|
||||
|
||||
// Mint the legacy next-auth v4 session JWE WITHOUT next-auth (removed); the app still accepts it via
|
||||
// @civitai/auth's decodeLegacySessionCookie. dir/A256GCM with an HKDF-derived key — mirrors that decoder.
|
||||
const ENC_INFO = 'NextAuth.js Generated Encryption Key';
|
||||
const derivedKey = (secret) => new Uint8Array(hkdfSync('sha256', secret, '', ENC_INFO, 32));
|
||||
|
||||
// Representative authed routes (kept small so 5 runs x N routes stays inside the
|
||||
// build pool's time budget). All reachable by ci-smoke-gold. `/` = SSR home,
|
||||
// `/models` = heavy list/feed, `/generate` = client-heavy generator, and
|
||||
@@ -63,7 +69,11 @@ async function main() {
|
||||
if (!BASE_URL) throw new Error('BASE_URL is required (e.g. https://pr-123.civitaic.com)');
|
||||
|
||||
const token = { user: GOLD, sub: String(GOLD.id), id: uuid(), signedAt: Date.now() };
|
||||
const value = await encode({ token, secret: SECRET, maxAge: MAX_AGE_S });
|
||||
const value = await new EncryptJWT(token)
|
||||
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(Math.floor(Date.now() / 1000) + MAX_AGE_S)
|
||||
.encrypt(derivedKey(SECRET));
|
||||
|
||||
const base = JSON.parse(
|
||||
fs.readFileSync(path.join(process.cwd(), 'lighthouserc.json'), 'utf8')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test as setup } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import { encode } from 'next-auth/jwt';
|
||||
import { EncryptJWT } from 'jose';
|
||||
import { hkdfSync } from 'node:crypto';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { PREVIEW_USERS, type PreviewRole, storageStatePath } from './preview-fixtures';
|
||||
|
||||
@@ -39,6 +40,12 @@ const PREVIEW_URL = process.env.PREVIEW_URL;
|
||||
const COOKIE_NAME = '__Secure-civitai-token'; // libs/auth.ts — https preview => __Secure- prefix
|
||||
const MAX_AGE_S = 30 * 24 * 60 * 60;
|
||||
|
||||
// Mint the LEGACY next-auth v4 session cookie (a `dir`/`A256GCM` JWE, HKDF-derived key) WITHOUT next-auth, which
|
||||
// is now removed. The app still ACCEPTS it via @civitai/auth's decodeLegacySessionCookie during the cutover, so
|
||||
// this mirrors that decoder's key derivation exactly.
|
||||
const ENC_INFO = 'NextAuth.js Generated Encryption Key';
|
||||
const derivedKey = (secret: string) => new Uint8Array(hkdfSync('sha256', secret, '', ENC_INFO, 32));
|
||||
|
||||
async function mintStorageState(role: PreviewRole): Promise<string> {
|
||||
const u = PREVIEW_USERS[role];
|
||||
|
||||
@@ -59,7 +66,11 @@ async function mintStorageState(role: PreviewRole): Promise<string> {
|
||||
};
|
||||
|
||||
const token = { user, sub: String(u.id), id: uuid(), signedAt: Date.now() };
|
||||
const value = await encode({ token, secret: SECRET as string, maxAge: MAX_AGE_S });
|
||||
const value = await new EncryptJWT(token)
|
||||
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(Math.floor(Date.now() / 1000) + MAX_AGE_S)
|
||||
.encrypt(derivedKey(SECRET as string));
|
||||
|
||||
const { hostname } = new URL(PREVIEW_URL as string);
|
||||
const storageState = {
|
||||
|
||||
Reference in New Issue
Block a user