Skip to content
Open
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
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
AUTH_DISABLED_FOR_DEV=false
DATABASE_URL="postgresql://user:password@host:port/db"
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
SERVER_ACTIONS_ALLOWED_ORIGINS=*
STANDARD_TIER_BILLING_CYCLE="yearly"
STANDARD_TIER_CREDITS=500
STANDARD_TIER_MONTHLY_PRICE=500
STANDARD_TIER_PRICE_ID="price_standard_500_yearly"
ENABLE_SHARE=true
ENABLE_AUTH=true
5 changes: 3 additions & 2 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,10 @@ async function submit(formData?: FormData, skip?: boolean) {
}

const userId = await getCurrentUserIdOnServer()
if (!userId) throw new Error('Unauthorized')
const guestChatEnabled = process.env.ENABLE_GUEST_CHAT === 'true';
if (!userId && !guestChatEnabled) throw new Error('Unauthorized')
const userInputAction = formData?.get('input') as string;
const currentSystemPrompt = (await getSystemPrompt(userId)) || '';
const currentSystemPrompt = (userId ? (await getSystemPrompt(userId)) : '') || '';
const retrievedContext = userInputAction ? await retrieveContext(userInput, aiState.get().chatId) : [];
const augmentedSystemPrompt = retrievedContext.length > 0 ? `Context: ${retrievedContext.join('\n')}\n${currentSystemPrompt}` : currentSystemPrompt;
const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google'
Expand Down
3 changes: 2 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const maxDuration = 60
export async function POST(req: Request) {
const { messages } = await req.json()
const userId = await getCurrentUserIdOnServer()
const guestChatEnabled = process.env.ENABLE_GUEST_CHAT === 'true'

if (!userId) {
if (!userId && !guestChatEnabled) {
return new Response('Unauthorized', { status: 401 })
}

Expand Down
15 changes: 4 additions & 11 deletions app/api/user/credits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSupabaseServerClient } from '@/lib/supabase/client';
import { TIERS, parseTier, getTierConfig } from '@/lib/utils/subscription';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';

export async function GET(req: NextRequest) {
try {
const supabase = getSupabaseServerClient();
const {
data: { user },
error: userError
} = await supabase.auth.getUser();
const userId = await getCurrentUserIdOnServer();

if (userError || !user) {
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
Expand All @@ -22,7 +18,7 @@ export async function GET(req: NextRequest) {

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

if (!dbUser) {
Expand All @@ -33,9 +29,6 @@ export async function GET(req: NextRequest) {
}

const tier = parseTier(dbUser.tier);
// If user is not on Standard tier, they might not need credits logic,
// but for now we return the credits regardless.
// If the tier doesn't support credits (e.g. Free or Pro), the UI can handle it.

return NextResponse.json({
credits: dbUser.credits,
Expand Down
14 changes: 5 additions & 9 deletions app/api/user/upgrade/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSupabaseServerClient } from '@/lib/supabase/client';
import { TIER_CONFIGS, TIERS, parseTier } from '@/lib/utils/subscription';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';

export async function POST(req: NextRequest) {
try {
const supabase = getSupabaseServerClient();
const {
data: { user },
error: userError
} = await supabase.auth.getUser();
const userId = await getCurrentUserIdOnServer();

if (userError || !user) {
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
Expand Down Expand Up @@ -41,7 +37,7 @@ export async function POST(req: NextRequest) {

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

if (!currentUser) {
Expand All @@ -62,7 +58,7 @@ export async function POST(req: NextRequest) {
tier: tier,
credits: newCreditsTotal
})
.where(eq(users.id, user.id))
.where(eq(users.id, userId))
.returning();

return NextResponse.json({
Expand Down
63 changes: 63 additions & 0 deletions app/auth/auth-client-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client"

export const dynamic = 'force-dynamic'

import Image from "next/image"
import { AuthPage } from "@/components/auth"
import { useAuth } from "@/lib/auth/v0"

function Logo() {
return (
<div className="flex items-center gap-2 text-xl font-semibold">
<Image src="/images/logo-green.png" alt="QCX Logo" width={32} height={32} />
QCX
</div>
)
}

function ArtPanel() {
return (
<div className="relative flex h-full w-full items-center justify-center overflow-hidden rounded-3xl">
<Image src="/images/abstract-art.png" alt="Abstract art" fill className="object-cover" priority />
</div>
)
}

export function AuthClientPage() {
const {
isLoading,
error,
magicLinkSent,
magicLinkEmail,
handleGoogleSignIn,
handleMagicLink,
resetError,
resetMagicLink,
} = useAuth({
// Optional callbacks for additional handling
onMagicLinkSent: (email) => {
console.log("Magic link sent to:", email)
},
onError: (error) => {
console.error("Auth error:", error)
},
})
Comment on lines +36 to +44

Choose a reason for hiding this comment

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

Client component logs auth events (console.log / console.error) for magic link and errors. In production, this can leak PII (email) into client logs and create noisy telemetry.

Since these callbacks are "optional" and not used for UX, it's better to remove them or gate them behind a dev check.

Suggestion

Remove these callbacks or gate them:

onMagicLinkSent: (email) => {
  if (process.env.NODE_ENV === 'development') {
    console.log('Magic link sent to:', email)
  }
},
onError: (error) => {
  if (process.env.NODE_ENV === 'development') {
    console.error('Auth error:', error)
  }
},

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that removes/gates these logs.


return (
<AuthPage
title="Welcome to QCX"
subtitle="Let's get you started with Quality Computer Experiences"
logo={<Logo />}
onGoogleSignIn={handleGoogleSignIn}
onMagicLinkSubmit={handleMagicLink}
showGitHub={false}
decorativePanel={<ArtPanel />}
isLoading={isLoading}
error={error}
magicLinkSent={magicLinkSent}
magicLinkEmail={magicLinkEmail}
onResetMagicLink={resetMagicLink}
onResetError={resetError}
/>
)
}
11 changes: 9 additions & 2 deletions app/auth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ export async function GET(request: Request) {
})

if (code) {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseAnonKey) {
return NextResponse.redirect(`${origin}/auth/auth-code-error?error=Missing+Supabase+Configuration`)
}

const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
supabaseUrl,
supabaseAnonKey,
{
cookies: {
async get(name: string) {
Expand Down
68 changes: 11 additions & 57 deletions app/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,17 @@
"use client"
import { getCurrentUserIdOnServer } from "@/lib/auth/get-current-user"
import { redirect } from "next/navigation"
import { AuthClientPage } from "./auth-client-page"

export const dynamic = 'force-dynamic'

import Image from "next/image"
import { AuthPage } from "@/components/auth"
import { useAuth } from "@/lib/auth/v0"
export default async function LoginPage() {
const userId = await getCurrentUserIdOnServer()
const isAuthEnabled = process.env.ENABLE_AUTH === 'true'

function Logo() {
return (
<div className="flex items-center gap-2 text-xl font-semibold">
<Image src="/images/logo-green.png" alt="QCX Logo" width={32} height={32} />
QCX
</div>
)
}

function ArtPanel() {
return (
<div className="relative flex h-full w-full items-center justify-center overflow-hidden rounded-3xl">
<Image src="/images/abstract-art.png" alt="Abstract art" fill className="object-cover" priority />
</div>
)
}

export default function LoginPage() {
const {
isLoading,
error,
magicLinkSent,
magicLinkEmail,
handleGoogleSignIn,
handleMagicLink,
resetError,
resetMagicLink,
} = useAuth({
// Optional callbacks for additional handling
onMagicLinkSent: (email) => {
console.log("Magic link sent to:", email)
},
onError: (error) => {
console.error("Auth error:", error)
},
})
// If auth is disabled, redirect to home as we are always "logged in" as anonymous user
if (!isAuthEnabled && userId) {
redirect('/')
}

return (
<AuthPage
title="Welcome to QCX"
subtitle="Let's get you started with Quality Computer Experiences"
logo={<Logo />}
onGoogleSignIn={handleGoogleSignIn}
onMagicLinkSubmit={handleMagicLink}
showGitHub={false}
decorativePanel={<ArtPanel />}
isLoading={isLoading}
error={error}
magicLinkSent={magicLinkSent}
magicLinkEmail={magicLinkEmail}
onResetMagicLink={resetMagicLink}
onResetError={resetError}
/>
)
return <AuthClientPage />
}
13 changes: 10 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { Chat } from '@/components/chat'
import { nanoid } from '@/lib/utils'
import { AI } from './actions'
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'
import { getSupabaseUserAndSessionOnServer } from '@/lib/auth/get-current-user'
import { redirect } from 'next/navigation'
import { MapDataProvider } from '@/components/map/map-data-context'
import { ensureUserExists } from '@/lib/actions/users'

export const maxDuration = 60
export const dynamic = 'force-dynamic'

export default async function Page() {
const userId = await getCurrentUserIdOnServer()
const { user } = await getSupabaseUserAndSessionOnServer()
const guestChatEnabled = process.env.ENABLE_GUEST_CHAT === 'true'

if (!userId) {
if (!user && !guestChatEnabled) {
redirect('/auth')
}

// Ensure user exists in public.users table if they are authenticated
if (user) {
await ensureUserExists(user.id, user.email)
}

Comment on lines 11 to 24

Choose a reason for hiding this comment

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

app/page.tsx now always redirects to /auth when user is null. But in your new design, ENABLE_AUTH=false should result in an anonymous user being returned (so user is non-null). If env vars are missing while ENABLE_AUTH=true, getSupabaseUserAndSessionOnServer() returns { user: null, error: Error(...) } and you silently redirect to /auth rather than surfacing a misconfiguration.

That can create an infinite loop or confusing UX: /auth may itself depend on server auth state and environment.

Suggestion

Consider handling the "auth enabled but misconfigured" case explicitly.

For example:

  • If error is non-null, render a simple configuration error page (or throw to trigger Next error boundary) instead of redirecting.
  • Alternatively, in /auth route, detect misconfiguration and show a clear message.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that threads error through and avoids the redirect loop.

const id = nanoid()
return (
<AI initialAIState={{ chatId: id, messages: [] }}>
Expand Down
19 changes: 13 additions & 6 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Chat } from '@/components/chat';
import { getChat, getChatMessages } from '@/lib/actions/chat';
import { AI } from '@/app/actions';
import { MapDataProvider } from '@/components/map/map-data-context';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
import { getSupabaseUserAndSessionOnServer } from '@/lib/auth/get-current-user';
import { ensureUserExists } from '@/lib/actions/users';
import type { AIMessage } from '@/lib/types';

export const maxDuration = 60;
Expand All @@ -14,22 +15,28 @@ export interface SearchPageProps {

export async function generateMetadata({ params }: SearchPageProps) {
const { id } = await params;
const userId = await getCurrentUserIdOnServer();
const chat = await getChat(id, userId);
const { user } = await getSupabaseUserAndSessionOnServer();
const chat = await getChat(id, user?.id);
return {
title: chat?.title?.toString().slice(0, 50) || 'Search',
};
}

export default async function SearchPage({ params }: SearchPageProps) {
const { id } = await params;
const userId = await getCurrentUserIdOnServer();
const { user } = await getSupabaseUserAndSessionOnServer();
const guestChatEnabled = process.env.ENABLE_GUEST_CHAT === 'true';

if (!userId) {
if (!user && !guestChatEnabled) {
redirect('/');
}

const chat = await getChat(id, userId);
// Ensure user exists if authenticated
if (user) {
await ensureUserExists(user.id, user.email);
}

const chat = await getChat(id, user?.id);

if (!chat) {
notFound();
Expand Down
Loading