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 [signSkipper, setSignSkipper] = useState<SignatureValue | ''>('')
|
||||||
const [signCrew, setSignCrew] = useState<SignatureValue | ''>('')
|
const [signCrew, setSignCrew] = useState<SignatureValue | ''>('')
|
||||||
const [canSignSkipper, setCanSignSkipper] = useState(false)
|
const [canSignSkipper, setCanSignSkipper] = useState(false)
|
||||||
const [hasWriteCollaborators, setHasWriteCollaborators] = useState(false)
|
const [canSignCrew, setCanSignCrew] = useState(false)
|
||||||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||||
const [entryHash, setEntryHash] = useState('')
|
const [entryHash, setEntryHash] = useState('')
|
||||||
|
|
||||||
@@ -362,8 +362,11 @@ export default function LogEntryEditor({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLogbookAccess(logbookId).then((access) => {
|
getLogbookAccess(logbookId).then((access) => {
|
||||||
if (!access) return
|
if (!access) return
|
||||||
setCanSignSkipper(access.isOwner || access.role === 'WRITE')
|
setCanSignSkipper(access.isOwner)
|
||||||
setHasWriteCollaborators(access.writeCollaboratorCount > 0)
|
setCanSignCrew(
|
||||||
|
access.role === 'WRITE' ||
|
||||||
|
(access.isOwner && access.writeCollaboratorCount === 0)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}, [logbookId])
|
}, [logbookId])
|
||||||
|
|
||||||
@@ -429,6 +432,7 @@ export default function LogEntryEditor({
|
|||||||
const crewSignatureValid = !isPasskeySignature(signCrew) || isSignatureValidForEntry(signCrew, entryHash)
|
const crewSignatureValid = !isPasskeySignature(signCrew) || isSignatureValidForEntry(signCrew, entryHash)
|
||||||
|
|
||||||
const handlePasskeySignSkipper = async () => {
|
const handlePasskeySignSkipper = async () => {
|
||||||
|
if (!canSignSkipper) return
|
||||||
const confirmed = await confirmSignWarning()
|
const confirmed = await confirmSignWarning()
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
@@ -446,6 +450,7 @@ export default function LogEntryEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePasskeySignCrew = async () => {
|
const handlePasskeySignCrew = async () => {
|
||||||
|
if (!canSignCrew) return
|
||||||
const confirmed = await confirmSignWarning()
|
const confirmed = await confirmSignWarning()
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
@@ -1697,13 +1702,17 @@ export default function LogEntryEditor({
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
isOnline={isOnline}
|
isOnline={isOnline}
|
||||||
canSignSkipper={canSignSkipper}
|
canSignSkipper={canSignSkipper}
|
||||||
hasWriteCollaborators={hasWriteCollaborators}
|
canSignCrew={canSignCrew}
|
||||||
signSkipper={signSkipper}
|
signSkipper={signSkipper}
|
||||||
signCrew={signCrew}
|
signCrew={signCrew}
|
||||||
skipperSignatureValid={skipperSignatureValid}
|
skipperSignatureValid={skipperSignatureValid}
|
||||||
crewSignatureValid={crewSignatureValid}
|
crewSignatureValid={crewSignatureValid}
|
||||||
onSignSkipperChange={setSignSkipper}
|
onSignSkipperChange={(value) => {
|
||||||
onSignCrewChange={setSignCrew}
|
if (canSignSkipper && !readOnly) setSignSkipper(value)
|
||||||
|
}}
|
||||||
|
onSignCrewChange={(value) => {
|
||||||
|
if (canSignCrew && !readOnly) setSignCrew(value)
|
||||||
|
}}
|
||||||
onPasskeySignSkipper={handlePasskeySignSkipper}
|
onPasskeySignSkipper={handlePasskeySignSkipper}
|
||||||
onPasskeySignCrew={handlePasskeySignCrew}
|
onPasskeySignCrew={handlePasskeySignCrew}
|
||||||
onBeforeSign={confirmSignWarning}
|
onBeforeSign={confirmSignWarning}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface SignatureSectionProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isOnline: boolean
|
isOnline: boolean
|
||||||
canSignSkipper: boolean
|
canSignSkipper: boolean
|
||||||
hasWriteCollaborators: boolean
|
canSignCrew: boolean
|
||||||
signSkipper: SignatureValue | ''
|
signSkipper: SignatureValue | ''
|
||||||
signCrew: SignatureValue | ''
|
signCrew: SignatureValue | ''
|
||||||
skipperSignatureValid: boolean
|
skipperSignatureValid: boolean
|
||||||
@@ -189,7 +189,7 @@ export default function SignatureSection({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isOnline,
|
isOnline,
|
||||||
canSignSkipper,
|
canSignSkipper,
|
||||||
hasWriteCollaborators,
|
canSignCrew,
|
||||||
signSkipper,
|
signSkipper,
|
||||||
signCrew,
|
signCrew,
|
||||||
skipperSignatureValid,
|
skipperSignatureValid,
|
||||||
@@ -203,7 +203,7 @@ export default function SignatureSection({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const showSkipperPasskey = canSignSkipper && isOnline
|
const showSkipperPasskey = canSignSkipper && isOnline
|
||||||
const showCrewPasskey = hasWriteCollaborators && isOnline
|
const showCrewPasskey = canSignCrew && isOnline && !canSignSkipper
|
||||||
const hasSignature = !!(signSkipper || signCrew)
|
const hasSignature = !!(signSkipper || signCrew)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,7 +228,7 @@ export default function SignatureSection({
|
|||||||
passkeySignature={isPasskeySignature(signSkipper) ? signSkipper : undefined}
|
passkeySignature={isPasskeySignature(signSkipper) ? signSkipper : undefined}
|
||||||
signatureValid={skipperSignatureValid}
|
signatureValid={skipperSignatureValid}
|
||||||
showPasskey={showSkipperPasskey}
|
showPasskey={showSkipperPasskey}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly || !canSignSkipper}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
classicHint={showSkipperPasskey ? t('logs.sign_classic_or_passkey') : undefined}
|
classicHint={showSkipperPasskey ? t('logs.sign_classic_or_passkey') : undefined}
|
||||||
offlineHint={!isOnline && canSignSkipper ? t('logs.sign_offline_hint') : undefined}
|
offlineHint={!isOnline && canSignSkipper ? t('logs.sign_offline_hint') : undefined}
|
||||||
@@ -245,7 +245,7 @@ export default function SignatureSection({
|
|||||||
passkeySignature={isPasskeySignature(signCrew) ? signCrew : undefined}
|
passkeySignature={isPasskeySignature(signCrew) ? signCrew : undefined}
|
||||||
signatureValid={crewSignatureValid}
|
signatureValid={crewSignatureValid}
|
||||||
showPasskey={showCrewPasskey}
|
showPasskey={showCrewPasskey}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly || !canSignCrew}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
|
classicHint={showCrewPasskey ? t('logs.sign_crew_passkey_hint') : undefined}
|
||||||
onChange={onSignCrewChange}
|
onChange={onSignCrewChange}
|
||||||
|
|||||||
@@ -99,14 +99,7 @@ async function isAuthorizedSigner(
|
|||||||
role: 'skipper' | 'crew'
|
role: 'skipper' | 'crew'
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (role === 'skipper') {
|
if (role === 'skipper') {
|
||||||
// Skipper signing: owner or WRITE collaborator (design §2.1), using their own passkey.
|
return signerUserId === ownerUserId
|
||||||
if (signerUserId === ownerUserId) return true
|
|
||||||
const collaboration = await prisma.collaboration.findUnique({
|
|
||||||
where: {
|
|
||||||
logbookId_userId: { logbookId, userId: signerUserId }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return collaboration?.role === 'WRITE'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const collaboration = await prisma.collaboration.findUnique({
|
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' })
|
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(
|
const allowCredentials = await getAllowCredentialsForRole(
|
||||||
logbookId,
|
logbookId,
|
||||||
role,
|
role,
|
||||||
|
|||||||
Reference in New Issue
Block a user