Skip to content
Closed
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
72 changes: 72 additions & 0 deletions apps/web/src/app/api/data/create_collection/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
db,
dorm,
main_schema,
} from "../../../../../../../packages/db/src/index";
import { headers } from "next/headers";
import { auth } from "@devlogs_hosting/auth";

export const POST = async (req: Request) => {
let statusCode;
try {
const header = await headers();
const session = await auth.api.getSession({
headers: header,
});
if (!session) {
statusCode = 401;
throw new Error("ERR_NOT_LOGGED_IN");
}

const body = await req.json();
const { title, slug } = body;

if (!title || !slug) {
statusCode = 400;
throw new Error("Title and slug are required");
}

const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (!slugRegex.test(slug)) {
statusCode = 400;
throw new Error(
"Slug must be lowercase alphanumeric with hyphens only",
);
}

const existing = await db
.select()
.from(main_schema.collections)
.where(dorm.eq(main_schema.collections.slug, slug));

if (existing.length > 0) {
statusCode = 409;
throw new Error("A collection with this slug already exists");
}
Comment on lines +37 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The slug pre-check is still race-prone.

packages/db/src/schema/main.ts:69-80 already enforces collections.slug as UNIQUE, so two concurrent requests can both pass the select at Lines 37-40 and the loser then turns into a generic 500 from the catch block. Please make the insert the source of truth and translate duplicate-key failures back to 409.

Also applies to: 49-55

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

In `@apps/web/src/app/api/data/create_collection/route.ts` around lines 37 - 45,
The current pre-check using
db.select(...).where(dorm.eq(main_schema.collections.slug, slug)) is race-prone;
instead let the insert be authoritative and catch unique-constraint violations
from the insert into main_schema.collections, translating that specific
duplicate-key error into a 409 with the message "A collection with this slug
already exists". Remove or treat the select pre-check as advisory, perform the
insert directly, and update the existing catch/error-handling around the insert
to detect the DB unique-constraint error for main_schema.collections.slug (and
return statusCode = 409) while allowing other errors to bubble as before; apply
the same change to the second similar block referenced (lines 49-55).


const collectionId = crypto.randomUUID();

await db.insert(main_schema.collections).values({
collectionId,
title,
slug,
items: {},
byUser: session.user.id,
});

return Response.json({
success: true,
data: { collectionId, title, slug },
message: "Collection created successfully",
});
} catch (e: any) {
return Response.json(
{
success: false,
data: null,
message: e.message,
},
{ status: statusCode || 500 },
);
}
};
93 changes: 88 additions & 5 deletions apps/web/src/app/api/data/create_share_link/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,97 @@
import type { NextRequest } from "next/server";
import { auth } from "@devlogs_hosting/auth";
import { headers } from "next/headers";
import {
dorm,
main_schema,
auth_schema,
db,
} from "../../../../../../../packages/db/src/index";

export const POST = (request: NextRequest) => {
const body = request.json();
return new Response("ok", {
status: 403,
});
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

function generateSlug(length = 8) {
let slug = "";
for (let i = 0; i < length; i++) {
slug += characters.charAt(Math.floor(Math.random() * characters.length));
}
return slug;
}

export const POST = async (request: NextRequest) => {
try {
const body: any = await request.json();
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return Response.json(
{ success: false, msg: "Authentication required" },
{ status: 401 },
);
}
const userId = session.session.userId;

// Check if user is banned
const user = await db
.select({ banned: auth_schema.user.banned })
.from(auth_schema.user)
.where(dorm.eq(auth_schema.user.id, userId))
.limit(1);

if (user[0]?.banned) {
return Response.json(
{
success: false,
msg: "You have been banned by the instance admins.",
},
{ status: 403 },
);
}

const { targetUrl, customSlug } = body;

if (!targetUrl || typeof targetUrl !== "string") {
return Response.json(
{ success: false, msg: "targetUrl is required" },
{ status: 400 },
);
}

const urlSlug = customSlug || generateSlug();
Comment on lines +53 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing validation for targetUrl format and customSlug sanitization.

  1. targetUrl is only checked for existence but not validated as a proper URL. Invalid URLs could be stored.
  2. customSlug is used directly without sanitization - special characters, spaces, or excessively long values could cause issues.
🛡️ Proposed validation improvements
     const { targetUrl, customSlug } = body;

     if (!targetUrl || typeof targetUrl !== "string") {
       return Response.json(
         { success: false, msg: "targetUrl is required" },
         { status: 400 },
       );
     }

+    // Validate URL format
+    try {
+      new URL(targetUrl);
+    } catch {
+      return Response.json(
+        { success: false, msg: "Invalid URL format" },
+        { status: 400 },
+      );
+    }
+
+    // Validate customSlug if provided
+    if (customSlug) {
+      if (typeof customSlug !== "string" || !/^[a-zA-Z0-9_-]{1,50}$/.test(customSlug)) {
+        return Response.json(
+          { success: false, msg: "Invalid slug format. Use only letters, numbers, hyphens, and underscores (max 50 chars)" },
+          { status: 400 },
+        );
+      }
+    }
+
     const urlSlug = customSlug || generateSlug();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/data/create_share_link/route.ts` around lines 53 - 62,
Validate that targetUrl is a well-formed URL and sanitize/validate customSlug
before using it: parse/validate targetUrl (e.g., via the URL constructor or a
strict regex) and return a 400 response if invalid; for customSlug, trim it,
enforce a max length (e.g., 64 chars), allow only safe characters (a-z0-9 and
hyphens/underscores) and reject or normalize anything else (or fall back to
generateSlug()), and ensure generateSlug() is still used when customSlug is
absent or invalid; apply these checks where targetUrl and customSlug are read in
the create_share_link route handler.


// Check if slug already exists
const existing = await db
.select()
.from(main_schema.urlShorter)
.where(dorm.eq(main_schema.urlShorter.urlSlug, urlSlug))
.limit(1);

if (existing.length > 0) {
return Response.json(
{ success: false, msg: "Slug already taken" },
{ status: 409 },
);
}

await db.insert(main_schema.urlShorter).values({
urlSlug,
targetUrl,
byUser: userId,
});
Comment on lines +64 to +82
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Race condition: check-then-insert pattern may cause confusing errors.

Two concurrent requests with the same customSlug can both pass the existence check before either inserts. The DB's UNIQUE constraint (per schema) will reject the second insert, but the error will bubble up as a 500 with a database error message rather than a clean 409 conflict response.

Consider catching the unique constraint violation and returning a proper 409:

🛡️ Proposed fix to handle constraint violation
-    await db.insert(main_schema.urlShorter).values({
-      urlSlug,
-      targetUrl,
-      byUser: userId,
-    });
+    try {
+      await db.insert(main_schema.urlShorter).values({
+        urlSlug,
+        targetUrl,
+        byUser: userId,
+      });
+    } catch (insertError: any) {
+      // Handle unique constraint violation (concurrent insert race)
+      if (insertError.code === "23505") {
+        return Response.json(
+          { success: false, msg: "Slug already taken" },
+          { status: 409 },
+        );
+      }
+      throw insertError;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if slug already exists
const existing = await db
.select()
.from(main_schema.urlShorter)
.where(dorm.eq(main_schema.urlShorter.urlSlug, urlSlug))
.limit(1);
if (existing.length > 0) {
return Response.json(
{ success: false, msg: "Slug already taken" },
{ status: 409 },
);
}
await db.insert(main_schema.urlShorter).values({
urlSlug,
targetUrl,
byUser: userId,
});
// Check if slug already exists
const existing = await db
.select()
.from(main_schema.urlShorter)
.where(dorm.eq(main_schema.urlShorter.urlSlug, urlSlug))
.limit(1);
if (existing.length > 0) {
return Response.json(
{ success: false, msg: "Slug already taken" },
{ status: 409 },
);
}
try {
await db.insert(main_schema.urlShorter).values({
urlSlug,
targetUrl,
byUser: userId,
});
} catch (insertError: any) {
// Handle unique constraint violation (concurrent insert race)
if (insertError.code === "23505") {
return Response.json(
{ success: false, msg: "Slug already taken" },
{ status: 409 },
);
}
throw insertError;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/data/create_share_link/route.ts` around lines 64 - 82,
The check-then-insert for main_schema.urlShorter with urlSlug can race; wrap the
db.insert(main_schema.urlShorter).values(...) in a try/catch and handle
unique-constraint failures by returning the same 409 Response.json({ success:
false, msg: "Slug already taken" }, { status: 409 }); detect the constraint
violation by inspecting the DB error (e.g., Postgres code '23505' or the
error.constraint/name/message) so legitimate unique-violation errors map to 409
while other errors rethrow or return a 500.


return Response.json({
success: true,
msg: "",
urlSlug,
shortUrl: `/u/${urlSlug}`,
});
} catch (e: any) {
console.error(e);
return Response.json(
{ success: false, msg: e.message },
{ status: 500 },
);
}
Comment on lines +90 to +96
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid exposing internal error messages to clients.

Returning e.message directly (line 93) could leak database errors, stack traces, or internal implementation details. Return a generic message to the client while keeping the detailed logging server-side.

🛡️ Proposed fix
   } catch (e: any) {
     console.error(e);
     return Response.json(
-      { success: false, msg: e.message },
+      { success: false, msg: "An unexpected error occurred" },
       { status: 500 },
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e: any) {
console.error(e);
return Response.json(
{ success: false, msg: e.message },
{ status: 500 },
);
}
} catch (e: any) {
console.error(e);
return Response.json(
{ success: false, msg: "An unexpected error occurred" },
{ status: 500 },
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/data/create_share_link/route.ts` around lines 90 - 96,
The catch block in create_share_link route returns e.message to the client which
can leak internals; instead, log the full error server-side (using console.error
or process logger) and return a generic Response.json payload like { success:
false, msg: "Internal server error" } with status 500; update the catch in
route.ts (the catch surrounding the createShareLink handler / Response.json
call) to remove e.message from the response while preserving detailed logging.

};
6 changes: 3 additions & 3 deletions apps/web/src/app/api/data/get_all_collections/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export const GET = async () => {
.from(main_schema.collections)
.where(dorm.eq(main_schema.collections.byUser, session.user.id));
return Response.json({
data: getCollections.map((i) => {
(i.collectionId, i.slug, i.title);
}),
data: getCollections.map((i) => ({
collectionId: i.collectionId, slug: i.slug, title: i.title,
})),
message: "",
});
} catch (e: any) {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/api/data/public_data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const GET = async (request: NextRequest) => {
}

if (!/^\d+$/.test(offset)) {
throw new Error("ERR_OFFSET_PARAM_NOT_A_NUMBER");
}
if (!Number.isSafeInteger(Number(offset))) {
throw new Error("ERR_OFFSET_PARAM_NOT_A_SAFE_INTEGER");
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/app/api/data/publish/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ export const POST = async (request: NextRequest) => {
},
});
const result = await upload.done();
console.log(result);
console.log(`Successfully uploaded: ${fsName}`);

return Response.json({
msg: "File uploaded successfully",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/api/data/publish/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const POST = async (request: NextRequest) => {
return Response.json(
{
success: false,
msg: "You have been banned by the instence admins.",
msg: "You have been banned by the instance admins.",
},
{ status: 403 },
);
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/app/api/data/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const POST = async (request: NextRequest) => {
}
return Response.json({ success: true, msg: "Deleted User" });
} catch (e: any) {
console.log(e);
console.error(e);
statusCode = 403;
throw new Error(e.message || "ERR_GENERIC");
}
Expand Down Expand Up @@ -293,7 +293,7 @@ export const POST = async (request: NextRequest) => {
.where(dorm.eq(main_schema.userPosts.byUser, body.user));
return Response.json({ success: true, msg: "Banned User" });
} catch (e: any) {
console.log(e);
console.error(e);
statusCode = 500;
throw new Error(e.message || "ERR_GENERIC");
}
Expand All @@ -317,7 +317,7 @@ export const POST = async (request: NextRequest) => {
msg: "Revoked the user's sessions",
});
} catch (e: any) {
console.log(e);
console.error(e);
statusCode = 500;
throw new Error(e.message || "ERR_GENERIC");
}
Expand All @@ -335,7 +335,7 @@ export const POST = async (request: NextRequest) => {
headers: await headers(),
});
} catch (e: any) {
console.log(e);
console.error(e);
throw new Error(e.message || "ERR_GENERIC");
}
}
Expand All @@ -357,7 +357,7 @@ export const POST = async (request: NextRequest) => {
{ status: 200 },
);
} catch (e: any) {
console.log(e);
console.error(e);
statusCode = 500;
throw new Error(e.message || "ERR_GENERIC");
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/c/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function Page(props: {
const content: (typeof main_schema.collections.$inferSelect)[] = await db
.select()
.from(main_schema.collections)
.where(dorm.eq(main_schema.userPosts.postId, slug));
.where(dorm.eq(main_schema.collections.slug, slug));

if (content.length === 0) {
notFound();
Expand Down Expand Up @@ -64,7 +64,7 @@ export async function generateMetadata({
const content: (typeof main_schema.collections.$inferSelect)[] = await db
.select()
.from(main_schema.collections)
.where(dorm.eq(main_schema.userPosts.postId, resolvedParams.slug));
.where(dorm.eq(main_schema.collections.slug, resolvedParams.slug));

if (content.length === 0) {
return {
Expand Down
64 changes: 64 additions & 0 deletions apps/web/src/app/dashboard/collections/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import Link from "next/link";
import type { Route } from "next";
import { Button } from "@/components/ui/button";

export default function CollectionsClient() {
const { data, isLoading, error } = useQuery({
queryKey: ["collections"],
queryFn: async () => {
const req = await fetch("/api/data/get_all_collections");
const res = await req.json();
if (!req.ok) {
throw new Error(res.message || "Failed to fetch collections");
}
return res.data as {
collectionId: string;
slug: string;
title: string;
}[];
},
});

if (error) {
toast.error(error.message);
}
Comment on lines +25 to +27
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Toast called during render will fire repeatedly.

Calling toast.error() directly in the component body means it will execute on every re-render while error is truthy, potentially showing duplicate toasts. Move this to a useEffect to fire only once when the error state changes.

🐛 Proposed fix using useEffect
+"use client";
+import { useEffect } from "react";
 import { useQuery } from "@tanstack/react-query";
 import { toast } from "sonner";
 import Link from "next/link";
 import type { Route } from "next";
 import { Button } from "@/components/ui/button";

 export default function CollectionsClient() {
   const { data, isLoading, error } = useQuery({
     queryKey: ["collections"],
     queryFn: async () => {
       const req = await fetch("/api/data/get_all_collections");
       const res = await req.json();
       if (!req.ok) {
         throw new Error(res.message || "Failed to fetch collections");
       }
       return res.data as {
         collectionId: string;
         slug: string;
         title: string;
       }[];
     },
   });

-  if (error) {
-    toast.error(error.message);
-  }
+  useEffect(() => {
+    if (error) {
+      toast.error(error.message);
+    }
+  }, [error]);

   return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error) {
toast.error(error.message);
}
"use client";
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import Link from "next/link";
import type { Route } from "next";
import { Button } from "@/components/ui/button";
export default function CollectionsClient() {
const { data, isLoading, error } = useQuery({
queryKey: ["collections"],
queryFn: async () => {
const req = await fetch("/api/data/get_all_collections");
const res = await req.json();
if (!req.ok) {
throw new Error(res.message || "Failed to fetch collections");
}
return res.data as {
collectionId: string;
slug: string;
title: string;
}[];
},
});
useEffect(() => {
if (error) {
toast.error(error.message);
}
}, [error]);
return (
// Rest of the JSX...
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/dashboard/collections/client.tsx` around lines 25 - 27, The
component is calling toast.error(error.message) during render which will trigger
on every re-render while error is truthy; move this logic into a useEffect so
the toast fires only once when error changes: add import of useEffect (if
missing), create a useEffect hook that depends on [error] and inside it check if
(error) then call toast.error(error.message); keep the original error variable
and toast.error call but remove the direct call from the component body to
prevent duplicate toasts.


return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<Link href={"/dashboard/collections/create" as Route}>
<Button>Create Collection</Button>
</Link>
</div>
{isLoading && <p className="text-gray-500">Loading collections...</p>}
{data && data.length === 0 && (
<p className="text-gray-500 italic">
No collections yet. Create one to get started.
</p>
)}
{data && data.length > 0 && (
<div className="flex flex-col gap-2">
{data.map((collection) => (
<div
key={collection.collectionId}
className="border rounded-lg p-4 flex items-center justify-between"
>
<div>
<h3 className="font-medium">{collection.title}</h3>
<p className="text-sm text-gray-500">/{collection.slug}</p>
</div>
<Link
href={`/c/${collection.slug}` as Route}
>
<Button variant="outline">View</Button>
</Link>
</div>
))}
</div>
)}
</div>
);
}
Loading