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
2 changes: 1 addition & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getMutableAIState
} from 'ai/rsc'
import { CoreMessage, ToolResultPart } from 'ai'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
import { Section } from '@/components/section'
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Chat } from '@/components/chat'
import {nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import { AI } from './actions'

export const maxDuration = 60
Expand Down
2 changes: 1 addition & 1 deletion components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UserMessage } from './user-message'
import { Button } from './ui/button'
import { ArrowRight, Plus, Paperclip, X, Sprout } from 'lucide-react'
import Textarea from 'react-textarea-autosize'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import { useSettingsStore } from '@/lib/store/settings'
import { PartialRelated } from '@/lib/schema/related'
import { getSuggestions } from '@/lib/actions/suggest'
Expand Down
2 changes: 1 addition & 1 deletion components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Search } from 'lucide-react'
import { useMap } from './map/map-context'
import { useActions, useUIState } from 'ai/rsc'
import { AI } from '@/app/actions'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import { UserMessage } from './user-message'
import { toast } from 'sonner'
import { useSettingsStore } from '@/lib/store/settings'
Expand Down
26 changes: 4 additions & 22 deletions components/history-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,13 @@ import React, { cache } from 'react';
import HistoryItem from './history-item';
import { ClearHistory } from './clear-history';
import { getChats } from '@/lib/actions/chat';

// Define the type for the chat data returned by getChats
type ChatData = {
userId: string;
id: string;
title: string;
createdAt: Date;
visibility: string | null;
};

// Define the Chat type expected by HistoryItem
type Chat = {
userId: string;
id: string;
title: string;
createdAt: Date;
visibility: string | null;
path: string;
};
import type { Chat as DrizzleChat } from '@/lib/actions/chat-db';

type HistoryListProps = {
userId?: string;
};

const loadChats = cache(async (userId?: string): Promise<ChatData[] | null> => {
const loadChats = cache(async (userId?: string): Promise<DrizzleChat[] | null> => {
return await getChats(userId);
});

Expand All @@ -52,12 +34,12 @@ export async function HistoryList({ userId }: HistoryListProps) {
No search history
</div>
) : (
chats.map((chat: ChatData) => (
chats.map((chat: DrizzleChat) => (
<HistoryItem
key={chat.id}
chat={{
...chat,
path: '', // Provide default or derived value
path: chat.path || `/search/${chat.id}`,
}}
/>
))
Expand Down
2 changes: 1 addition & 1 deletion components/resolution-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ResolutionImage } from './resolution-image'
import { Button } from './ui/button'
import { useActions, useUIState } from 'ai/rsc'
import { AI } from '@/app/actions'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'
import { UserMessage } from './user-message'
import { toast } from 'sonner'
import { CompareSlider } from './compare-slider'
Expand Down
2 changes: 1 addition & 1 deletion components/search-related.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { AI } from '@/app/actions'
import { UserMessage } from './user-message'
import { PartialRelated } from '@/lib/schema/related'
import { nanoid } from 'nanoid'
import { nanoid } from '@/lib/utils'

export interface SearchRelatedProps {
relatedQueries: StreamableValue<PartialRelated, any>
Expand Down
78 changes: 44 additions & 34 deletions components/settings/components/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"use client"

import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import * as Tabs from "@radix-ui/react-tabs";
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { FormProvider, UseFormReturn } from "react-hook-form"; import React from "react";
import { Loader2, Save, RotateCcw } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
// Or, if the file does not exist, create it as shown below.
import { SystemPromptForm } from "./system-prompt-form"
import { ModelSelectionForm } from "./model-selection-form"
'use client'

import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import * as Tabs from '@radix-ui/react-tabs'
import { useRouter } from 'next/navigation'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

useRouter is imported and instantiated but never used.

router was previously used for router.refresh(), which has been removed. The import and declaration are now dead code.

Proposed fix
-import { useRouter } from 'next/navigation'
-  const router = useRouter()

Also applies to: 67-67

🤖 Prompt for AI Agents
In `@components/settings/components/settings.tsx` at line 6, Remove the unused
Next.js router import and its instantiation: delete the "useRouter" import and
any "const router = useRouter()" declaration in the settings component (symbols:
useRouter, router) so there is no dead code left; before removing, search the
file for any remaining calls to router.refresh() or router usage and remove or
replace them if found.

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Loader2, Save, RotateCcw } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { SystemPromptForm } from './system-prompt-form'
import { ModelSelectionForm } from './model-selection-form'
import { UserManagementForm } from './user-management-form';
import { Form } from "@/components/ui/form"
import { useSettingsStore, MapProvider } from "@/lib/store/settings";
Expand All @@ -22,8 +21,10 @@ import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/hooks/use-toast"
import { getSystemPrompt, saveSystemPrompt } from "../../../lib/actions/chat"
import { getSelectedModel, saveSelectedModel } from "../../../lib/actions/users"
import { useCurrentUser } from "@/lib/auth/use-current-user"
import { SettingsSkeleton } from './settings-skeleton'

// Define the form schema
// Define the form schema with enum validation for roles
const settingsFormSchema = z.object({
systemPrompt: z
.string()
Expand All @@ -43,7 +44,7 @@ const settingsFormSchema = z.object({
role: z.enum(["admin", "editor", "viewer"]),
}),
),
newUserEmail: z.string().email().optional(),
newUserEmail: z.string().email().optional().or(z.literal('')),
newUserRole: z.enum(["admin", "editor", "viewer"]).optional(),
})

Expand All @@ -54,10 +55,7 @@ const defaultValues: Partial<SettingsFormValues> = {
systemPrompt:
"You are a planetary copilot, an AI assistant designed to help users with information about planets, space exploration, and astronomy. Provide accurate, educational, and engaging responses about our solar system and beyond.",
selectedModel: "Grok 4.2",
users: [
{ id: "1", email: "admin@example.com", role: "admin" },
{ id: "2", email: "user@example.com", role: "editor" },
],
users: [],
}

interface SettingsProps {
Expand All @@ -67,16 +65,16 @@ interface SettingsProps {
export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
const { toast } = useToast()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [currentTab, setCurrentTab] = useState(initialTab);
const { mapProvider, setMapProvider } = useSettingsStore();
const { user, loading: authLoading } = useCurrentUser();

useEffect(() => {
setCurrentTab(initialTab);
}, [initialTab]);

// TODO: Replace 'anonymous' with actual user ID from session/auth context
const userId = 'anonymous';
const userId = user?.id;

const form = useForm<SettingsFormValues>({
resolver: zodResolver(settingsFormSchema),
Expand All @@ -85,6 +83,8 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {

useEffect(() => {
async function fetchData() {
if (!userId || authLoading) return;

const [existingPrompt, selectedModel] = await Promise.all([
getSystemPrompt(userId),
getSelectedModel(),
Expand All @@ -98,10 +98,23 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
}
}
fetchData();
}, [form, userId]);
}, [form, userId, authLoading]);

if (authLoading) {
return <SettingsSkeleton />;
}

async function onSubmit(data: SettingsFormValues) {
setIsLoading(true)
if (!userId) {
toast({
title: "Error",
description: "You must be logged in to save settings.",
variant: "destructive",
});
return;
}

setIsSaving(true)

try {
// Save the system prompt and selected model
Expand All @@ -124,9 +137,6 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
title: "Settings updated",
description: "Your settings have been saved successfully.",
})

// Refresh the page to reflect changes
// router.refresh(); // Consider if refresh is needed or if optimistic update is enough
} catch (error: any) {
// Error notification
toast({
Expand All @@ -135,7 +145,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
variant: "destructive",
})
} finally {
setIsLoading(false)
setIsSaving(false)
}
}

Expand Down Expand Up @@ -223,12 +233,12 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {

<Card>
<CardFooter className="flex justify-between pt-6">
<Button type="button" variant="outline" onClick={onReset} disabled={isLoading}>
<Button type="button" variant="outline" onClick={onReset} disabled={isSaving}>
<RotateCcw className="mr-2 h-4 w-4" />
Reset to Defaults
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<Button type="submit" disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
Expand Down
25 changes: 11 additions & 14 deletions components/settings/components/user-management-form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// File: components/settings/components/user-management-form.tsx
import React, { useState } from 'react';
import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { Button } from '@/components/ui/button';
Expand All @@ -9,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Trash2, Edit3, UserPlus, Loader2 } from 'lucide-react';
import { useToast } from '@/components/ui/hooks/use-toast';
import { addUser } from '@/lib/actions/users';
import { addUser, type UserRole } from '@/lib/actions/users';
import type { SettingsFormValues } from './settings';

interface UserManagementFormProps {
Expand All @@ -24,15 +23,11 @@ export function UserManagementForm({ form }: UserManagementFormProps) {
const { toast } = useToast();
const [isAddingUser, setIsAddingUser] = useState(false);

// const watchNewUserEmail = form.watch("newUserEmail", ""); // Not strictly needed for logic below
// const watchNewUserRole = form.watch("newUserRole", "viewer"); // Not strictly needed for logic below

const handleAddUser = async () => {
setIsAddingUser(true);
const newUserEmail = form.getValues("newUserEmail");
const newUserRole = form.getValues("newUserRole") || "viewer"; // Ensure role has a default
const newUserRole = form.getValues("newUserRole") || "viewer";

// Client-side validation first
if (!newUserEmail) {
form.setError("newUserEmail", { type: "manual", message: "Email is required." });
setIsAddingUser(false);
Expand All @@ -43,27 +38,29 @@ export function UserManagementForm({ form }: UserManagementFormProps) {
setIsAddingUser(false);
return;
}
// Client-side check if user already exists in the local list
if (fields.some(user => user.email === newUserEmail)) {
form.setError("newUserEmail", { type: "manual", message: "User with this email already exists locally." });
setIsAddingUser(false);
return;
}
// Clear any previous local errors for newUserEmail if client checks pass
form.clearErrors("newUserEmail");

try {
const result = await addUser('default-user', { email: newUserEmail, role: newUserRole });
const result = await addUser({ email: newUserEmail, role: newUserRole as UserRole });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking lib/actions/users.ts for addUser implementation ==="
fd -n "users.ts" -path "*/lib/actions/*" | head -1 | xargs cat -n | sed -n '40,65p'

echo ""
echo "=== Searching for schema files to find constraints ==="
fd -e sql | head -20

echo ""
echo "=== Checking for unique constraint on users.email in schema ==="
fd -e sql -o -e ts -o -e js | xargs grep -n "email.*unique\|unique.*email" 2>/dev/null | head -20

Repository: QueueLab/QCX

Length of output: 700


🏁 Script executed:

#!/bin/bash

echo "=== Find lib/actions/users.ts ==="
git ls-files | grep "lib/actions/users.ts"

echo ""
echo "=== Read lib/actions/users.ts (full file to understand addUser) ==="
git ls-files | grep "lib/actions/users.ts" | xargs wc -l

echo ""
echo "=== Read drizzle migration files to check schema ==="
git ls-files | grep "drizzle/migrations" | head -2 | xargs wc -l

Repository: QueueLab/QCX

Length of output: 373


🏁 Script executed:

#!/bin/bash

echo "=== Find and read lib/actions/users.ts ==="
git ls-files | grep "lib/actions/users.ts" | xargs cat -n

echo ""
echo "=== Check migration 0000 for schema ==="
cat drizzle/migrations/0000_sweet_metal_master.sql

echo ""
echo "=== Check migration 0001 for changes ==="
cat drizzle/migrations/0001_add_calendar_notes.sql

Repository: QueueLab/QCX

Length of output: 9144


🏁 Script executed:

#!/bin/bash

echo "=== Find and read database schema definition ==="
git ls-files | grep -E "lib/db/schema" | xargs cat -n

echo ""
echo "=== Search for RLS policies on users table ==="
rg "users.*POLICY|CREATE POLICY.*users" --type=sql

echo ""
echo "=== Search for triggers related to auth sync ==="
rg "trigger|CREATE.*FUNCTION" --type=sql -i

echo ""
echo "=== Check for any Supabase client admin methods ==="
rg "createUser|inviteUserByEmail|auth.*admin" --type=ts --type=js

Repository: QueueLab/QCX

Length of output: 8316


🏁 Script executed:

#!/bin/bash

echo "=== Search for auth callbacks, signup, or user sync flows ==="
rg "auth.*callback|onAuthStateChanged|session.*user|auth.*user.*create" --type=ts --type=js -i

echo ""
echo "=== Check for middleware or hooks handling auth state ==="
git ls-files | grep -E "middleware|auth.*hook|callback" | head -20

echo ""
echo "=== Search for any existing unique constraints in schema ==="
rg "unique()" --type=ts | head -20

Repository: QueueLab/QCX

Length of output: 554


🏁 Script executed:

#!/bin/bash

echo "=== Read middleware.ts to understand auth flow ==="
cat -n middleware.ts

echo ""
echo "=== Check lib/auth/get-current-user.ts ==="
cat -n lib/auth/get-current-user.ts

echo ""
echo "=== Search for signup or user creation entry points ==="
rg "signup|register|createUser|newUser" --type=ts --type=tsx -i | head -30

Repository: QueueLab/QCX

Length of output: 6827


🏁 Script executed:

#!/bin/bash

echo "=== Search for signup/register endpoints ==="
fd -e ts -e tsx -e js -e jsx | xargs grep -l "signup\|register\|auth.*route\|/api/auth" 2>/dev/null | head -20

echo ""
echo "=== Check for any API routes related to auth ==="
find . -path "*/api/auth*" -o -path "*/api/*auth*" 2>/dev/null | head -20

echo ""
echo "=== Verify: does addUser expect auth.users to already exist? ==="
rg "email.*already.*exist|duplicate.*email|constraint" lib/actions/users.ts

Repository: QueueLab/QCX

Length of output: 253


🏁 Script executed:

#!/bin/bash

echo "=== Check lib/supabase/client.ts ==="
cat -n lib/supabase/client.ts

echo ""
echo "=== Look for any documentation or comments about user management ==="
rg "admin.*user|invite.*user|sync.*auth" --type=ts -i | head -20

echo ""
echo "=== Check where addUser is actually called from ==="
rg "addUser" --type=ts --type=tsx

Repository: QueueLab/QCX

Length of output: 2638


🏁 Script executed:

#!/bin/bash

echo "=== Find where addUser is called ==="
rg "addUser" --type=ts | grep -v "export async function addUser"

echo ""
echo "=== Check user-management-form.tsx to see the context ==="
find . -name "user-management-form.tsx" | xargs cat -n | head -100

Repository: QueueLab/QCX

Length of output: 5244


addUser creates records with disconnected UUIDs, not linked to auth.users.

The addUser function (lines 49–56) inserts a row with an auto-generated UUID that has no relationship to the user's Supabase Auth identity. Additionally, the users.email field lacks a unique constraint, so the error message "Email might already exist" is misleading—duplicates can be silently created in the database. Without a mechanism to sync these public.users records with auth.users, any actual Supabase authentication will not find a matching row in your app's user table.

Consider either:

  • (a) Prevent manual user creation and instead sync Supabase Auth signups to public.users via a trigger on auth.users
  • (b) Add a unique constraint on email and reconcile records during sign-up
  • (c) Use the Supabase Admin API to create actual Auth users (not just database rows) when onboarding new users
🤖 Prompt for AI Agents
In `@components/settings/components/user-management-form.tsx` at line 49, The
addUser flow inserts a standalone public.users row with an auto-generated UUID
that isn’t linked to Supabase Auth, and users.email lacks a unique constraint so
duplicate DB-only records can be created; fix by either (A) preventing manual
creation in addUser and instead rely on syncing auth.users → public.users via a
DB trigger (update/remove addUser UI/path), (B) enforce a UNIQUE constraint on
public.users.email and change addUser to create/get an auth user via the
Supabase Admin API then upsert the linked public.users record using the auth
user id, or (C) if keeping DB-only creation, modify addUser to check for
existing email (and return a precise error) and implement a reconciliation step
to link or merge records with auth.users; reference addUser, public.users,
auth.users, and users.email when making these changes.


if (result.error) {
toast({ title: 'Error adding user', description: result.error, variant: 'destructive' });
form.setError("newUserEmail", { type: "manual", message: result.error }); // Show server error on field
form.setError("newUserEmail", { type: "manual", message: result.error });
} else if (result.user) {
toast({ title: 'User Added', description: `${result.user.email} was successfully added.` });
append(result.user); // Add user with ID from server
append({
id: result.user.id,
email: result.user.email || '',
role: result.user.role || 'viewer'
});
form.resetField("newUserEmail");
form.resetField("newUserRole"); // Or set to default: form.setValue("newUserRole", "viewer");
form.clearErrors("newUserEmail"); // Clear any previous errors
form.resetField("newUserRole");
form.clearErrors("newUserEmail");
}
} catch (error) {
console.error("Failed to add user:", error);
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/chat-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function saveChat(chatData: NewChat, messagesData: Omit<NewMessage,
} else {
// Optionally update chat metadata here if needed, e.g., title
if (chatData.title) {
await tx.update(chats).set({ title: chatData.title }).where(eq(chats.id, chatId));
await tx.update(chats).set({ title: chatData.title, visibility: chatData.visibility, updatedAt: chatData.updatedAt }).where(eq(chats.id, chatId));
}
}
} else { // No chat ID, create new chat
Expand All @@ -119,7 +119,7 @@ export async function saveChat(chatData: NewChat, messagesData: Omit<NewMessage,
chatId: chatId!, // Ensure chatId is set for all messages
userId: msg.userId || chatData.userId!, // Ensure userId is set
}));
await tx.insert(messages).values(messagesToInsert);
await tx.insert(messages).values(messagesToInsert).onConflictDoUpdate({ target: messages.id, set: { content: sql`EXCLUDED.content`, role: sql`EXCLUDED.role` } });
}
return chatId;
});
Expand Down
Loading