diff --git a/Dockerfile b/Dockerfile index 1a51686..f79e048 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,22 @@ FROM node:20-alpine AS base # Dependencies stage FROM base AS deps WORKDIR /app -COPY package.json package-lock.json* ./ -COPY prisma ./prisma/ +COPY rtrips-online/package.json rtrips-online/package-lock.json* ./ +COPY rtrips-online/prisma ./prisma/ +# Copy local SDK dependency (package.json references file:../encryptid-sdk) +COPY encryptid-sdk /encryptid-sdk/ RUN npm ci || npm install +# Ensure SDK is properly linked in node_modules +RUN rm -rf node_modules/@encryptid/sdk && \ + mkdir -p node_modules/@encryptid && \ + cp -r /encryptid-sdk node_modules/@encryptid/sdk + # Build stage FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules -COPY . . +COPY rtrips-online/ . RUN npx prisma generate RUN npm run build diff --git a/docker-compose.yml b/docker-compose.yml index 931aa39..0a95261 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: rtrips: - build: . + build: + context: .. + dockerfile: rtrips-online/Dockerfile container_name: rtrips-online restart: unless-stopped environment: diff --git a/src/app/api/trips/[id]/bookings/route.ts b/src/app/api/trips/[id]/bookings/route.ts index ab39486..457fd89 100644 --- a/src/app/api/trips/[id]/bookings/route.ts +++ b/src/app/api/trips/[id]/bookings/route.ts @@ -1,11 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const booking = await prisma.booking.create({ data: { diff --git a/src/app/api/trips/[id]/canvas/route.ts b/src/app/api/trips/[id]/canvas/route.ts index 3bf0d32..7f88ee4 100644 --- a/src/app/api/trips/[id]/canvas/route.ts +++ b/src/app/api/trips/[id]/canvas/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { pushShapesToCanvas } from '@/lib/canvas-sync'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; /** * POST /api/trips/[id]/canvas @@ -10,9 +11,14 @@ import { pushShapesToCanvas } from '@/lib/canvas-sync'; * Stores the canvasSlug back on the Trip record. */ export async function POST( - _request: NextRequest, + request: NextRequest, { params }: { params: { id: string } } ) { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; try { const trip = await prisma.trip.findUnique({ where: { id: params.id }, diff --git a/src/app/api/trips/[id]/destinations/route.ts b/src/app/api/trips/[id]/destinations/route.ts index 15d86df..5029ac6 100644 --- a/src/app/api/trips/[id]/destinations/route.ts +++ b/src/app/api/trips/[id]/destinations/route.ts @@ -1,11 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const destination = await prisma.destination.create({ data: { diff --git a/src/app/api/trips/[id]/expenses/route.ts b/src/app/api/trips/[id]/expenses/route.ts index 6792b53..399670f 100644 --- a/src/app/api/trips/[id]/expenses/route.ts +++ b/src/app/api/trips/[id]/expenses/route.ts @@ -1,16 +1,23 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const expense = await prisma.expense.create({ data: { tripId: params.id, - paidById: body.paidById, + paidById: auth.user.id, description: body.description, amount: body.amount, currency: body.currency || 'USD', diff --git a/src/app/api/trips/[id]/itinerary/route.ts b/src/app/api/trips/[id]/itinerary/route.ts index 47559a9..d31d1b5 100644 --- a/src/app/api/trips/[id]/itinerary/route.ts +++ b/src/app/api/trips/[id]/itinerary/route.ts @@ -1,11 +1,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const item = await prisma.itineraryItem.create({ data: { diff --git a/src/app/api/trips/[id]/packing/[itemId]/route.ts b/src/app/api/trips/[id]/packing/[itemId]/route.ts index 271a279..12adeb3 100644 --- a/src/app/api/trips/[id]/packing/[itemId]/route.ts +++ b/src/app/api/trips/[id]/packing/[itemId]/route.ts @@ -1,13 +1,20 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function PATCH( - _request: NextRequest, + request: NextRequest, { params }: { params: { id: string; itemId: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + const { id, itemId } = await params; + const role = await requireTripRole(auth.user.id, id, 'MEMBER'); + if (role instanceof NextResponse) return role; + // Verify item belongs to trip const item = await prisma.packingItem.findFirst({ where: { id: itemId, tripId: id }, diff --git a/src/app/api/trips/[id]/packing/route.ts b/src/app/api/trips/[id]/packing/route.ts index 3a04cf5..2edbf4f 100644 --- a/src/app/api/trips/[id]/packing/route.ts +++ b/src/app/api/trips/[id]/packing/route.ts @@ -1,16 +1,23 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const item = await prisma.packingItem.create({ data: { tripId: params.id, - addedById: body.addedById, + addedById: auth.user.id, name: body.name, category: body.category, quantity: body.quantity ?? 1, diff --git a/src/app/api/trips/[id]/route.ts b/src/app/api/trips/[id]/route.ts index b546204..5620486 100644 --- a/src/app/api/trips/[id]/route.ts +++ b/src/app/api/trips/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; export async function GET( _request: NextRequest, @@ -37,6 +38,12 @@ export async function PUT( { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'EDITOR'); + if (role instanceof NextResponse) return role; + const body = await request.json(); const trip = await prisma.trip.update({ where: { id: params.id }, @@ -62,10 +69,16 @@ export async function PUT( } export async function DELETE( - _request: NextRequest, + request: NextRequest, { params }: { params: { id: string } } ) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'OWNER'); + if (role instanceof NextResponse) return role; + await prisma.trip.delete({ where: { id: params.id } }); return NextResponse.json({ ok: true }); } catch (error) { diff --git a/src/app/api/trips/[id]/sync/route.ts b/src/app/api/trips/[id]/sync/route.ts index 4c5826d..198df3c 100644 --- a/src/app/api/trips/[id]/sync/route.ts +++ b/src/app/api/trips/[id]/sync/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth'; /** * POST /api/trips/[id]/sync @@ -13,6 +14,11 @@ export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + + const role = await requireTripRole(auth.user.id, params.id, 'MEMBER'); + if (role instanceof NextResponse) return role; try { const body = await request.json(); const { shapeId, type, data } = body; diff --git a/src/app/api/trips/parse/route.ts b/src/app/api/trips/parse/route.ts index 2366528..02dc7a7 100644 --- a/src/app/api/trips/parse/route.ts +++ b/src/app/api/trips/parse/route.ts @@ -1,8 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { parseTrip } from '@/lib/gemini'; +import { requireAuth, isAuthed } from '@/lib/auth'; export async function POST(request: NextRequest) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + const { text } = await request.json(); if (!text || typeof text !== 'string' || text.trim().length === 0) { diff --git a/src/app/api/trips/route.ts b/src/app/api/trips/route.ts index 7e2c7ae..5ba065f 100644 --- a/src/app/api/trips/route.ts +++ b/src/app/api/trips/route.ts @@ -2,9 +2,36 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { generateSlug } from '@/lib/slug'; import { ParsedTrip } from '@/lib/types'; +import { getAuthUser, requireAuth, isAuthed } from '@/lib/auth'; -export async function GET() { +export async function GET(request: NextRequest) { try { + const auth = await getAuthUser(request); + + if (auth) { + // Authenticated: return trips the user collaborates on + const trips = await prisma.trip.findMany({ + where: { + collaborators: { some: { userId: auth.user.id } }, + }, + include: { + destinations: { orderBy: { sortOrder: 'asc' } }, + collaborators: { include: { user: true } }, + _count: { + select: { + itineraryItems: true, + bookings: true, + expenses: true, + packingItems: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + return NextResponse.json(trips); + } + + // Unauthenticated: return all trips (demo access) const trips = await prisma.trip.findMany({ include: { destinations: { orderBy: { sortOrder: 'asc' } }, @@ -33,6 +60,9 @@ export async function GET() { export async function POST(request: NextRequest) { try { + const auth = await requireAuth(request); + if (!isAuthed(auth)) return auth; + const body = await request.json(); const { parsed, rawInput }: { parsed: ParsedTrip; rawInput: string } = body; @@ -66,6 +96,15 @@ export async function POST(request: NextRequest) { }, }); + // Add creator as OWNER collaborator + await tx.tripCollaborator.create({ + data: { + tripId: newTrip.id, + userId: auth.user.id, + role: 'OWNER', + }, + }); + // Create destinations if (parsed.destinations.length > 0) { await tx.destination.createMany({ @@ -118,6 +157,7 @@ export async function POST(request: NextRequest) { destinations: { orderBy: { sortOrder: 'asc' } }, itineraryItems: { orderBy: { sortOrder: 'asc' } }, bookings: true, + collaborators: { include: { user: true } }, }, }); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1bc6a49..a02a957 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' +import { AuthProvider } from '@/components/AuthProvider' const inter = Inter({ subsets: ['latin'], @@ -26,7 +27,9 @@ export default function RootLayout({ return (
- {children} +