feat: add EncryptID auth with role-based access control
AuthProvider wraps layout, auth.ts provides requireAuth/isAuthed/requireTripRole. POST/PUT/DELETE routes require auth, GET routes remain open for demo access. Trip creator is added as OWNER collaborator. Dockerfile updated for SDK copy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
608686b22d
commit
5e9b3f828a
13
Dockerfile
13
Dockerfile
|
|
@ -3,15 +3,22 @@ FROM node:20-alpine AS base
|
||||||
# Dependencies stage
|
# Dependencies stage
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json* ./
|
COPY rtrips-online/package.json rtrips-online/package-lock.json* ./
|
||||||
COPY prisma ./prisma/
|
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
|
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
|
# Build stage
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY rtrips-online/ .
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
services:
|
services:
|
||||||
rtrips:
|
rtrips:
|
||||||
build: .
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: rtrips-online/Dockerfile
|
||||||
container_name: rtrips-online
|
container_name: rtrips-online
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const booking = await prisma.booking.create({
|
const booking = await prisma.booking.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { pushShapesToCanvas } from '@/lib/canvas-sync';
|
import { pushShapesToCanvas } from '@/lib/canvas-sync';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/trips/[id]/canvas
|
* POST /api/trips/[id]/canvas
|
||||||
|
|
@ -10,9 +11,14 @@ import { pushShapesToCanvas } from '@/lib/canvas-sync';
|
||||||
* Stores the canvasSlug back on the Trip record.
|
* Stores the canvasSlug back on the Trip record.
|
||||||
*/
|
*/
|
||||||
export async function POST(
|
export async function POST(
|
||||||
_request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ 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 {
|
try {
|
||||||
const trip = await prisma.trip.findUnique({
|
const trip = await prisma.trip.findUnique({
|
||||||
where: { id: params.id },
|
where: { id: params.id },
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const destination = await prisma.destination.create({
|
const destination = await prisma.destination.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const expense = await prisma.expense.create({
|
const expense = await prisma.expense.create({
|
||||||
data: {
|
data: {
|
||||||
tripId: params.id,
|
tripId: params.id,
|
||||||
paidById: body.paidById,
|
paidById: auth.user.id,
|
||||||
description: body.description,
|
description: body.description,
|
||||||
amount: body.amount,
|
amount: body.amount,
|
||||||
currency: body.currency || 'USD',
|
currency: body.currency || 'USD',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const item = await prisma.itineraryItem.create({
|
const item = await prisma.itineraryItem.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
_request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string; itemId: string } }
|
{ params }: { params: { id: string; itemId: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const auth = await requireAuth(request);
|
||||||
|
if (!isAuthed(auth)) return auth;
|
||||||
|
|
||||||
const { id, itemId } = await params;
|
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
|
// Verify item belongs to trip
|
||||||
const item = await prisma.packingItem.findFirst({
|
const item = await prisma.packingItem.findFirst({
|
||||||
where: { id: itemId, tripId: id },
|
where: { id: itemId, tripId: id },
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const item = await prisma.packingItem.create({
|
const item = await prisma.packingItem.create({
|
||||||
data: {
|
data: {
|
||||||
tripId: params.id,
|
tripId: params.id,
|
||||||
addedById: body.addedById,
|
addedById: auth.user.id,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
category: body.category,
|
category: body.category,
|
||||||
quantity: body.quantity ?? 1,
|
quantity: body.quantity ?? 1,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_request: NextRequest,
|
_request: NextRequest,
|
||||||
|
|
@ -37,6 +38,12 @@ export async function PUT(
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 body = await request.json();
|
||||||
const trip = await prisma.trip.update({
|
const trip = await prisma.trip.update({
|
||||||
where: { id: params.id },
|
where: { id: params.id },
|
||||||
|
|
@ -62,10 +69,16 @@ export async function PUT(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
_request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
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 } });
|
await prisma.trip.delete({ where: { id: params.id } });
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { requireAuth, isAuthed, requireTripRole } from '@/lib/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/trips/[id]/sync
|
* POST /api/trips/[id]/sync
|
||||||
|
|
@ -13,6 +14,11 @@ export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ 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 {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { shapeId, type, data } = body;
|
const { shapeId, type, data } = body;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { parseTrip } from '@/lib/gemini';
|
import { parseTrip } from '@/lib/gemini';
|
||||||
|
import { requireAuth, isAuthed } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const auth = await requireAuth(request);
|
||||||
|
if (!isAuthed(auth)) return auth;
|
||||||
|
|
||||||
const { text } = await request.json();
|
const { text } = await request.json();
|
||||||
|
|
||||||
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,36 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { generateSlug } from '@/lib/slug';
|
import { generateSlug } from '@/lib/slug';
|
||||||
import { ParsedTrip } from '@/lib/types';
|
import { ParsedTrip } from '@/lib/types';
|
||||||
|
import { getAuthUser, requireAuth, isAuthed } from '@/lib/auth';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
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({
|
const trips = await prisma.trip.findMany({
|
||||||
include: {
|
include: {
|
||||||
destinations: { orderBy: { sortOrder: 'asc' } },
|
destinations: { orderBy: { sortOrder: 'asc' } },
|
||||||
|
|
@ -33,6 +60,9 @@ export async function GET() {
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const auth = await requireAuth(request);
|
||||||
|
if (!isAuthed(auth)) return auth;
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { parsed, rawInput }: { parsed: ParsedTrip; rawInput: string } = body;
|
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
|
// Create destinations
|
||||||
if (parsed.destinations.length > 0) {
|
if (parsed.destinations.length > 0) {
|
||||||
await tx.destination.createMany({
|
await tx.destination.createMany({
|
||||||
|
|
@ -118,6 +157,7 @@ export async function POST(request: NextRequest) {
|
||||||
destinations: { orderBy: { sortOrder: 'asc' } },
|
destinations: { orderBy: { sortOrder: 'asc' } },
|
||||||
itineraryItems: { orderBy: { sortOrder: 'asc' } },
|
itineraryItems: { orderBy: { sortOrder: 'asc' } },
|
||||||
bookings: true,
|
bookings: true,
|
||||||
|
collaborators: { include: { user: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
import { AuthProvider } from '@/components/AuthProvider'
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
|
|
@ -26,7 +27,9 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${inter.variable} font-sans antialiased`}>
|
<body className={`${inter.variable} font-sans antialiased`}>
|
||||||
{children}
|
<AuthProvider>
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { EncryptIDProvider } from '@encryptid/sdk/ui/react';
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
// Cast to any to bypass React 18/19 type mismatch between SDK and app
|
||||||
|
const Provider = EncryptIDProvider as any;
|
||||||
|
return (
|
||||||
|
<Provider serverUrl={process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL}>
|
||||||
|
{children}
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { getEncryptIDSession } from '@encryptid/sdk/server/nextjs';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { prisma } from './prisma';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface AuthResult {
|
||||||
|
user: User;
|
||||||
|
did: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNAUTHORIZED = NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authenticated user from request, or null if not authenticated.
|
||||||
|
* Upserts User in DB by DID (find-or-create).
|
||||||
|
*/
|
||||||
|
export async function getAuthUser(request: Request): Promise<AuthResult | null> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const claims: any = await getEncryptIDSession(request);
|
||||||
|
if (!claims) return null;
|
||||||
|
|
||||||
|
const did: string | undefined = claims.did || claims.sub;
|
||||||
|
if (!did) return null;
|
||||||
|
|
||||||
|
const username: string | null = claims.username || null;
|
||||||
|
const user = await prisma.user.upsert({
|
||||||
|
where: { did },
|
||||||
|
update: { username: username || undefined },
|
||||||
|
create: { did, username },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, did };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require authentication. Returns auth result or a 401 NextResponse.
|
||||||
|
*/
|
||||||
|
export async function requireAuth(request: Request): Promise<AuthResult | NextResponse> {
|
||||||
|
const result = await getAuthUser(request);
|
||||||
|
if (!result) return UNAUTHORIZED;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard for successful auth */
|
||||||
|
export function isAuthed(result: AuthResult | NextResponse): result is AuthResult {
|
||||||
|
return !(result instanceof NextResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a role on a trip.
|
||||||
|
* Returns the role or null if no access.
|
||||||
|
*/
|
||||||
|
export async function getTripRole(
|
||||||
|
userId: string,
|
||||||
|
tripId: string
|
||||||
|
): Promise<'OWNER' | 'EDITOR' | 'VIEWER' | 'MEMBER' | null> {
|
||||||
|
const collab = await prisma.tripCollaborator.findUnique({
|
||||||
|
where: { userId_tripId: { userId, tripId } },
|
||||||
|
});
|
||||||
|
return collab?.role ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require at least the specified role on a trip.
|
||||||
|
* Returns the member role or a 403 NextResponse.
|
||||||
|
*/
|
||||||
|
export async function requireTripRole(
|
||||||
|
userId: string,
|
||||||
|
tripId: string,
|
||||||
|
minRole: 'VIEWER' | 'MEMBER' | 'EDITOR' | 'OWNER' = 'VIEWER'
|
||||||
|
): Promise<'OWNER' | 'EDITOR' | 'VIEWER' | 'MEMBER' | NextResponse> {
|
||||||
|
const role = await getTripRole(userId, tripId);
|
||||||
|
if (!role) {
|
||||||
|
return NextResponse.json({ error: 'Not a collaborator on this trip' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hierarchy = { VIEWER: 0, MEMBER: 1, EDITOR: 2, OWNER: 3 };
|
||||||
|
if (hierarchy[role] < hierarchy[minRole]) {
|
||||||
|
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return role;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue