diff --git a/client/src/components/InvitationAcceptance.tsx b/client/src/components/InvitationAcceptance.tsx index e28a3f5..e53d882 100644 --- a/client/src/components/InvitationAcceptance.tsx +++ b/client/src/components/InvitationAcceptance.tsx @@ -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) diff --git a/client/src/services/db.ts b/client/src/services/db.ts index 212a4c5..81379a5 100644 --- a/client/src/services/db.ts +++ b/client/src/services/db.ts @@ -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' + }) } } diff --git a/client/src/services/logbook.ts b/client/src/services/logbook.ts index f40fa7a..85dc456 100644 --- a/client/src/services/logbook.ts +++ b/client/src/services/logbook.ts @@ -43,8 +43,6 @@ export async function fetchLogbooks(): Promise { throw new Error('Master key not found. User must log in.') } - const sharedLogbookIds = new Set() - if (navigator.onLine) { try { const response = await fetch(API_BASE, { @@ -61,7 +59,6 @@ export async function fetchLogbooks(): Promise { // 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 { 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 { 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 { 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 { id: localId, encryptedTitle: encryptedTitleStr, updatedAt: now, - isSynced: 0 + isSynced: 0, + isShared: 0 }) await db.syncQueue.put({ diff --git a/server/src/routes/sign.ts b/server/src/routes/sign.ts index 18f49a8..ea89d1e 100644 --- a/server/src/routes/sign.ts +++ b/server/src/routes/sign.ts @@ -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 { 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: {