From 4484724d38814ffe86c77ff1614c3d6f734d9bfd Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 30 May 2026 19:21:51 +0200 Subject: [PATCH] fix(logs): Skipper- und Crew-Unterschrift rollenbasiert trennen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jede Rolle darf nur das eigene Signaturfeld bearbeiten; Passkey-Freigabe auf dem Server entsprechend eingeschränkt. Co-authored-by: Cursor --- client/src/components/LogEntryEditor.tsx | 21 +++++++++++++++------ client/src/components/SignatureSection.tsx | 10 +++++----- server/src/routes/sign.ts | 19 +++++++++++-------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index b5b0dd3..1624679 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -138,7 +138,7 @@ export default function LogEntryEditor({ const [signSkipper, setSignSkipper] = useState('') const [signCrew, setSignCrew] = useState('') const [canSignSkipper, setCanSignSkipper] = useState(false) - const [hasWriteCollaborators, setHasWriteCollaborators] = useState(false) + const [canSignCrew, setCanSignCrew] = useState(false) const [isOnline, setIsOnline] = useState(navigator.onLine) const [entryHash, setEntryHash] = useState('') @@ -362,8 +362,11 @@ export default function LogEntryEditor({ useEffect(() => { getLogbookAccess(logbookId).then((access) => { if (!access) return - setCanSignSkipper(access.isOwner || access.role === 'WRITE') - setHasWriteCollaborators(access.writeCollaboratorCount > 0) + setCanSignSkipper(access.isOwner) + setCanSignCrew( + access.role === 'WRITE' || + (access.isOwner && access.writeCollaboratorCount === 0) + ) }) }, [logbookId]) @@ -429,6 +432,7 @@ export default function LogEntryEditor({ const crewSignatureValid = !isPasskeySignature(signCrew) || isSignatureValidForEntry(signCrew, entryHash) const handlePasskeySignSkipper = async () => { + if (!canSignSkipper) return const confirmed = await confirmSignWarning() if (!confirmed) return @@ -446,6 +450,7 @@ export default function LogEntryEditor({ } const handlePasskeySignCrew = async () => { + if (!canSignCrew) return const confirmed = await confirmSignWarning() if (!confirmed) return @@ -1697,13 +1702,17 @@ export default function LogEntryEditor({ disabled={saving} isOnline={isOnline} canSignSkipper={canSignSkipper} - hasWriteCollaborators={hasWriteCollaborators} + canSignCrew={canSignCrew} signSkipper={signSkipper} signCrew={signCrew} skipperSignatureValid={skipperSignatureValid} crewSignatureValid={crewSignatureValid} - onSignSkipperChange={setSignSkipper} - onSignCrewChange={setSignCrew} + onSignSkipperChange={(value) => { + if (canSignSkipper && !readOnly) setSignSkipper(value) + }} + onSignCrewChange={(value) => { + if (canSignCrew && !readOnly) setSignCrew(value) + }} onPasskeySignSkipper={handlePasskeySignSkipper} onPasskeySignCrew={handlePasskeySignCrew} onBeforeSign={confirmSignWarning} diff --git a/client/src/components/SignatureSection.tsx b/client/src/components/SignatureSection.tsx index b64aa2e..dbf7f59 100644 --- a/client/src/components/SignatureSection.tsx +++ b/client/src/components/SignatureSection.tsx @@ -13,7 +13,7 @@ interface SignatureSectionProps { disabled?: boolean isOnline: boolean canSignSkipper: boolean - hasWriteCollaborators: boolean + canSignCrew: boolean signSkipper: SignatureValue | '' signCrew: SignatureValue | '' skipperSignatureValid: boolean @@ -189,7 +189,7 @@ export default function SignatureSection({ disabled = false, isOnline, canSignSkipper, - hasWriteCollaborators, + canSignCrew, signSkipper, signCrew, skipperSignatureValid, @@ -203,7 +203,7 @@ export default function SignatureSection({ const { t } = useTranslation() const showSkipperPasskey = canSignSkipper && isOnline - const showCrewPasskey = hasWriteCollaborators && isOnline + const showCrewPasskey = canSignCrew && isOnline && !canSignSkipper const hasSignature = !!(signSkipper || signCrew) return ( @@ -228,7 +228,7 @@ export default function SignatureSection({ passkeySignature={isPasskeySignature(signSkipper) ? signSkipper : undefined} signatureValid={skipperSignatureValid} showPasskey={showSkipperPasskey} - readOnly={readOnly} + readOnly={readOnly || !canSignSkipper} disabled={disabled} classicHint={showSkipperPasskey ? t('logs.sign_classic_or_passkey') : undefined} offlineHint={!isOnline && canSignSkipper ? t('logs.sign_offline_hint') : undefined} @@ -245,7 +245,7 @@ export default function SignatureSection({ passkeySignature={isPasskeySignature(signCrew) ? signCrew : undefined} signatureValid={crewSignatureValid} showPasskey={showCrewPasskey} - readOnly={readOnly} + readOnly={readOnly || !canSignCrew} disabled={disabled} classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined} onChange={onSignCrewChange} diff --git a/server/src/routes/sign.ts b/server/src/routes/sign.ts index ccb23f9..e694ab7 100644 --- a/server/src/routes/sign.ts +++ b/server/src/routes/sign.ts @@ -99,14 +99,7 @@ async function isAuthorizedSigner( role: 'skipper' | 'crew' ): Promise { if (role === 'skipper') { - // Skipper signing: owner or WRITE collaborator (design §2.1), using their own passkey. - if (signerUserId === ownerUserId) return true - const collaboration = await prisma.collaboration.findUnique({ - where: { - logbookId_userId: { logbookId, userId: signerUserId } - } - }) - return collaboration?.role === 'WRITE' + return signerUserId === ownerUserId } const collaboration = await prisma.collaboration.findUnique({ @@ -138,6 +131,16 @@ router.post('/options', async (req: any, res) => { return res.status(403).json({ error: 'Forbidden: WRITE access required to sign entries' }) } + const authorized = await isAuthorizedSigner( + logbookId, + access.logbook.userId, + req.userId, + role + ) + if (!authorized) { + return res.status(403).json({ error: 'Forbidden: Signer not authorized for this role' }) + } + const allowCredentials = await getAllowCredentialsForRole( logbookId, role,