-
Notifications
You must be signed in to change notification settings - Fork 55
Share Conversations #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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