Skip to content
Merged
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.41.0",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^1.17.0",
"motion": "^12.40.0",
"next": "^15.2.3",
Expand All @@ -55,6 +56,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.0.15",
"@types/better-sqlite3": "^7.6.13",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
95 changes: 95 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 38 additions & 5 deletions src/app/mock/[projectId]/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type NextRequest, NextResponse } from "next/server";
import { db } from "~/server/db";
import { endpoints_table, projects_table } from "~/server/db/schema";
import { auth_configs_table, endpoints_table, projects_table } from "~/server/db/schema";
import { and, eq } from "drizzle-orm";
import { getData, generateAndSaveData } from "~/lib/endpoint-data-store";
import jwt from "jsonwebtoken";

interface Params {
projectId: string;
Expand All @@ -21,7 +22,7 @@ function matchPath(pattern: string, incoming: string): boolean {
return regex.test(incoming);
}

async function handler(req: NextRequest, { params }: { params: Params }) {
async function handler(req: NextRequest, { params }: { params: Params | Promise<Params> }) {
const { projectId, path } = await params;
const incomingPath = "/" + path.join("/");
const method = req.method;
Expand All @@ -33,7 +34,6 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
const project = await db.query.projects_table.findFirst({
where: eq(projects_table.id, projectId),
});

if (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
Expand All @@ -46,13 +46,46 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
const candidates = await db.query.endpoints_table.findMany({
where: and(eq(endpoints_table.projectId, projectId), eq(endpoints_table.method, method)),
});

const endpoint = candidates.find((e) => matchPath(e.path, strippedPath));

if (!endpoint) {
return NextResponse.json({ error: "No matching endpoint found" }, { status: 404 });
}

const authConfig = await db.query.auth_configs_table.findFirst({
where: eq(auth_configs_table.endpointId, endpoint.id),
});

if (authConfig?.isLoginEndpoint) {
const data =
getData(projectId, endpoint.id) ??
generateAndSaveData(
projectId,
endpoint.id,
endpoint.responseSchema,
endpoint.responseCount ?? 1,
);

const token = jwt.sign({ sub: endpoint.id }, project.secret, {
expiresIn: authConfig.tokenExpirySeconds,
});

return NextResponse.json({ token, data }, { status: endpoint.statusCode });
}

if (authConfig?.requiresAuth) {
const header = req.headers.get("authorization");
const token = header?.startsWith("Bearer ") ? header.slice(7) : null;

if (!token) {
return NextResponse.json({ error: "Missing token" }, { status: 401 });
}
try {
jwt.verify(token, project.secret);
} catch {
return NextResponse.json({ error: "Invalid or expired token" }, { status: 401 });
}
}

if (endpoint.delayMs > 0) {
await new Promise((r) => setTimeout(r, endpoint.delayMs));
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function Codebar({ lang, code }: CodebarProps) {
};

return (
<div className="flex h-9 items-center justify-between rounded-t-lg border-b border-zinc-800 px-3 bg-zinc-900 shrink-0">
<div className="flex h-9 items-center justify-between border-b border-zinc-800 px-3 bg-zinc-900 shrink-0">
<span className="text-xs text-zinc-400 font-mono">{lang}</span>
<Button
variant="ghost"
Expand Down Expand Up @@ -69,10 +69,10 @@ export function CodeBlock({
showLanguage={false}
style={{ "--max-height": maxHeight } as React.CSSProperties} // Pass maxHeight safely as a CSS Variable
className={cn(
"font-mono text-xs flex-1",
"font-mono text-xs flex-1 rounded-none",
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-700 hover:scrollbar-thumb-zinc-500",
"[&>pre]:my-0 [&>pre]:p-4 [&>pre]:overflow-x-auto",
"[&>pre]:max-h-[var(--max-height)]", // Cleanly bind variable to Tailwind target
"[&>pre]:my-0 [&>pre]:p-4 [&>pre]:overflow-x-auto [&>pre]:rounded-none",
"[&>pre]:max-h-[var(--max-height)]",
)}
>
{code}
Expand Down
1 change: 0 additions & 1 deletion src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export function Toaster() {
position="top-right"
closeButton
richColors
expand
toastOptions={{
classNames: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
Expand Down
11 changes: 7 additions & 4 deletions src/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row",
className,
)}
{...props}
/>
);
}

const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
Expand Down Expand Up @@ -51,10 +54,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className,
)}
{...props}
Expand Down
41 changes: 41 additions & 0 deletions src/hooks/use-auth-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { api } from "~/trpc/react";
import { toast } from "sonner";

function invalidateAuthConfigQueries(utils: ReturnType<typeof api.useUtils>) {
void utils.authConfig.getByEndpoint.invalidate();
void utils.endpoint.getByProject.invalidate();
void utils.endpoint.getById.invalidate();
}

export function useAuthConfig(endpointId: string | null | undefined) {
return api.authConfig.getByEndpoint.useQuery(
{ endpointId: endpointId ?? "" },
{ enabled: !!endpointId },
);
}

export function useUpsertAuthConfig() {
const utils = api.useUtils();
return api.authConfig.upsert.useMutation({
onSuccess: () => {
invalidateAuthConfigQueries(utils);
toast.success("Authentication settings saved");
},
onError: (error) => {
toast.error(error.message || "Failed to save authentication settings");
},
});
}

export function useDeleteAuthConfig() {
const utils = api.useUtils();
return api.authConfig.delete.useMutation({
onSuccess: () => {
invalidateAuthConfigQueries(utils);
toast.success("Authentication settings reset");
},
onError: (error) => {
toast.error(error.message || "Failed to delete authentication settings");
},
});
}
Loading
Loading