Skip to content

Conversation

@stonewerner
Copy link
Contributor

Share conversations publicly with a link

@foobarfighter
Copy link
Contributor

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.

Comment on lines 16 to 20
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));
Copy link
Contributor

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.

Comment on lines 1 to 71
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 });
}
}
Copy link
Contributor

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.

Comment on lines 38 to 72
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]);
Copy link
Contributor

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?

@stonewerner
Copy link
Contributor Author

Addressed some comments. In a functional yet unsecure state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants