fix: Passkey-Sign Challenge und Signatur-Moduswechsel

WebAuthn-Challenge wird als Bytes übergeben und unter options.challenge
gespeichert. Passkey/Klassisch-Toggle erlaubt Wechsel zwischen Freigabearten.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 16:40:01 +02:00
co-authored by Cursor
parent ce47fe5fdc
commit 878a18e9f7
5 changed files with 231 additions and 74 deletions
+184 -64
View File
@@ -1,10 +1,13 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Check } from 'lucide-react'
import SignaturePad from './SignaturePad.tsx'
import PasskeySignButton from './PasskeySignButton.tsx'
import type { SignatureValue } from '../types/signatures.js'
import type { PasskeySignature, SignatureValue } from '../types/signatures.js'
import { isPasskeySignature } from '../utils/signatures.js'
type SignatureMode = 'passkey' | 'classic'
interface SignatureSectionProps {
readOnly?: boolean
disabled?: boolean
@@ -26,6 +29,157 @@ function padValue(value: SignatureValue | ''): string {
return value
}
function modeFromValue(value: SignatureValue | '', passkeyAvailable: boolean): SignatureMode {
if (isPasskeySignature(value)) return 'passkey'
if (value) return 'classic'
return passkeyAvailable ? 'passkey' : 'classic'
}
interface RoleSignatureBlockProps {
roleLabel: string
passkeyLabel: string
padId: string
value: SignatureValue | ''
passkeySignature?: PasskeySignature
signatureValid: boolean
showPasskey: boolean
readOnly: boolean
disabled: boolean
classicHint?: string
offlineHint?: string
onChange: (value: SignatureValue | '') => void
onPasskeySign: () => Promise<void>
}
function RoleSignatureBlock({
roleLabel,
passkeyLabel,
padId,
value,
passkeySignature,
signatureValid,
showPasskey,
readOnly,
disabled,
classicHint,
offlineHint,
onChange,
onPasskeySign
}: RoleSignatureBlockProps) {
const { t } = useTranslation()
const [mode, setMode] = useState<SignatureMode>(() => modeFromValue(value, showPasskey))
useEffect(() => {
setMode(modeFromValue(value, showPasskey))
}, [value, showPasskey])
const switchToClassic = () => {
setMode('classic')
if (isPasskeySignature(value)) onChange('')
}
const switchToPasskey = () => {
setMode('passkey')
if (value && !isPasskeySignature(value)) onChange('')
}
const handlePadChange = (next: string) => {
setMode('classic')
onChange(next)
}
if (readOnly) {
if (isPasskeySignature(value)) {
return (
<div className="signature-role-block">
<PasskeySignButton
label={passkeyLabel}
signature={value}
signatureValid={signatureValid}
disabled={disabled}
canSign={false}
onSign={onPasskeySign}
/>
</div>
)
}
return (
<div className="signature-role-block">
<SignaturePad
id={padId}
label={roleLabel}
value={padValue(value)}
onChange={() => {}}
disabled={disabled}
readOnly
/>
</div>
)
}
const showPasskeyPanel = showPasskey && mode === 'passkey'
const showClassicPanel = !showPasskey || mode === 'classic'
return (
<div className="signature-role-block">
{showPasskey && (
<div className="signature-mode-toggle" role="tablist" aria-label={passkeyLabel}>
<button
type="button"
role="tab"
aria-selected={mode === 'passkey'}
className={`signature-mode-btn ${mode === 'passkey' ? 'active' : ''}`}
onClick={switchToPasskey}
>
{t('logs.sign_mode_passkey')}
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'classic'}
className={`signature-mode-btn ${mode === 'classic' ? 'active' : ''}`}
onClick={switchToClassic}
>
{t('logs.sign_mode_classic')}
</button>
</div>
)}
{showPasskeyPanel && (
<PasskeySignButton
label={passkeyLabel}
signature={passkeySignature}
signatureValid={signatureValid}
disabled={disabled}
canSign
onSign={onPasskeySign}
onClear={passkeySignature ? switchToClassic : undefined}
/>
)}
{showClassicPanel && (
<>
<SignaturePad
id={padId}
label={roleLabel}
value={padValue(value)}
onChange={handlePadChange}
disabled={disabled}
readOnly={false}
/>
{classicHint && !passkeySignature && (
<p className="signature-hint">{classicHint}</p>
)}
</>
)}
{offlineHint && !showPasskey && (
<p className="signature-hint">{offlineHint}</p>
)}
</div>
)
}
export default function SignatureSection({
readOnly = false,
disabled = false,
@@ -43,9 +197,6 @@ export default function SignatureSection({
}: SignatureSectionProps) {
const { t } = useTranslation()
const skipperPasskey = isPasskeySignature(signSkipper) ? signSkipper : undefined
const crewPasskey = isPasskeySignature(signCrew) ? signCrew : undefined
const showSkipperPasskey = isOwner && isOnline
const showCrewPasskey = hasWriteCollaborators && isOnline
@@ -57,67 +208,36 @@ export default function SignatureSection({
</div>
<div className="form-grid signature-grid">
<div className="signature-role-block">
{showSkipperPasskey && (
<PasskeySignButton
label={t('logs.sign_skipper')}
signature={skipperPasskey}
signatureValid={skipperSignatureValid}
disabled={disabled}
canSign={!readOnly}
onSign={onPasskeySignSkipper}
onClear={skipperPasskey ? () => onSignSkipperChange('') : undefined}
/>
)}
<RoleSignatureBlock
roleLabel={t('logs.sign_skipper')}
passkeyLabel={t('logs.sign_skipper')}
padId="sign-skipper"
value={signSkipper}
passkeySignature={isPasskeySignature(signSkipper) ? signSkipper : undefined}
signatureValid={skipperSignatureValid}
showPasskey={showSkipperPasskey}
readOnly={readOnly}
disabled={disabled}
classicHint={showSkipperPasskey ? t('logs.sign_classic_or_passkey') : undefined}
offlineHint={!isOnline && isOwner ? t('logs.sign_offline_hint') : undefined}
onChange={onSignSkipperChange}
onPasskeySign={onPasskeySignSkipper}
/>
{!skipperPasskey && (
<SignaturePad
id="sign-skipper"
label={t('logs.sign_skipper')}
value={padValue(signSkipper)}
onChange={onSignSkipperChange}
disabled={disabled}
readOnly={readOnly}
/>
)}
{showSkipperPasskey && !skipperPasskey && !readOnly && (
<p className="signature-hint">{t('logs.sign_classic_or_passkey')}</p>
)}
{!isOnline && isOwner && !readOnly && (
<p className="signature-hint">{t('logs.sign_offline_hint')}</p>
)}
</div>
<div className="signature-role-block">
{showCrewPasskey && (
<PasskeySignButton
label={t('logs.sign_crew')}
signature={crewPasskey}
signatureValid={crewSignatureValid}
disabled={disabled}
canSign={!readOnly}
onSign={onPasskeySignCrew}
onClear={crewPasskey ? () => onSignCrewChange('') : undefined}
/>
)}
{!crewPasskey && (
<SignaturePad
id="sign-crew"
label={t('logs.sign_crew')}
value={padValue(signCrew)}
onChange={onSignCrewChange}
disabled={disabled}
readOnly={readOnly}
/>
)}
{showCrewPasskey && !crewPasskey && !readOnly && (
<p className="signature-hint">{t('logs.sign_crew_passkey_hint')}</p>
)}
</div>
<RoleSignatureBlock
roleLabel={t('logs.sign_crew')}
passkeyLabel={t('logs.sign_crew')}
padId="sign-crew"
value={signCrew}
passkeySignature={isPasskeySignature(signCrew) ? signCrew : undefined}
signatureValid={crewSignatureValid}
showPasskey={showCrewPasskey}
readOnly={readOnly}
disabled={disabled}
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
onChange={onSignCrewChange}
onPasskeySign={onPasskeySignCrew}
/>
</div>
</div>
)