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 masterKey = getActiveMasterKey()
|
||||||
const activeUserId = localStorage.getItem('active_userid')
|
const activeUserId = localStorage.getItem('active_userid')
|
||||||
if (!masterKey || !activeUserId) {
|
if (!masterKey || !activeUserId) {
|
||||||
|
autoAcceptStarted.current = false
|
||||||
setError(isDe
|
setError(isDe
|
||||||
? 'Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).'
|
? 'Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).'
|
||||||
: 'Incomplete session — please log in again (user ID missing).')
|
: 'Incomplete session — please log in again (user ID missing).')
|
||||||
@@ -184,7 +185,8 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
|||||||
id: logbookId,
|
id: logbookId,
|
||||||
encryptedTitle: rawEncryptedTitle,
|
encryptedTitle: rawEncryptedTitle,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
isSynced: 1
|
isSynced: 1,
|
||||||
|
isShared: 1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +204,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading || accepting || autoAcceptStarted.current) return
|
if (loading || accepting || autoAcceptStarted.current) return
|
||||||
if (!isLoggedIn || !logbookId || !logbookKey || !token) return
|
if (!isLoggedIn || !logbookId || !logbookKey || !token) return
|
||||||
if (!sessionReady()) return
|
if (!sessionReady()) {
|
||||||
|
autoAcceptStarted.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
autoAcceptStarted.current = true
|
autoAcceptStarted.current = true
|
||||||
void handleAccept()
|
void handleAccept()
|
||||||
@@ -240,10 +245,17 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!recoveryInput.trim() || !encryptedPayloads) return
|
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)
|
setLoading(true)
|
||||||
setAuthError(null)
|
setAuthError(null)
|
||||||
try {
|
try {
|
||||||
const resolvedUser = username.trim() || encryptedPayloads.username
|
|
||||||
const success = await completeLoginWithRecovery(resolvedUser, recoveryInput.trim(), encryptedPayloads)
|
const success = await completeLoginWithRecovery(resolvedUser, recoveryInput.trim(), encryptedPayloads)
|
||||||
if (success) {
|
if (success) {
|
||||||
setShowRecoveryFallback(false)
|
setShowRecoveryFallback(false)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface LocalLogbook {
|
|||||||
encryptedTitle: string
|
encryptedTitle: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
isSynced: number // 1 = yes, 0 = pending local modifications
|
isSynced: number // 1 = yes, 0 = pending local modifications
|
||||||
|
isShared?: number // 1 = collaborator copy, 0 or unset = owned
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalYacht {
|
export interface LocalYacht {
|
||||||
@@ -120,6 +121,17 @@ class DaagboxDatabase extends Dexie {
|
|||||||
gpsTracks: 'entryId, logbookId, updatedAt',
|
gpsTracks: 'entryId, logbookId, updatedAt',
|
||||||
logbookKeys: 'logbookId'
|
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.')
|
throw new Error('Master key not found. User must log in.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedLogbookIds = new Set<string>()
|
|
||||||
|
|
||||||
if (navigator.onLine) {
|
if (navigator.onLine) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_BASE, {
|
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
|
// Decrypt and save logbook keys locally if they exist
|
||||||
for (const lb of serverLogbooks) {
|
for (const lb of serverLogbooks) {
|
||||||
const isShared = lb.userId !== userId
|
const isShared = lb.userId !== userId
|
||||||
if (isShared) sharedLogbookIds.add(lb.id)
|
|
||||||
|
|
||||||
const encryptedKeyStr = isShared
|
const encryptedKeyStr = isShared
|
||||||
? lb.collaborators?.[0]?.encryptedLogbookKey
|
? lb.collaborators?.[0]?.encryptedLogbookKey
|
||||||
@@ -105,7 +102,8 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
|||||||
id: lb.id,
|
id: lb.id,
|
||||||
encryptedTitle: lb.encryptedTitle,
|
encryptedTitle: lb.encryptedTitle,
|
||||||
updatedAt: lb.updatedAt || new Date().toISOString(),
|
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
|
// Clear existing cache for this user and insert new ones
|
||||||
@@ -128,7 +126,7 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
|
|||||||
title,
|
title,
|
||||||
updatedAt: lb.updatedAt,
|
updatedAt: lb.updatedAt,
|
||||||
isSynced: lb.isSynced === 1,
|
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,
|
id: serverLb.id,
|
||||||
encryptedTitle: serverLb.encryptedTitle,
|
encryptedTitle: serverLb.encryptedTitle,
|
||||||
updatedAt: serverLb.updatedAt,
|
updatedAt: serverLb.updatedAt,
|
||||||
isSynced: 1
|
isSynced: 1,
|
||||||
|
isShared: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -216,7 +215,8 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
|
|||||||
id: localId,
|
id: localId,
|
||||||
encryptedTitle: encryptedTitleStr,
|
encryptedTitle: encryptedTitleStr,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
isSynced: 0
|
isSynced: 0,
|
||||||
|
isShared: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
await db.syncQueue.put({
|
await db.syncQueue.put({
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ async function getLogbookWithAccess(logbookId: string, userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasWriteAccess(access: { isOwner: boolean; collaboration?: { role: string } | null }) {
|
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'
|
return access.isOwner || access.collaboration?.role === 'WRITE'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +107,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.
|
||||||
if (signerUserId === ownerUserId) return true
|
if (signerUserId === ownerUserId) return true
|
||||||
const collaboration = await prisma.collaboration.findUnique({
|
const collaboration = await prisma.collaboration.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user