import { Router } from 'express' import { prisma } from '../db.js' const router = Router() // Middleware to extract user ID from headers const requireUser = (req: any, res: any, next: any) => { const userId = req.headers['x-user-id'] if (!userId) { return res.status(401).json({ error: 'Unauthorized: X-User-Id header missing' }) } req.userId = userId next() } router.use(requireUser) // 1. Push local changes to the server router.post('/push', async (req: any, res) => { try { const { items } = req.body if (!items || !Array.isArray(items)) { return res.status(400).json({ error: 'items array is required' }) } const results = [] for (const item of items) { const { action, type, payloadId, logbookId, data, updatedAt } = item const itemUpdatedAt = new Date(updatedAt) try { // Authorize: Check if logbook belongs to user // Exception: If action is create logbook, the logbook might not exist yet, // so we authorize based on user creating a logbook with their userId. if (type === 'logbook' && action === 'create') { const existing = await prisma.logbook.findUnique({ where: { id: logbookId } }) if (existing && existing.userId !== req.userId) { results.push({ payloadId, status: 'error', error: 'Forbidden: Logbook belongs to another user' }) continue } const parsed = JSON.parse(data) await prisma.logbook.upsert({ where: { id: logbookId }, create: { id: logbookId, userId: req.userId, encryptedTitle: parsed.encryptedTitle, encryptedKey: parsed.encryptedKey || null, iv: parsed.iv || null, tag: parsed.tag || null, updatedAt: itemUpdatedAt }, update: { encryptedTitle: parsed.encryptedTitle, encryptedKey: parsed.encryptedKey || null, iv: parsed.iv || null, tag: parsed.tag || null, updatedAt: itemUpdatedAt } }) results.push({ payloadId, status: 'success' }) continue } // Standard Authorization: Logbook must exist and belong to user or collaborator const logbook = await prisma.logbook.findUnique({ where: { id: logbookId } }) if (!logbook) { results.push({ payloadId, status: 'error', error: 'Logbook not found' }) continue } const isOwner = logbook.userId === req.userId const isCollaborator = await prisma.collaboration.findUnique({ where: { logbookId_userId: { logbookId, userId: req.userId } } }) if (!isOwner && !isCollaborator) { results.push({ payloadId, status: 'error', error: 'Forbidden: Access denied' }) continue } if (type === 'logbook' && action === 'delete') { if (!isOwner) { results.push({ payloadId, status: 'error', error: 'Forbidden: Only owner can delete logbook' }) continue } await prisma.logbook.delete({ where: { id: logbookId } }) results.push({ payloadId, status: 'success' }) continue } if (action === 'delete') { if (type === 'yacht') { await prisma.yachtPayload.deleteMany({ where: { logbookId } }) } else if (type === 'deviation') { await prisma.deviationPayload.deleteMany({ where: { logbookId } }) } else if (type === 'crew') { await prisma.crewPayload.deleteMany({ where: { logbookId, payloadId } }) } else if (type === 'entry') { await prisma.entryPayload.deleteMany({ where: { logbookId, payloadId } }) } else if (type === 'photo') { await prisma.photoPayload.deleteMany({ where: { logbookId, payloadId } }) } else if (type === 'gpsTrack') { await prisma.gpsTrackPayload.deleteMany({ where: { logbookId, entryId: payloadId } }) } else { results.push({ payloadId, status: 'error', error: `Unsupported delete type: ${type}` }) continue } results.push({ payloadId, status: 'success' }) continue } // Parse payload for create/update operations const parsed = JSON.parse(data) const encryptedData = parsed.encryptedData || parsed.ciphertext const { iv, tag } = parsed if (type === 'yacht') { { const existing = await prisma.yachtPayload.findUnique({ where: { logbookId } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } await prisma.yachtPayload.upsert({ where: { logbookId }, create: { logbookId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } else if (type === 'deviation') { { const existing = await prisma.deviationPayload.findUnique({ where: { logbookId } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } await prisma.deviationPayload.upsert({ where: { logbookId }, create: { logbookId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } else if (type === 'crew') { { const existing = await prisma.crewPayload.findUnique({ where: { logbookId_payloadId: { logbookId, payloadId } } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } await prisma.crewPayload.upsert({ where: { logbookId_payloadId: { logbookId, payloadId } }, create: { logbookId, payloadId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } else if (type === 'entry') { { const existing = await prisma.entryPayload.findUnique({ where: { logbookId_payloadId: { logbookId, payloadId } } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } await prisma.entryPayload.upsert({ where: { logbookId_payloadId: { logbookId, payloadId } }, create: { logbookId, payloadId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } else if (type === 'photo') { { const existing = await prisma.photoPayload.findUnique({ where: { logbookId_payloadId: { logbookId, payloadId } } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } const entryId = parsed.entryId || '' await prisma.photoPayload.upsert({ where: { logbookId_payloadId: { logbookId, payloadId } }, create: { logbookId, payloadId, entryId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } else if (type === 'gpsTrack') { { const existing = await prisma.gpsTrackPayload.findUnique({ where: { entryId: payloadId } }) if (existing && new Date(existing.updatedAt) > itemUpdatedAt) { results.push({ payloadId, status: 'conflict', reason: 'Server version is newer' }) continue } await prisma.gpsTrackPayload.upsert({ where: { entryId: payloadId }, create: { logbookId, entryId: payloadId, encryptedData, iv, tag, updatedAt: itemUpdatedAt }, update: { encryptedData, iv, tag, updatedAt: itemUpdatedAt } }) } } results.push({ payloadId, status: 'success' }) } catch (err: any) { console.error(`Error processing sync item ${payloadId}:`, err) results.push({ payloadId, status: 'error', error: err.message || 'Operation failed' }) } } return res.json({ results }) } catch (error: any) { console.error('Error during sync push:', error) return res.status(500).json({ error: error.message || 'Internal server error' }) } }) // 2. Pull server updates to the client router.get('/pull', async (req: any, res) => { try { const { logbookId } = req.query if (!logbookId) { return res.status(400).json({ error: 'logbookId is required' }) } // Authorize: Check if logbook belongs to user or is collaborator const logbook = await prisma.logbook.findUnique({ where: { id: logbookId } }) if (!logbook) { return res.status(404).json({ error: 'Logbook not found' }) } const isOwner = logbook.userId === req.userId const isCollaborator = await prisma.collaboration.findUnique({ where: { logbookId_userId: { logbookId, userId: req.userId } } }) if (!isOwner && !isCollaborator) { return res.status(403).json({ error: 'Forbidden: Access denied' }) } const yacht = await prisma.yachtPayload.findUnique({ where: { logbookId } }) const deviation = await prisma.deviationPayload.findUnique({ where: { logbookId } }) const crews = await prisma.crewPayload.findMany({ where: { logbookId } }) const entries = await prisma.entryPayload.findMany({ where: { logbookId } }) const photos = await prisma.photoPayload.findMany({ where: { logbookId } }) const gpsTracks = await prisma.gpsTrackPayload.findMany({ where: { logbookId } }) return res.json({ yacht, deviation, crews, entries, photos, gpsTracks }) } catch (error: any) { console.error('Error during sync pull:', error) return res.status(500).json({ error: error.message || 'Internal server error' }) } }) export default router