mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
refactor: the login's code step and the reservation row shapes get modules of their own
Both files had grown past their size limit with this round's fixes. The two-factor step moves to pages/login/MfaCodeStep.tsx and the reservation row types to reservation-rows.ts, re-exported from the repository, and both size baselines come down to match.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))',
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{t('login.mfaCodeLabel')}
|
||||
</label>
|
||||
{/* 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. */}
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
value={email}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="sr-only"
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<KeyRound
|
||||
size={15}
|
||||
className="text-[#9ca3af]"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
insetInlineStart: 13,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
// Text, not numeric: backup codes carry letters. The hints are set here
|
||||
// rather than left to native/keyboardFields.ts, because autoFocus gives
|
||||
// the field focus before any touch could prepare it.
|
||||
inputMode="text"
|
||||
autoComplete="one-time-code"
|
||||
autoCapitalize="characters"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
enterKeyHint="go"
|
||||
value={mfaCode}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setMfaCode(e.target.value.toUpperCase().slice(0, 24))
|
||||
}
|
||||
placeholder={t('login.mfaPlaceholder')}
|
||||
required
|
||||
autoFocus
|
||||
style={inputBase}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#111827')}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#e5e7eb')}
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: '#9ca3af', marginTop: 8 }}>
|
||||
{t('login.mfaHint')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMfaStep(false);
|
||||
setMfaToken('');
|
||||
setMfaCode('');
|
||||
setError('');
|
||||
}}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#6b7280',
|
||||
fontSize: 'calc(13px * var(--fs-scale-body, 1))',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
{t('login.mfaBack')}
|
||||
</button>
|
||||
</div>
|
||||
<MfaCodeStep
|
||||
email={email}
|
||||
code={mfaCode}
|
||||
onCode={setMfaCode}
|
||||
inputStyle={inputBase}
|
||||
onBack={() => {
|
||||
setMfaStep(false);
|
||||
setMfaToken('');
|
||||
setMfaCode('');
|
||||
setError('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Username (register only) */}
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))',
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{t('login.mfaCodeLabel')}
|
||||
</label>
|
||||
{/* 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. */}
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
value={email}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="sr-only"
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<KeyRound
|
||||
size={15}
|
||||
className="text-[#9ca3af]"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
insetInlineStart: 13,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
// Text, not numeric: backup codes carry letters. The hints are set here
|
||||
// rather than left to native/keyboardFields.ts, because autoFocus gives
|
||||
// the field focus before any touch could prepare it.
|
||||
inputMode="text"
|
||||
autoComplete="one-time-code"
|
||||
autoCapitalize="characters"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
enterKeyHint="go"
|
||||
value={code}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCode(e.target.value.toUpperCase().slice(0, 24))}
|
||||
placeholder={t('login.mfaPlaceholder')}
|
||||
required
|
||||
autoFocus
|
||||
style={inputStyle}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#111827')}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#e5e7eb')}
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: '#9ca3af', marginTop: 8 }}>
|
||||
{t('login.mfaHint')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#6b7280',
|
||||
fontSize: 'calc(13px * var(--fs-scale-body, 1))',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
{t('login.mfaBack')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user