diff --git a/server/index.ts b/server/index.ts index 79058ea..9e7d0f5 100644 --- a/server/index.ts +++ b/server/index.ts @@ -91,7 +91,6 @@ declare global { interface Request { user?: User; userOrgRoleId?: number; - orgId?: string; userOrgId?: string; userOrgIds?: string[]; } diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 647b3e8..71fd0b9 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -1,39 +1,56 @@ -import { Request, Response, NextFunction } from 'express'; -import { z } from 'zod'; -import { db } from '@server/db'; -import { orgs } from '@server/db/schema'; -import { eq } from 'drizzle-orm'; +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { Org, orgs } from "@server/db/schema"; +import { eq } from "drizzle-orm"; import response from "@server/utils/response"; -import HttpCode from '@server/types/HttpCode'; -import createHttpError from 'http-errors'; -import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; -import logger from '@server/logger'; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; +import logger from "@server/logger"; const getOrgSchema = z.object({ - orgId: z.string() + orgId: z.string(), }); -export async function getOrg(req: Request, res: Response, next: NextFunction): Promise { +export type GetOrgResponse = { + org: Org; +} + +export async function getOrg( + req: Request, + res: Response, + next: NextFunction, +): Promise { try { const parsedParams = getOrgSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - parsedParams.error.errors.map(e => e.message).join(', ') - ) + parsedParams.error.errors.map((e) => e.message).join(", "), + ), ); } const { orgId } = parsedParams.data; // Check if the user has permission to list sites - const hasPermission = await checkUserActionPermission(ActionsEnum.getOrg, req); + const hasPermission = await checkUserActionPermission( + ActionsEnum.getOrg, + req, + ); 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", + ), + ); } - const org = await db.select() + const org = await db + .select() .from(orgs) .where(eq(orgs.orgId, orgId)) .limit(1); @@ -42,13 +59,15 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P return next( createHttpError( HttpCode.NOT_FOUND, - `Organization with ID ${orgId} not found` - ) + `Organization with ID ${orgId} not found`, + ), ); } - return response(res, { - data: org[0], + return response(res, { + data: { + org: org[0], + }, success: true, error: false, message: "Organization retrieved successfully", @@ -56,6 +75,11 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P }); } 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...", + ), + ); } } diff --git a/server/routers/org/listOrgs.ts b/server/routers/org/listOrgs.ts index 7456096..dd53879 100644 --- a/server/routers/org/listOrgs.ts +++ b/server/routers/org/listOrgs.ts @@ -1,79 +1,98 @@ -import { Request, Response, NextFunction } from 'express'; -import { z } from 'zod'; -import { db } from '@server/db'; -import { orgs } from '@server/db/schema'; +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { Org, orgs } from "@server/db/schema"; import response from "@server/utils/response"; -import HttpCode from '@server/types/HttpCode'; -import createHttpError from 'http-errors'; -import { sql, inArray } from 'drizzle-orm'; -import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; -import logger from '@server/logger'; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql, inArray } from "drizzle-orm"; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; +import logger from "@server/logger"; const listOrgsSchema = 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() + .transform(Number) + .pipe(z.number().int().positive().default(10)), + offset: z + .string() + .optional() + .transform(Number) + .pipe(z.number().int().nonnegative().default(0)), }); -export async function listOrgs(req: Request, res: Response, next: NextFunction): Promise { +export type ListOrgsResponse = { + organizations: Org[]; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listOrgs( + req: Request, + res: Response, + next: NextFunction, +): Promise { try { const parsedQuery = listOrgsSchema.safeParse(req.query); if (!parsedQuery.success) { return next( createHttpError( HttpCode.BAD_REQUEST, - parsedQuery.error.errors.map(e => e.message).join(', ') - ) + parsedQuery.error.errors.map((e) => e.message).join(", "), + ), ); } const { limit, offset } = parsedQuery.data; // Check if the user has permission to list sites - const hasPermission = await checkUserActionPermission(ActionsEnum.listOrgs, req); + const hasPermission = await checkUserActionPermission( + ActionsEnum.listOrgs, + req, + ); 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", + ), + ); } // Use the userOrgs passed from the middleware const userOrgIds = req.userOrgIds; if (!userOrgIds || userOrgIds.length === 0) { - return res.status(HttpCode.OK).send( - response(res, { - data: { - organizations: [], - pagination: { - total: 0, - limit, - offset, - }, + return response(res, { + data: { + organizations: [], + pagination: { + total: 0, + limit, + offset, }, - success: true, - error: false, - message: "No organizations found for the user", - status: HttpCode.OK, - }) - ); + }, + success: true, + error: false, + message: "No organizations found for the user", + status: HttpCode.OK, + }); } - const organizations = await db.select() + const organizations = await db + .select() .from(orgs) .where(inArray(orgs.orgId, userOrgIds)) .limit(limit) .offset(offset); - const totalCountResult = await db.select({ count: sql`cast(count(*) as integer)` }) + const totalCountResult = await db + .select({ count: sql`cast(count(*) as integer)` }) .from(orgs) .where(inArray(orgs.orgId, userOrgIds)); const totalCount = totalCountResult[0].count; - // // Add the user's role for each organization - // const organizationsWithRoles = organizations.map(org => ({ - // ...org, - // userRole: req.userOrgRoleIds[org.orgId], - // })); - - return response(res, { + return response(res, { data: { organizations, pagination: { @@ -89,6 +108,11 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction): }); } 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...", + ), + ); } } diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index f7ed186..33d96f9 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -79,7 +79,7 @@ function queryResources( } } -export type ListSitesResponse = { +export type ListResourcesResponse = { resources: NonNullable>>; pagination: { total: number; limit: number; offset: number }; }; @@ -126,7 +126,7 @@ export async function listResources( ); } - if (orgId && orgId !== req.orgId) { + if (orgId && orgId !== req.userOrgId) { return next( createHttpError( HttpCode.FORBIDDEN, @@ -167,7 +167,7 @@ export async function listResources( const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; - return response(res, { + return response(res, { data: { resources: resourcesList, pagination: { diff --git a/src/app/[orgId]/components/Header.tsx b/src/app/[orgId]/components/Header.tsx index 647682b..5a2d521 100644 --- a/src/app/[orgId]/components/Header.tsx +++ b/src/app/[orgId]/components/Header.tsx @@ -77,7 +77,7 @@ export default function Header({ email, orgName, name }: HeaderProps) { - + {name || email} diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index dcf40bb..802e883 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -5,6 +5,10 @@ import Header from "./components/Header"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { cache } from "react"; +import { internal } from "@app/api"; +import { AxiosResponse } from "axios"; +import { GetOrgResponse } from "@server/routers/org"; +import { authCookieHeader } from "@app/api/cookies"; export const metadata: Metadata = { title: "Configuration", @@ -15,22 +19,22 @@ const topNavItems = [ { title: "Sites", href: "/{orgId}/sites", - icon: , + icon: , }, { title: "Resources", href: "/{orgId}/resources", - icon: , + icon: , }, { title: "Users", href: "/{orgId}/users", - icon: , + icon: , }, { title: "General", href: "/{orgId}/general", - icon: , + icon: , }, ]; @@ -43,14 +47,21 @@ export default async function ConfigurationLaytout({ children, params, }: ConfigurationLaytoutProps) { - const loadUser = cache(async () => await verifySession()); - - const user = await loadUser(); + const user = await verifySession(); if (!user) { redirect("/auth/login"); } + try { + await internal.get>( + `/org/${params.orgId}`, + authCookieHeader(), + ); + } catch { + redirect(`/`); + } + return ( <>
diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 9af30d8..bd3ed39 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,7 +1,11 @@ -export default async function Page() { - return ( - <> -

IDK what this will show...

- - ); +import { redirect } from "next/navigation"; + +type OrgPageProps = { + params: { orgId: string }; +}; + +export default async function Page({ params }: OrgPageProps) { + redirect(`/${params.orgId}/sites`); + + return <>; } diff --git a/src/app/[orgId]/resources/components/ResourcesDataTable.tsx b/src/app/[orgId]/resources/components/ResourcesDataTable.tsx new file mode 100644 index 0000000..36c85ca --- /dev/null +++ b/src/app/[orgId]/resources/components/ResourcesDataTable.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { Input } from "@app/components/ui/input"; +import { DataTablePagination } from "@app/components/DataTablePagination"; +import { Plus } from "lucide-react"; + +interface ResourcesDataTableProps { + columns: ColumnDef[]; + data: TData[]; + addResource?: () => void; +} + +export function ResourcesDataTable({ + addResource, + columns, + data, +}: ResourcesDataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, + }); + + return ( +
+
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="max-w-sm mr-2" + /> + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No resources. Create one to get started. + + + )} + +
+
+
+ +
+
+ ); +} diff --git a/src/app/[orgId]/resources/components/ResourcesTable.tsx b/src/app/[orgId]/resources/components/ResourcesTable.tsx new file mode 100644 index 0000000..56386b6 --- /dev/null +++ b/src/app/[orgId]/resources/components/ResourcesTable.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ResourcesDataTable } from "./ResourcesDataTable"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export type ResourceRow = { + id: string; + name: string; + orgId: string; +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + }, + }, + { + id: "actions", + cell: ({ row }) => { + const resourceRow = row.original; + + return ( + + + + + + + + View settings + + + + + ); + }, + }, +]; + +type ResourcesTableProps = { + resources: ResourceRow[]; + orgId: string; +}; + +export default function SitesTable({ resources, orgId }: ResourcesTableProps) { + const router = useRouter(); + + return ( + { + router.push(`/${orgId}/resources/create`); + }} + /> + ); +} diff --git a/src/app/[orgId]/resources/page.tsx b/src/app/[orgId]/resources/page.tsx index 4b89334..3b3c461 100644 --- a/src/app/[orgId]/resources/page.tsx +++ b/src/app/[orgId]/resources/page.tsx @@ -1,14 +1,45 @@ -export default async function Page() { +import { internal } from "@app/api"; +import { authCookieHeader } from "@app/api/cookies"; +import ResourcesTable, { ResourceRow } from "./components/ResourcesTable"; +import { AxiosResponse } from "axios"; +import { ListResourcesResponse } from "@server/routers/resource"; + +type ResourcesPageProps = { + params: { orgId: string }; +}; + +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; + } catch (e) { + console.error("Error fetching resources", e); + } + + const resourceRows: ResourceRow[] = resources.map((resource) => { + return { + id: resource.resourceId.toString(), + name: resource.name, + orgId: params.orgId, + }; + }); + return ( <> -
+

Manage Resources

- Create secure proxies to your private resources. + Create secure proxies to your private applications.

+ + ); } diff --git a/src/app/[orgId]/sites/[niceId]/components/create-site.tsx b/src/app/[orgId]/sites/[niceId]/components/create-site.tsx index 936506e..5f2b1d8 100644 --- a/src/app/[orgId]/sites/[niceId]/components/create-site.tsx +++ b/src/app/[orgId]/sites/[niceId]/components/create-site.tsx @@ -177,13 +177,13 @@ sh get-docker.sh`; /> {form.watch("method") === "wg" && !isLoading ? (
-                            {wgConfig}
+                            {wgConfig}
                         
) : form.watch("method") === "wg" && isLoading ? (

Loading WireGuard configuration...

) : (
-                            {newtConfig}
+                            {newtConfig}
                         
)}
diff --git a/src/app/[orgId]/sites/components/DataTable.tsx b/src/app/[orgId]/sites/components/SitesDataTable.tsx similarity index 92% rename from src/app/[orgId]/sites/components/DataTable.tsx rename to src/app/[orgId]/sites/components/SitesDataTable.tsx index c1807af..dcbe954 100644 --- a/src/app/[orgId]/sites/components/DataTable.tsx +++ b/src/app/[orgId]/sites/components/SitesDataTable.tsx @@ -23,7 +23,7 @@ import { import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; -import { DataTablePagination } from "./DataTablePagination"; +import { DataTablePagination } from "../../../../components/DataTablePagination"; import { Plus } from "lucide-react"; interface DataTableProps { @@ -32,7 +32,7 @@ interface DataTableProps { addSite?: () => void; } -export function DataTable({ +export function SitesDataTable({ addSite, columns, data, @@ -59,7 +59,7 @@ export function DataTable({
({ } className="max-w-sm mr-2" /> -
diff --git a/src/app/[orgId]/sites/components/SitesTable.tsx b/src/app/[orgId]/sites/components/SitesTable.tsx index 1b6e0c0..0a92795 100644 --- a/src/app/[orgId]/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/sites/components/SitesTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "./DataTable"; +import { SitesDataTable } from "./SitesDataTable"; import { DropdownMenu, DropdownMenuContent, @@ -99,7 +99,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { const router = useRouter(); return ( - { diff --git a/src/app/[orgId]/sites/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx similarity index 100% rename from src/app/[orgId]/sites/components/DataTablePagination.tsx rename to src/components/DataTablePagination.tsx