Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Admin Pack Feature to Improve Dashboard Functionality #47

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
8 changes: 2 additions & 6 deletions apps/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dotenv from "dotenv";

import paymentRoutes from "./routes/payment.routes";
import { router as webhookRouter } from "./routes/webhook.routes";
import { router as packRoutes } from "./routes/packs.routes";

const IMAGE_GEN_CREDITS = 1;
const TRAIN_MODEL_CREDITS = 20;
Expand Down Expand Up @@ -223,13 +224,7 @@ app.post("/pack/generate", authMiddleware, async (req, res) => {
});
});

app.get("/pack/bulk", async (req, res) => {
const packs = await prismaClient.packs.findMany({});

res.json({
packs,
});
});

app.get("/image/bulk", authMiddleware, async (req, res) => {
const ids = req.query.ids as string[];
Expand Down Expand Up @@ -356,6 +351,7 @@ app.post("/fal-ai/webhook/image", async (req, res) => {

app.use("/payment", paymentRoutes);
app.use("/api/webhook", webhookRouter);
app.use("/pack", packRoutes);

app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
Expand Down
24 changes: 23 additions & 1 deletion apps/backend/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare global {
interface Request {
userId?: string;
user?: {
role?: "admin" | "user";
email: string;
};
}
Expand All @@ -19,14 +20,15 @@ export async function authMiddleware(
next: NextFunction
) {
try {
console.log("Authenticating request");
const authHeader = req.headers["authorization"];
const token = authHeader?.split(" ")[1];

if (!token) {
res.status(401).json({ message: "No token provided" });
return;
}

// Debug logs
console.log("Received token:", token);

Expand Down Expand Up @@ -62,6 +64,7 @@ export async function authMiddleware(

// Fetch user details from Clerk
const user = await clerkClient.users.getUser(userId);
console.log("User details:", user);
const primaryEmail = user.emailAddresses.find(
(email) => email.id === user.primaryEmailAddressId
);
Expand All @@ -70,11 +73,13 @@ export async function authMiddleware(
console.error("No email found for user");
res.status(400).json({ message: "User email not found" });
return;

}

// Attach the user ID and email to the request
req.userId = userId;
req.user = {
role: (user.publicMetadata.role as "admin" | "user") || "user",
email: primaryEmail.emailAddress,
};

Expand All @@ -99,3 +104,20 @@ export async function authMiddleware(
return;
}
}

export async function adminMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
console.log("Checking user role");
await authMiddleware(req, res, () => {
console.log("User role:", req.user?.role);
if (req.user?.role !== "admin") {
res.status(403).json({ error: "Unauthorized" });
return;
}

next();
});
}
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"start": "bun run index.ts",
"dev": "bun run index.ts"
"dev": "bun run --watch index.ts"
},
"devDependencies": {
"@types/bun": "latest"
Expand Down
119 changes: 119 additions & 0 deletions apps/backend/routes/packs.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { prismaClient } from "db";
import { Router } from "express";
import { adminMiddleware } from "../middleware";
import { packSchema } from "common/types";

export const router: Router = Router();

// Fetch all packs
router.get("/bulk", async (req, res) => {
try {
const packs = await prismaClient.packs.findMany();
res.status(200).json({ packs });
} catch (error) {
res.status(500).json({ error: "Error fetching packs" });
}
});

// Fetch a pack by ID
router.get("/:id", async (req, res) => {
try {
const pack = await prismaClient.packs.findUnique({
where: {
id: req.params.id,
},
include: {
prompts: true,
},
});

if (!pack) {
res.status(404).json({ error: "Pack not found" });
return;
}

res.status(200).json({ pack });
} catch (error) {
res.status(500).json({ error: "Error fetching pack" });
}
});

// Create a new pack
router.post("/",adminMiddleware, async (req, res) => {
try {
const parsedata = packSchema.safeParse(req.body);
if (!parsedata.success) {
res.status(400).json({ error: parsedata.error });
return;
}

const newPack = await prismaClient.packs.create({
data: {
name: parsedata.data.name,
description: parsedata.data.description,
imageUrl1: parsedata.data.imageUrl1,
imageUrl2: parsedata.data.imageUrl2,
prompts: {
create: parsedata.data.prompts.map((prompt) => ({
prompt: prompt.prompt,
})),
},
},
});

res.status(201).json({ pack: newPack });
} catch (error) {
res.status(500).json({ error: "Error creating pack" });
}
});

// Update a pack by ID
router.put("/:id",adminMiddleware, async (req, res) => {
try {
const parsedata = packSchema.safeParse(req.body);
if (!parsedata.success) {
res.status(400).json({ error: parsedata.error });
return;
}
const updatedPack = await prismaClient.packs.update({
where: {
id: parsedata.data.id,
},
data: {
name: parsedata.data.name,
description: parsedata.data.description,
imageUrl1: parsedata.data.imageUrl1,
imageUrl2: parsedata.data.imageUrl2,
prompts: {
upsert: parsedata.data.prompts.map((prompt) => ({
where: { id: prompt.id },
update: { prompt: prompt.prompt },
create: { prompt: prompt.prompt },
})),
},
},
});

res.status(200).json({
pack: updatedPack,
});
} catch (error) {
res.status(500).json({ error: "Error updating pack" });
}
});

// Delete a pack by ID
router.delete("/:id",adminMiddleware, async (req, res) => {
try {
await prismaClient.packs.delete({
where: {
id: req.params.id,
},
});

res.status(204).json({ message: "Pack deleted" });
} catch (error) {
console.error(error);
res.status(500).json({ error: "Error deleting pack" });
}
});
3 changes: 3 additions & 0 deletions apps/studio/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env
12 changes: 12 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "studio",
"version": "0.0.0",
"scripts": {
"dev": "prisma studio --schema ../../packages/db/prisma/schema.prisma --port 3005",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"devDependencies": {
"prisma": "6.3.1"
}
}
5 changes: 5 additions & 0 deletions apps/studio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "@repo/typescript-config/nextjs.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
File renamed without changes.
12 changes: 12 additions & 0 deletions apps/web/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Appbar } from "@/components/Appbar";
import { Footer } from "@/components/Footer";

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="relative flex min-h-screen flex-col">
<Appbar />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { UploadModal } from "@/components/ui/upload"
import { useState } from "react"
import { TrainModelInput } from "common/inferred"
import axios from "axios"
import { BACKEND_URL } from "../config"
import { BACKEND_URL } from "../../config"
import { useRouter } from "next/navigation"
import { useAuth } from "@clerk/nextjs"

Expand Down Expand Up @@ -53,7 +53,6 @@ export default function Train() {
const response = await axios.post(`${BACKEND_URL}/ai/training`, input, {
headers: {
Authorization

: `Bearer ${token}`
}
});
Expand Down
71 changes: 71 additions & 0 deletions apps/web/app/admin/packs/[packId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";
import { useState, useEffect,use } from "react";
import axios from "axios";

import { PackForm } from "@/components/packs/pack-form";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Pack } from "@/types";

export default function Page({ params }:{params:{packId:string}}) {
const { packId } = use(params);
const [pack, setPack] = useState<Pack | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);

useEffect(() => {
const fetchPack = async () => {
setIsLoading(true);
setIsError(false);
try {
const response = await axios.get(`/pack/${packId}`);
console.log(response.data.pack);
setPack(response.data.pack);
} catch (error) {
console.error("Failed to fetch pack:", error);
setIsError(true);
} finally {
setIsLoading(false);
}
};
fetchPack();
}, [packId]);

if (isLoading) {
return <div>Loading pack...</div>;
}
if (isError) {
return <div>Failed to load the pack. Please try again later.</div>;
}

return (
<div className="container mx-auto px-4 py-6 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sticky bg-background top-0 z-10 py-4">
<div className="space-y-1">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/admin">Admin</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/admin/packs">Packs</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{pack?.name || "Pack"}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</div>
{pack && <PackForm pack={pack} />}
</div>
);
}
42 changes: 42 additions & 0 deletions apps/web/app/admin/packs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Suspense } from "react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";

import { PackGrid } from "@/components/packs/pack-grid";
import { PackGridSkeleton } from "@/components/packs/pack-grid-skeleton";
import { AddPackButton } from "@/components/packs/add-pack-button";

export default function Page() {
return (
<div className="container mx-auto px-4 py-6 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sticky bg-background top-0 z-10 py-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/admin">Admin</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Packs</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<AddPackButton />
</div>

<div>
<h1 className="text-2xl font-bold tracking-tight">Content Packs</h1>
</div>

<Suspense fallback={<PackGridSkeleton />}>
<PackGrid />
</Suspense>
</div>
);
}
Loading