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
4 changes: 2 additions & 2 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ async function clearChat() {
const aiState = getMutableAIState<typeof AI>()

aiState.done({
chatId: nanoid(),
chatId: crypto.randomUUID(),
messages: []
})
}
Expand All @@ -572,7 +572,7 @@ export type UIState = {
}[]

const initialAIState: AIState = {
chatId: nanoid(),
chatId: 'new-chat',
messages: []
}

Expand Down
33 changes: 7 additions & 26 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
export const dynamic = "force-dynamic";
import { NextResponse, NextRequest } from 'next/server';
import { saveChat, createMessage, NewChat, NewMessage } from '@/lib/actions/chat-db';
import { saveChat, type NewChat, type NewMessage } from '@/lib/actions/chat-db';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
// import { generateUUID } from '@/lib/utils'; // Assuming generateUUID is in lib/utils as per PR context - not needed for PKs

// This is a simplified POST handler. PR #533's version might be more complex,
// potentially handling streaming AI responses and then saving.
// For now, this focuses on the database interaction part.
export async function POST(request: NextRequest) {
try {
const userId = await getCurrentUserIdOnServer();
Expand All @@ -14,47 +11,31 @@ export async function POST(request: NextRequest) {
}

const body = await request.json();

// Example: Distinguish between creating a new chat vs. adding a message to existing chat
// The actual structure of `body` would depend on client-side implementation.
// Let's assume a simple case: creating a new chat with an initial message.
const { title, initialMessageContent, role = 'user' } = body;
const { title, initialMessageContent, role = 'user', chatId } = body;

if (!initialMessageContent) {
return NextResponse.json({ error: 'Initial message content is required' }, { status: 400 });
}

const newChatData: NewChat = {
// id: generateUUID(), // Drizzle schema now has defaultRandom for UUIDs
id: chatId,
userId: userId,
title: title || 'New Chat', // Default title if not provided
// createdAt: new Date(), // Handled by defaultNow() in schema
visibility: 'private', // Default visibility
title: title || 'New Chat',
visibility: 'private',
};
Comment on lines 13 to 25
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.

Action required

3. Chat updates lack ownership check 🐞 Bug ⛨ Security

POST /api/chat now accepts a client-supplied chatId and passes it to chat-db.saveChat, but
saveChat selects/updates chats by chats.id only (no userId constraint). If a chat UUID is
guessed/leaked, this allows cross-user chat mutation (title/visibility updates and message
insertion).
Agent Prompt
### Issue description
`/api/chat` accepts a client-provided `chatId`, and `chat-db.saveChat` updates chats by `id` only. This can allow a user to write into another user's chat if they know the UUID.

### Issue Context
- `app/api/chat/route.ts` takes `chatId` from request JSON and sets it as `NewChat.id`.
- `lib/actions/chat-db.ts::saveChat` checks existence and updates using `where(eq(chats.id, chatId))` without scoping to `userId`.

### Fix Focus Areas
- app/api/chat/route.ts[13-25]
- lib/actions/chat-db.ts[95-113]
- lib/db/schema.ts[30-40]

### Suggested fix
- In `chat-db.saveChat`, scope all read/update operations by both `chats.id` and `chats.userId`:
  - `where(and(eq(chats.id, chatId), eq(chats.userId, chatData.userId)))`
  - Apply the same constraint to `update(chats)...where(...)`.
- If a chat with the requested `id` exists but belongs to a different user, reject (throw/return null) rather than updating.
- In `POST /api/chat`, validate `chatId` (UUID format) and decide whether you truly want to allow client-chosen IDs; if not required, ignore it and let the DB generate the ID server-side.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


// Use a transaction if creating chat and first message together
// For simplicity here, let's assume saveChat handles chat creation and returns ID, then we create a message.
// A more robust `saveChat` might create the chat and first message in one go.
// The `saveChat` in chat-db.ts is designed to handle this.

const firstMessage: Omit<NewMessage, 'chatId'> = {
// id: generateUUID(), // Drizzle schema now has defaultRandom for UUIDs
// chatId is omitted as it will be set by saveChat
userId: userId,
role: role as NewMessage['role'], // Ensure role type matches schema expectation
role: role as NewMessage['role'],
content: initialMessageContent,
// createdAt: new Date(), // Handled by defaultNow() in schema, not strictly needed here
};

// The saveChat in chat-db.ts is designed to take initial messages.
const savedChatId = await saveChat(newChatData, [firstMessage]);

if (!savedChatId) {
return NextResponse.json({ error: 'Failed to save chat' }, { status: 500 });
}

// Fetch the newly created chat and message to return (optional, but good for client)
// For now, just return success and the new chat ID.
return NextResponse.json({ message: 'Chat created successfully', chatId: savedChatId }, { status: 201 });

} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions app/api/chats/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const dynamic = "force-dynamic";
import { NextResponse, NextRequest } from 'next/server';
import { getChatsPage } from '@/lib/actions/chat-db';
import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
Expand Down
61 changes: 34 additions & 27 deletions app/api/embeddings/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const dynamic = "force-dynamic";
// app/api/embeddings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Storage } from '@google-cloud/storage';
Expand All @@ -9,38 +10,44 @@ import proj4 from 'proj4';

// Configuration from environment variables
const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID || 'gen-lang-client-0663384776';
const GCP_CREDENTIALS_PATH = process.env.GCP_CREDENTIALS_PATH || '/home/ubuntu/gcp_credentials.json';
const AEF_INDEX_PATH = process.env.AEF_INDEX_PATH || path.join(process.cwd(), 'aef_index.csv');
const GCP_CREDENTIALS_PATH = process.env.GCP_CREDENTIALS_PATH || path.join(/*turbopackIgnore: true*/ process.cwd(), 'gcp_credentials.json');
const AEF_INDEX_PATH = process.env.AEF_INDEX_PATH || path.join(/*turbopackIgnore: true*/ process.cwd(), 'aef_index.csv');

// Initialize GCS client
const storage = new Storage({
keyFilename: GCP_CREDENTIALS_PATH,
projectId: GCP_PROJECT_ID,
});

// Load and parse the index file
let indexData: any[] | null = null;
// Load and parse the index file using a promise-based singleton to prevent redundant I/O
let indexPromise: Promise<any[]> | null = null;

function loadIndex() {
if (indexData) return indexData;

if (!fs.existsSync(AEF_INDEX_PATH)) {
throw new Error(
`AlphaEarth index file not found at ${AEF_INDEX_PATH}. ` +
'Please run the download_index.js script to download it.'
);
}

const fileContent = fs.readFileSync(AEF_INDEX_PATH, 'utf-8');

indexData = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});

console.log(`Loaded AlphaEarth index with ${indexData.length} entries`);

return indexData;
async function loadIndex() {
if (indexPromise) return indexPromise;

indexPromise = (async () => {
try {
// Use fs.promises for non-blocking I/O
const fileContent = await fs.promises.readFile(AEF_INDEX_PATH, 'utf-8');
const parsed = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
console.log(`Loaded AlphaEarth index with ${parsed.length} entries`);
return parsed;
} catch (error) {
indexPromise = null; // Reset on failure so we can retry
if ((error as any).code === 'ENOENT') {
throw new Error(
`AlphaEarth index file not found at ${AEF_INDEX_PATH}. ` +
'Please run the download_index.js script to download it.'
);
}
throw error;
}
})();

return indexPromise;
}

// Function to check if a point is within bounds
Expand All @@ -56,8 +63,8 @@ function isPointInBounds(
}

// Function to find the file containing the given location
function findFileForLocation(lat: number, lon: number, year: number) {
const index = loadIndex();
async function findFileForLocation(lat: number, lon: number, year: number) {
const index = await loadIndex();

for (const entry of index) {
if (parseInt(entry.year) !== year) continue;
Expand Down Expand Up @@ -164,7 +171,7 @@ export async function GET(req: NextRequest) {
}

// Find the file containing this location
const fileInfo = findFileForLocation(lat, lon, year);
const fileInfo = await findFileForLocation(lat, lon, year);

if (!fileInfo) {
return NextResponse.json(
Expand Down
6 changes: 2 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { Chat } from '@/components/chat'
import { nanoid } from '@/lib/utils'
import { AI } from './actions'

export const maxDuration = 60

import { MapDataProvider } from '@/components/map/map-data-context'

export default function Page() {
const id = nanoid()
return (
<AI initialAIState={{ chatId: id, messages: [] }}>
<AI initialAIState={{ chatId: 'new-chat', messages: [] }}>
<MapDataProvider>
<Chat id={id} />
<Chat />
</MapDataProvider>
</AI>
)
Expand Down
16 changes: 16 additions & 0 deletions app_page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Chat } from '@/components/chat'
import { AI } from './actions'

Comment on lines +1 to +3
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.

Action required

1. Stray page file compiled 🐞 Bug ≡ Correctness

The new root-level app_page.tsx is included by tsconfig.json (**/*.tsx) and imports
./actions, which resolves relative to repo root and does not match the actual app/actions.tsx
location. This can break next build/typecheck with a module resolution error.
Agent Prompt
### Issue description
A root-level `app_page.tsx` is being typechecked (tsconfig includes `**/*.tsx`) and it imports `./actions`, which is not a valid path from repo root (actions live under `app/actions.tsx`). This can fail the TypeScript/Next build.

### Issue Context
This file appears to be a duplicate of `app/page.tsx`, but its location makes its relative imports incorrect.

### Fix Focus Areas
- tsconfig.json[31-37]
- app_page.tsx[1-16]
- app/page.tsx[1-16]

### Suggested fix
- Delete `app_page.tsx` if it was added accidentally, **or** move it under `app/` as `app/page.tsx` (and ensure only one page exists).
- If you intentionally need this file at repo root, fix imports to use absolute aliases (e.g. `import { AI } from '@/app/actions'`) and ensure it won’t be treated as a route unintentionally.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

export const maxDuration = 60

import { MapDataProvider } from '@/components/map/map-data-context'

export default function Page() {
return (
<AI initialAIState={{ chatId: 'new-chat', messages: [] }}>
<MapDataProvider>
<Chat />
</MapDataProvider>
</AI>
)
}
12 changes: 10 additions & 2 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ type ChatProps = {
id?: string // This is the chatId
}

export function Chat({ id }: ChatProps) {
export function Chat({ id: initialId }: ChatProps) {
const [id, setId] = useState(initialId || '')

useEffect(() => {
if (!id || id === 'new-chat') {
setId(crypto.randomUUID())
}
}, [id])
Comment on lines +29 to +36
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.

Action required

2. Chat ids diverge client/server 🐞 Bug ≡ Correctness

On the landing page, AI state initializes with chatId: 'new-chat' while the Chat component
generates a different UUID in local state and updates the URL to /search/{uuid}. Server
persistence uses the AI state’s chatId, and chat-db.saveChat deletes 'new-chat' to let the DB
generate another UUID, causing chat fragmentation and DB writes (e.g., updateDrawingContext) to
target a non-existent chatId.
Agent Prompt
### Issue description
The landing page uses a placeholder AI `chatId` (`'new-chat'`), while the client `Chat` generates a different UUID and updates the URL. Server persistence uses the AI state's `chatId`, and the DB layer deletes `'new-chat'` and generates yet another UUID. This results in inconsistent IDs between URL/client/server/DB and can fragment chats or break DB writes.

### Issue Context
- Client `Chat` generates an id and rewrites the URL.
- AI state still holds `'new-chat'` unless explicitly updated.
- `lib/actions/chat.ts` persists using `chat.id` from AI state.
- `chat-db.saveChat` deletes `'new-chat'` so DB generates a different UUID.
- `updateDrawingContext(chatId, ...)` writes messages keyed by the client id, but `messages.chatId` must reference an existing `chats.id`.

### Fix Focus Areas
- app/page.tsx[8-13]
- components/chat.tsx[29-36]
- components/chat.tsx[80-84]
- app/actions.tsx[574-659]
- lib/actions/chat.ts[90-147]
- lib/actions/chat-db.ts[83-113]

### Suggested fix
Pick a single source of truth for `chatId` and propagate it consistently:
1) **Client-generated UUID becomes authoritative**:
   - In `Chat`, use `useAIState` setter (or another action) to set AI state's `chatId` once the UUID is generated.
   - Stop deleting `'new-chat'` in `chat-db.saveChat` once AI state is updated; instead, ensure DB `chats.id` is created using that UUID.
2) **Or server-generated UUID becomes authoritative**:
   - Create the chat on the server before any message/context writes and return the UUID to the client; update URL/client state with that UUID.

Also ensure `updateDrawingContext` only runs after a `chats` row exists for the chosen `chatId` (or create it first).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


const router = useRouter()
const path = usePathname()
const [messages] = useUIState()
Expand Down Expand Up @@ -70,7 +78,7 @@ export function Chat({ id }: ChatProps) {
}, [])

useEffect(() => {
if (!path.includes('search') && messages.length === 1) {
if (id && id !== 'new-chat' && !path.includes('search') && messages.length === 1) {
window.history.replaceState({}, '', `/search/${id}`)
}
}, [id, path, messages.length]) // OPTIMIZATION: Use messages.length instead of full array
Expand Down
5 changes: 5 additions & 0 deletions lib/actions/chat-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export async function saveChat(chatData: NewChat, messagesData: Omit<NewMessage,
return null;
}

// Handle placeholder 'new-chat' ID from client side
if (chatData.id === 'new-chat') {
delete (chatData as any).id;
}

// Transaction to ensure atomicity
return db.transaction(async (tx) => {
let chatId = chatData.id;
Expand Down
6 changes: 2 additions & 4 deletions lib/auth/get-current-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,14 @@ export async function getSupabaseUserAndSessionOnServer(): Promise<{
},
async set(name: string, value: string, options: CookieOptions): Promise<void> {
try {
const store = await cookieStore;
store.set({ name, value, ...options }); // Set cookie with options
(await cookieStore).set({ name, value, ...options }); // Set cookie with options
} catch (error) {
// console.warn(`[Auth] Failed to set cookie ${name}:`, error);
}
},
async remove(name: string, options: CookieOptions): Promise<void> {
try {
const store = await cookieStore;
store.set({ name, value: '', ...options, maxAge: 0 }); // Delete cookie by setting maxAge to 0
(await cookieStore).set({ name, value: '', ...options, maxAge: 0 }); // Delete cookie by setting maxAge to 0
} catch (error) {
// console.warn(`[Auth] Failed to delete cookie ${name}:`, error);
}
Expand Down