Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9fa8c0edf | |||
| adf02acd45 | |||
| 3992db9d61 | |||
| 51f6a1b291 | |||
| 0b07d8b3d3 | |||
| a07e033e62 | |||
| bbe63dfb47 | |||
| 57f63ad486 |
+66
-5
@@ -2433,6 +2433,32 @@ html.scheme-dark .themed-select-option.is-selected {
|
|||||||
.track-map-container {
|
.track-map-container {
|
||||||
height: min(360px, 45svh);
|
height: min(360px, 45svh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sails-picker-pills {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sails-picker-container.is-collapsible .sails-picker-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sail-pill {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.sails-picker-container.is-collapsible.is-collapsed .sails-picker-pills {
|
||||||
|
max-height: 3.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sail-pill {
|
||||||
|
padding: 2px 7px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================== */
|
/* ========================================== */
|
||||||
@@ -2693,16 +2719,48 @@ html.theme-cupertino .events-scroll-container {
|
|||||||
|
|
||||||
/* Event Editor Interactive Sails Picker */
|
/* Event Editor Interactive Sails Picker */
|
||||||
.sails-picker-container {
|
.sails-picker-container {
|
||||||
display: flex;
|
grid-column: 1 / -1;
|
||||||
flex-direction: column;
|
margin-top: -4px;
|
||||||
gap: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sails-picker-pills {
|
.sails-picker-pills {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sails-picker-container.is-collapsible.is-collapsed .sails-picker-pills {
|
||||||
|
max-height: 3.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sails-picker-container.is-collapsible.is-collapsed .sails-picker-pills::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1.25rem;
|
||||||
|
background: linear-gradient(to bottom, transparent, var(--app-surface, #0f172a));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sails-picker-toggle {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 4px auto 0;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--app-text-muted, #94a3b8);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sails-picker-toggle:hover {
|
||||||
|
color: var(--app-accent, #fbbf24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sail-pill {
|
.sail-pill {
|
||||||
@@ -2715,6 +2773,7 @@ html.theme-cupertino .events-scroll-container {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sail-pill:hover {
|
.sail-pill:hover {
|
||||||
@@ -2742,7 +2801,9 @@ html.theme-cupertino .events-scroll-container {
|
|||||||
background: rgba(56, 189, 248, 0.15);
|
background: rgba(56, 189, 248, 0.15);
|
||||||
border-color: #38bdf8;
|
border-color: #38bdf8;
|
||||||
color: #38bdf8;
|
color: #38bdf8;
|
||||||
}.grid-span-2 {
|
}
|
||||||
|
|
||||||
|
.grid-span-2 {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -18,7 +18,8 @@ import { UnsavedChangesProvider, useUnsavedChangesContext } from './context/Unsa
|
|||||||
import {
|
import {
|
||||||
logoutUser,
|
logoutUser,
|
||||||
checkServerSession,
|
checkServerSession,
|
||||||
hasUnlockedLocalCrypto
|
hasUnlockedLocalSession,
|
||||||
|
persistSessionUserId
|
||||||
} from './services/auth.js'
|
} from './services/auth.js'
|
||||||
import AppErrorBoundary from './components/AppErrorBoundary.tsx'
|
import AppErrorBoundary from './components/AppErrorBoundary.tsx'
|
||||||
import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js'
|
||||||
@@ -225,7 +226,8 @@ function App() {
|
|||||||
/** After PWA/bfcache resume, React state may still say "logged in" while the master key is gone. */
|
/** After PWA/bfcache resume, React state may still say "logged in" while the master key is gone. */
|
||||||
const enforceUnlockedSession = useCallback(() => {
|
const enforceUnlockedSession = useCallback(() => {
|
||||||
if (isViewerMode || isDemoMode || isAcceptingInvite) return
|
if (isViewerMode || isDemoMode || isAcceptingInvite) return
|
||||||
if (isAuthenticated && !hasUnlockedLocalCrypto()) {
|
// Require full local session (incl. userId) so API calls are not left headless.
|
||||||
|
if (isAuthenticated && !hasUnlockedLocalSession()) {
|
||||||
clearAuthenticatedAppState()
|
clearAuthenticatedAppState()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -267,11 +269,12 @@ function App() {
|
|||||||
const session = await checkServerSession()
|
const session = await checkServerSession()
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
||||||
if (session.authenticated && session.userId) {
|
if (session.authenticated) {
|
||||||
localStorage.setItem('active_userid', session.userId)
|
persistSessionUserId(session.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.authenticated && hasUnlockedLocalCrypto()) {
|
// Cookie alone is insufficient — need in-memory master key, username, and userId for API.
|
||||||
|
if (session.authenticated && hasUnlockedLocalSession()) {
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
const savedLogbookId = localStorage.getItem('active_logbook_id')
|
const savedLogbookId = localStorage.getItem('active_logbook_id')
|
||||||
const savedLogbookTitle = localStorage.getItem('active_logbook_title')
|
const savedLogbookTitle = localStorage.getItem('active_logbook_title')
|
||||||
@@ -280,7 +283,7 @@ function App() {
|
|||||||
setActiveLogbookTitle(savedLogbookTitle)
|
setActiveLogbookTitle(savedLogbookTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// authenticated without local crypto: stay on login (cookie alone is not enough)
|
// authenticated + crypto but no userId: stay on login (enforceUnlockedSession guards active UI)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
console.warn('Session restore failed:', err)
|
console.warn('Session restore failed:', err)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '../services/auth.js'
|
} from '../services/auth.js'
|
||||||
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
|
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
|
||||||
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
|
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
|
||||||
|
import DisclaimerModal from './DisclaimerModal.tsx'
|
||||||
import BetaBadge from './BetaBadge.tsx'
|
import BetaBadge from './BetaBadge.tsx'
|
||||||
|
|
||||||
interface AuthOnboardingProps {
|
interface AuthOnboardingProps {
|
||||||
@@ -50,6 +51,7 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
|
|||||||
|
|
||||||
const [isNewRegistration, setIsNewRegistration] = useState(false)
|
const [isNewRegistration, setIsNewRegistration] = useState(false)
|
||||||
const [showDisclaimer, setShowDisclaimer] = useState(false)
|
const [showDisclaimer, setShowDisclaimer] = useState(false)
|
||||||
|
const [showHelp, setShowHelp] = useState(false)
|
||||||
|
|
||||||
const finishAuth = () => {
|
const finishAuth = () => {
|
||||||
if (isNewRegistration) {
|
if (isNewRegistration) {
|
||||||
@@ -410,6 +412,7 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
|
|||||||
|
|
||||||
// Render 3: Standard Login / Registration options form
|
// Render 3: Standard Login / Registration options form
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="auth-card glass">
|
<div className="auth-card glass">
|
||||||
<div className="auth-brand">
|
<div className="auth-brand">
|
||||||
<img src="/logo.png" alt="Kapteins Daagbok" className="auth-logo-img" />
|
<img src="/logo.png" alt="Kapteins Daagbok" className="auth-logo-img" />
|
||||||
@@ -570,15 +573,23 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="auth-footer">
|
<div className="auth-footer">
|
||||||
<button className="btn-icon-text" onClick={toggleLanguage}>
|
<button type="button" className="btn-icon-text" onClick={toggleLanguage}>
|
||||||
<Languages size={18} />
|
<Languages size={18} />
|
||||||
{i18n.language.startsWith('de') ? 'English' : 'Deutsch'}
|
{i18n.language.startsWith('de') ? 'English' : 'Deutsch'}
|
||||||
</button>
|
</button>
|
||||||
<a href="#help" className="btn-icon-text link-sec">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-icon-text link-sec"
|
||||||
|
onClick={() => setShowHelp(true)}
|
||||||
|
title={t('disclaimer.button_title')}
|
||||||
|
aria-label={t('disclaimer.button_title')}
|
||||||
|
>
|
||||||
<HelpCircle size={18} />
|
<HelpCircle size={18} />
|
||||||
{t('auth.help')}
|
{t('auth.help')}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DisclaimerModal open={showHelp} onClose={() => setShowHelp(false)} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getLogbookKey } from '../services/logbookKeys.js'
|
|||||||
import { encryptJson, decryptJson } from '../services/crypto.js'
|
import { encryptJson, decryptJson } from '../services/crypto.js'
|
||||||
import { syncLogbook } from '../services/sync.js'
|
import { syncLogbook } from '../services/sync.js'
|
||||||
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
||||||
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload, Pencil, X } from 'lucide-react'
|
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload, Pencil, X, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import PhotoCapture from './PhotoCapture.tsx'
|
import PhotoCapture from './PhotoCapture.tsx'
|
||||||
import SignatureSection from './SignatureSection.tsx'
|
import SignatureSection from './SignatureSection.tsx'
|
||||||
import TrackMap from './TrackMap.tsx'
|
import TrackMap from './TrackMap.tsx'
|
||||||
@@ -176,6 +176,7 @@ export default function LogEntryEditor({
|
|||||||
const [evCurrent, setEvCurrent] = useState('')
|
const [evCurrent, setEvCurrent] = useState('')
|
||||||
const [evHeel, setEvHeel] = useState('')
|
const [evHeel, setEvHeel] = useState('')
|
||||||
const [evSailsOrMotor, setEvSailsOrMotor] = useState('')
|
const [evSailsOrMotor, setEvSailsOrMotor] = useState('')
|
||||||
|
const [sailsPickerExpanded, setSailsPickerExpanded] = useState(false)
|
||||||
const [evLogReading, setEvLogReading] = useState('')
|
const [evLogReading, setEvLogReading] = useState('')
|
||||||
const [evDistance, setEvDistance] = useState('')
|
const [evDistance, setEvDistance] = useState('')
|
||||||
const [evGpsLat, setEvGpsLat] = useState('')
|
const [evGpsLat, setEvGpsLat] = useState('')
|
||||||
@@ -842,6 +843,9 @@ export default function LogEntryEditor({
|
|||||||
? ['Großsegel', 'Genua', 'Fock', 'Spinnaker', 'Gennaker']
|
? ['Großsegel', 'Genua', 'Fock', 'Spinnaker', 'Gennaker']
|
||||||
: ['Mainsail', 'Genoa', 'Jib', 'Spinnaker', 'Gennaker']
|
: ['Mainsail', 'Genoa', 'Jib', 'Spinnaker', 'Gennaker']
|
||||||
|
|
||||||
|
const eventSailOptions = yachtSails.length > 0 ? yachtSails : defaultSails
|
||||||
|
const showSailsPickerToggle = eventSailOptions.length + 1 > 6
|
||||||
|
|
||||||
const toggleSailOrMotor = (item: string) => {
|
const toggleSailOrMotor = (item: string) => {
|
||||||
let currentItems = evSailsOrMotor
|
let currentItems = evSailsOrMotor
|
||||||
.split(/\s*(?:\+|\bplus\b|,)\s*/i)
|
.split(/\s*(?:\+|\bplus\b|,)\s*/i)
|
||||||
@@ -865,6 +869,15 @@ export default function LogEntryEditor({
|
|||||||
return currentItems.includes(item.toLowerCase())
|
return currentItems.includes(item.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const motorPropulsionLabel = t('logs.motor_propulsion')
|
||||||
|
const sortedEventSailOptions = [...eventSailOptions].sort((a, b) => {
|
||||||
|
const aActive = isItemActive(a)
|
||||||
|
const bActive = isItemActive(b)
|
||||||
|
if (aActive === bActive) return 0
|
||||||
|
return aActive ? -1 : 1
|
||||||
|
})
|
||||||
|
const isMotorActive = isItemActive(motorPropulsionLabel)
|
||||||
|
|
||||||
const clearEventForm = () => {
|
const clearEventForm = () => {
|
||||||
setEvTime(currentLocalTimeHHMM())
|
setEvTime(currentLocalTimeHHMM())
|
||||||
setEvMgk('')
|
setEvMgk('')
|
||||||
@@ -1559,25 +1572,6 @@ export default function LogEntryEditor({
|
|||||||
onChange={(e) => setEvSailsOrMotor(e.target.value)}
|
onChange={(e) => setEvSailsOrMotor(e.target.value)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<div className="sails-picker-container">
|
|
||||||
<div className="sails-picker-pills">
|
|
||||||
{(yachtSails.length > 0 ? yachtSails : defaultSails).map((sail) => (
|
|
||||||
<span
|
|
||||||
key={sail}
|
|
||||||
className={`sail-pill ${isItemActive(sail) ? 'active' : ''}`}
|
|
||||||
onClick={() => toggleSailOrMotor(sail)}
|
|
||||||
>
|
|
||||||
{sail}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<span
|
|
||||||
className={`sail-pill motor-pill ${isItemActive(t('logs.motor_propulsion')) ? 'active' : ''}`}
|
|
||||||
onClick={() => toggleSailOrMotor(t('logs.motor_propulsion'))}
|
|
||||||
>
|
|
||||||
{t('logs.motor_propulsion')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
@@ -1592,7 +1586,63 @@ export default function LogEntryEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group" style={{ gridColumn: 'span 2' }}>
|
<div
|
||||||
|
className={[
|
||||||
|
'sails-picker-container grid-span-2',
|
||||||
|
showSailsPickerToggle ? 'is-collapsible' : '',
|
||||||
|
showSailsPickerToggle && !sailsPickerExpanded ? 'is-collapsed' : '',
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
>
|
||||||
|
<div className="sails-picker-pills">
|
||||||
|
{isMotorActive && (
|
||||||
|
<span
|
||||||
|
className={`sail-pill motor-pill active`}
|
||||||
|
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
||||||
|
>
|
||||||
|
{motorPropulsionLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{sortedEventSailOptions.map((sail) => (
|
||||||
|
<span
|
||||||
|
key={sail}
|
||||||
|
className={`sail-pill ${isItemActive(sail) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleSailOrMotor(sail)}
|
||||||
|
>
|
||||||
|
{sail}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{!isMotorActive && (
|
||||||
|
<span
|
||||||
|
className="sail-pill motor-pill"
|
||||||
|
onClick={() => toggleSailOrMotor(motorPropulsionLabel)}
|
||||||
|
>
|
||||||
|
{motorPropulsionLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showSailsPickerToggle && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sails-picker-toggle"
|
||||||
|
onClick={() => setSailsPickerExpanded((prev) => !prev)}
|
||||||
|
aria-expanded={sailsPickerExpanded}
|
||||||
|
>
|
||||||
|
{sailsPickerExpanded ? (
|
||||||
|
<>
|
||||||
|
<ChevronUp size={14} aria-hidden="true" />
|
||||||
|
{t('logs.sails_picker_show_less')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown size={14} aria-hidden="true" />
|
||||||
|
{t('logs.sails_picker_show_more')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group grid-span-2">
|
||||||
<label>{t('logs.event_remarks')}</label>
|
<label>{t('logs.event_remarks')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"welcome": "Willkommen bei Kapteins Daagbok",
|
"welcome": "Willkommen bei Kapteins Daagbok",
|
||||||
"tagline": "Sicheres, E2E-verschlüsseltes maritimes Logbuch.",
|
"tagline": "Dein sicheres, E2E-verschlüsseltes maritimes Logbuch.",
|
||||||
"register": "Mit Passkey registrieren",
|
"register": "Mit Passkey registrieren",
|
||||||
"login": "Mit Passkey anmelden",
|
"login": "Mit Passkey anmelden",
|
||||||
"login_as": "Anmelden als {{name}}",
|
"login_as": "Anmelden als {{name}}",
|
||||||
@@ -222,6 +222,8 @@
|
|||||||
"event_heel": "Krängung (°)",
|
"event_heel": "Krängung (°)",
|
||||||
"event_sails": "Segelführung / Motor",
|
"event_sails": "Segelführung / Motor",
|
||||||
"motor_propulsion": "Maschinenfahrt",
|
"motor_propulsion": "Maschinenfahrt",
|
||||||
|
"sails_picker_show_more": "Alle Segel anzeigen",
|
||||||
|
"sails_picker_show_less": "Weniger anzeigen",
|
||||||
"motor_hours": "Maschinenstunden (gesamt)",
|
"motor_hours": "Maschinenstunden (gesamt)",
|
||||||
"fuel_per_motor_hour": "Verbrauch pro Maschinenstunde",
|
"fuel_per_motor_hour": "Verbrauch pro Maschinenstunde",
|
||||||
"event_distance": "Distanz (sm)",
|
"event_distance": "Distanz (sm)",
|
||||||
|
|||||||
@@ -222,6 +222,8 @@
|
|||||||
"event_heel": "Heel Angle (°)",
|
"event_heel": "Heel Angle (°)",
|
||||||
"event_sails": "Sails / Motor Status",
|
"event_sails": "Sails / Motor Status",
|
||||||
"motor_propulsion": "Engine Propulsion",
|
"motor_propulsion": "Engine Propulsion",
|
||||||
|
"sails_picker_show_more": "Show all sails",
|
||||||
|
"sails_picker_show_less": "Show less",
|
||||||
"motor_hours": "Engine hours (total)",
|
"motor_hours": "Engine hours (total)",
|
||||||
"fuel_per_motor_hour": "Consumption per engine hour",
|
"fuel_per_motor_hour": "Consumption per engine hour",
|
||||||
"event_distance": "Distance (nm)",
|
"event_distance": "Distance (nm)",
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export function hasUnlockedLocalSession(): boolean {
|
|||||||
return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid')
|
return hasUnlockedLocalCrypto() && !!localStorage.getItem('active_userid')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persist server session user id when the /session response includes it. */
|
||||||
|
export function persistSessionUserId(userId: string | undefined): void {
|
||||||
|
if (userId) {
|
||||||
|
localStorage.setItem('active_userid', userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function reauthWithPasskey(): Promise<boolean> {
|
export async function reauthWithPasskey(): Promise<boolean> {
|
||||||
const options = await apiJson<any>(`${API_BASE}/reauth-options`, {
|
const options = await apiJson<any>(`${API_BASE}/reauth-options`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
|||||||
@@ -32,3 +32,22 @@ describe('local session unlock checks', () => {
|
|||||||
expect(hasUnlockedLocalCrypto()).toBe(false)
|
expect(hasUnlockedLocalCrypto()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('persistSessionUserId', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stores userId when provided', async () => {
|
||||||
|
const { persistSessionUserId } = await import('./auth.js')
|
||||||
|
persistSessionUserId('user-42')
|
||||||
|
expect(localStorage.getItem('active_userid')).toBe('user-42')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not clear existing userId when omitted', async () => {
|
||||||
|
const { persistSessionUserId } = await import('./auth.js')
|
||||||
|
localStorage.setItem('active_userid', 'user-1')
|
||||||
|
persistSessionUserId(undefined)
|
||||||
|
expect(localStorage.getItem('active_userid')).toBe('user-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user