fix: Skipper-Signatur für WRITE-Collaborators und Events-Hash

WRITE-Collaborators dürfen Skipper-Freigaben leisten; der Eintrags-Hash sortiert events nach time, damit Umordnungen die Passkey-Signatur invalidieren.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 16:57:00 +02:00
parent 241b2fdf63
commit 1710007efe
4 changed files with 53 additions and 21 deletions
+21 -11
View File
@@ -60,14 +60,18 @@ async function getLogbookWithAccess(logbookId: string, userId: string) {
return { logbook, isOwner, collaboration }
}
function hasWriteAccess(access: { isOwner: boolean; collaboration?: { role: string } | null }) {
return access.isOwner || access.collaboration?.role === 'WRITE'
}
async function getAllowCredentialsForRole(
logbookId: string,
ownerUserId: string,
role: 'skipper' | 'crew'
role: 'skipper' | 'crew',
requestingUserId: string
) {
if (role === 'skipper') {
const credentials = await prisma.credential.findMany({
where: { userId: ownerUserId }
where: { userId: requestingUserId }
})
return credentials.map((cred) => ({
id: Buffer.from(cred.credentialId, 'base64url'),
@@ -102,7 +106,13 @@ async function isAuthorizedSigner(
role: 'skipper' | 'crew'
): Promise<boolean> {
if (role === 'skipper') {
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({
@@ -130,21 +140,21 @@ router.post('/options', async (req: any, res) => {
return res.status(403).json({ error: 'Forbidden: Access denied' })
}
if (role === 'skipper' && !access.isOwner) {
return res.status(403).json({ error: 'Forbidden: Only the logbook owner can sign as skipper' })
if (!hasWriteAccess(access)) {
return res.status(403).json({ error: 'Forbidden: WRITE access required to sign entries' })
}
const allowCredentials = await getAllowCredentialsForRole(
logbookId,
access.logbook.userId,
role
role,
req.userId
)
if (allowCredentials.length === 0) {
return res.status(400).json({
error: role === 'crew'
? 'No write collaborators with passkeys found'
: 'No passkey credentials found for owner'
: 'No passkey credentials found for signer'
})
}
@@ -209,8 +219,8 @@ router.post('/verify', async (req: any, res) => {
return res.status(403).json({ error: 'Forbidden: Access denied' })
}
if (role === 'skipper' && !access.isOwner) {
return res.status(403).json({ error: 'Forbidden: Only the logbook owner can sign as skipper' })
if (!hasWriteAccess(access)) {
return res.status(403).json({ error: 'Forbidden: WRITE access required to sign entries' })
}
const dbCred = await prisma.credential.findUnique({