Conversation
4660029 to
8ed8741
Compare
|
Did you implement more than just public share? I thought we scoped this down but mid-review I saw what looks like UI components to do more than public share. I have a lot of comments around YAGNI for features other than public share. |
| const messages = await db | ||
| .select() | ||
| .from(schema.messages) | ||
| .where(and(eq(schema.messages.tenantId, tenantId), eq(schema.messages.conversationId, conversationId))) | ||
| .orderBy(asc(schema.messages.createdAt)); |
There was a problem hiding this comment.
This looks like a security bug. If you know the tenant ID and the conversationId then you can access any conversation even if you aren't logged in.
app/(shared)/public/conversations/[conversationId]/messages/route.ts
Outdated
Show resolved
Hide resolved
| import { eq } from "drizzle-orm"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| import { createShareRequestSchema } from "@/lib/api"; | ||
| import db from "@/lib/server/db"; | ||
| import { sharedConversations } from "@/lib/server/db/schema"; | ||
| import { getConversation } from "@/lib/server/service"; | ||
| import { requireAuthContext, requireAuthContextFromRequest } from "@/lib/server/utils"; | ||
|
|
||
| export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { | ||
| try { | ||
| const { conversationId } = await params; | ||
|
|
||
| // Parse the request body | ||
| const body = await request.json().catch(() => ({})); | ||
| const validationResult = createShareRequestSchema.safeParse(body); | ||
| if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); | ||
|
|
||
| const { slug } = validationResult.data; | ||
| const { profile, tenant } = await requireAuthContext(slug); | ||
|
|
||
| // Verify conversation ownership | ||
| const conversation = await getConversation(tenant.id, profile.id, conversationId); | ||
| // Create share record | ||
| const [share] = await db | ||
| .insert(sharedConversations) | ||
| .values({ | ||
| conversationId: conversation.id, | ||
| tenantId: tenant.id, | ||
| createdBy: profile.id, | ||
| accessType: body.accessType, | ||
| recipientEmails: body.recipientEmails || [], | ||
| expiresAt: body.expiresAt, | ||
| }) | ||
| .returning(); | ||
|
|
||
| return Response.json({ shareId: share.id }); | ||
| } catch (error) { | ||
| console.error("Failed to create share:", error); | ||
| return new Response("Internal Server Error", { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| // Get all shares for a conversation | ||
| export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { | ||
| try { | ||
| const { profile, tenant } = await requireAuthContextFromRequest(request); | ||
| const { conversationId } = await params; | ||
|
|
||
| // Verify conversation ownership | ||
| await getConversation(tenant.id, profile.id, conversationId); | ||
|
|
||
| // Get all shares for this conversation | ||
| const shares = await db | ||
| .select({ | ||
| shareId: sharedConversations.id, | ||
| accessType: sharedConversations.accessType, | ||
| createdAt: sharedConversations.createdAt, | ||
| expiresAt: sharedConversations.expiresAt, | ||
| recipientEmails: sharedConversations.recipientEmails, | ||
| }) | ||
| .from(sharedConversations) | ||
| .where(eq(sharedConversations.conversationId, conversationId)) | ||
| .orderBy(sharedConversations.createdAt); | ||
|
|
||
| return Response.json(shares); | ||
| } catch (error) { | ||
| console.error("Error fetching shares:", error); | ||
| return new Response("Internal Server Error", { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
I would suggest that we Keep It Simple Stupid here. We aren't implementing anything other than public share so I don't think we need this endpoint. We can just update the conversation directly.
| useEffect(() => { | ||
| let isMounted = true; // Flag to prevent state updates on unmounted component | ||
| (async () => { | ||
| try { | ||
| const res = await fetch(`/public/conversations/${conversationId}/messages`, { | ||
| method: "POST", | ||
| body: JSON.stringify({ createdBy, tenantId: tenant.id }), | ||
| }); | ||
| console.log(res); | ||
| if (!res.ok) { | ||
| console.error("Could not load conversation:", res.statusText); | ||
| if (isMounted) { | ||
| setMessages([{ role: "system", content: "Error loading conversation.", id: "error-message" }]); | ||
| } | ||
| return; | ||
| } | ||
| const json = await res.json(); | ||
| const parsedMessages = conversationMessagesResponseSchema.parse(json); | ||
| console.log(parsedMessages); | ||
| if (isMounted) { | ||
| setMessages(parsedMessages); | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to fetch or parse messages:", error); | ||
| if (isMounted) { | ||
| setMessages([{ role: "system", content: "Error loading conversation.", id: "error-message" }]); | ||
| } | ||
| } | ||
| })(); | ||
|
|
||
| return () => { | ||
| isMounted = false; // Cleanup function to set flag on unmount | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps -- Run only once on mount | ||
| }, [conversationId, tenant.slug]); |
There was a problem hiding this comment.
Getting messy. Should this be a custom hook?
3f37806 to
eab8f4c
Compare
|
Addressed some comments. In a functional yet unsecure state. |
Share conversations publicly with a link