fix(logs): Skipper- und Crew-Unterschrift rollenbasiert trennen
Jede Rolle darf nur das eigene Signaturfeld bearbeiten; Passkey-Freigabe auf dem Server entsprechend eingeschränkt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -138,7 +138,7 @@ export default function LogEntryEditor({
|
||||
const [signSkipper, setSignSkipper] = useState<SignatureValue | ''>('')
|
||||
const [signCrew, setSignCrew] = useState<SignatureValue | ''>('')
|
||||
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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -99,14 +99,7 @@ async function isAuthorizedSigner(
|
||||
role: 'skipper' | 'crew'
|
||||
): Promise<boolean> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user