Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions app/api/user/credits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ export async function GET(req: NextRequest) {
}

// Get user from database
const dbUser = await db.query.users.findFirst({
let dbUser = await db.query.users.findFirst({
where: eq(users.id, user.id)
});

if (!dbUser) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
// Create user if they don't exist in the database
const [newUser] = await db.insert(users).values({
id: user.id,
credits: 0,
tier: 'free'
}).returning();
dbUser = newUser;
}
Comment on lines 23 to 36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “create user if missing” flow can still throw a 500 (or fail the request) if two requests race and both attempt to insert the same users.id concurrently (e.g., multiple tabs hitting credits fetch on first login). This can reintroduce the auth/credits error you’re trying to fix.

Consider making the insert idempotent (upsert / on-conflict-do-nothing) and then re-select, or handle unique-constraint violations explicitly.

Suggestion

Make user creation race-safe by using an idempotent insert (if supported by your Drizzle dialect) and then re-fetch:

  • Prefer onConflictDoNothing / onDuplicateKeyUpdate style APIs (dialect-dependent)
  • Or catch unique-violation and continue

Example (Postgres-style Drizzle):

await db
  .insert(users)
  .values({ id: user.id, credits: 0, tier: 'free' })
  .onConflictDoNothing({ target: users.id })

const dbUser = await db.query.users.findFirst({ where: eq(users.id, user.id) })

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change.


const tier = parseTier(dbUser.tier);
Expand Down
2 changes: 1 addition & 1 deletion components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ export function Chat({ id }: ChatProps) {
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
<div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-16 md:pt-20 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
<CreditsDisplay className="mb-2" />
{isCalendarOpen ? (
<CalendarNotepad chatId={id} />
) : (
<>
<CreditsDisplay className="mb-2" />
<ChatPanel
ref={chatPanelRef}
messages={messages}
Expand Down
20 changes: 13 additions & 7 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
CircleUserRound,
Map,
CalendarDays,
TentTree
TentTree,
Sprout
} from 'lucide-react'
import { MapToggle } from './map-toggle'
import { ProfileToggle } from './profile-toggle'
Expand All @@ -32,9 +33,9 @@ export const Header = () => {
</a>
</div>

<div className="absolute left-1 flex items-center">
<History location="header">
<div className="flex items-center cursor-pointer group">
<div className="absolute left-1 flex items-center gap-1">
<div className="flex items-center group">
<a href="/" className="flex items-center cursor-pointer">
<Button variant="ghost" size="icon" className="group-hover:bg-accent">
<Image
src="/images/logo.svg"
Expand All @@ -47,7 +48,12 @@ export const Header = () => {
<h1 className="text-2xl font-poppins font-semibold text-primary group-hover:text-primary/80 transition-colors">
QCX
</h1>
</div>
</a>
</div>
<History location="header">
<Button variant="ghost" size="icon" className="text-primary hover:bg-accent" title="Chat History">
<Sprout className="h-5 w-5" />
</Button>
</History>
</div>

Expand All @@ -63,7 +69,7 @@ export const Header = () => {

<div id="header-search-portal" />

<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)}>
<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)} title="Usage & Billing">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>

Expand All @@ -73,7 +79,7 @@ export const Header = () => {
{/* Mobile menu buttons */}
<div className="flex md:hidden gap-2">

<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)}>
<Button variant="ghost" size="icon" onClick={() => setIsUsageOpen(true)} title="Usage & Billing">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle/>
Expand Down
2 changes: 1 addition & 1 deletion components/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function History({ location, children }: HistoryProps) {
</Button>
)}
</SheetTrigger>
<SheetContent side="left" className="w-64 rounded-tr-xl rounded-br-xl" data-testid="history-panel">
<SheetContent side="right" className="w-64 rounded-tl-xl rounded-bl-xl" data-testid="history-panel">
<CreditsDisplay className="mb-4 mt-4" />
<SheetHeader>
<SheetTitle className="flex items-center gap-1 text-sm font-normal mb-2">
Expand Down
17 changes: 17 additions & 0 deletions lib/actions/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@ export async function saveChat(chat: Chat, userId: string): Promise<string | nul
return null
}

// Ensure user exists in the database before saving chat
const { db } = await import('@/lib/db');
const { users } = await import('@/lib/db/schema');
const { eq } = await import('drizzle-orm');

const dbUser = await db.query.users.findFirst({
where: eq(users.id, effectiveUserId)
});

if (!dbUser) {
await db.insert(users).values({
id: effectiveUserId,
credits: 0,
tier: 'free'
});
}

Comment on lines +155 to +171

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a sequence of findFirst then insert has the same race condition risk as the credits endpoint (double insert on concurrent requests). Also, using dynamic await import(...) inside a hot path will add overhead and makes bundling/traceability harder; this is not a great tradeoff for a core action.

Since this is server-side code, prefer static imports at module scope and make the insert idempotent.

Suggestion

Refactor to static imports and an idempotent insert:

  1. Move imports to top-level:
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
  1. Use conflict-safe insert + optional re-fetch (dialect-dependent):
await db
  .insert(users)
  .values({ id: effectiveUserId, credits: 0, tier: 'free' })
  .onConflictDoNothing({ target: users.id })

(Or catch unique-constraint errors.)

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this refactor.

const { data, error } = await supabaseSaveChat(chat, effectiveUserId)

if (error) {
Expand Down
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.