From 4b86ff73cf50e2d8109bc7621c42b08adc4a9317 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:18:51 +0000 Subject: [PATCH] build: move local development off port 3000 Run the dev API on 6262 and Vite on 6263 with strictPort; production defaults stay on 3000. Define the dev client origin once and read the typed appConfig instead of repeating its fallback in every gateway. --- .github/workflows/e2e-runner.yml | 2 +- client/README.md | 4 +-- client/vite.config.ts | 4 ++- docs/DEVELOPMENT.md | 20 +++++------ server/.env.example | 4 +-- server/README.md | 4 +-- server/src/common/utils/ws-cors.utils.test.ts | 4 +-- server/src/common/utils/ws-cors.utils.ts | 3 +- server/src/config/config.test.ts | 2 +- server/src/config/config.ts | 3 +- server/src/config/dev-client-origin.ts | 5 +++ server/src/config/env.validation.ts | 4 ++- server/src/main.ts | 3 +- .../modules/auth/oidc/oidc.service.test.ts | 12 ++----- server/src/modules/auth/oidc/oidc.service.ts | 11 ++++--- .../modules/auth/oidc/redirect-uri.test.ts | 10 +++--- server/src/modules/auth/oidc/redirect-uri.ts | 2 +- .../authors/author-enrichment.gateway.test.ts | 6 ++-- .../authors/author-enrichment.gateway.ts | 9 ++--- .../modules/kobo/kobo-sync.controller.test.ts | 8 ++--- .../src/modules/kobo/kobo-sync.controller.ts | 5 ++- .../migration-progress.gateway.test.ts | 8 ++--- .../migration/migration-progress.gateway.ts | 9 ++--- .../notification/notification.gateway.test.ts | 4 +-- .../notification/notification.gateway.ts | 9 ++--- .../podcast/podcast-events.stream.test.ts | 2 +- .../modules/podcast/podcast.gateway.test.ts | 2 +- server/src/modules/podcast/podcast.gateway.ts | 9 ++--- .../src/modules/scanner/scan.gateway.test.ts | 6 ++-- server/src/modules/scanner/scan.gateway.ts | 9 ++--- server/src/modules/user/user.service.test.ts | 33 +++++++++++-------- server/src/modules/user/user.service.ts | 11 ++++--- server/src/scripts/bench-user-statistics.ts | 4 +-- .../app-settings-oidc-contract.e2e-spec.ts | 12 +++---- .../auth-recovery-oidc-logout.e2e-spec.ts | 2 +- 35 files changed, 132 insertions(+), 113 deletions(-) create mode 100644 server/src/config/dev-client-origin.ts diff --git a/.github/workflows/e2e-runner.yml b/.github/workflows/e2e-runner.yml index 79f6443c9..3bb6423f4 100644 --- a/.github/workflows/e2e-runner.yml +++ b/.github/workflows/e2e-runner.yml @@ -36,7 +36,7 @@ env: JWT_EXPIRES_IN: 15m JWT_REFRESH_EXPIRES_IN: 7d BOOKS_PATH: ../local/data - APP_URL: http://localhost:5173 + APP_URL: http://localhost:6263 jobs: run-suite: diff --git a/client/README.md b/client/README.md index a1c2bce33..35bb3f01f 100644 --- a/client/README.md +++ b/client/README.md @@ -6,9 +6,9 @@ Vue 3 SPA built with Vite and Tailwind CSS v4. From the repo root: `pnpm dev` (starts both server and client). -Client only: `pnpm dev` (from this directory). Runs at http://localhost:5173. +Client only: `pnpm dev` (from this directory). Runs at http://localhost:6263. -The Vite dev server proxies `/api` and `/socket.io` requests to the backend at `localhost:3000`. +The Vite dev server proxies `/api` and `/socket.io` requests to the backend at `localhost:6262`. ## IDE setup diff --git a/client/vite.config.ts b/client/vite.config.ts index 765b3923f..996af90f8 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -9,7 +9,7 @@ import { VitePWA } from 'vite-plugin-pwa' const apiAgent = new Agent({ keepAlive: true }) /** Lets a second dev client point at a throwaway API instance, so restart testing leaves the main stack alone. */ -const apiTarget = process.env.BOOKORBIT_API_TARGET ?? 'http://localhost:3000' +const apiTarget = process.env.BOOKORBIT_API_TARGET ?? 'http://localhost:6262' // https://vite.dev/config/ export default defineConfig({ @@ -141,6 +141,8 @@ export default defineConfig({ exclude: ['@embedpdf/core', '@embedpdf/core/vue'], }, server: { + port: 6263, + strictPort: true, host: true, allowedHosts: true, proxy: { diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f177c98a6..5c94213f2 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -12,8 +12,8 @@ BookOrbit is a pnpm monorepo with three workspaces: a Vue 3 single-page app, a N ```mermaid graph LR - Client["Client
Vue 3 SPA
localhost:5173"] - Server["Server
NestJS + Fastify
localhost:3000/api"] + Client["Client
Vue 3 SPA
localhost:6263"] + Server["Server
NestJS + Fastify
localhost:6262/api"] DB["PostgreSQL
Drizzle ORM"] WS["WebSocket
Socket.IO"] Types["Shared Types
@bookorbit/types"] @@ -162,8 +162,8 @@ If everything worked, you will see the server, client, and types watcher start u | Check | Expected | | ---------------------------------------- | ------------------------------------------ | -| Open http://localhost:5173 | BookOrbit login page loads | -| Open http://localhost:3000/api/v1/health | Returns `{"status":"ok"}` | +| Open http://localhost:6263 | BookOrbit login page loads | +| Open http://localhost:6262/api/v1/health | Returns `{"status":"ok"}` | | `docker ps` | Shows `bookorbit-dev-db` container running | --- @@ -178,8 +178,8 @@ This starts the server, client, and types watcher concurrently with hot reload. | Service | URL | | ------- | ------------------------- | -| Client | http://localhost:5173 | -| API | http://localhost:3000/api | +| Client | http://localhost:6263 | +| API | http://localhost:6262/api | If PostgreSQL is not running, start it first with `pnpm run db:up`. @@ -195,8 +195,8 @@ When enabled, open: | Docs | URL | | ------------ | ----------------------------------- | -| Swagger UI | http://localhost:3000/api/docs | -| OpenAPI JSON | http://localhost:3000/api/docs-json | +| Swagger UI | http://localhost:6262/api/docs | +| OpenAPI JSON | http://localhost:6262/api/docs-json |
Running workspaces separately @@ -234,7 +234,7 @@ These are the variables you are most likely to need during development. See `ser | `PODCAST_ENCRYPTION_KEY` | Podcast URL encryption key; required in production | Falls back to `JWT_SECRET` in development | | `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` | | `LIBRARY_BROWSE_ROOT` | Library folder picker root | `/` | -| `APP_URL` | Base URL for email links | `http://localhost:5173` | +| `APP_URL` | Base URL for email links | `http://localhost:6263` | | `SWAGGER_ENABLED` | Serve Swagger UI and OpenAPI JSON | `false` | | `DISABLE_LOCAL_AUTH` | Disable password authentication | `false` | @@ -471,7 +471,7 @@ pnpm run e2e:run -- **Port already in use** -The server defaults to port 3000 (configured via `PORT` in `server/.env`) and the client to 5173 (Vite default). If either is occupied, find the process that owns it and stop it, or change the server port in `server/.env`. +Local development runs the server on port 6262 (`PORT` in `server/.env`) and the client on 6263 (`server.port` in `client/vite.config.ts`, with `strictPort`, so Vite fails instead of silently moving). Both avoid 3000, which many other self-hosted apps and dev tools claim. A server started without `PORT`, such as the Docker image, still listens on 3000. If either is occupied, find the process that owns it and stop it, or change the server port in `server/.env`. **Hot reload not picking up changes** diff --git a/server/.env.example b/server/.env.example index 4865c8366..4aea18221 100644 --- a/server/.env.example +++ b/server/.env.example @@ -2,7 +2,7 @@ DATABASE_URL=postgres://bookorbit:bookorbit@localhost:5432/bookorbit # App -PORT=3000 +PORT=6262 # Listen address (IPv4 or IPv6, without brackets). Unset or blank keeps 0.0.0.0. # Use 127.0.0.1 for local-only native deployments. # HOST=0.0.0.0 @@ -92,7 +92,7 @@ APP_DATA_PATH=../local/data # DISABLE_LOCAL_AUTH=false # App URL (used for building password reset links in emails) -APP_URL=http://localhost:5173 +APP_URL=http://localhost:6263 # Email encryption key (optional but recommended - 64-char hex string for AES-256) # Generate with: openssl rand -hex 32 diff --git a/server/README.md b/server/README.md index b24613057..e994dbeac 100644 --- a/server/README.md +++ b/server/README.md @@ -20,8 +20,8 @@ SWAGGER_ENABLED=true pnpm start:dev Then open: -- Swagger UI: http://localhost:3000/api/docs -- OpenAPI JSON: http://localhost:3000/api/docs-json +- Swagger UI: http://localhost:6262/api/docs +- OpenAPI JSON: http://localhost:6262/api/docs-json Dev watch uses a Nest config without the Swagger metadata generator so the server does not restart immediately after startup. Full build output still uses diff --git a/server/src/common/utils/ws-cors.utils.test.ts b/server/src/common/utils/ws-cors.utils.test.ts index b3b3164c3..13db12dff 100644 --- a/server/src/common/utils/ws-cors.utils.test.ts +++ b/server/src/common/utils/ws-cors.utils.test.ts @@ -12,7 +12,7 @@ afterEach(() => { describe('wsCorsOrigin', () => { it('falls back to the dev client when nothing is configured', () => { delete process.env.CLIENT_URL; - expect(wsCorsOrigin()).toBe('http://localhost:5173'); + expect(wsCorsOrigin()).toBe('http://localhost:6263'); }); it('drops a trailing slash, which no Origin header ever carries', () => { @@ -32,6 +32,6 @@ describe('wsCorsOrigin', () => { it('falls back rather than throwing on a malformed value', () => { process.env.CLIENT_URL = 'not a url'; - expect(wsCorsOrigin()).toBe('http://localhost:5173'); + expect(wsCorsOrigin()).toBe('http://localhost:6263'); }); }); diff --git a/server/src/common/utils/ws-cors.utils.ts b/server/src/common/utils/ws-cors.utils.ts index 806e8d72e..9846ef9b1 100644 --- a/server/src/common/utils/ws-cors.utils.ts +++ b/server/src/common/utils/ws-cors.utils.ts @@ -1,5 +1,4 @@ -/** What a dev client is served from, and the only origin allowed when nothing was configured. */ -const DEV_CLIENT_ORIGIN = 'http://localhost:5173'; +import { DEV_CLIENT_ORIGIN } from '../../config/dev-client-origin'; /** * The origin a socket gateway's CORS check allows. diff --git a/server/src/config/config.test.ts b/server/src/config/config.test.ts index 9101b1cb0..e21fa32da 100644 --- a/server/src/config/config.test.ts +++ b/server/src/config/config.test.ts @@ -64,7 +64,7 @@ describe('config', () => { expect(appConfig()).toEqual({ nodeEnv: 'development', host: '0.0.0.0', - appUrl: 'http://localhost:5173', + appUrl: 'http://localhost:6263', nativeRedirectUri: 'bookorbit://oauth2-callback', version: 'Local build', githubReleasesRepo: 'bookorbit/bookorbit', diff --git a/server/src/config/config.ts b/server/src/config/config.ts index fe8e0f3b5..b7d8c9a45 100644 --- a/server/src/config/config.ts +++ b/server/src/config/config.ts @@ -1,10 +1,11 @@ import { registerAs } from '@nestjs/config'; import { join, resolve } from 'path'; +import { DEV_CLIENT_ORIGIN } from './dev-client-origin'; export const appConfig = registerAs('app', () => ({ nodeEnv: process.env.NODE_ENV ?? 'development', host: process.env.HOST?.trim() || '0.0.0.0', - appUrl: process.env.APP_URL ?? 'http://localhost:5173', + appUrl: process.env.APP_URL ?? DEV_CLIENT_ORIGIN, // Private-use scheme the iOS app registers with ASWebAuthenticationSession. Compiled into the // client binary, so it is the same for every provider on every deployment; configurable only so // a rebranded fork can change it. diff --git a/server/src/config/dev-client-origin.ts b/server/src/config/dev-client-origin.ts new file mode 100644 index 000000000..caab04a2f --- /dev/null +++ b/server/src/config/dev-client-origin.ts @@ -0,0 +1,5 @@ +/** + * Where `pnpm dev` serves the web client (`server.port` in `client/vite.config.ts`), and the origin + * every client-facing URL falls back to when `APP_URL` or `CLIENT_URL` is unset. + */ +export const DEV_CLIENT_ORIGIN = 'http://localhost:6263'; diff --git a/server/src/config/env.validation.ts b/server/src/config/env.validation.ts index 3ee0bc760..04891c0f1 100644 --- a/server/src/config/env.validation.ts +++ b/server/src/config/env.validation.ts @@ -5,6 +5,8 @@ import { z } from 'zod'; import { APP_FEATURES } from '@bookorbit/types'; +import { DEV_CLIENT_ORIGIN } from './dev-client-origin'; + const BOOLEAN_ENV_VALUES = ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off']; const TRUST_PROXY_BOOLEAN_VALUES = ['true', 'false', 'yes', 'no', 'on', 'off']; @@ -81,7 +83,7 @@ const envSchema = z.object({ AUDIOLESS_EPUB_MAX_METADATA_BYTES: z.coerce.number().int().positive().optional(), AUDIOLESS_EPUB_MAX_OUTPUT_BYTES: z.coerce.number().int().positive().optional(), CLIENT_URL: z.string().url().optional(), - APP_URL: z.string().url().default('http://localhost:5173'), + APP_URL: z.string().url().default(DEV_CLIENT_ORIGIN), TRUST_PROXY: trustProxyEnv(), EMAIL_ENCRYPTION_KEY: z.string().optional(), MIGRATION_ENCRYPTION_KEY: z.string().optional(), diff --git a/server/src/main.ts b/server/src/main.ts index c32ac891f..7c0e17e65 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -14,6 +14,7 @@ import fastifyHelmet from '@fastify/helmet'; import fastifyCompress from '@fastify/compress'; import type { FastifyInstance } from 'fastify'; import { appConfig } from './config/config'; +import { DEV_CLIENT_ORIGIN } from './config/dev-client-origin'; import { setupSwaggerDocs } from './swagger'; import { parseBooleanEnv, @@ -93,7 +94,7 @@ async function bootstrap() { if (process.env.NODE_ENV !== 'production') { app.enableCors({ - origin: process.env.CLIENT_URL ?? 'http://localhost:5173', + origin: process.env.CLIENT_URL ?? DEV_CLIENT_ORIGIN, credentials: true, }); } diff --git a/server/src/modules/auth/oidc/oidc.service.test.ts b/server/src/modules/auth/oidc/oidc.service.test.ts index 649d746f8..9a17838a3 100644 --- a/server/src/modules/auth/oidc/oidc.service.test.ts +++ b/server/src/modules/auth/oidc/oidc.service.test.ts @@ -2,7 +2,7 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { OidcService } from './oidc.service'; -const APP_URL = 'http://localhost:5173'; +const APP_URL = 'http://localhost:6263'; const VALID_REDIRECT_URI = `${APP_URL}/oauth2-callback`; const NATIVE_REDIRECT_URI = 'bookorbit://oauth2-callback'; @@ -82,13 +82,7 @@ function makeService() { const auditEvents = { emit: vi.fn(), }; - const configService = { - get: vi.fn().mockImplementation((key: string) => { - if (key === 'app.appUrl') return APP_URL; - if (key === 'app.nativeRedirectUri') return NATIVE_REDIRECT_URI; - return undefined; - }), - }; + const appConfiguration = { appUrl: APP_URL, nativeRedirectUri: NATIVE_REDIRECT_URI }; const authenticationPolicy = { isPasswordLoginEnabled: vi.fn().mockReturnValue(true), }; @@ -106,7 +100,7 @@ function makeService() { userService as never, authService as never, auditEvents as never, - configService as never, + appConfiguration as never, authenticationPolicy as never, ); diff --git a/server/src/modules/auth/oidc/oidc.service.ts b/server/src/modules/auth/oidc/oidc.service.ts index 0bf2fdc7a..277abe1da 100644 --- a/server/src/modules/auth/oidc/oidc.service.ts +++ b/server/src/modules/auth/oidc/oidc.service.ts @@ -1,6 +1,6 @@ import { OidcCallbackDto } from '../dto/oidc-callback.dto'; -import { BadRequestException, Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { BadRequestException, Inject, Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { compare } from 'bcryptjs'; import type { FastifyReply } from 'fastify'; import { AuditAction, AuditResource, AuthenticationMethod, OidcCallbackResponse, OidcErrorCode, Permission } from '@bookorbit/types'; @@ -20,6 +20,7 @@ import { OidcStateService } from './oidc-state.service'; import { OidcTokenClientService } from './oidc-token-client.service'; import { OidcTokenValidatorService } from './oidc-token-validator.service'; import { AuthenticationPolicyService } from '../../../common/services/authentication-policy.service'; +import { appConfig } from '../../../config/config'; function isUniqueViolation(error: unknown): boolean { if (!error || typeof error !== 'object') return false; @@ -53,11 +54,11 @@ export class OidcService { private readonly userService: UserService, private readonly authService: AuthService, private readonly auditEvents: AuditEventsService, - private readonly configService: ConfigService, + @Inject(appConfig.KEY) appConfiguration: ConfigType, private readonly authenticationPolicy: AuthenticationPolicyService, ) { - this.appUrl = (this.configService.get('app.appUrl') ?? 'http://localhost:5173').replace(/\/$/, ''); - this.nativeRedirectUri = this.configService.get('app.nativeRedirectUri') ?? 'bookorbit://oauth2-callback'; + this.appUrl = appConfiguration.appUrl.replace(/\/$/, ''); + this.nativeRedirectUri = appConfiguration.nativeRedirectUri; } async generateState(providerSlug: string): Promise<{ state: string; authorizationEndpoint: string }> { diff --git a/server/src/modules/auth/oidc/redirect-uri.test.ts b/server/src/modules/auth/oidc/redirect-uri.test.ts index 743af3ebe..de0f7f497 100644 --- a/server/src/modules/auth/oidc/redirect-uri.test.ts +++ b/server/src/modules/auth/oidc/redirect-uri.test.ts @@ -1,22 +1,22 @@ import { isAllowedRedirectUri } from './redirect-uri'; const POLICY = { - appUrl: 'http://localhost:5173', + appUrl: 'http://localhost:6263', nativeRedirectUri: 'bookorbit://oauth2-callback', }; describe('isAllowedRedirectUri', () => { describe('web redirect URI', () => { it('accepts the exact web callback', () => { - expect(isAllowedRedirectUri('http://localhost:5173/oauth2-callback', POLICY)).toBe(true); + expect(isAllowedRedirectUri('http://localhost:6263/oauth2-callback', POLICY)).toBe(true); }); it('accepts a web callback carrying a query string', () => { - expect(isAllowedRedirectUri('http://localhost:5173/oauth2-callback?redirect=/library', POLICY)).toBe(true); + expect(isAllowedRedirectUri('http://localhost:6263/oauth2-callback?redirect=/library', POLICY)).toBe(true); }); it('tolerates a trailing slash on the configured app URL', () => { - expect(isAllowedRedirectUri('http://localhost:5173/oauth2-callback', { ...POLICY, appUrl: 'http://localhost:5173/' })).toBe(true); + expect(isAllowedRedirectUri('http://localhost:6263/oauth2-callback', { ...POLICY, appUrl: 'http://localhost:6263/' })).toBe(true); }); it('rejects a different origin', () => { @@ -24,7 +24,7 @@ describe('isAllowedRedirectUri', () => { }); it('rejects a different path on the right origin', () => { - expect(isAllowedRedirectUri('http://localhost:5173/somewhere-else', POLICY)).toBe(false); + expect(isAllowedRedirectUri('http://localhost:6263/somewhere-else', POLICY)).toBe(false); }); }); diff --git a/server/src/modules/auth/oidc/redirect-uri.ts b/server/src/modules/auth/oidc/redirect-uri.ts index b4dfaccc0..ac9bd92fe 100644 --- a/server/src/modules/auth/oidc/redirect-uri.ts +++ b/server/src/modules/auth/oidc/redirect-uri.ts @@ -6,7 +6,7 @@ */ export interface RedirectUriPolicy { - /** Web client origin, e.g. `http://localhost:5173`. Trailing slash already stripped. */ + /** Web client origin, e.g. `https://books.example.com`. Trailing slash already stripped. */ appUrl: string; /** Private-use scheme the native clients hand to the IdP, e.g. `bookorbit://oauth2-callback`. */ nativeRedirectUri: string; diff --git a/server/src/modules/authors/author-enrichment.gateway.test.ts b/server/src/modules/authors/author-enrichment.gateway.test.ts index 07b764db1..f7a5e1921 100644 --- a/server/src/modules/authors/author-enrichment.gateway.test.ts +++ b/server/src/modules/authors/author-enrichment.gateway.test.ts @@ -9,7 +9,7 @@ function makeGateway() { const queueRepo = { getStatusSummary: vi.fn() }; const enrichmentConfig = { isPaused: vi.fn() }; const session = { getSnapshot: vi.fn() }; - const configService = { get: vi.fn().mockReturnValue('http://localhost:5173') }; + const appConfiguration = { appUrl: 'http://localhost:6263' }; const gateway = new AuthorEnrichmentGateway( jwtService as any, @@ -17,7 +17,7 @@ function makeGateway() { queueRepo as any, enrichmentConfig as any, session as any, - configService as any, + appConfiguration as any, ); return { gateway, jwtService, authService, queueRepo, enrichmentConfig, session }; @@ -90,7 +90,7 @@ describe('AuthorEnrichmentGateway', () => { expect(server.engine.opts.cors).toEqual({ methods: ['GET'], - origin: 'http://localhost:5173', + origin: 'http://localhost:6263', credentials: true, }); }); diff --git a/server/src/modules/authors/author-enrichment.gateway.ts b/server/src/modules/authors/author-enrichment.gateway.ts index d07f01d06..85e4e1e0d 100644 --- a/server/src/modules/authors/author-enrichment.gateway.ts +++ b/server/src/modules/authors/author-enrichment.gateway.ts @@ -1,5 +1,5 @@ -import { ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { ForbiddenException, Inject, Logger, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Permission, type AuthenticationMethod, type AuthorEnrichmentStatusEvent } from '@bookorbit/types'; @@ -12,6 +12,7 @@ import { AuthorEnrichmentRepository } from './author-enrichment.repository'; import { AuthorEnrichmentSessionService } from './author-enrichment-session.service'; import { AuthorEnrichmentConfigService } from './author-enrichment-config.service'; import { rejectSocketConnection } from '../../common/utils/ws-auth.utils'; +import { appConfig } from '../../config/config'; export const AUTHOR_ENRICHMENT_STATUS_EVENT = 'author-enrichment:status'; @@ -27,9 +28,9 @@ export class AuthorEnrichmentGateway implements OnGatewayInit, OnGatewayConnecti private readonly queueRepo: AuthorEnrichmentRepository, private readonly enrichmentConfig: AuthorEnrichmentConfigService, private readonly session: AuthorEnrichmentSessionService, - config: ConfigService, + @Inject(appConfig.KEY) app: ConfigType, ) { - this.clientOrigin = config.get('app.appUrl') ?? 'http://localhost:5173'; + this.clientOrigin = app.appUrl; } afterInit(server: Server): void { diff --git a/server/src/modules/kobo/kobo-sync.controller.test.ts b/server/src/modules/kobo/kobo-sync.controller.test.ts index 9f8561c43..2f8c11173 100644 --- a/server/src/modules/kobo/kobo-sync.controller.test.ts +++ b/server/src/modules/kobo/kobo-sync.controller.test.ts @@ -96,7 +96,7 @@ describe('KoboSyncController', () => { const req = { headers: { 'x-forwarded-host': '192.168.8.134', - 'x-forwarded-port': '5173', + 'x-forwarded-port': '6263', 'x-forwarded-proto': 'http', host: '192.168.8.134', }, @@ -108,9 +108,9 @@ describe('KoboSyncController', () => { const payload = controller.initialization({ deviceToken: 'dev' } as never, req as never); const resources = payload.Resources as Record; - expect(resources.image_host).toBe('http://192.168.8.134:5173'); - expect(resources.library_sync).toBe('http://192.168.8.134:5173/api/v1/kobo/dev/v1/library/sync'); - expect(resources.reading_state).toBe('http://192.168.8.134:5173/api/v1/kobo/dev/v1/library/{Ids}/state'); + expect(resources.image_host).toBe('http://192.168.8.134:6263'); + expect(resources.library_sync).toBe('http://192.168.8.134:6263/api/v1/kobo/dev/v1/library/sync'); + expect(resources.reading_state).toBe('http://192.168.8.134:6263/api/v1/kobo/dev/v1/library/{Ids}/state'); }); it('initialization does not append x-forwarded-port when it is the default for the scheme', () => { diff --git a/server/src/modules/kobo/kobo-sync.controller.ts b/server/src/modules/kobo/kobo-sync.controller.ts index 99e0d7270..6f4bb903c 100644 --- a/server/src/modules/kobo/kobo-sync.controller.ts +++ b/server/src/modules/kobo/kobo-sync.controller.ts @@ -31,12 +31,15 @@ import { KoboSyncService } from './services/kobo-sync.service'; import { KoboReadingStateService } from './services/kobo-reading-state.service'; import type { KoboProxyResponse } from './services/kobo-proxy.service'; import { KoboProxyService } from './services/kobo-proxy.service'; +import { DEV_CLIENT_ORIGIN } from '../../config/dev-client-origin'; import { KOBO_STORE_RESOURCES } from './kobo-store-resources'; import { KoboBookIdentityService } from './services/kobo-book-identity.service'; import { KoboSyncHistoryService } from './services/kobo-sync-history.service'; import { decodeSyncToken, isUsableKoboSyncToken, withKoboSyncToken } from './services/kobo-sync-token'; const STORE_SYNC_TIMEOUT_MS = 8_000; +// A request that reached the API through the Vite dev proxy names the client's port, not the API's. +const DEV_CLIENT_PORT = new URL(DEV_CLIENT_ORIGIN).port; const KOBO_SYNC_TOKEN_HEADER = 'x-kobo-synctoken'; // Kobo answers a delete for a tag it does not know with a client error, and the device keeps the @@ -88,7 +91,7 @@ function buildReadingServicesBaseUrl(req: FastifyRequest, baseUrl: string): stri if (!localPort) return baseUrl; const url = new URL(baseUrl); - if (url.port !== '5173') return baseUrl; + if (url.port !== DEV_CLIENT_PORT) return baseUrl; const proto = readHeaderValue(req.headers['x-forwarded-proto']) ?? req.protocol; url.protocol = proto + ':'; diff --git a/server/src/modules/migration/migration-progress.gateway.test.ts b/server/src/modules/migration/migration-progress.gateway.test.ts index 23ec001ea..fa6502624 100644 --- a/server/src/modules/migration/migration-progress.gateway.test.ts +++ b/server/src/modules/migration/migration-progress.gateway.test.ts @@ -35,11 +35,9 @@ function makeGateway() { findRunById: vi.fn(), listRunMetrics: vi.fn(), }; - const configService = { - get: vi.fn().mockReturnValue('http://localhost:5173'), - }; + const appConfiguration = { appUrl: 'http://localhost:6263' }; - const gateway = new MigrationProgressGateway(jwtService as never, authService as never, repo as never, configService as never); + const gateway = new MigrationProgressGateway(jwtService as never, authService as never, repo as never, appConfiguration as never); return { gateway, jwtService, authService, repo }; } @@ -64,7 +62,7 @@ describe('MigrationProgressGateway', () => { expect(server.engine.opts.cors).toEqual({ methods: ['GET'], - origin: 'http://localhost:5173', + origin: 'http://localhost:6263', credentials: true, }); }); diff --git a/server/src/modules/migration/migration-progress.gateway.ts b/server/src/modules/migration/migration-progress.gateway.ts index 54e115d8f..89607dd38 100644 --- a/server/src/modules/migration/migration-progress.gateway.ts +++ b/server/src/modules/migration/migration-progress.gateway.ts @@ -1,5 +1,5 @@ -import { ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { ForbiddenException, Inject, Logger, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @@ -11,6 +11,7 @@ import { AuthService } from '../auth/auth.service'; import { MigrationRepository } from './migration.repository'; import { sanitizeRunForApi } from './core/api-sanitizers'; import { rejectSocketConnection } from '../../common/utils/ws-auth.utils'; +import { appConfig } from '../../config/config'; @WebSocketGateway({ namespace: '/migration', cors: { credentials: true } }) export class MigrationProgressGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @@ -22,9 +23,9 @@ export class MigrationProgressGateway implements OnGatewayInit, OnGatewayConnect private readonly jwtService: JwtService, private readonly authService: AuthService, private readonly repo: MigrationRepository, - config: ConfigService, + @Inject(appConfig.KEY) app: ConfigType, ) { - this.clientOrigin = config.get('app.appUrl') ?? 'http://localhost:5173'; + this.clientOrigin = app.appUrl; } afterInit(server: Server): void { diff --git a/server/src/modules/notification/notification.gateway.test.ts b/server/src/modules/notification/notification.gateway.test.ts index 7942c364a..1ec0e5f44 100644 --- a/server/src/modules/notification/notification.gateway.test.ts +++ b/server/src/modules/notification/notification.gateway.test.ts @@ -6,7 +6,7 @@ import { NotificationGateway } from './notification.gateway'; import { EMPTY_CONTENT_FILTER_RULES } from '@bookorbit/types'; function makeGateway() { - return new NotificationGateway({} as never, {} as never, {} as never, { get: vi.fn().mockReturnValue('http://localhost:5173') } as never); + return new NotificationGateway({} as never, {} as never, {} as never, { appUrl: 'http://localhost:6263' } as never); } function makeUser(overrides: Partial = {}): RequestUser { @@ -84,7 +84,7 @@ describe('NotificationGateway handshake rejection', () => { }; const userService = { findByIdWithPermissions: vi.fn().mockResolvedValue(overrides.user ?? null) }; const notificationRepo = { countUnread: vi.fn().mockResolvedValue(0) }; - const config = { get: vi.fn().mockReturnValue('http://localhost:5173') }; + const config = { appUrl: 'http://localhost:6263' }; const gateway = new NotificationGateway(jwtService as never, userService as never, notificationRepo as never, config as never); const client = { id: 'sock-1', handshake: { auth: { token: 'jwt' } }, data: {}, join: vi.fn(), emit: vi.fn(), disconnect: vi.fn() }; return { gateway, client }; diff --git a/server/src/modules/notification/notification.gateway.ts b/server/src/modules/notification/notification.gateway.ts index 9f7bd24f3..8fb6427e6 100644 --- a/server/src/modules/notification/notification.gateway.ts +++ b/server/src/modules/notification/notification.gateway.ts @@ -1,5 +1,5 @@ -import { ForbiddenException, Logger, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { ForbiddenException, Inject, Logger, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Permission } from '@bookorbit/types'; @@ -11,6 +11,7 @@ import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils'; import { UserService } from '../user/user.service'; import { NotificationRepository } from './notification.repository'; import { rejectSocketConnection } from '../../common/utils/ws-auth.utils'; +import { appConfig } from '../../config/config'; @WebSocketGateway({ namespace: '/notifications', cors: { credentials: true } }) export class NotificationGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @@ -22,9 +23,9 @@ export class NotificationGateway implements OnGatewayInit, OnGatewayConnection, private readonly jwtService: JwtService, private readonly userService: UserService, private readonly notificationRepo: NotificationRepository, - config: ConfigService, + @Inject(appConfig.KEY) app: ConfigType, ) { - this.clientOrigin = config.get('app.appUrl') ?? 'http://localhost:5173'; + this.clientOrigin = app.appUrl; } afterInit(server: Server): void { diff --git a/server/src/modules/podcast/podcast-events.stream.test.ts b/server/src/modules/podcast/podcast-events.stream.test.ts index 0b91901d2..ab79001d8 100644 --- a/server/src/modules/podcast/podcast-events.stream.test.ts +++ b/server/src/modules/podcast/podcast-events.stream.test.ts @@ -59,7 +59,7 @@ describe('podcast event stream over HTTP', () => { events = app.get(PodcastEventsService); socketEmit = vi.fn(); - gateway = new PodcastGateway({} as never, {} as never, {} as never, events, { get: vi.fn().mockReturnValue('http://localhost:5173') } as never); + gateway = new PodcastGateway({} as never, {} as never, {} as never, events, { appUrl: 'http://localhost:6263' } as never); gateway['server'] = { to: vi.fn().mockReturnValue({ emit: socketEmit }) } as never; }); diff --git a/server/src/modules/podcast/podcast.gateway.test.ts b/server/src/modules/podcast/podcast.gateway.test.ts index c6c104b53..ddfbe791a 100644 --- a/server/src/modules/podcast/podcast.gateway.test.ts +++ b/server/src/modules/podcast/podcast.gateway.test.ts @@ -13,7 +13,7 @@ function makeGateway() { publishImportProgress: vi.fn(), publishRetentionEvicted: vi.fn(), }; - const config = { get: vi.fn().mockReturnValue('http://localhost:5173') }; + const config = { appUrl: 'http://localhost:6263' }; const gateway = new PodcastGateway(jwtService as never, authService as never, libraries as never, events as never, config as never); return { gateway, jwtService, authService, libraries, events }; } diff --git a/server/src/modules/podcast/podcast.gateway.ts b/server/src/modules/podcast/podcast.gateway.ts index cc07c44fb..2ac9cf81c 100644 --- a/server/src/modules/podcast/podcast.gateway.ts +++ b/server/src/modules/podcast/podcast.gateway.ts @@ -1,5 +1,5 @@ -import { Logger, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Inject, Logger, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @@ -19,6 +19,7 @@ import { AuthService } from '../auth/auth.service'; import { LibraryService } from '../library/library.service'; import { PodcastEventsService } from './podcast-events.service'; import { rejectSocketConnection } from '../../common/utils/ws-auth.utils'; +import { appConfig } from '../../config/config'; @WebSocketGateway({ namespace: '/podcasts', cors: { credentials: true } }) export class PodcastGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @@ -31,9 +32,9 @@ export class PodcastGateway implements OnGatewayInit, OnGatewayConnection, OnGat private readonly authService: AuthService, private readonly libraries: LibraryService, private readonly events: PodcastEventsService, - config: ConfigService, + @Inject(appConfig.KEY) app: ConfigType, ) { - this.clientOrigin = config.get('app.appUrl') ?? 'http://localhost:5173'; + this.clientOrigin = app.appUrl; } afterInit(server: Server): void { diff --git a/server/src/modules/scanner/scan.gateway.test.ts b/server/src/modules/scanner/scan.gateway.test.ts index e707fd6c4..f20f775ea 100644 --- a/server/src/modules/scanner/scan.gateway.test.ts +++ b/server/src/modules/scanner/scan.gateway.test.ts @@ -8,8 +8,8 @@ function makeGateway() { const authService = { validateSessionUser: vi.fn() }; const scanJobStore = { get: vi.fn(), isRunning: vi.fn() }; const achievementEvents = new AchievementEventsService(); - const configService = { get: vi.fn().mockReturnValue('http://localhost:5173') }; - const gateway = new ScanGateway(jwtService as any, authService as any, scanJobStore as any, achievementEvents, configService as any); + const appConfiguration = { appUrl: 'http://localhost:6263' }; + const gateway = new ScanGateway(jwtService as any, authService as any, scanJobStore as any, achievementEvents, appConfiguration as any); return { gateway, jwtService, authService, scanJobStore, achievementEvents }; } @@ -180,7 +180,7 @@ describe('subscription lifecycle', () => { expect(server.engine.opts.cors).toEqual({ methods: ['GET'], - origin: 'http://localhost:5173', + origin: 'http://localhost:6263', credentials: true, }); }); diff --git a/server/src/modules/scanner/scan.gateway.ts b/server/src/modules/scanner/scan.gateway.ts index 330d976f9..25e517a76 100644 --- a/server/src/modules/scanner/scan.gateway.ts +++ b/server/src/modules/scanner/scan.gateway.ts @@ -1,5 +1,5 @@ -import { Logger, OnModuleDestroy, OnModuleInit, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Inject, Logger, OnModuleDestroy, OnModuleInit, UnauthorizedException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @@ -25,6 +25,7 @@ import { } from '../achievement/achievement-events.service'; import { ScanJobStore } from './scan-job-store.service'; import { rejectSocketConnection } from '../../common/utils/ws-auth.utils'; +import { appConfig } from '../../config/config'; @WebSocketGateway({ namespace: '/scan', cors: { credentials: true } }) export class ScanGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, OnModuleInit, OnModuleDestroy { @@ -40,9 +41,9 @@ export class ScanGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa private readonly authService: AuthService, private readonly scanJobStore: ScanJobStore, private readonly achievementEvents: AchievementEventsService, - config: ConfigService, + @Inject(appConfig.KEY) app: ConfigType, ) { - this.clientOrigin = config.get('app.appUrl') ?? 'http://localhost:5173'; + this.clientOrigin = app.appUrl; } onModuleInit(): void { diff --git a/server/src/modules/user/user.service.test.ts b/server/src/modules/user/user.service.test.ts index 7d4774ab3..48db38445 100644 --- a/server/src/modules/user/user.service.test.ts +++ b/server/src/modules/user/user.service.test.ts @@ -52,7 +52,7 @@ describe('UserService', () => { replaceFilters: vi.fn(), }; - const config = { get: vi.fn() }; + const appConfiguration = { appUrl: '' }; const appSettingsService = { getDefaultLibraryAccessLibraryIds: vi.fn(), }; @@ -68,13 +68,21 @@ describe('UserService', () => { vi.spyOn(Logger.prototype, 'log').mockImplementation(() => undefined); vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); events = new UserEventsService(); - service = new UserService(userRepo as any, config as any, contentFilterRepo as any, appSettingsService as any, userStatistics as any, events, { - assertPasswordLoginEnabled: vi.fn(), - } as any); + service = new UserService( + userRepo as any, + appConfiguration as any, + contentFilterRepo as any, + appSettingsService as any, + userStatistics as any, + events, + { + assertPasswordLoginEnabled: vi.fn(), + } as any, + ); mockHash.mockResolvedValue('hashed-secret'); mockRandomBytes.mockReturnValue(Buffer.from('abcd', 'hex')); - config.get.mockReturnValue('https://app.example.com'); + appConfiguration.appUrl = 'https://app.example.com'; appSettingsService.getDefaultLibraryAccessLibraryIds.mockResolvedValue([]); userRepo.create.mockResolvedValue({ id: 10, username: 'newuser', name: 'New User' }); @@ -196,7 +204,6 @@ describe('UserService', () => { it('createUser skips permission/library writes when lists are empty', async () => { userRepo.findByUsername.mockResolvedValue(null); - config.get.mockReturnValue(undefined); const result = await service.createUser({ username: 'newuser', @@ -208,7 +215,7 @@ describe('UserService', () => { expect(userRepo.setPermissions).not.toHaveBeenCalled(); expect(userRepo.assignViewerLibraries).not.toHaveBeenCalled(); - expect(result.resetUrl).toBe('http://localhost:5173/reset-password?token=reset-token'); + expect(result.resetUrl).toBe('https://app.example.com/reset-password?token=reset-token'); }); it('findById returns user and throws when missing', async () => { @@ -775,13 +782,13 @@ describe('UserService', () => { await expect(service.adminResetPassword(2, reqUser({ isSuperuser: true }))).rejects.toBeInstanceOf(BadRequestException); }); - it('adminResetPassword returns default app URL when config is missing', async () => { + it('adminResetPassword builds the reset link from the configured app URL', async () => { userRepo.findByIdWithPermissions.mockResolvedValue({ id: 2, isSuperuser: false, provisioningMethod: 'local' }); userRepo.generateResetToken.mockResolvedValue('token-2'); - config.get.mockReturnValue(undefined); + appConfiguration.appUrl = 'http://localhost:6263'; await expect(service.adminResetPassword(2, reqUser({ isSuperuser: true }))).resolves.toEqual({ - resetUrl: 'http://localhost:5173/reset-password?token=token-2', + resetUrl: 'http://localhost:6263/reset-password?token=token-2', }); }); }); @@ -814,7 +821,7 @@ describe('UserService.updateSeriesCollapsePreferences', () => { findByUserIdWithNames: vi.fn(), replaceFilters: vi.fn(), }; - const config = { get: vi.fn() }; + const appConfiguration = { appUrl: '' }; const appSettingsService = { getDefaultLibraryAccessLibraryIds: vi.fn(), }; @@ -824,11 +831,11 @@ describe('UserService.updateSeriesCollapsePreferences', () => { beforeEach(() => { vi.resetAllMocks(); - config.get.mockReturnValue('http://localhost:5173'); + appConfiguration.appUrl = 'http://localhost:6263'; appSettingsService.getDefaultLibraryAccessLibraryIds.mockResolvedValue([]); service = new UserService( userRepo as any, - config as any, + appConfiguration as any, contentFilterRepo as any, appSettingsService as any, userStatistics as any, diff --git a/server/src/modules/user/user.service.ts b/server/src/modules/user/user.service.ts index 4b84b1d7f..b67f1cf28 100644 --- a/server/src/modules/user/user.service.ts +++ b/server/src/modules/user/user.service.ts @@ -1,5 +1,5 @@ -import { BadRequestException, ConflictException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { BadRequestException, ConflictException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { hash } from 'bcryptjs'; import { randomBytes } from 'crypto'; import { Permission } from '@bookorbit/types'; @@ -22,6 +22,7 @@ import { UserRepository, type UserListQuery } from './user.repository'; import { AppSettingsService } from '../app-settings/app-settings.service'; import { UserStatisticsService } from '../user-statistics/user-statistics.service'; import { AuthenticationPolicyService } from '../../common/services/authentication-policy.service'; +import { appConfig } from '../../config/config'; /** The band is a to-do list, not a second roster. */ const ATTENTION_BAND_LIMIT = 8; @@ -33,7 +34,7 @@ export class UserService { constructor( private readonly userRepo: UserRepository, - private readonly config: ConfigService, + @Inject(appConfig.KEY) private readonly appConfiguration: ConfigType, private readonly contentFilterRepo: ContentFilterRepository, private readonly appSettingsService: AppSettingsService, private readonly userStatistics: UserStatisticsService, @@ -158,7 +159,7 @@ export class UserService { await this.userRepo.assignViewerLibraries(user.id, libraryIds); } - const appUrl = this.config.get('app.appUrl') ?? 'http://localhost:5173'; + const appUrl = this.appConfiguration.appUrl; const rawToken = await this.userRepo.generateResetToken(user.id); const resetUrl = `${appUrl}/reset-password?token=${rawToken}`; @@ -439,7 +440,7 @@ export class UserService { if (target.provisioningMethod === 'shared') { throw new BadRequestException('Shared accounts do not have passwords'); } - const appUrl = this.config.get('app.appUrl') ?? 'http://localhost:5173'; + const appUrl = this.appConfiguration.appUrl; const rawToken = await this.userRepo.generateResetToken(targetUserId); return { resetUrl: `${appUrl}/reset-password?token=${rawToken}` }; } diff --git a/server/src/scripts/bench-user-statistics.ts b/server/src/scripts/bench-user-statistics.ts index 23460a98e..2e8492545 100644 --- a/server/src/scripts/bench-user-statistics.ts +++ b/server/src/scripts/bench-user-statistics.ts @@ -25,7 +25,7 @@ const REQUEST_DELAY_MS = Number.parseInt(process.env.BENCH_REQUEST_DELAY_MS ?? ' const MAX_429_RETRIES = Number.parseInt(process.env.BENCH_MAX_429_RETRIES ?? '2', 10); const OUTPUT_PATH = process.env.BENCH_OUTPUT_PATH ?? ''; const BENCH_BASE_URL = process.env.BENCH_BASE_URL; -const CANDIDATE_BASE_URLS = ['http://localhost:3010', 'http://localhost:3000']; +const CANDIDATE_BASE_URLS = ['http://localhost:6262', 'http://localhost:3010', 'http://localhost:3000']; const CASES: BenchCase[] = [ { name: 'summary', path: '/api/v1/user-statistics/summary' }, @@ -79,7 +79,7 @@ async function resolveBaseUrl(): Promise { if (await isHealthy(candidate)) return candidate; } - throw new Error('Could not detect a running API base URL. Set BENCH_BASE_URL (for example: http://localhost:3000).'); + throw new Error('Could not detect a running API base URL. Set BENCH_BASE_URL (for example: http://localhost:6262).'); } async function getBenchUser(): Promise<{ id: number; tokenVersion: number }> { diff --git a/server/test/app-settings-oidc-contract.e2e-spec.ts b/server/test/app-settings-oidc-contract.e2e-spec.ts index c6d87d607..b622f1073 100644 --- a/server/test/app-settings-oidc-contract.e2e-spec.ts +++ b/server/test/app-settings-oidc-contract.e2e-spec.ts @@ -579,7 +579,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( const callbackPayload = { code: 'authorization-code-provisioned', codeVerifier: 'verifier-provisioned', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-provisioned', state, }; @@ -691,7 +691,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( payload: { code: 'authorization-code-linked', codeVerifier: 'verifier-linked', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-linked', state, }, @@ -800,7 +800,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( payload: { code: 'authorization-code-invalid-state', codeVerifier: 'verifier-invalid-state', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-invalid-state', state: `invalid-${randomUUID()}`, }, @@ -822,7 +822,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( payload: { code: 'authorization-code-missing-subject', codeVerifier: 'verifier-missing-subject', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-missing-subject', state, }, @@ -864,7 +864,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( payload: { code: 'authorization-code-disabled', codeVerifier: 'verifier-disabled', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-disabled', state: disabledState, }, @@ -903,7 +903,7 @@ describe('App settings OIDC contract (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, ( payload: { code: 'authorization-code-inactive', codeVerifier: 'verifier-inactive', - redirectUri: 'http://localhost:5173/oauth2-callback', + redirectUri: 'http://localhost:6263/oauth2-callback', nonce: 'nonce-inactive', state: inactiveState, }, diff --git a/server/test/auth-recovery-oidc-logout.e2e-spec.ts b/server/test/auth-recovery-oidc-logout.e2e-spec.ts index 6f4b33891..fb0554e9e 100644 --- a/server/test/auth-recovery-oidc-logout.e2e-spec.ts +++ b/server/test/auth-recovery-oidc-logout.e2e-spec.ts @@ -769,7 +769,7 @@ describe('Auth recovery and OIDC logout hardening (e2e)', () => { url: '/api/v1/auth/logout', headers: { cookie: cookieHeader(session.jar), - origin: 'http://localhost:5173', + origin: 'http://localhost:6263', }, }); expect(logoutResponse.statusCode).toBe(200);