From e63473d522e6d2a229b809c04845a0604a3e8098 Mon Sep 17 00:00:00 2001 From: Howard Date: Fri, 6 Mar 2026 22:28:41 +0800 Subject: [PATCH] Fix broken dashboard: bugs, missing features, and incomplete implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix collections page querying wrong table (userPosts → collections) - Fix inverted filter validation in user post display - Fix collections API returning undefined (arrow fn body) - Fix missing offset validation throw in public_data API - Fix Zod validation (z.email() → z.string().email()) in login - Fix Content-Type header typos (appilcation → application) - Fix admin settings toggles sending wrong values and overwriting state - Fix post edit system (remove invalid metadata export, add tag editing) - Remove debug console.logs and fix typos (instence, geint-sans) - Implement collections list & create pages with API - Implement share link / URL shortener API (was returning 403) - Fix user account page (name save, password reset) Co-Authored-By: Claude Opus 4.6 --- .../app/api/data/create_collection/route.ts | 72 ++++++++++++++ .../app/api/data/create_share_link/route.ts | 93 ++++++++++++++++++- .../app/api/data/get_all_collections/route.ts | 6 +- .../web/src/app/api/data/public_data/route.ts | 1 + .../src/app/api/data/publish/file/route.ts | 2 - apps/web/src/app/api/data/publish/route.ts | 2 +- apps/web/src/app/api/data/settings/route.ts | 10 +- apps/web/src/app/c/[slug]/page.tsx | 4 +- .../src/app/dashboard/collections/client.tsx | 64 +++++++++++++ .../dashboard/collections/create/client.tsx | 84 +++++++++++++++++ .../app/dashboard/collections/create/page.tsx | 18 +++- .../src/app/dashboard/collections/page.tsx | 18 +++- .../src/app/dashboard/posts/create/client.tsx | 1 - .../dashboard/posts/edit/[slug]/client.tsx | 81 ++++++++++------ .../src/app/dashboard/posts/manage/client.tsx | 8 +- .../dashboard/settings/clientComponents.tsx | 24 ++--- .../src/app/dashboard/user/account/client.tsx | 93 +++++++++++++++++-- .../app/dashboard/user/manage_all/client.tsx | 4 +- apps/web/src/app/login/client.tsx | 6 +- apps/web/src/app/search/client.tsx | 2 +- apps/web/src/app/search/page.tsx | 2 +- .../web/src/app/user/[userid]/postDisplay.tsx | 5 +- .../src/components/publicPostsAndVideos.tsx | 2 - apps/web/src/index.css | 2 +- packages/auth/src/index.ts | 1 - 25 files changed, 510 insertions(+), 95 deletions(-) create mode 100644 apps/web/src/app/api/data/create_collection/route.ts create mode 100644 apps/web/src/app/dashboard/collections/client.tsx create mode 100644 apps/web/src/app/dashboard/collections/create/client.tsx diff --git a/apps/web/src/app/api/data/create_collection/route.ts b/apps/web/src/app/api/data/create_collection/route.ts new file mode 100644 index 0000000..cb6950e --- /dev/null +++ b/apps/web/src/app/api/data/create_collection/route.ts @@ -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"); + } + + 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 }, + ); + } +}; diff --git a/apps/web/src/app/api/data/create_share_link/route.ts b/apps/web/src/app/api/data/create_share_link/route.ts index 9f66aa5..1da5ca7 100644 --- a/apps/web/src/app/api/data/create_share_link/route.ts +++ b/apps/web/src/app/api/data/create_share_link/route.ts @@ -1,4 +1,6 @@ import type { NextRequest } from "next/server"; +import { auth } from "@devlogs_hosting/auth"; +import { headers } from "next/headers"; import { dorm, main_schema, @@ -6,9 +8,90 @@ import { 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(); + + // 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, + }); + + 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 }, + ); + } }; diff --git a/apps/web/src/app/api/data/get_all_collections/route.ts b/apps/web/src/app/api/data/get_all_collections/route.ts index 1e8f470..dfb4712 100644 --- a/apps/web/src/app/api/data/get_all_collections/route.ts +++ b/apps/web/src/app/api/data/get_all_collections/route.ts @@ -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) { diff --git a/apps/web/src/app/api/data/public_data/route.ts b/apps/web/src/app/api/data/public_data/route.ts index 7f9c82f..8712660 100644 --- a/apps/web/src/app/api/data/public_data/route.ts +++ b/apps/web/src/app/api/data/public_data/route.ts @@ -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"); diff --git a/apps/web/src/app/api/data/publish/file/route.ts b/apps/web/src/app/api/data/publish/file/route.ts index 3e0e2e7..3c157a4 100644 --- a/apps/web/src/app/api/data/publish/file/route.ts +++ b/apps/web/src/app/api/data/publish/file/route.ts @@ -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", diff --git a/apps/web/src/app/api/data/publish/route.ts b/apps/web/src/app/api/data/publish/route.ts index e58138c..e603a03 100644 --- a/apps/web/src/app/api/data/publish/route.ts +++ b/apps/web/src/app/api/data/publish/route.ts @@ -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 }, ); diff --git a/apps/web/src/app/api/data/settings/route.ts b/apps/web/src/app/api/data/settings/route.ts index a7a0a43..a1c563c 100644 --- a/apps/web/src/app/api/data/settings/route.ts +++ b/apps/web/src/app/api/data/settings/route.ts @@ -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"); } @@ -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"); } @@ -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"); } @@ -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"); } } @@ -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"); } diff --git a/apps/web/src/app/c/[slug]/page.tsx b/apps/web/src/app/c/[slug]/page.tsx index ebea90f..5895bc2 100644 --- a/apps/web/src/app/c/[slug]/page.tsx +++ b/apps/web/src/app/c/[slug]/page.tsx @@ -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(); @@ -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 { diff --git a/apps/web/src/app/dashboard/collections/client.tsx b/apps/web/src/app/dashboard/collections/client.tsx new file mode 100644 index 0000000..d806cb1 --- /dev/null +++ b/apps/web/src/app/dashboard/collections/client.tsx @@ -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); + } + + return ( +
+
+ + + +
+ {isLoading &&

Loading collections...

} + {data && data.length === 0 && ( +

+ No collections yet. Create one to get started. +

+ )} + {data && data.length > 0 && ( +
+ {data.map((collection) => ( +
+
+

{collection.title}

+

/{collection.slug}

+
+ + + +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/app/dashboard/collections/create/client.tsx b/apps/web/src/app/dashboard/collections/create/client.tsx new file mode 100644 index 0000000..f8db5a0 --- /dev/null +++ b/apps/web/src/app/dashboard/collections/create/client.tsx @@ -0,0 +1,84 @@ +"use client"; +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; + +export default function CreateCollectionClient() { + const router = useRouter(); + const [title, setTitle] = useState(""); + const [slug, setSlug] = useState(""); + + const createCollection = useMutation({ + mutationFn: async () => { + const req = await fetch("/api/data/create_collection", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, slug }), + }); + const res = await req.json(); + if (!req.ok) { + throw new Error(res.message || "Failed to create collection"); + } + return res; + }, + onSuccess: () => { + toast.success("Collection created!"); + router.push("/dashboard/collections"); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); + + const handleTitleChange = (value: string) => { + setTitle(value); + setSlug( + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""), + ); + }; + + return ( +
+
+ + handleTitleChange(e.target.value)} + placeholder="My Collection" + className="border rounded px-3 py-2" + /> +
+
+ + setSlug(e.target.value)} + placeholder="my-collection" + className="border rounded px-3 py-2" + /> +

+ URL-friendly identifier. Lowercase letters, numbers, and hyphens only. +

+
+ +
+ ); +} diff --git a/apps/web/src/app/dashboard/collections/create/page.tsx b/apps/web/src/app/dashboard/collections/create/page.tsx index ce2f86e..2fa0303 100644 --- a/apps/web/src/app/dashboard/collections/create/page.tsx +++ b/apps/web/src/app/dashboard/collections/create/page.tsx @@ -1,8 +1,24 @@ +import { redirect } from "next/navigation"; +import { headers } from "next/headers"; +import { auth } from "@devlogs_hosting/auth"; +import CreateCollectionClient from "./client"; + export default async function Page() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + redirect("/login"); + } + return (
Create A Collection -
{" "} +
+
+ +
); } diff --git a/apps/web/src/app/dashboard/collections/page.tsx b/apps/web/src/app/dashboard/collections/page.tsx index b53fe77..8961a38 100644 --- a/apps/web/src/app/dashboard/collections/page.tsx +++ b/apps/web/src/app/dashboard/collections/page.tsx @@ -1,8 +1,24 @@ +import { redirect } from "next/navigation"; +import { headers } from "next/headers"; +import { auth } from "@devlogs_hosting/auth"; +import CollectionsClient from "./client"; + export default async function Page() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + redirect("/login"); + } + return (
Collections -
{" "} +
+
+ +
); } diff --git a/apps/web/src/app/dashboard/posts/create/client.tsx b/apps/web/src/app/dashboard/posts/create/client.tsx index d6b3fe2..6c79621 100644 --- a/apps/web/src/app/dashboard/posts/create/client.tsx +++ b/apps/web/src/app/dashboard/posts/create/client.tsx @@ -160,7 +160,6 @@ export default function Dashboard({ }), }); const res = await req.json(); - console.log(res); if (!res.success) { setIsPending(false); console.error(`ERR_SERVER_RESPOSE: ${res.msg}`); diff --git a/apps/web/src/app/dashboard/posts/edit/[slug]/client.tsx b/apps/web/src/app/dashboard/posts/edit/[slug]/client.tsx index f483b7f..0a7ed50 100644 --- a/apps/web/src/app/dashboard/posts/edit/[slug]/client.tsx +++ b/apps/web/src/app/dashboard/posts/edit/[slug]/client.tsx @@ -1,10 +1,10 @@ -// THIS REALLY NEEDS TO BE DONE :( "use client"; import { PublicPostsAndVideos } from "@/components/publicPostsAndVideos"; -import { main_schema, db, dorm } from "../../../../../../../../packages/db/src"; +import { main_schema } from "../../../../../../../../packages/db/src"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { useState } from "react"; import { Tabs, TabsList, TabsContent, TabsTrigger } from "@/components/ui/tabs"; import { useMutation } from "@tanstack/react-query"; @@ -13,6 +13,7 @@ type Post = typeof main_schema.userPosts.$inferSelect; export default function Client({ orgPost }: { orgPost: Post }) { const [post, setPost] = useState(orgPost); + const [tagInput, setTagInput] = useState(""); const submitRequest = useMutation({ mutationFn: async () => { toast.promise( @@ -41,6 +42,27 @@ export default function Client({ orgPost }: { orgPost: Post }) { ); }, }); + + const tags = (post.tags as string[]) || []; + + const deleteTag = (tag: string) => { + setPost({ ...post, tags: tags.filter((t) => t !== tag) }); + }; + + const addTag = () => { + const trimmed = tagInput.replaceAll(" ", ""); + if (trimmed.length === 0) { + toast.error("This cannot be empty"); + return; + } + if (tags.includes(trimmed)) { + toast.error("This tag is already used in this post."); + return; + } + setPost({ ...post, tags: [...tags, trimmed] }); + setTagInput(""); + }; + return (
@@ -50,18 +72,7 @@ export default function Client({ orgPost }: { orgPost: Post }) { defaultValue={post.status} className="" onValueChange={(vl) => { - setPost({ - postId: post.postId, - type: post.type, - createdAt: post.createdAt, - updatedAt: post.updatedAt, - byUser: post.byUser, - textData: post.textData, - imageUrl: post.imageUrl, - videoUrl: post.videoUrl, - status: vl, - tags: post.tags, - }); + setPost({ ...post, status: vl }); }} > @@ -72,23 +83,37 @@ export default function Client({ orgPost }: { orgPost: Post }) {
+ + tags: + setTagInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === "Space") { + e.preventDefault(); + addTag(); + } + }} + /> + +
+ {tags.map((it: string) => ( + + ))} +