Browse Source

Api changes

Owen Schwartz 8 months ago
parent
commit
0fa3382cda

+ 9 - 7
server/index.ts

@@ -36,13 +36,15 @@ app.prepare().then(() => {
     externalServer.use(cors());
     externalServer.use(cors());
     externalServer.use(cookieParser());
     externalServer.use(cookieParser());
     externalServer.use(express.json());
     externalServer.use(express.json());
-    externalServer.use(
-        rateLimitMiddleware({
-            windowMin: 1,
-            max: 100,
-            type: "IP_ONLY",
-        }),
-    );
+    if (!dev) {
+        externalServer.use(
+            rateLimitMiddleware({
+                windowMin: 1,
+                max: 100,
+                type: "IP_ONLY",
+            }),
+        );
+    }
 
 
     const prefix = `/api/v1`;
     const prefix = `/api/v1`;
     externalServer.use(prefix, unauthenticated);
     externalServer.use(prefix, unauthenticated);

+ 2 - 1
server/routers/resource/createResource.ts

@@ -8,9 +8,10 @@ import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
 import { eq, and } from 'drizzle-orm';
 import { eq, and } from 'drizzle-orm';
+import stoi from '@server/utils/stoi';
 
 
 const createResourceParamsSchema = z.object({
 const createResourceParamsSchema = z.object({
-    siteId: z.string().transform(Number).pipe(z.number().int().positive()),
+            siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()),
     orgId: z.string()
     orgId: z.string()
 });
 });
 
 

+ 5 - 2
server/routers/resource/listResources.ts

@@ -13,10 +13,11 @@ import createHttpError from "http-errors";
 import { sql, eq, or, inArray, and, count } from "drizzle-orm";
 import { sql, eq, or, inArray, and, count } from "drizzle-orm";
 import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
 import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
 import logger from "@server/logger";
 import logger from "@server/logger";
+import stoi from "@server/utils/stoi";
 
 
 const listResourcesParamsSchema = z
 const listResourcesParamsSchema = z
     .object({
     .object({
-        siteId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
+        siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()),
         orgId: z.string().optional(),
         orgId: z.string().optional(),
     })
     })
     .refine((data) => !!data.siteId !== !!data.orgId, {
     .refine((data) => !!data.siteId !== !!data.orgId, {
@@ -27,7 +28,7 @@ const listResourcesSchema = z.object({
     limit: z
     limit: z
         .string()
         .string()
         .optional()
         .optional()
-        .default("0")
+        .default("1000")
         .transform(Number)
         .transform(Number)
         .pipe(z.number().int().nonnegative()),
         .pipe(z.number().int().nonnegative()),
 
 
@@ -90,6 +91,8 @@ export async function listResources(
     next: NextFunction,
     next: NextFunction,
 ): Promise<any> {
 ): Promise<any> {
     try {
     try {
+        logger.info(JSON.stringify(req.query, null, 2));
+        logger.info(JSON.stringify(req.params, null, 2));
         const parsedQuery = listResourcesSchema.safeParse(req.query);
         const parsedQuery = listResourcesSchema.safeParse(req.query);
         if (!parsedQuery.success) {
         if (!parsedQuery.success) {
             return next(
             return next(

+ 1 - 1
server/routers/role/createRole.ts

@@ -9,7 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
 
 
 const createRoleParamsSchema = z.object({
 const createRoleParamsSchema = z.object({
-    orgId: z.number().int().positive()
+    orgId: z.string()
 });
 });
 
 
 const createRoleSchema = z.object({
 const createRoleSchema = z.object({

+ 0 - 1
server/routers/site/deleteSite.ts

@@ -9,7 +9,6 @@ import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
 
 
-
 const API_BASE_URL = "http://localhost:3000";
 const API_BASE_URL = "http://localhost:3000";
 
 
 // Define Zod schema for request parameters validation
 // Define Zod schema for request parameters validation

+ 2 - 1
server/routers/site/getSite.ts

@@ -8,10 +8,11 @@ import HttpCode from '@server/types/HttpCode';
 import createHttpError from 'http-errors';
 import createHttpError from 'http-errors';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
+import stoi from '@server/utils/stoi';
 
 
 // Define Zod schema for request parameters validation
 // Define Zod schema for request parameters validation
 const getSiteSchema = z.object({
 const getSiteSchema = z.object({
-    siteId: z.string().transform(Number).pipe(z.number().int().positive()).optional(),
+            siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()).optional(),
     niceId: z.string().optional(),
     niceId: z.string().optional(),
     orgId: z.string().optional(),
     orgId: z.string().optional(),
 });
 });

+ 1 - 1
server/routers/target/createTarget.ts

@@ -9,7 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
 import logger from '@server/logger';
 import logger from '@server/logger';
 
 
 const createTargetParamsSchema = z.object({
 const createTargetParamsSchema = z.object({
-    resourceId: z.string().uuid(),
+    resourceId: z.string(),
 });
 });
 
 
 const createTargetSchema = z.object({
 const createTargetSchema = z.object({

+ 77 - 40
server/routers/target/listTargets.ts

@@ -1,35 +1,73 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { targets, resources } from '@server/db/schema';
+import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
+import { db } from "@server/db";
+import { targets, resources } from "@server/db/schema";
+import HttpCode from "@server/types/HttpCode";
 import response from "@server/utils/response";
 import response from "@server/utils/response";
-import HttpCode from '@server/types/HttpCode';
-import createHttpError from 'http-errors';
-import { sql, eq } from 'drizzle-orm';
-import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
-import logger from '@server/logger';
+import { eq, sql } from "drizzle-orm";
+import { NextFunction, Request, Response } from "express";
+import createHttpError from "http-errors";
+import { z } from "zod";
+import { fromError } from "zod-validation-error";
+import logger from "@server/logger";
 
 
 const listTargetsParamsSchema = z.object({
 const listTargetsParamsSchema = z.object({
-    resourceId: z.string().optional()
+    resourceId: z.string()
 });
 });
 
 
 const listTargetsSchema = z.object({
 const listTargetsSchema = z.object({
-    limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
-    offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)),
+    limit: z
+        .string()
+        .optional()
+        .default("1000")
+        .transform(Number)
+        .pipe(z.number().int().positive()),
+    offset: z
+        .string()
+        .optional()
+        .default("0")
+        .transform(Number)
+        .pipe(z.number().int().nonnegative()),
 });
 });
 
 
-export async function listTargets(req: Request, res: Response, next: NextFunction): Promise<any> {
+function queryTargets(resourceId: string) {
+    let baseQuery = db
+        .select({
+            targetId: targets.targetId,
+            ip: targets.ip,
+            method: targets.method,
+            port: targets.port,
+            protocol: targets.protocol,
+            enabled: targets.enabled,
+            resourceId: targets.resourceId,
+            // resourceName: resources.name,
+        })
+        .from(targets)
+        // .leftJoin(resources, eq(targets.resourceId, resources.resourceId))
+        .where(eq(targets.resourceId, resourceId));
+
+    return baseQuery;
+}
+
+export type ListTargetsResponse = {
+    targets: Awaited<ReturnType<typeof queryTargets>>;
+    pagination: { total: number; limit: number; offset: number };
+};
+
+export async function listTargets(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
     try {
     try {
         const parsedQuery = listTargetsSchema.safeParse(req.query);
         const parsedQuery = listTargetsSchema.safeParse(req.query);
         if (!parsedQuery.success) {
         if (!parsedQuery.success) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
                     HttpCode.BAD_REQUEST,
-                    parsedQuery.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedQuery.error)
                 )
                 )
             );
             );
         }
         }
-
         const { limit, offset } = parsedQuery.data;
         const { limit, offset } = parsedQuery.data;
 
 
         const parsedParams = listTargetsParamsSchema.safeParse(req.params);
         const parsedParams = listTargetsParamsSchema.safeParse(req.params);
@@ -37,44 +75,38 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map(e => e.message).join(', ')
+                    fromError(parsedParams.error)
                 )
                 )
             );
             );
         }
         }
-
         const { resourceId } = parsedParams.data;
         const { resourceId } = parsedParams.data;
 
 
-        // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listTargets, req);
+        // Check if the user has permission to list targets
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listTargets,
+            req
+        );
         if (!hasPermission) {
         if (!hasPermission) {
-            return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
+            return next(
+                createHttpError(
+                    HttpCode.FORBIDDEN,
+                    "User does not have permission to perform this action"
+                )
+            );
         }
         }
 
 
-        let baseQuery: any = db
-            .select({
-                targetId: targets.targetId,
-                ip: targets.ip,
-                method: targets.method,
-                port: targets.port,
-                protocol: targets.protocol,
-                enabled: targets.enabled,
-                resourceName: resources.name,
-            })
-            .from(targets)
-            .leftJoin(resources, eq(targets.resourceId, resources.resourceId));
-
-        let countQuery: any = db.select({ count: sql<number>`cast(count(*) as integer)` }).from(targets);
+        const baseQuery = queryTargets(resourceId);
 
 
-        if (resourceId) {
-            baseQuery = baseQuery.where(eq(targets.resourceId, resourceId));
-            countQuery = countQuery.where(eq(targets.resourceId, resourceId));
-        }
+        let countQuery = db
+            .select({ count: sql<number>`cast(count(*) as integer)` })
+            .from(targets)
+            .where(eq(targets.resourceId, resourceId));
 
 
         const targetsList = await baseQuery.limit(limit).offset(offset);
         const targetsList = await baseQuery.limit(limit).offset(offset);
         const totalCountResult = await countQuery;
         const totalCountResult = await countQuery;
         const totalCount = totalCountResult[0].count;
         const totalCount = totalCountResult[0].count;
 
 
-        return response(res, {
+        return response<ListTargetsResponse>(res, {
             data: {
             data: {
                 targets: targetsList,
                 targets: targetsList,
                 pagination: {
                 pagination: {
@@ -90,6 +122,11 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio
         });
         });
     } catch (error) {
     } catch (error) {
         logger.error(error);
         logger.error(error);
-        return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred..."
+            )
+        );
     }
     }
 }
 }

+ 1 - 1
server/routers/user/addUserSite.ts

@@ -11,7 +11,7 @@ import { eq } from 'drizzle-orm';
 
 
 const addUserSiteSchema = z.object({
 const addUserSiteSchema = z.object({
     userId: z.string(),
     userId: z.string(),
-    siteId: z.string().transform(Number).pipe(z.number().int().positive()),
+            siteId: z.string().optional().transform(stoi).pipe(z.number().int().positive().optional()),
 });
 });
 
 
 export async function addUserSite(req: Request, res: Response, next: NextFunction): Promise<any> {
 export async function addUserSite(req: Request, res: Response, next: NextFunction): Promise<any> {

+ 8 - 0
server/utils/stoi.ts

@@ -0,0 +1,8 @@
+export default function stoi(val: any) {
+    if (typeof val === "string") {
+        return parseInt(val) 
+    }
+    else {
+        return val;  
+    } 
+}

+ 0 - 18
src/app/[orgId]/resources/[resourceId]/account/page.tsx

@@ -1,18 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { AccountForm } from "@/components/account-form"
-
-export default function SettingsAccountPage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Account</h3>
-        <p className="text-sm text-muted-foreground">
-          Update your account settings. Set your preferred language and
-          timezone.
-        </p>
-      </div>
-      <Separator />
-      <AccountForm />
-    </div>
-  )
-}

+ 0 - 18
src/app/[orgId]/resources/[resourceId]/appearance/page.tsx

@@ -1,18 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { AppearanceForm } from "@/components/appearance-form"
-
-export default function SettingsAppearancePage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Appearance</h3>
-        <p className="text-sm text-muted-foreground">
-          Customize the appearance of the app. Automatically switch between day
-          and night themes.
-        </p>
-      </div>
-      <Separator />
-      <AppearanceForm />
-    </div>
-  )
-}

+ 5 - 6
src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx

@@ -5,20 +5,19 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
 
 
 const sidebarNavItems = [
 const sidebarNavItems = [
     {
     {
-        title: "General",
+        title: "Create",
         href: "/{orgId}/resources/{resourceId}",
         href: "/{orgId}/resources/{resourceId}",
     },
     },
-    // {
-    //     title: "Appearance",
-    //     href: "/{orgId}/resources/{resourceId}/appearance",
-    // },
+    {
+        title: "Targets",
+        href: "/{orgId}/resources/{resourceId}/targets",
+    },
     // {
     // {
     //     title: "Notifications",
     //     title: "Notifications",
     //     href: "/{orgId}/resources/{resourceId}/notifications",
     //     href: "/{orgId}/resources/{resourceId}/notifications",
     // },
     // },
 ]
 ]
 
 
-
 export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
 export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
     const { resource } = useResourceContext();
     const { resource } = useResourceContext();
     return (<div className="hidden space-y-6 0 pb-16 md:block">
     return (<div className="hidden space-y-6 0 pb-16 md:block">

+ 0 - 17
src/app/[orgId]/resources/[resourceId]/display/page.tsx

@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { DisplayForm } from "@/components/display-form"
-
-export default function SettingsDisplayPage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Display</h3>
-        <p className="text-sm text-muted-foreground">
-          Turn items on or off to control what&apos;s displayed in the app.
-        </p>
-      </div>
-      <Separator />
-      <DisplayForm />
-    </div>
-  )
-}

+ 0 - 15
src/app/[orgId]/resources/[resourceId]/layout.tsx

@@ -20,21 +20,6 @@ export const metadata: Metadata = {
     description: "Advanced form example using react-hook-form and Zod.",
     description: "Advanced form example using react-hook-form and Zod.",
 };
 };
 
 
-const sidebarNavItems = [
-    {
-        title: "Profile",
-        href: "/{orgId}/resources/{resourceId}",
-    },
-    // {
-    //     title: "Appearance",
-    //     href: "/{orgId}/resources/{resourceId}/appearance",
-    // },
-    // {
-    //     title: "Notifications",
-    //     href: "/{orgId}/resources/{resourceId}/notifications",
-    // },
-]
-
 interface SettingsLayoutProps {
 interface SettingsLayoutProps {
     children: React.ReactNode;
     children: React.ReactNode;
     params: { resourceId: string; orgId: string };
     params: { resourceId: string; orgId: string };

+ 0 - 17
src/app/[orgId]/resources/[resourceId]/notifications/page.tsx

@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { NotificationsForm } from "@/components/notifications-form"
-
-export default function SettingsNotificationsPage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Notifications</h3>
-        <p className="text-sm text-muted-foreground">
-          Configure how you receive notifications.
-        </p>
-      </div>
-      <Separator />
-      <NotificationsForm />
-    </div>
-  )
-}

+ 198 - 0
src/app/[orgId]/resources/[resourceId]/targets/page.tsx

@@ -0,0 +1,198 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { PlusCircle, Trash2, Server, Globe, Cpu, Power } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Switch } from "@/components/ui/switch"
+import { Badge } from "@/components/ui/badge"
+import api from "@app/api"
+import { AxiosResponse } from "axios"
+import { ListTargetsResponse } from "@server/routers/target/listTargets"
+
+export default function ReverseProxyTargets({ params }: { params: { resourceId: string } }) {
+    const [targets, setTargets] = useState<ListTargetsResponse["targets"]>([])
+    const [nextId, setNextId] = useState(1)
+
+    useEffect(() => {
+        if (typeof window !== "undefined") {
+            const fetchSites = async () => {
+                const res = await api.get<AxiosResponse<ListTargetsResponse>>(`/resource/${params.resourceId}/targets`);
+                setTargets(res.data.data.targets);
+            };
+            fetchSites();
+        }
+    }, []);
+
+    const [newTarget, setNewTarget] = useState({
+        resourceId: params.resourceId,
+        ip: "",
+        method: "GET",
+        port: 80,
+        protocol: "http",
+    })
+
+    const addTarget = async () => {
+        const res = await api.put(`/resource/${params.resourceId}/target`, {
+            ...newTarget,
+            resourceId: undefined
+        })
+        .catch((err) => {
+            console.error(err)
+            
+        });
+
+        setTargets([...targets, { ...newTarget, targetId: nextId, enabled: true }])
+        setNextId(nextId + 1)
+        setNewTarget({
+            resourceId: params.resourceId,
+            ip: "",
+            method: "GET",
+            port: 80,
+            protocol: "http",
+        })
+    }
+
+    const removeTarget = async (targetId: number) => {
+        setTargets(targets.filter((target) => target.targetId !== targetId))
+        const res = await api.delete(`/target/${targetId}`)
+    }
+
+    const toggleTarget = (targetId: number) => {
+        setTargets(
+            targets.map((target) =>
+                target.targetId === targetId ? { ...target, enabled: !target.enabled } : target
+            )
+        )
+        const res = api.post(`/target/${targetId}`, { enabled: !targets.find((target) => target.targetId === targetId)?.enabled })
+
+        // Add a visual feedback
+        const targetElement = document.getElementById(`target-${targetId}`)
+        if (targetElement) {
+            targetElement.classList.add('scale-105', 'transition-transform')
+            setTimeout(() => {
+                targetElement.classList.remove('scale-105', 'transition-transform')
+            }, 200)
+        }
+    }
+
+    return (
+        <div className="space-y-6">
+            {/* <Card>
+                <CardHeader> */}
+                    {/* <CardTitle>Add New Target</CardTitle>
+                </CardHeader>
+                <CardContent> */}
+                    <form
+                        onSubmit={(e) => {
+                            e.preventDefault()
+                            addTarget()
+                        }}
+                        className="space-y-4"
+                    >
+                        <div className="grid grid-cols-2 gap-4">
+                            <div className="space-y-2">
+                                <Label htmlFor="ip">IP Address</Label>
+                                <Input
+                                    id="ip"
+                                    value={newTarget.ip}
+                                    onChange={(e) => setNewTarget({ ...newTarget, ip: e.target.value })}
+                                    required
+                                />
+                            </div>
+                            <div className="space-y-2">
+                                <Label htmlFor="method">Method</Label>
+                                <Select
+                                    value={newTarget.method}
+                                    onValueChange={(value) => setNewTarget({ ...newTarget, method: value })}
+                                >
+                                    <SelectTrigger id="method">
+                                        <SelectValue placeholder="Select method" />
+                                    </SelectTrigger>
+                                    <SelectContent>
+                                        <SelectItem value="GET">GET</SelectItem>
+                                        <SelectItem value="POST">POST</SelectItem>
+                                        <SelectItem value="PUT">PUT</SelectItem>
+                                        <SelectItem value="DELETE">DELETE</SelectItem>
+                                    </SelectContent>
+                                </Select>
+                            </div>
+                            <div className="space-y-2">
+                                <Label htmlFor="port">Port</Label>
+                                <Input
+                                    id="port"
+                                    type="number"
+                                    value={newTarget.port}
+                                    onChange={(e) => setNewTarget({ ...newTarget, port: parseInt(e.target.value) })}
+                                    required
+                                />
+                            </div>
+                            <div className="space-y-2">
+                                <Label htmlFor="protocol">Protocol</Label>
+                                <Select
+                                    value={newTarget.protocol}
+                                    onValueChange={(value) => setNewTarget({ ...newTarget, protocol: value })}
+                                >
+                                    <SelectTrigger id="protocol">
+                                        <SelectValue placeholder="Select protocol" />
+                                    </SelectTrigger>
+                                    <SelectContent>
+                                        <SelectItem value="http">HTTP</SelectItem>
+                                        <SelectItem value="https">HTTPS</SelectItem>
+                                    </SelectContent>
+                                </Select>
+                            </div>
+                        </div>
+                        <Button type="submit">
+                            <PlusCircle className="mr-2 h-4 w-4" /> Add Target
+                        </Button>
+                    </form>
+                {/* </CardContent>
+            </Card> */}
+
+            <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+                {targets.map((target) => (
+                    <Card key={target.targetId} id={`target-${target.targetId}`} >
+                        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                            <CardTitle className="text-sm font-medium flex items-center">
+                                <Server className="mr-2 h-4 w-4" />
+                                Target {target.targetId}
+                            </CardTitle>
+                            <Switch
+                                checked={target.enabled}
+                                onCheckedChange={() => toggleTarget(target.targetId)}
+                            />
+                        </CardHeader>
+                        <CardContent>
+                            <div className="grid gap-2">
+                                <div className="flex items-center">
+                                    <Globe className="mr-2 h-4 w-4 text-muted-foreground" />
+                                    <span className="text-sm">{target.ip}:{target.port}</span>
+                                </div>
+                                <div className="flex items-center">
+                                    <Cpu className="mr-2 h-4 w-4 text-muted-foreground" />
+                                    <span className="text-sm">{target.resourceId}</span>
+                                </div>
+                                <div className="flex items-center space-x-2">
+                                    <Badge variant={target.enabled ? "default" : "secondary"}>{target.method}</Badge>
+                                    <Badge variant={target.enabled ? "default" : "secondary"}>{target.protocol?.toUpperCase()}</Badge>
+                                </div>
+                            </div>
+                            <Button
+                                variant="destructive"
+                                size="sm"
+                                className="mt-4 w-full"
+                                onClick={() => removeTarget(target.targetId)}
+                            >
+                                <Trash2 className="mr-2 h-4 w-4" /> Remove
+                            </Button>
+                        </CardContent>
+                    </Card>
+                ))}
+            </div>
+        </div>
+    )
+}

+ 5 - 5
src/app/[orgId]/resources/page.tsx

@@ -11,11 +11,11 @@ type ResourcesPageProps = {
 export default async function Page({ params }: ResourcesPageProps) {
 export default async function Page({ params }: ResourcesPageProps) {
     let resources: ListResourcesResponse["resources"] = [];
     let resources: ListResourcesResponse["resources"] = [];
     try {
     try {
-        // const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
-        //     `/org/${params.orgId}/resources`,
-        //     authCookieHeader(),
-        // );
-        // resources = res.data.data.resources;
+        const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
+            `/org/${params.orgId}/resources`,
+            authCookieHeader(),
+        );
+        resources = res.data.data.resources;
     } catch (e) {
     } catch (e) {
         console.error("Error fetching resources", e);
         console.error("Error fetching resources", e);
     }
     }

+ 1 - 0
src/providers/ResourceProvider.tsx

@@ -32,6 +32,7 @@ export function ResourceProvider({ children, resource: serverResource }: Resourc
         } catch (error) {
         } catch (error) {
             console.error(error);
             console.error(error);
             toast({
             toast({
+                variant: "destructive",
                 title: "Error updating resource...",
                 title: "Error updating resource...",
             })
             })
         }
         }

+ 1 - 0
src/providers/SiteProvider.tsx

@@ -32,6 +32,7 @@ export function SiteProvider({ children, site: serverSite }: SiteProviderProps)
         } catch (error) {
         } catch (error) {
             console.error(error);
             console.error(error);
             toast({
             toast({
+                variant: "destructive",
                 title: "Error updating site...",
                 title: "Error updating site...",
             })
             })
         }
         }