fix: Einladungs-Auto-Accept, isShared-Cache und Recovery-Validierung
Auto-Accept kann nach Session-Verlust erneut starten, isShared wird offline in Dexie persistiert, und leere Recovery-Benutzernamen werden abgefangen. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -137,6 +137,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
const masterKey = getActiveMasterKey()
|
||||
const activeUserId = localStorage.getItem('active_userid')
|
||||
if (!masterKey || !activeUserId) {
|
||||
autoAcceptStarted.current = false
|
||||
setError(isDe
|
||||
? 'Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).'
|
||||
: 'Incomplete session — please log in again (user ID missing).')
|
||||
@@ -184,7 +185,8 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
id: logbookId,
|
||||
encryptedTitle: rawEncryptedTitle,
|
||||
updatedAt: new Date().toISOString(),
|
||||
isSynced: 1
|
||||
isSynced: 1,
|
||||
isShared: 1
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,7 +204,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
useEffect(() => {
|
||||
if (loading || accepting || autoAcceptStarted.current) return
|
||||
if (!isLoggedIn || !logbookId || !logbookKey || !token) return
|
||||
if (!sessionReady()) return
|
||||
if (!sessionReady()) {
|
||||
autoAcceptStarted.current = false
|
||||
return
|
||||
}
|
||||
|
||||
autoAcceptStarted.current = true
|
||||
void handleAccept()
|
||||
@@ -240,10 +245,17 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
e.preventDefault()
|
||||
if (!recoveryInput.trim() || !encryptedPayloads) return
|
||||
|
||||
const resolvedUser = (username.trim() || encryptedPayloads.username || '').trim()
|
||||
if (!resolvedUser) {
|
||||
setAuthError(isDe
|
||||
? 'Benutzername konnte nicht ermittelt werden — bitte erneut anmelden.'
|
||||
: 'Could not determine username — please try logging in again.')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setAuthError(null)
|
||||
try {
|
||||
const resolvedUser = username.trim() || encryptedPayloads.username
|
||||
const success = await completeLoginWithRecovery(resolvedUser, recoveryInput.trim(), encryptedPayloads)
|
||||
if (success) {
|
||||
setShowRecoveryFallback(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface LocalLogbook {
|
||||
encryptedTitle: string
|
||||
updatedAt: string
|
||||
isSynced: number // 1 = yes, 0 = pending local modifications
|
||||
isShared?: number // 1 = collaborator copy, 0 or unset = owned
|
||||
}
|
||||
|
||||
export interface LocalYacht {
|
||||
@@ -120,6 +121,17 @@ class DaagboxDatabase extends Dexie {
|
||||
gpsTracks: 'entryId, logbookId, updatedAt',
|
||||
logbookKeys: 'logbookId'
|
||||
})
|
||||
this.version(4).stores({
|
||||
logbooks: 'id, encryptedTitle, updatedAt, isSynced, isShared',
|
||||
yachts: 'logbookId, updatedAt',
|
||||
crews: 'payloadId, logbookId, updatedAt',
|
||||
deviations: 'logbookId, updatedAt',
|
||||
entries: 'payloadId, logbookId, updatedAt',
|
||||
syncQueue: '++id, action, type, payloadId, logbookId',
|
||||
photos: 'payloadId, entryId, logbookId, updatedAt',
|
||||
gpsTracks: 'entryId, logbookId, updatedAt',
|
||||
logbookKeys: 'logbookId'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
||||
throw new Error('Master key not found. User must log in.')
|
||||
}
|
||||
|
||||
const sharedLogbookIds = new Set<string>()
|
||||
|
||||
if (navigator.onLine) {
|
||||
try {
|
||||
const response = await fetch(API_BASE, {
|
||||
@@ -61,7 +59,6 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
||||
// Decrypt and save logbook keys locally if they exist
|
||||
for (const lb of serverLogbooks) {
|
||||
const isShared = lb.userId !== userId
|
||||
if (isShared) sharedLogbookIds.add(lb.id)
|
||||
|
||||
const encryptedKeyStr = isShared
|
||||
? lb.collaborators?.[0]?.encryptedLogbookKey
|
||||
@@ -105,7 +102,8 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
||||
id: lb.id,
|
||||
encryptedTitle: lb.encryptedTitle,
|
||||
updatedAt: lb.updatedAt || new Date().toISOString(),
|
||||
isSynced: 1
|
||||
isSynced: 1,
|
||||
isShared: lb.userId !== userId ? 1 : 0
|
||||
}))
|
||||
|
||||
// Clear existing cache for this user and insert new ones
|
||||
@@ -128,7 +126,7 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
||||
title,
|
||||
updatedAt: lb.updatedAt,
|
||||
isSynced: lb.isSynced === 1,
|
||||
isShared: sharedLogbookIds.has(lb.id)
|
||||
isShared: lb.isShared === 1
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,7 +193,8 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
|
||||
id: serverLb.id,
|
||||
encryptedTitle: serverLb.encryptedTitle,
|
||||
updatedAt: serverLb.updatedAt,
|
||||
isSynced: 1
|
||||
isSynced: 1,
|
||||
isShared: 0
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -216,7 +215,8 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
|
||||
id: localId,
|
||||
encryptedTitle: encryptedTitleStr,
|
||||
updatedAt: now,
|
||||
isSynced: 0
|
||||
isSynced: 0,
|
||||
isShared: 0
|
||||
})
|
||||
|
||||
await db.syncQueue.put({
|
||||
|
||||
@@ -61,6 +61,7 @@ async function getLogbookWithAccess(logbookId: string, userId: string) {
|
||||
}
|
||||
|
||||
function hasWriteAccess(access: { isOwner: boolean; collaboration?: { role: string } | null }) {
|
||||
// Intentional (HYBRID-ELECTRONIC-SIGNATURES.md §2.1): owner OR WRITE collaborator may sign entries.
|
||||
return access.isOwner || access.collaboration?.role === 'WRITE'
|
||||
}
|
||||
|
||||
@@ -106,6 +107,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: {
|
||||
|
||||
Reference in New Issue
Block a user