-
-
Notifications
You must be signed in to change notification settings - Fork 6
Fix chat persistence by using direct inserts #384
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: refactor/collaboration-ui-integration
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,50 @@ | ||
| 'use server' | ||
|
|
||
| import { getSupabaseServerClient } from '@/lib/supabase/client' | ||
| import { getSupabaseServiceRoleClient } from './service-role' | ||
| import { type Chat, type AIMessage } from '@/lib/types' | ||
| import { PostgrestError } from '@supabase/supabase-js' | ||
|
|
||
| export async function saveChat(chat: Chat, userId: string): Promise<{ data: string | null; error: PostgrestError | null }> { | ||
| const supabase = getSupabaseServerClient() | ||
| const supabase = getSupabaseServiceRoleClient() | ||
|
|
||
| // Insert into chats table | ||
| const { data: chatData, error: chatError } = await supabase | ||
| .from('chats') | ||
| .insert({ | ||
| id: chat.id, | ||
| user_id: userId, | ||
| title: chat.title, | ||
| }) | ||
| .select('id') | ||
| .single() | ||
|
|
||
| if (chatError) { | ||
| console.error('Error saving chat:', chatError) | ||
| return { data: null, error: chatError } | ||
| } | ||
|
|
||
| const messagesToInsert = chat.messages.map(message => ({ | ||
| id: message.id, | ||
| chat_id: chat.id, | ||
| user_id: userId, | ||
| role: message.role, | ||
| content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content), | ||
| createdAt: message.createdAt ? new Date(message.createdAt).toISOString() : new Date().toISOString(), | ||
| created_at: message.createdAt ? new Date(message.createdAt).toISOString() : new Date().toISOString(), | ||
| })) | ||
|
|
||
| const { data, error } = await supabase.rpc('save_chat_with_messages', { | ||
| chat_id: chat.id, | ||
| user_id: userId, | ||
| title: chat.title, | ||
| messages: messagesToInsert, | ||
| }) | ||
| const { error: messagesError } = await supabase | ||
| .from('messages') | ||
| .insert(messagesToInsert) | ||
|
|
||
| if (error) { | ||
| console.error('Error saving chat with messages:', error) | ||
| return { data: null, error } | ||
| if (messagesError) { | ||
| console.error('Error saving messages:', messagesError) | ||
| // Attempt to delete the chat if messages fail to save | ||
| await supabase.from('chats').delete().eq('id', chat.id) | ||
| return { data: null, error: messagesError } | ||
|
Comment on lines
+40
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rollback logic is not safe as-is:
Also, the sequence is not atomic; partial writes can occur. Prefer a transactional approach (RPC or SQL function) when you need to guarantee all-or-nothing behavior. SuggestionHandle rollback failures explicitly and make the rollback safer by deleting only if the chat insert in this request succeeded and by checking for a known marker (e.g., inserted row matches If you can, move this to a single database transaction:
At minimum:
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion. |
||
| } | ||
|
|
||
| return { data: data as string, error: null } | ||
| return { data: chatData.id, error: null } | ||
| } | ||
|
|
||
| export async function getMessagesByChatId(chatId: string): Promise<{ data: any[] | null; error: PostgrestError | null }> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { createServerClient, type CookieOptions } from '@supabase/ssr' | ||
| import { cookies } from 'next/headers' | ||
|
|
||
| export function getSupabaseServiceRoleClient() { | ||
| const cookieStore = cookies() | ||
|
|
||
| return createServerClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.SUPABASE_SERVICE_ROLE_KEY!, | ||
| { | ||
| cookies: { | ||
| async get(name: string) { | ||
| const store = await cookieStore | ||
| return store.get(name)?.value | ||
| }, | ||
| async set(name: string, value: string, options: CookieOptions) { | ||
| const store = await cookieStore | ||
| store.set({ name, value, ...options }) | ||
| }, | ||
| async remove(name: string, options: CookieOptions) { | ||
| const store = await cookieStore | ||
| store.set({ name, value: '', ...options }) | ||
| }, | ||
| }, | ||
| } | ||
| ) | ||
|
Comment on lines
+1
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A safer pattern is to use the plain Supabase JS client (or a server client configured without cookie persistence) for service role usage, and keep it completely separate from request/session cookie handling. SuggestionCreate a dedicated service-role client that does not read/write Next.js cookies. For example, use Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion. |
||
| } | ||
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.
Using the service role client inside a general persistence function is a high-risk elevation of privilege. As written,
saveChat(chat, userId)trusts the caller-provideduserIdand then performs privileged inserts, which can allow cross-user writes if this server action is ever reachable by an untrusted caller or if a bug passes the wronguserId.Given the PR description (“bypass a missing
INSERTpolicy on thechatstable”), this should be treated as a temporary workaround and tightly scoped. At minimum, enforce thatuserIdcomes from the authenticated session on the server (not a caller argument), or keep the service-role client usage isolated to the minimal operation and add explicit checks/guardrails.Suggestion
Refactor
saveChatsouserIdis derived from the current session (server-side) and cannot be overridden by the caller, and/or keep the service role client limited to only thechatsinsert while using the regular server client for user-scoped operations.Example direction:
getSupabaseServerClient().auth.getUser()(or your existing auth util) and ignore the passeduserId.userIdentirely.if (userId !== session.user.id) throw.Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.