Implement E2E-compliant anonymous read-only logbook sharing links

This commit is contained in:
2026-05-28 20:47:52 +02:00
parent b3978ed294
commit 20ff2a0baa
14 changed files with 1172 additions and 359 deletions
+52 -38
View File
@@ -12,42 +12,56 @@ function escapeCsvValue(val: string | number | undefined | null): string {
return str;
}
export async function exportLogbookToCsv(logbookId: string): Promise<string> {
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) {
throw new Error('Encryption key not found. User must log in.')
}
// 1. Fetch Yacht details
export async function exportLogbookToCsv(logbookId: string, preloadedData?: { yacht: any; entries: any[] }): Promise<string> {
let yachtName = '', homePort = '', owner = '', charter = '', registration = '', callsign = '', atis = '', mmsi = '';
const yachtRecord = await db.yachts.get(logbookId);
if (yachtRecord) {
try {
const yacht = await decryptJson(yachtRecord.encryptedData, yachtRecord.iv, yachtRecord.tag, masterKey);
yachtName = yacht.name || '';
homePort = yacht.port || '';
owner = yacht.owner || '';
charter = yacht.charter || '';
registration = yacht.registration || '';
callsign = yacht.callsign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
} catch (e) {
console.error('Failed to decrypt yacht details for CSV:', e);
}
}
let decryptedEntries: any[] = [];
// 2. Fetch logbook entries
const localEntries = await db.entries.where({ logbookId }).toArray();
const decryptedEntries = [];
for (const entry of localEntries) {
try {
const dec = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey);
if (dec) {
decryptedEntries.push(dec);
if (preloadedData) {
const yacht = preloadedData.yacht || {};
yachtName = yacht.name || '';
homePort = yacht.port || '';
owner = yacht.owner || '';
charter = yacht.charter || '';
registration = yacht.registration || '';
callsign = yacht.callsign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
decryptedEntries = [...preloadedData.entries];
} else {
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) {
throw new Error('Encryption key not found. User must log in.')
}
// 1. Fetch Yacht details
const yachtRecord = await db.yachts.get(logbookId);
if (yachtRecord) {
try {
const yacht = await decryptJson(yachtRecord.encryptedData, yachtRecord.iv, yachtRecord.tag, masterKey);
yachtName = yacht.name || '';
homePort = yacht.port || '';
owner = yacht.owner || '';
charter = yacht.charter || '';
registration = yacht.registration || '';
callsign = yacht.callsign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
} catch (e) {
console.error('Failed to decrypt yacht details for CSV:', e);
}
}
// 2. Fetch logbook entries
const localEntries = await db.entries.where({ logbookId }).toArray();
for (const entry of localEntries) {
try {
const dec = await decryptJson(entry.encryptedData, entry.iv, entry.tag, masterKey);
if (dec) {
decryptedEntries.push(dec);
}
} catch (e) {
console.error('Failed to decrypt entry for CSV:', e);
}
} catch (e) {
console.error('Failed to decrypt entry for CSV:', e);
}
}
@@ -127,8 +141,8 @@ export async function exportLogbookToCsv(logbookId: string): Promise<string> {
return rows.map(r => r.join(',')).join('\n');
}
export async function downloadCsv(logbookId: string, title: string): Promise<void> {
const csvContent = await exportLogbookToCsv(logbookId);
export async function downloadCsv(logbookId: string, title: string, preloadedData?: { yacht: any; entries: any[] }): Promise<void> {
const csvContent = await exportLogbookToCsv(logbookId, preloadedData);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -142,8 +156,8 @@ export async function downloadCsv(logbookId: string, title: string): Promise<voi
document.body.removeChild(link);
}
export async function shareCsv(logbookId: string, title: string): Promise<void> {
const csvContent = await exportLogbookToCsv(logbookId);
export async function shareCsv(logbookId: string, title: string, preloadedData?: { yacht: any; entries: any[] }): Promise<void> {
const csvContent = await exportLogbookToCsv(logbookId, preloadedData);
const filename = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_logbook.csv`;
const file = new File([csvContent], filename, { type: 'text/csv' });
@@ -158,7 +172,7 @@ export async function shareCsv(logbookId: string, title: string): Promise<void>
} catch (e: any) {
if (e.name !== 'AbortError') {
console.error('Sharing failed, falling back to download:', e);
await downloadCsv(logbookId, title);
await downloadCsv(logbookId, title, preloadedData);
}
}
} else {
+44 -30
View File
@@ -4,41 +4,55 @@ import { getActiveMasterKey } from './auth.js'
import { getLogbookKey } from './logbookKeys.js'
import { decryptJson } from './crypto.js'
export async function generateLogbookPagePdf(logbookId: string, entryId: string): Promise<jsPDF> {
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) {
throw new Error('Encryption key not found. Please log in.')
}
// 1. Fetch Yacht details
export async function generateLogbookPagePdf(logbookId: string, entryId: string, preloadedData?: { yacht: any; entry: any }): Promise<jsPDF> {
let yachtName = '', homePort = '', registration = '', callsign = '', atis = '', mmsi = '';
const yachtRecord = await db.yachts.get(logbookId);
if (yachtRecord) {
try {
const yacht = await decryptJson(yachtRecord.encryptedData, yachtRecord.iv, yachtRecord.tag, masterKey);
yachtName = yacht.name || '';
homePort = yacht.port || '';
// owner not needed in PDF layout
registration = yacht.registrationNumber || yacht.registration || '';
callsign = yacht.callSign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
} catch (e) {
console.error('Failed to decrypt yacht details for PDF:', e);
let entry: any = null;
if (preloadedData) {
const yacht = preloadedData.yacht || {};
yachtName = yacht.name || '';
homePort = yacht.port || '';
registration = yacht.registrationNumber || yacht.registration || '';
callsign = yacht.callSign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
entry = preloadedData.entry;
} else {
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) {
throw new Error('Encryption key not found. Please log in.')
}
// 1. Fetch Yacht details
const yachtRecord = await db.yachts.get(logbookId);
if (yachtRecord) {
try {
const yacht = await decryptJson(yachtRecord.encryptedData, yachtRecord.iv, yachtRecord.tag, masterKey);
yachtName = yacht.name || '';
homePort = yacht.port || '';
registration = yacht.registrationNumber || yacht.registration || '';
callsign = yacht.callSign || '';
atis = yacht.atis || '';
mmsi = yacht.mmsi || '';
} catch (e) {
console.error('Failed to decrypt yacht details for PDF:', e);
}
}
// 2. Fetch active Entry
const entryRecord = await db.entries.get(entryId);
if (!entryRecord) {
throw new Error('Entry not found');
}
entry = await decryptJson(entryRecord.encryptedData, entryRecord.iv, entryRecord.tag, masterKey);
}
// 2. Fetch active Entry
const entryRecord = await db.entries.get(entryId);
if (!entryRecord) {
throw new Error('Entry not found');
}
const entry = await decryptJson(entryRecord.encryptedData, entryRecord.iv, entryRecord.tag, masterKey);
if (!entry) {
throw new Error('Failed to decrypt entry');
throw new Error('Failed to load entry');
}
// Create PDF landscape A4
const doc = new jsPDF({
orientation: 'landscape',
@@ -217,8 +231,8 @@ export async function generateLogbookPagePdf(logbookId: string, entryId: string)
return doc;
}
export async function downloadLogbookPagePdf(logbookId: string, entryId: string, dateStr: string): Promise<void> {
const doc = await generateLogbookPagePdf(logbookId, entryId);
export async function downloadLogbookPagePdf(logbookId: string, entryId: string, dateStr: string, preloadedData?: { yacht: any; entry: any }): Promise<void> {
const doc = await generateLogbookPagePdf(logbookId, entryId, preloadedData);
const filename = `logbook_${dateStr.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.pdf`;
doc.save(filename);
}