diff --git a/server/index.ts b/server/index.ts index 9e7d0f54..ec8f97f6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -36,13 +36,15 @@ app.prepare().then(() => { externalServer.use(cors()); externalServer.use(cookieParser()); 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`; externalServer.use(prefix, unauthenticated); diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 1b81e9bd..f6ac25d3 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -8,9 +8,10 @@ import createHttpError from 'http-errors'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; import { eq, and } from 'drizzle-orm'; +import stoi from '@server/utils/stoi'; 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() }); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 76e7b04b..215f579a 100644 --- a/server/routers/resource/listResources.ts +++ b/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 { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import logger from "@server/logger"; +import stoi from "@server/utils/stoi"; const listResourcesParamsSchema = z .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(), }) .refine((data) => !!data.siteId !== !!data.orgId, { @@ -27,7 +28,7 @@ const listResourcesSchema = z.object({ limit: z .string() .optional() - .default("0") + .default("1000") .transform(Number) .pipe(z.number().int().nonnegative()), @@ -90,6 +91,8 @@ export async function listResources( next: NextFunction, ): Promise { try { + logger.info(JSON.stringify(req.query, null, 2)); + logger.info(JSON.stringify(req.params, null, 2)); const parsedQuery = listResourcesSchema.safeParse(req.query); if (!parsedQuery.success) { return next( diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index 940f57e7..01d9af2d 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -9,7 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; const createRoleParamsSchema = z.object({ - orgId: z.number().int().positive() + orgId: z.string() }); const createRoleSchema = z.object({ diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 5a99320f..3a0acfa5 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -9,7 +9,6 @@ import createHttpError from 'http-errors'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; - const API_BASE_URL = "http://localhost:3000"; // Define Zod schema for request parameters validation diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index 79701750..927a3628 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -8,10 +8,11 @@ import HttpCode from '@server/types/HttpCode'; import createHttpError from 'http-errors'; import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; +import stoi from '@server/utils/stoi'; // Define Zod schema for request parameters validation 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(), orgId: z.string().optional(), }); diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index eb216c95..c244c74b 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -9,7 +9,7 @@ import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; import logger from '@server/logger'; const createTargetParamsSchema = z.object({ - resourceId: z.string().uuid(), + resourceId: z.string(), }); const createTargetSchema = z.object({ diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 6873f0d0..e172bfdd 100644 --- a/server/routers/target/listTargets.ts +++ b/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 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({ - resourceId: z.string().optional() + resourceId: z.string() }); 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 { +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>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listTargets( + req: Request, + res: Response, + next: NextFunction +): Promise { try { const parsedQuery = listTargetsSchema.safeParse(req.query); if (!parsedQuery.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - parsedQuery.error.errors.map(e => e.message).join(', ') + fromError(parsedQuery.error) ) ); } - const { limit, offset } = parsedQuery.data; const parsedParams = listTargetsParamsSchema.safeParse(req.params); @@ -37,44 +75,38 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio return next( createHttpError( HttpCode.BAD_REQUEST, - parsedParams.error.errors.map(e => e.message).join(', ') + fromError(parsedParams.error) + ) + ); + } + const { resourceId } = parsedParams.data; + + // Check if the user has permission to list targets + const hasPermission = await checkUserActionPermission( + ActionsEnum.listTargets, + req + ); + if (!hasPermission) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have permission to perform this action" ) ); } - const { resourceId } = parsedParams.data; + const baseQuery = queryTargets(resourceId); - // Check if the user has permission to list sites - const hasPermission = await checkUserActionPermission(ActionsEnum.listTargets, req); - if (!hasPermission) { - 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, - }) + let countQuery = db + .select({ count: sql`cast(count(*) as integer)` }) .from(targets) - .leftJoin(resources, eq(targets.resourceId, resources.resourceId)); - - let countQuery: any = db.select({ count: sql`cast(count(*) as integer)` }).from(targets); - - if (resourceId) { - baseQuery = baseQuery.where(eq(targets.resourceId, resourceId)); - countQuery = countQuery.where(eq(targets.resourceId, resourceId)); - } + .where(eq(targets.resourceId, resourceId)); const targetsList = await baseQuery.limit(limit).offset(offset); const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; - return response(res, { + return response(res, { data: { targets: targetsList, pagination: { @@ -90,6 +122,11 @@ export async function listTargets(req: Request, res: Response, next: NextFunctio }); } catch (error) { logger.error(error); - return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred..." + ) + ); } } \ No newline at end of file diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index 8be945a0..07965619 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -11,7 +11,7 @@ import { eq } from 'drizzle-orm'; const addUserSiteSchema = z.object({ 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 { diff --git a/server/utils/stoi.ts b/server/utils/stoi.ts new file mode 100644 index 00000000..8fa42b54 --- /dev/null +++ b/server/utils/stoi.ts @@ -0,0 +1,8 @@ +export default function stoi(val: any) { + if (typeof val === "string") { + return parseInt(val) + } + else { + return val; + } +} \ No newline at end of file diff --git a/src/app/[orgId]/resources/[resourceId]/account/page.tsx b/src/app/[orgId]/resources/[resourceId]/account/page.tsx deleted file mode 100644 index 03df0d89..00000000 --- a/src/app/[orgId]/resources/[resourceId]/account/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/account-form" - -export default function SettingsAccountPage() { - return ( -
-
-

Account

-

- Update your account settings. Set your preferred language and - timezone. -

-
- - -
- ) -} diff --git a/src/app/[orgId]/resources/[resourceId]/appearance/page.tsx b/src/app/[orgId]/resources/[resourceId]/appearance/page.tsx deleted file mode 100644 index ca038aa2..00000000 --- a/src/app/[orgId]/resources/[resourceId]/appearance/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/appearance-form" - -export default function SettingsAppearancePage() { - return ( -
-
-

Appearance

-

- Customize the appearance of the app. Automatically switch between day - and night themes. -

-
- - -
- ) -} diff --git a/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx b/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx index 7e928d95..a34333b5 100644 --- a/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx +++ b/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx @@ -5,20 +5,19 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; const sidebarNavItems = [ { - title: "General", + title: "Create", href: "/{orgId}/resources/{resourceId}", }, - // { - // title: "Appearance", - // href: "/{orgId}/resources/{resourceId}/appearance", - // }, + { + title: "Targets", + href: "/{orgId}/resources/{resourceId}/targets", + }, // { // title: "Notifications", // href: "/{orgId}/resources/{resourceId}/notifications", // }, ] - export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) { const { resource } = useResourceContext(); return (
diff --git a/src/app/[orgId]/resources/[resourceId]/display/page.tsx b/src/app/[orgId]/resources/[resourceId]/display/page.tsx deleted file mode 100644 index f934f6e6..00000000 --- a/src/app/[orgId]/resources/[resourceId]/display/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { DisplayForm } from "@/components/display-form" - -export default function SettingsDisplayPage() { - return ( -
-
-

Display

-

- Turn items on or off to control what's displayed in the app. -

-
- - -
- ) -} diff --git a/src/app/[orgId]/resources/[resourceId]/layout.tsx b/src/app/[orgId]/resources/[resourceId]/layout.tsx index f2fe124a..6ba0c6d3 100644 --- a/src/app/[orgId]/resources/[resourceId]/layout.tsx +++ b/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.", }; -const sidebarNavItems = [ - { - title: "Profile", - href: "/{orgId}/resources/{resourceId}", - }, - // { - // title: "Appearance", - // href: "/{orgId}/resources/{resourceId}/appearance", - // }, - // { - // title: "Notifications", - // href: "/{orgId}/resources/{resourceId}/notifications", - // }, -] - interface SettingsLayoutProps { children: React.ReactNode; params: { resourceId: string; orgId: string }; diff --git a/src/app/[orgId]/resources/[resourceId]/notifications/page.tsx b/src/app/[orgId]/resources/[resourceId]/notifications/page.tsx deleted file mode 100644 index 7c5c5ec0..00000000 --- a/src/app/[orgId]/resources/[resourceId]/notifications/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { NotificationsForm } from "@/components/notifications-form" - -export default function SettingsNotificationsPage() { - return ( -
-
-

Notifications

-

- Configure how you receive notifications. -

-
- - -
- ) -} diff --git a/src/app/[orgId]/resources/[resourceId]/targets/page.tsx b/src/app/[orgId]/resources/[resourceId]/targets/page.tsx new file mode 100644 index 00000000..0d6689f5 --- /dev/null +++ b/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([]) + const [nextId, setNextId] = useState(1) + + useEffect(() => { + if (typeof window !== "undefined") { + const fetchSites = async () => { + const res = await api.get>(`/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 ( +
+ {/* + */} + {/* Add New Target + + */} +
{ + e.preventDefault() + addTarget() + }} + className="space-y-4" + > +
+
+ + setNewTarget({ ...newTarget, ip: e.target.value })} + required + /> +
+
+ + +
+
+ + setNewTarget({ ...newTarget, port: parseInt(e.target.value) })} + required + /> +
+
+ + +
+
+ +
+ {/*
+
*/} + +
+ {targets.map((target) => ( + + + + + Target {target.targetId} + + toggleTarget(target.targetId)} + /> + + +
+
+ + {target.ip}:{target.port} +
+
+ + {target.resourceId} +
+
+ {target.method} + {target.protocol?.toUpperCase()} +
+
+ +
+
+ ))} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/[orgId]/resources/page.tsx b/src/app/[orgId]/resources/page.tsx index 95e25aaf..80cf9ba1 100644 --- a/src/app/[orgId]/resources/page.tsx +++ b/src/app/[orgId]/resources/page.tsx @@ -11,11 +11,11 @@ type ResourcesPageProps = { export default async function Page({ params }: ResourcesPageProps) { let resources: ListResourcesResponse["resources"] = []; try { - // const res = await internal.get>( - // `/org/${params.orgId}/resources`, - // authCookieHeader(), - // ); - // resources = res.data.data.resources; + const res = await internal.get>( + `/org/${params.orgId}/resources`, + authCookieHeader(), + ); + resources = res.data.data.resources; } catch (e) { console.error("Error fetching resources", e); } diff --git a/src/providers/ResourceProvider.tsx b/src/providers/ResourceProvider.tsx index 5c6fb22d..015a0f6f 100644 --- a/src/providers/ResourceProvider.tsx +++ b/src/providers/ResourceProvider.tsx @@ -32,6 +32,7 @@ export function ResourceProvider({ children, resource: serverResource }: Resourc } catch (error) { console.error(error); toast({ + variant: "destructive", title: "Error updating resource...", }) } diff --git a/src/providers/SiteProvider.tsx b/src/providers/SiteProvider.tsx index e1ab7d22..fbc4f2e5 100644 --- a/src/providers/SiteProvider.tsx +++ b/src/providers/SiteProvider.tsx @@ -32,6 +32,7 @@ export function SiteProvider({ children, site: serverSite }: SiteProviderProps) } catch (error) { console.error(error); toast({ + variant: "destructive", title: "Error updating site...", }) }