Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 3 additions & 0 deletions client/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import Header from "@/components/Header";

export const metadata = { title: "Admin | Recall" };

// Cache for 5 minutes — prevents hammering Supabase's admin API on every load
export const revalidate = 300;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

export default async function AdminPage() {
// Auth check
const supabase = await createClient();
Expand Down
7 changes: 3 additions & 4 deletions client/src/app/api/bookmarks/[id]/tags/[tagId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { createClient } from "@/lib/supabase/server";
import { getUserFromRequest } from "@/lib/supabase/get-user";

export async function DELETE(
_req: NextRequest,
req: NextRequest,
{ params }: { params: Promise<{ id: string; tagId: string }> }
) {
const { id, tagId } = await params;

const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
const user = await getUserFromRequest(req);
if (!user) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

try {
Expand Down
7 changes: 3 additions & 4 deletions client/src/app/api/bookmarks/[id]/tags/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { createClient } from "@/lib/supabase/server";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { Prisma } from "@/generated/prisma";
import { tagRatelimit } from "@/lib/ratelimit";

Expand All @@ -10,14 +10,13 @@ export async function POST(
) {
const { id } = await params;

const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
const user = await getUserFromRequest(req);
if (!user) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

const { success } = await tagRatelimit.limit(user.id);
if (!success)
return NextResponse.json(
{ error: "too many requests slow down" },
{ error: "too many requests - slow down" },
{ status: 429 },
);

Expand Down
3 changes: 2 additions & 1 deletion client/src/app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
const { success } = await bookmarkRatelimit.limit(user.id);
if (!success)
return NextResponse.json(
{ error: "too many requests slow down" },
{ error: "too many requests - slow down" },
{ status: 429 },
);

Expand Down Expand Up @@ -123,6 +123,7 @@ export async function GET(req: NextRequest) {
const bookmarks = await prisma.bookmark.findMany({
where: { userId: user.id },
orderBy: { createdAt: "desc" },
take: 500, // safety cap — pagination should be added when needed
include: { tags: true },
});
return NextResponse.json(bookmarks);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { NextRequest } from "next/server";

// DELETE /api/collections/[id]/bookmarks/[bookmarkId] — remove a bookmark from a collection
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; bookmarkId: string }> }
) {
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { id: collectionId, bookmarkId } = await params;

// Verify collection belongs to user
const collection = await prisma.collection.findUnique({
where: { id: collectionId },
});
if (!collection || collection.userId !== user.id) {
return Response.json({ error: "not found" }, { status: 404 });
}

try {
await prisma.collectionBookmark.delete({
where: { collectionId_bookmarkId: { collectionId, bookmarkId } },
});
} catch (e: unknown) {
if ((e as { code?: string })?.code === "P2025") {
return Response.json({ error: "not found" }, { status: 404 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return Response.json({ error: "failed to remove bookmark from collection" }, { status: 500 });
}

return new Response(null, { status: 204 });
}
52 changes: 52 additions & 0 deletions client/src/app/api/collections/[id]/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { NextRequest } from "next/server";

// POST /api/collections/[id]/bookmarks — add a bookmark to a collection
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { id: collectionId } = await params;

// Verify collection belongs to user
const collection = await prisma.collection.findUnique({
where: { id: collectionId },
});
if (!collection || collection.userId !== user.id) {
return Response.json({ error: "not found" }, { status: 404 });
}

let body: { bookmarkId?: unknown };
try {
body = await req.json();
} catch {
return Response.json({ error: "invalid json" }, { status: 400 });
}

const bookmarkId =
typeof body.bookmarkId === "string" ? body.bookmarkId.trim() : "";
if (!bookmarkId) {
return Response.json({ error: "bookmarkId is required" }, { status: 400 });
}

// Verify bookmark belongs to user
const bookmark = await prisma.bookmark.findUnique({
where: { id: bookmarkId },
});
if (!bookmark || bookmark.userId !== user.id) {
return Response.json({ error: "bookmark not found" }, { status: 404 });
}

// Upsert to avoid duplicate errors
await prisma.collectionBookmark.upsert({
where: { collectionId_bookmarkId: { collectionId, bookmarkId } },
create: { collectionId, bookmarkId },
update: {},
});

return new Response(null, { status: 204 });
}
26 changes: 26 additions & 0 deletions client/src/app/api/collections/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { NextRequest } from "next/server";

// DELETE /api/collections/[id]
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { id } = await params;

const collection = await prisma.collection.findUnique({ where: { id } });
if (!collection || collection.userId !== user.id) {
return Response.json({ error: "not found" }, { status: 404 });
}

try {
await prisma.collection.delete({ where: { id } });
} catch {
return Response.json({ error: "failed to delete collection" }, { status: 500 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return new Response(null, { status: 204 });
}
43 changes: 43 additions & 0 deletions client/src/app/api/collections/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { NextRequest } from "next/server";

// GET /api/collections — list all collections for the user
export async function GET(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const collections = await prisma.collection.findMany({
where: { userId: user.id },
orderBy: { createdAt: "asc" },
});

return Response.json(collections);
}

// POST /api/collections — create a new collection
export async function POST(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

let body: { name?: unknown };
try {
body = await req.json();
} catch {
return Response.json({ error: "invalid json" }, { status: 400 });
}

const name = typeof body.name === "string" ? body.name.trim() : "";
if (!name) return Response.json({ error: "name is required" }, { status: 400 });

let collection;
try {
collection = await prisma.collection.create({
data: { name, userId: user.id },
});
Comment on lines +30 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject duplicate collection names per user.

This still allows the same account to create two collections with the same visible name. DashboardClient renders only col.name, so duplicate pills and delete confirmations become ambiguous very quickly. Please block duplicates here and back that up with user-scoped uniqueness instead of inserting another row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/api/collections/route.ts` around lines 30 - 38, Check for an
existing collection with the same name for the current user before creating and
enforce user-scoped uniqueness in the database schema: in route.ts, query
prisma.collection.findFirst or findUnique with where: { userId: user.id, name }
and return a 409/400 JSON error if found, and also add a unique constraint to
the Collection model (@@unique([userId, name]) or equivalent) in the Prisma
schema and run a migration; finally, handle Prisma unique-constraint errors
around prisma.collection.create (catch the specific error code and return the
same 409/400) so creating duplicates is prevented both at the app and DB level.

} catch {
return Response.json({ error: "failed to create collection" }, { status: 500 });
}

return Response.json(collection, { status: 201 });
}
22 changes: 15 additions & 7 deletions client/src/app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,26 @@ import { redirect } from "next/navigation";
export const metadata = { title: "Your Bookmarks | Recall" };

async function DashboardLoader({ userId }: { userId: string }) {
const bookmarks = await prisma.bookmark.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
include: { tags: true },
});
const [bookmarks, collections] = await Promise.all([
prisma.bookmark.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
include: { tags: true, collections: { select: { collectionId: true } } },
}),
prisma.collection.findMany({
where: { userId },
orderBy: { createdAt: "asc" },
select: { id: true, name: true },
}),
]);

const serialized = bookmarks.map((b) => ({
const serialized = bookmarks.map(({ collections: cols, ...b }) => ({
...b,
createdAt: b.createdAt.toISOString(),
collectionIds: cols.map((c) => c.collectionId),
}));

return <DashboardClient bookmarks={serialized} />;
return <DashboardClient bookmarks={serialized} initialCollections={collections} />;
}

function DashboardSkeleton() {
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function AuthForm() {
)}
{slow && !error && (
<p className="text-xs text-[var(--text-muted)]" aria-live="polite">
Still working this can take a few seconds on first load…
Still working - this can take a few seconds on first load…
</p>
)}

Expand Down
2 changes: 1 addition & 1 deletion client/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="en" suppressHydrationWarning data-scroll-behavior="smooth">
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
Expand Down
Loading
Loading