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:
+3
-2
@@ -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 (
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user