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);