diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index b3e2b7b14..93805aa4d 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -18,7 +18,7 @@ "src/index.css": 1636, "src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1506, "src/pages/JourneyDetailPage.test.tsx": 4028, - "src/pages/LoginPage.tsx": 1347, + "src/pages/LoginPage.tsx": 1294, "src/pages/TripPlannerPage.tsx": 1128, "src/pages/atlas/useAtlas.ts": 1207, "src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518, diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 61703be83..2945ae510 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -4,7 +4,6 @@ import { EyeOff, Fingerprint, Globe, - KeyRound, Loader2, Lock, Mail, @@ -18,6 +17,7 @@ import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'; import { IDP_LOGIN_URL, useLogin } from './login/useLogin'; import LoginWorld from './login/LoginWorld'; import NativeServerLink from './login/NativeServerLink'; +import MfaCodeStep from './login/MfaCodeStep'; import PasswordChecklist from '../components/shared/PasswordChecklist'; /** Fixed so the sky does not reshuffle on every render. */ @@ -739,93 +739,18 @@ export default function LoginPage(): React.ReactElement { )} {mode === 'login' && mfaStep && !passwordChangeStep && ( -
- - {/* The account the code belongs to, for password managers: they match a - one-time code to a saved login through the username in the same form, - and the email field is gone in this step. */} - -
- - ) => - setMfaCode(e.target.value.toUpperCase().slice(0, 24)) - } - placeholder={t('login.mfaPlaceholder')} - required - autoFocus - style={inputBase} - onFocus={(e: React.FocusEvent) => (e.target.style.borderColor = '#111827')} - onBlur={(e: React.FocusEvent) => (e.target.style.borderColor = '#e5e7eb')} - /> -
-

- {t('login.mfaHint')} -

- -
+ { + setMfaStep(false); + setMfaToken(''); + setMfaCode(''); + setError(''); + }} + /> )} {/* Username (register only) */} diff --git a/client/src/pages/login/MfaCodeStep.tsx b/client/src/pages/login/MfaCodeStep.tsx new file mode 100644 index 000000000..364f21d1d --- /dev/null +++ b/client/src/pages/login/MfaCodeStep.tsx @@ -0,0 +1,99 @@ +import { KeyRound } from 'lucide-react'; +import React from 'react'; +import { useTranslation } from '../../i18n'; + +interface MfaCodeStepProps { + /** The account being signed in, kept in the form for password managers. */ + email: string; + code: string; + onCode: (code: string) => void; + inputStyle: React.CSSProperties; + onBack: () => void; +} + +/** The login's second step: the one-time or backup code, and the way back. */ +export default function MfaCodeStep({ email, code, onCode, inputStyle, onBack }: MfaCodeStepProps) { + const { t } = useTranslation(); + return ( +
+ + {/* The account the code belongs to, for password managers: they match a + one-time code to a saved login through the username in the same form, + and the email field is gone in this step. */} + +
+ + ) => onCode(e.target.value.toUpperCase().slice(0, 24))} + placeholder={t('login.mfaPlaceholder')} + required + autoFocus + style={inputStyle} + onFocus={(e: React.FocusEvent) => (e.target.style.borderColor = '#111827')} + onBlur={(e: React.FocusEvent) => (e.target.style.borderColor = '#e5e7eb')} + /> +
+

+ {t('login.mfaHint')} +

+ +
+ ); +} diff --git a/server/scripts/size-baseline.json b/server/scripts/size-baseline.json index 1e5d04336..6cb07eaa3 100644 --- a/server/scripts/size-baseline.json +++ b/server/scripts/size-baseline.json @@ -2,7 +2,7 @@ "scripts/db-call-graph.mjs": 1036, "scripts/generate-entities.ts": 2259, "src/db/repositories/Places.repository.ts": 1437, - "src/db/repositories/Reservations.repository.ts": 1604, + "src/db/repositories/Reservations.repository.ts": 1559, "src/db/repositories/Trips.repository.ts": 1121, "src/db/repositories/Users.repository.ts": 1445, "src/nest/atlas/atlas.service.ts": 1177, diff --git a/server/src/db/repositories/Reservations.repository.ts b/server/src/db/repositories/Reservations.repository.ts index f64f6295c..a5eeace27 100644 --- a/server/src/db/repositories/Reservations.repository.ts +++ b/server/src/db/repositories/Reservations.repository.ts @@ -18,7 +18,10 @@ import type { PublicApiUnplannedPlaceRow, PublicApiUnscheduledReservationRow, } from './reservation-public-api-rows'; +import type { ReservationAllColumnsRow, ReservationJoinRow } from './reservation-rows'; +// The reservation row shapes live in their own module; re-exported so callers keep one import path. +export type { ReservationAllColumnsRow, ReservationJoinRow } from './reservation-rows'; // The public API row types moved to their own module; re-exported so callers keep one import path. export type { PublicApiAccommodationRow, @@ -27,31 +30,6 @@ export type { PublicApiUnscheduledReservationRow, }; -/** - * RS18/RR1's shared joined projection — `SELECT r.*, d.day_number, p.name as - * place_name, r.assignment_id, ap.place_id as accommodation_place_id, - * acc_p.name as accommodation_name, ap.start_day_id as - * accommodation_start_day_id, ap.end_day_id as accommodation_end_day_id, - * ap.check_in/check_in_end/check_out as accommodation_check_in/_check_in_end/_check_out FROM - * reservations r LEFT JOIN days d ON r.day_id = d.id LEFT JOIN places p ON - * r.place_id = p.id LEFT JOIN day_accommodations ap ON r.accommodation_id = - * ap.id LEFT JOIN places acc_p ON ap.place_id = acc_p.id`. The service - * folds `accommodation_id` (`Math.trunc(Number(x))`), `day_positions`, - * `endpoints` and `travelers` on afterward (unchanged from the legacy split). - */ -export interface ReservationJoinRow extends ReservationAllColumnsRow { - day_number: number | null; - place_name: string | null; - accommodation_place_id: number | null; - accommodation_name: string | null; - accommodation_start_day_id: number | null; - accommodation_end_day_id: number | null; - /** The stay's own times, which a hotel booking's card falls back to. */ - accommodation_check_in: string | null; - accommodation_check_in_end: string | null; - accommodation_check_out: string | null; -} - /** * Kysely typing for the RS18/RR1 join (`joinedQuery` below) — `reservations * .accommodation_id` is a bare `p.text().nullable()` column with no FK to @@ -1534,34 +1512,3 @@ export interface RoadtripCarrierRow { metadata: string | null; day_plan_position: number | null; } - -/** `SELECT *` — every scalar column of `Reservations`, RS35's shape. */ -export interface ReservationAllColumnsRow { - id: number; - trip_id: number; - day_id: number | null; - end_day_id: number | null; - place_id: number | null; - assignment_id: number | null; - title: string; - accommodation_id: string | null; - reservation_time: string | null; - reservation_end_time: string | null; - location: string | null; - confirmation_number: string | null; - notes: string | null; - status: string | null; - type: string | null; - created_at: string | null; - metadata: string | null; - day_plan_position: number | null; - needs_review: number; - external_source: string | null; - external_id: string | null; - external_owner_user_id: number | null; - external_synced_at: string | null; - sync_enabled: number | null; - external_hash: string | null; - url: string | null; - ingest_state: string; -} diff --git a/server/src/db/repositories/reservation-rows.ts b/server/src/db/repositories/reservation-rows.ts new file mode 100644 index 000000000..0d9e6dcf1 --- /dev/null +++ b/server/src/db/repositories/reservation-rows.ts @@ -0,0 +1,60 @@ +/** + * The row shapes ReservationsRepository reads: every column of a reservation, + * and the joined projection the trip list and the single reads share. + */ + +/** `SELECT *` — every scalar column of `Reservations`, RS35's shape. */ +export interface ReservationAllColumnsRow { + id: number; + trip_id: number; + day_id: number | null; + end_day_id: number | null; + place_id: number | null; + assignment_id: number | null; + title: string; + accommodation_id: string | null; + reservation_time: string | null; + reservation_end_time: string | null; + location: string | null; + confirmation_number: string | null; + notes: string | null; + status: string | null; + type: string | null; + created_at: string | null; + metadata: string | null; + day_plan_position: number | null; + needs_review: number; + external_source: string | null; + external_id: string | null; + external_owner_user_id: number | null; + external_synced_at: string | null; + sync_enabled: number | null; + external_hash: string | null; + url: string | null; + ingest_state: string; +} + +/** + * RS18/RR1's shared joined projection — `SELECT r.*, d.day_number, p.name as + * place_name, r.assignment_id, ap.place_id as accommodation_place_id, + * acc_p.name as accommodation_name, ap.start_day_id as + * accommodation_start_day_id, ap.end_day_id as accommodation_end_day_id, + * ap.check_in/check_in_end/check_out as accommodation_check_in/_check_in_end/_check_out FROM + * reservations r LEFT JOIN days d ON r.day_id = d.id LEFT JOIN places p ON + * r.place_id = p.id LEFT JOIN day_accommodations ap ON r.accommodation_id = + * ap.id LEFT JOIN places acc_p ON ap.place_id = acc_p.id`. The service + * folds `accommodation_id` (`Math.trunc(Number(x))`), `day_positions`, + * `endpoints` and `travelers` on afterward (unchanged from the legacy split). + */ +export interface ReservationJoinRow extends ReservationAllColumnsRow { + day_number: number | null; + place_name: string | null; + accommodation_place_id: number | null; + accommodation_name: string | null; + accommodation_start_day_id: number | null; + accommodation_end_day_id: number | null; + /** The stay's own times, which a hotel booking's card falls back to. */ + accommodation_check_in: string | null; + accommodation_check_in_end: string | null; + accommodation_check_out: string | null; +}