From 4cb4454d9f9e79514b40eb09c9bb5c54413eddaa Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 31 May 2026 14:13:04 +0200 Subject: [PATCH] Clean up dead code, dedupe helpers, fix the reset-password contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove server exports orphaned by the Express removal: the immich album-link helpers, seven route-only service exports, getFileByIdFull; de-export internal-only helpers (utcSuffix). - De-duplicate verifyTripAccess (9 identical copies -> services/tripAccess.ts) and avatarUrl (3 -> services/avatarUrl.ts); name the bcrypt cost (BCRYPT_COST) and the email regex (EMAIL_REGEX). Public API unchanged. - resetPasswordRequestSchema declared `password`, but the client sends and the service reads `new_password` — rename it so the contract matches and the client types resolve. - Make ATLAS-013 deterministic: stub the admin-1 GeoJSON download instead of fetching ~4600 features from GitHub during the test (it hung the suite). --- server/src/db/seeds.ts | 5 ++- server/src/services/adminService.ts | 9 +++-- server/src/services/authService.ts | 29 ++++++++------- server/src/services/avatarUrl.ts | 3 ++ server/src/services/budgetService.ts | 12 +++---- server/src/services/collabService.ts | 12 +++---- server/src/services/dayNoteService.ts | 6 ++-- server/src/services/dayService.ts | 6 ++-- server/src/services/fileService.ts | 6 ++-- server/src/services/mapsService.ts | 3 -- server/src/services/memories/immichService.ts | 36 ------------------- .../services/memories/photoResolverService.ts | 7 ---- .../src/services/memories/synologyService.ts | 7 ++-- .../src/services/memories/unifiedService.ts | 2 +- server/src/services/notifications.ts | 9 ----- server/src/services/oauthService.ts | 10 ------ server/src/services/packingService.ts | 6 ++-- server/src/services/reservationService.ts | 17 ++------- server/src/services/todoService.ts | 6 ++-- server/src/services/tripAccess.ts | 9 +++++ server/src/services/tripService.ts | 7 ++-- server/tests/integration/atlas.test.ts | 22 ++++++++++++ shared/src/auth/auth.schema.spec.ts | 6 ++-- shared/src/auth/auth.schema.ts | 4 ++- 24 files changed, 94 insertions(+), 145 deletions(-) create mode 100644 server/src/services/avatarUrl.ts create mode 100644 server/src/services/tripAccess.ts diff --git a/server/src/db/seeds.ts b/server/src/db/seeds.ts index 299e3f5a..46efc0cf 100644 --- a/server/src/db/seeds.ts +++ b/server/src/db/seeds.ts @@ -1,6 +1,9 @@ import Database from 'better-sqlite3'; import crypto from 'crypto'; +// bcrypt cost factor for the seeded admin password — kept in sync with authService. +const BCRYPT_COST = 12; + // Seeds run at startup before the DB admin panel can be used, so only env vars // are checked here. The granular password_login/password_registration DB toggles // are only relevant after the first user exists; at that point seeds have already @@ -40,7 +43,7 @@ function seedAdminAccount(db: Database.Database): void { email = 'admin@trek.local'; } - const hash = bcrypt.hashSync(password, 12); + const hash = bcrypt.hashSync(password, BCRYPT_COST); const username = 'admin'; db.prepare('INSERT INTO users (username, email, password_hash, role, must_change_password) VALUES (?, ?, ?, ?, 1)').run(username, email, hash, 'admin'); diff --git a/server/src/services/adminService.ts b/server/src/services/adminService.ts index b2c50438..7cce69c3 100644 --- a/server/src/services/adminService.ts +++ b/server/src/services/adminService.ts @@ -16,7 +16,10 @@ import { resolveAuthToggles } from './authService'; // ── Helpers ──────────────────────────────────────────────────────────────── -export function utcSuffix(ts: string | null | undefined): string | null { +// bcrypt cost factor for user passwords — kept in sync with authService. +const BCRYPT_COST = 12; + +function utcSuffix(ts: string | null | undefined): string | null { if (!ts) return null; return ts.endsWith('Z') ? ts : ts.replace(' ', 'T') + 'Z'; } @@ -94,7 +97,7 @@ export function createUser(data: { username: string; email: string; password: st const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email); if (existingEmail) return { error: 'Email already taken', status: 409 }; - const passwordHash = bcrypt.hashSync(password, 12); + const passwordHash = bcrypt.hashSync(password, BCRYPT_COST); const result = db.prepare( 'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)' @@ -136,7 +139,7 @@ export function updateUser(id: string, data: { username?: string; email?: string const pwCheck = validatePassword(password); if (!pwCheck.ok) return { error: pwCheck.reason, status: 400 }; } - const passwordHash = password ? bcrypt.hashSync(password, 12) : null; + const passwordHash = password ? bcrypt.hashSync(password, BCRYPT_COST) : null; db.prepare(` UPDATE users SET diff --git a/server/src/services/authService.ts b/server/src/services/authService.ts index 880f9199..1729a846 100644 --- a/server/src/services/authService.ts +++ b/server/src/services/authService.ts @@ -20,6 +20,9 @@ import { getFlightDistanceKm } from './distanceService'; import { verifyJwtAndLoadUser } from '../middleware/auth'; import { User } from '../types'; import { DEMO_EMAIL_PRIMARY, isDemoEmail } from './demo'; +import { avatarUrl } from './avatarUrl'; + +export { avatarUrl }; // --------------------------------------------------------------------------- // Constants @@ -27,10 +30,16 @@ import { DEMO_EMAIL_PRIMARY, isDemoEmail } from './demo'; authenticator.options = { window: 1 }; +// bcrypt cost factor for user passwords. Shared by register/changePassword/ +// resetPassword and the dummy-hash timing equaliser below — must stay in sync. +const BCRYPT_COST = 12; + +// Shape check for email input on register and profile update. +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + // Pre-computed bcrypt hash to equalise timing of "unknown email" and // "OIDC-only account" branches with the real verification path (CWE-208). -// Cost factor 12 matches register/changePassword/resetPassword — must stay in sync. -const DUMMY_PASSWORD_HASH = bcrypt.hashSync('__trek_no_such_user__', 12); +const DUMMY_PASSWORD_HASH = bcrypt.hashSync('__trek_no_such_user__', BCRYPT_COST); const MFA_SETUP_TTL_MS = 15 * 60 * 1000; const mfaSetupPending = new Map(); @@ -114,10 +123,6 @@ export function mask_stored_api_key(key: string | null | undefined): string | nu return maskKey(plain); } -export function avatarUrl(user: { avatar?: string | null }): string | null { - return user.avatar ? `/uploads/avatars/${user.avatar}` : null; -} - export function resolveAuthToggles(): { password_login: boolean; password_registration: boolean; @@ -377,8 +382,7 @@ export function registerUser(body: { const pwCheck = validatePassword(password); if (!pwCheck.ok) return { error: pwCheck.reason, status: 400 }; - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { + if (!EMAIL_REGEX.test(email)) { return { error: 'Invalid email format', status: 400 }; } @@ -387,7 +391,7 @@ export function registerUser(body: { return { error: 'Registration failed. Please try different credentials.', status: 409 }; } - const password_hash = bcrypt.hashSync(password, 12); + const password_hash = bcrypt.hashSync(password, BCRYPT_COST); const isFirstUser = userCount === 0; const role = isFirstUser ? 'admin' : 'user'; @@ -533,7 +537,7 @@ export function changePassword( return { error: 'Current password is incorrect', status: 401 }; } - const hash = bcrypt.hashSync(new_password, 12); + const hash = bcrypt.hashSync(new_password, BCRYPT_COST); db.prepare('UPDATE users SET password_hash = ?, must_change_password = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(hash, userId); return { success: true }; } @@ -608,8 +612,7 @@ export function updateSettings( if (email !== undefined) { const trimmed = email.trim(); - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!trimmed || !emailRegex.test(trimmed)) { + if (!trimmed || !EMAIL_REGEX.test(trimmed)) { return { error: 'Invalid email format', status: 400 }; } const conflict = db.prepare('SELECT id FROM users WHERE LOWER(email) = LOWER(?) AND id != ?').get(trimmed, userId); @@ -1249,7 +1252,7 @@ export function resetPassword(body: { } } - const newHash = bcrypt.hashSync(new_password, 12); + const newHash = bcrypt.hashSync(new_password, BCRYPT_COST); const newPv = (user.password_version ?? 0) + 1; db.transaction(() => { diff --git a/server/src/services/avatarUrl.ts b/server/src/services/avatarUrl.ts new file mode 100644 index 00000000..d3919f5f --- /dev/null +++ b/server/src/services/avatarUrl.ts @@ -0,0 +1,3 @@ +export function avatarUrl(user: { avatar?: string | null }): string | null { + return user.avatar ? `/uploads/avatars/${user.avatar}` : null; +} diff --git a/server/src/services/budgetService.ts b/server/src/services/budgetService.ts index 7c5934c4..e31d7410 100644 --- a/server/src/services/budgetService.ts +++ b/server/src/services/budgetService.ts @@ -1,17 +1,13 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { BudgetItem, BudgetItemMember } from '../types'; +import { avatarUrl } from './avatarUrl'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -export function avatarUrl(user: { avatar?: string | null }): string | null { - return user.avatar ? `/uploads/avatars/${user.avatar}` : null; -} - -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { avatarUrl }; +export { verifyTripAccess } from './tripAccess'; function loadItemMembers(itemId: number | string) { const rows = db.prepare(` diff --git a/server/src/services/collabService.ts b/server/src/services/collabService.ts index 08bd4529..2b6b0b71 100644 --- a/server/src/services/collabService.ts +++ b/server/src/services/collabService.ts @@ -1,8 +1,9 @@ import path from 'path'; import fs from 'fs'; -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { CollabNote, CollabPoll, CollabMessage, TripFile } from '../types'; import { checkSsrf, createPinnedDispatcher } from '../utils/ssrfGuard'; +import { avatarUrl } from './avatarUrl'; /* ------------------------------------------------------------------ */ /* Internal row types */ @@ -48,13 +49,8 @@ export interface LinkPreviewResult { /* Helpers */ /* ------------------------------------------------------------------ */ -export function avatarUrl(user: { avatar?: string | null }): string | null { - return user.avatar ? `/uploads/avatars/${user.avatar}` : null; -} - -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { avatarUrl }; +export { verifyTripAccess } from './tripAccess'; /* ------------------------------------------------------------------ */ /* Reactions */ diff --git a/server/src/services/dayNoteService.ts b/server/src/services/dayNoteService.ts index 436df86e..3758678a 100644 --- a/server/src/services/dayNoteService.ts +++ b/server/src/services/dayNoteService.ts @@ -1,9 +1,7 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { DayNote } from '../types'; -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { verifyTripAccess } from './tripAccess'; export function listNotes(dayId: string | number, tripId: string | number) { return db.prepare( diff --git a/server/src/services/dayService.ts b/server/src/services/dayService.ts index 8b3b6361..46f295c7 100644 --- a/server/src/services/dayService.ts +++ b/server/src/services/dayService.ts @@ -1,10 +1,8 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { loadTagsByPlaceIds, loadParticipantsByAssignmentIds, formatAssignmentWithPlace } from './queryHelpers'; import { AssignmentRow, Day, DayNote } from '../types'; -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { verifyTripAccess } from './tripAccess'; // --------------------------------------------------------------------------- // Day assignment helpers diff --git a/server/src/services/fileService.ts b/server/src/services/fileService.ts index 2e7f9d75..f119344c 100644 --- a/server/src/services/fileService.ts +++ b/server/src/services/fileService.ts @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs'; import type { Request } from 'express'; -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { consumeEphemeralToken } from './ephemeralTokens'; import { verifyJwtAndLoadUser } from '../middleware/auth'; import { TripFile } from '../types'; @@ -30,9 +30,7 @@ export const filesDir = path.join(__dirname, '../../uploads/files'); // Helpers // --------------------------------------------------------------------------- -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { verifyTripAccess } from './tripAccess'; export function getAllowedExtensions(): string { try { diff --git a/server/src/services/mapsService.ts b/server/src/services/mapsService.ts index c336135c..0d07a617 100644 --- a/server/src/services/mapsService.ts +++ b/server/src/services/mapsService.ts @@ -7,9 +7,6 @@ import { getAppUrl } from './notifications'; let googleApiCallCount = 0; -export function getGoogleApiCallCount(): number { return googleApiCallCount; } -export function resetGoogleApiCallCount(): void { googleApiCallCount = 0; } - function googleFetch(endpoint: string, label: string, init?: RequestInit): Promise { googleApiCallCount++; console.debug(`[Google API] #${googleApiCallCount} ${label} → ${endpoint}`); diff --git a/server/src/services/memories/immichService.ts b/server/src/services/memories/immichService.ts index f4491f9c..863f394b 100644 --- a/server/src/services/memories/immichService.ts +++ b/server/src/services/memories/immichService.ts @@ -349,42 +349,6 @@ export async function getAlbumPhotos( } } -export function listAlbumLinks(tripId: string) { - return db.prepare(` - SELECT tal.*, u.username - FROM trip_album_links tal - JOIN users u ON tal.user_id = u.id - WHERE tal.trip_id = ? - ORDER BY tal.created_at ASC - `).all(tripId); -} - -export function createAlbumLink( - tripId: string, - userId: number, - albumId: string, - albumName: string -): { success: boolean; error?: string } { - try { - db.prepare( - "INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, album_id, album_name, provider) VALUES (?, ?, ?, ?, 'immich')" - ).run(tripId, userId, albumId, albumName || ''); - return { success: true }; - } catch { - return { success: false, error: 'Album already linked' }; - } -} - -export function deleteAlbumLink(linkId: string, tripId: string, userId: number) { - db.transaction(() => { - const link = db.prepare('SELECT id FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?').get(linkId, tripId, userId); - if (link) { - db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND album_link_id = ?').run(tripId, linkId); - db.prepare('DELETE FROM trip_album_links WHERE id = ?').run(linkId); - } - })(); -} - export async function syncAlbumAssets( tripId: string, linkId: string, diff --git a/server/src/services/memories/photoResolverService.ts b/server/src/services/memories/photoResolverService.ts index 428c1aa4..3d272ca2 100644 --- a/server/src/services/memories/photoResolverService.ts +++ b/server/src/services/memories/photoResolverService.ts @@ -230,10 +230,3 @@ export function deleteTrekPhotoIfOrphan(photoId: number): void { db.prepare("DELETE FROM trek_photos WHERE id = ? AND provider != 'local'").run(photoId); } -// ── Delete local file for a trek_photo ────────────────────────────────── - -export function getTrekPhotoFilePath(photoId: number): string | null { - const photo = resolveTrekPhoto(photoId); - if (!photo || photo.provider !== 'local' || !photo.file_path) return null; - return path.join(__dirname, '../../../uploads', photo.file_path); -} diff --git a/server/src/services/memories/synologyService.ts b/server/src/services/memories/synologyService.ts index a6f268d5..83b44402 100644 --- a/server/src/services/memories/synologyService.ts +++ b/server/src/services/memories/synologyService.ts @@ -458,11 +458,12 @@ export async function listSynologyAlbums(userId: number): Promise>, extractPassphrase: (a: any) => string | undefined) => { if (result.status === 'rejected') return; - if (!result.value.success) { - console.warn('[Synology] album list partial failure:', (result.value as any).error?.message); + const value = result.value; + if ('error' in value) { + console.warn('[Synology] album list partial failure:', value.error.message); return; } - for (const album of result.value.data ?? []) { + for (const album of value.data ?? []) { const id = String(album.id); const passphrase = extractPassphrase(album); map.set(id, { id, albumName: album.name || '', assetCount: album.item_count || 0, passphrase }); diff --git a/server/src/services/memories/unifiedService.ts b/server/src/services/memories/unifiedService.ts index c884ee6b..01ea556f 100644 --- a/server/src/services/memories/unifiedService.ts +++ b/server/src/services/memories/unifiedService.ts @@ -299,7 +299,7 @@ async function _notifySharedTripPhotos( actorUserId: number, added: number, ): Promise> { - if (added <= 0) return fail('No photos shared, skipping notifications', 200); + if (added <= 0) return success(undefined); try { const actorRow = db.prepare('SELECT username, email FROM users WHERE id = ?').get(actorUserId) as { username: string | null, email: string | null }; diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index a59e53be..c13573dc 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -432,15 +432,6 @@ export function resolveNtfyUrl(adminCfg: NtfyConfig, userCfg: NtfyConfig | null) return `${base}/${encodeURIComponent(topic)}`; } -export function isNtfyConfiguredForUser(userId: number): boolean { - const cfg = getUserNtfyConfig(userId); - return !!(cfg?.topic); -} - -export function isNtfyConfiguredAdmin(): boolean { - return !!(getAppSetting('admin_ntfy_topic')); -} - function encodeHeaderValue(value: string): string { for (let i = 0; i < value.length; i++) { if (value.charCodeAt(i) > 0xFF) { diff --git a/server/src/services/oauthService.ts b/server/src/services/oauthService.ts index 2e782f97..45de3b0e 100644 --- a/server/src/services/oauthService.ts +++ b/server/src/services/oauthService.ts @@ -118,16 +118,6 @@ export function listOAuthClients(userId: number): Record[] { })); } -/** Returns true if the URI is a valid OAuth redirect target (HTTPS or localhost). */ -export function isValidRedirectUri(uri: string): boolean { - try { - const url = new URL(uri); - return url.protocol === 'https:' || url.hostname === 'localhost' || url.hostname === '127.0.0.1'; - } catch { - return false; - } -} - export function createOAuthClient( userId: number | null, name: string, diff --git a/server/src/services/packingService.ts b/server/src/services/packingService.ts index 19fa54d9..a9a78572 100644 --- a/server/src/services/packingService.ts +++ b/server/src/services/packingService.ts @@ -1,11 +1,9 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { avatarUrl } from './authService'; const BAG_COLORS = ['#6366f1', '#ec4899', '#f97316', '#10b981', '#06b6d4', '#8b5cf6', '#ef4444', '#f59e0b']; -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { verifyTripAccess } from './tripAccess'; // ── Items ────────────────────────────────────────────────────────────────── diff --git a/server/src/services/reservationService.ts b/server/src/services/reservationService.ts index 6e218010..5385c6a9 100644 --- a/server/src/services/reservationService.ts +++ b/server/src/services/reservationService.ts @@ -1,6 +1,8 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; import { Reservation } from '../types'; +export { verifyTripAccess } from './tripAccess'; + export interface ReservationEndpoint { id?: number; reservation_id?: number; @@ -17,10 +19,6 @@ export interface ReservationEndpoint { type EndpointInput = Omit & { sequence?: number }; -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} - function loadEndpointsByTrip(tripId: string | number): Map { const rows = db.prepare(` SELECT e.* FROM reservation_endpoints e @@ -298,15 +296,6 @@ export function updatePositions(tripId: string | number, positions: { id: number } } -export function getDayPositions(tripId: string | number, dayId: number | string) { - return db.prepare(` - SELECT rdp.reservation_id, rdp.position - FROM reservation_day_positions rdp - JOIN reservations r ON rdp.reservation_id = r.id - WHERE r.trip_id = ? AND rdp.day_id = ? - `).all(tripId, dayId) as { reservation_id: number; position: number }[]; -} - export function getReservation(id: string | number, tripId: string | number) { return db.prepare('SELECT * FROM reservations WHERE id = ? AND trip_id = ?').get(id, tripId) as Reservation | undefined; } diff --git a/server/src/services/todoService.ts b/server/src/services/todoService.ts index 3c2f14f3..d86c541d 100644 --- a/server/src/services/todoService.ts +++ b/server/src/services/todoService.ts @@ -1,8 +1,6 @@ -import { db, canAccessTrip } from '../db/database'; +import { db } from '../db/database'; -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} +export { verifyTripAccess } from './tripAccess'; // ── Items ────────────────────────────────────────────────────────────────── diff --git a/server/src/services/tripAccess.ts b/server/src/services/tripAccess.ts new file mode 100644 index 00000000..10b91c3f --- /dev/null +++ b/server/src/services/tripAccess.ts @@ -0,0 +1,9 @@ +import { canAccessTrip } from '../db/database'; + +/** + * Returns the trip row if the user is the owner or a member, otherwise undefined. + * Shared by the domain services so each one exposes the same access check. + */ +export function verifyTripAccess(tripId: string | number, userId: number) { + return canAccessTrip(tripId, userId); +} diff --git a/server/src/services/tripService.ts b/server/src/services/tripService.ts index 6138e45b..5d024b7e 100644 --- a/server/src/services/tripService.ts +++ b/server/src/services/tripService.ts @@ -1,6 +1,6 @@ import path from 'path'; import fs from 'fs'; -import { db, canAccessTrip, isOwner } from '../db/database'; +import { db, isOwner } from '../db/database'; import { Trip, User } from '../types'; import { listDays, listAccommodations } from './dayService'; import { listBudgetItems } from './budgetService'; @@ -25,10 +25,7 @@ export const TRIP_SELECT = ` // ── Access helpers ──────────────────────────────────────────────────────── -export function verifyTripAccess(tripId: string | number, userId: number) { - return canAccessTrip(tripId, userId); -} - +export { verifyTripAccess } from './tripAccess'; export { isOwner }; // ── Day generation ──────────────────────────────────────────────────────── diff --git a/server/tests/integration/atlas.test.ts b/server/tests/integration/atlas.test.ts index c99c199b..23bc16e6 100644 --- a/server/tests/integration/atlas.test.ts +++ b/server/tests/integration/atlas.test.ts @@ -51,6 +51,27 @@ let nestApp: INestApplication; let app: Application; beforeAll(async () => { + // Stub the admin-1 GeoJSON download so /regions/geo is deterministic and never + // hits the real network (the un-stubbed fetch of a ~4600-feature file from + // raw.githubusercontent.com is what made ATLAS-013 hang/time out under load). + // Any other outbound fetch (e.g. background reverse-geocoding) returns empty so + // no test depends on live network. + vi.stubGlobal('fetch', async (url: unknown) => { + if (String(url).includes('natural-earth-vector')) { + return new Response( + JSON.stringify({ + type: 'FeatureCollection', + features: [ + { type: 'Feature', properties: { iso_a2: 'DE' }, geometry: { type: 'Point', coordinates: [10, 51] } }, + { type: 'Feature', properties: { iso_a2: 'FR' }, geometry: { type: 'Point', coordinates: [2, 47] } }, + ], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + } + return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }); + }); + createTables(testDb); runMigrations(testDb); nestApp = await buildApp(); @@ -63,6 +84,7 @@ beforeEach(() => { }); afterAll(async () => { + vi.unstubAllGlobals(); await nestApp.close(); testDb.close(); }); diff --git a/shared/src/auth/auth.schema.spec.ts b/shared/src/auth/auth.schema.spec.ts index 92204f3b..959d2537 100644 --- a/shared/src/auth/auth.schema.spec.ts +++ b/shared/src/auth/auth.schema.spec.ts @@ -28,9 +28,9 @@ describe('loginRequestSchema', () => { describe('forgot/reset/change password schemas', () => { it('validate their required fields', () => { expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ token: 't', password: 'pw' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ token: 't', password: 'pw', mfa_code: '123456' }).success).toBe(true); - expect(resetPasswordRequestSchema.safeParse({ password: 'pw' }).success).toBe(false); + expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).success).toBe(true); + expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw', mfa_code: '123456' }).success).toBe(true); + expect(resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success).toBe(false); expect(changePasswordRequestSchema.safeParse({ current_password: 'a', new_password: 'b' }).success).toBe(true); expect(changePasswordRequestSchema.safeParse({ new_password: 'b' }).success).toBe(false); }); diff --git a/shared/src/auth/auth.schema.ts b/shared/src/auth/auth.schema.ts index bdbd503c..b5fa4653 100644 --- a/shared/src/auth/auth.schema.ts +++ b/shared/src/auth/auth.schema.ts @@ -29,7 +29,9 @@ export type ForgotPasswordRequest = z.infer; export const resetPasswordRequestSchema = z.object({ token: z.string(), - password: z.string(), + // The client sends `new_password` and the service reads `body.new_password`; + // the field was misnamed `password` here, which broke the client's typing. + new_password: z.string(), mfa_code: z.string().optional(), }); export type ResetPasswordRequest = z.infer;