Fix shared logbook access for crew after AI summary sync.

Correct owner detection while the logbook loads, preserve AI summaries on
live-log saves, skip corrupt entry decrypts, and never regenerate keys for
shared logbooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 11:48:45 +02:00
parent d637fbea16
commit caf85ad9eb
5 changed files with 32 additions and 6 deletions
+3 -2
View File
@@ -103,7 +103,7 @@ function App() {
[activeLogbookId] [activeLogbookId]
) )
const [activeAccessRole, setActiveAccessRole] = useState<LogbookAccessRole | null>('OWNER') const [activeAccessRole, setActiveAccessRole] = useState<LogbookAccessRole | null>(null)
useEffect(() => { useEffect(() => {
if (!activeLogbookId) { if (!activeLogbookId) {
@@ -574,7 +574,8 @@ function App() {
const logbookReadOnly = const logbookReadOnly =
activeLogbookRecord?.isShared === 1 && activeAccessRole === 'READ' activeLogbookRecord?.isShared === 1 && activeAccessRole === 'READ'
const isLogbookOwner = const isLogbookOwner =
activeAccessRole === 'OWNER' || activeLogbookRecord?.isShared !== 1 activeAccessRole === 'OWNER' ||
(activeLogbookRecord != null && activeLogbookRecord.isShared !== 1)
if (showUserProfile) { if (showUserProfile) {
return ( return (
+3 -3
View File
@@ -9,7 +9,7 @@ import { downloadCsv, shareCsv } from '../services/csvExport.js'
import { downloadLogbookPagePdf } from '../services/pdfExport.js' import { downloadLogbookPagePdf } from '../services/pdfExport.js'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
import { getErrorMessage } from '../utils/errors.js' import { getErrorMessage } from '../utils/errors.js'
import { findTodayEntryId } from '../services/quickEventLog.js' import { findTodayEntryId, tryDecryptEntryPayload } from '../services/quickEventLog.js'
import LogEntryEditor from './LogEntryEditor.tsx' import LogEntryEditor from './LogEntryEditor.tsx'
import LiveLogView from './LiveLogView.tsx' import LiveLogView from './LiveLogView.tsx'
import EntrySkipperSignBadge from './EntrySkipperSignBadge.tsx' import EntrySkipperSignBadge from './EntrySkipperSignBadge.tsx'
@@ -136,7 +136,7 @@ export default function LogEntriesList({
} }
await forEachInBatches(needsDecrypt, 8, async (entry) => { await forEachInBatches(needsDecrypt, 8, async (entry) => {
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey) const decrypted = await tryDecryptEntryPayload(entry, masterKey)
if (!decrypted) return if (!decrypted) return
const listCache = await buildEntryListCache(decrypted as Record<string, unknown>) const listCache = await buildEntryListCache(decrypted as Record<string, unknown>)
@@ -266,7 +266,7 @@ export default function LogEntriesList({
const decryptedEntries: Array<LogEntryTankSource & TravelDaySortable> = [] const decryptedEntries: Array<LogEntryTankSource & TravelDaySortable> = []
for (const entry of localEntries) { for (const entry of localEntries) {
const decrypted = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey) const decrypted = await tryDecryptEntryPayload(entry, masterKey)
if (decrypted) decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable) if (decrypted) decryptedEntries.push(decrypted as LogEntryTankSource & TravelDaySortable)
} }
+2
View File
@@ -527,6 +527,8 @@ export default function LogEntryEditor({
}, []) }, [])
useEffect(() => { useEffect(() => {
setCanSignSkipper(false)
setCanSignCrew(false)
getLogbookAccess(logbookId).then((access) => { getLogbookAccess(logbookId).then((access) => {
if (!access) return if (!access) return
setCanSignSkipper(access.isOwner) setCanSignSkipper(access.isOwner)
+12
View File
@@ -91,6 +91,7 @@ export function clearLogbookKeysCache() {
export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer> { export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer> {
const localLb = await db.logbooks.get(logbookId) const localLb = await db.logbooks.get(logbookId)
const encryptedTitle = localLb ? localLb.encryptedTitle : '' const encryptedTitle = localLb ? localLb.encryptedTitle : ''
const isShared = localLb?.isShared === 1
const masterKey = getActiveMasterKey() const masterKey = getActiveMasterKey()
let key = await getLogbookKey(logbookId) let key = await getLogbookKey(logbookId)
@@ -103,6 +104,11 @@ export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer>
// Key works, return it // Key works, return it
return key return key
} catch (err) { } catch (err) {
if (isShared) {
throw new Error(
'Shared logbook encryption key is missing or invalid. Please go online and refresh your logbooks.'
)
}
console.warn('Stored logbook key failed to decrypt title. Testing if master key works (legacy migration)...') console.warn('Stored logbook key failed to decrypt title. Testing if master key works (legacy migration)...')
try { try {
const parsed = JSON.parse(encryptedTitle) const parsed = JSON.parse(encryptedTitle)
@@ -145,6 +151,12 @@ export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer>
// If no logbook key exists yet // If no logbook key exists yet
if (!key) { if (!key) {
if (isShared) {
throw new Error(
'Shared logbook encryption key not found. Please go online and refresh your logbooks.'
)
}
if (encryptedTitle && masterKey) { if (encryptedTitle && masterKey) {
try { try {
// Check if title is already decryptable using masterKey (meaning it is a legacy logbook) // Check if title is already decryptable using masterKey (meaning it is a legacy logbook)
+12 -1
View File
@@ -124,11 +124,22 @@ function buildEncryptedPayload(
}) })
const clear = options.clearSignatures const clear = options.clearSignatures
return { const entryData: Record<string, unknown> = {
...payload, ...payload,
signSkipper: clear ? '' : (data.signSkipper ?? ''), signSkipper: clear ? '' : (data.signSkipper ?? ''),
signCrew: clear ? '' : (data.signCrew ?? '') signCrew: clear ? '' : (data.signCrew ?? '')
} }
const summary = typeof data.aiSummary === 'string' ? data.aiSummary.trim() : ''
if (summary) {
entryData.aiSummary = summary
entryData.aiSummaryGeneratedAt =
typeof data.aiSummaryGeneratedAt === 'string' && data.aiSummaryGeneratedAt
? data.aiSummaryGeneratedAt
: new Date().toISOString()
}
return entryData
} }
export async function loadEntry(logbookId: string, entryId: string): Promise<LoadedEntry | null> { export async function loadEntry(logbookId: string, entryId: string): Promise<LoadedEntry | null> {