From 3bb7efb7c67ff777a70b97c4d1f34d2516dd131d Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Mon, 14 Oct 2024 21:54:43 -0400 Subject: [PATCH] fixed listSites endpoint --- server/auth/actions.ts | 90 +++++----- server/routers/site/listSites.ts | 157 ++++++++++++------ .../[orgId]/resources/[resourceId]/layout.tsx | 2 +- src/app/[orgId]/sites/[siteId]/layout.tsx | 2 +- src/app/[orgId]/sites/page.tsx | 21 ++- 5 files changed, 180 insertions(+), 92 deletions(-) diff --git a/server/auth/actions.ts b/server/auth/actions.ts index ae43c25..df84ecd 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -1,9 +1,9 @@ -import { Request } from 'express'; -import { db } from '@server/db'; -import { userActions, roleActions, userOrgs } from '@server/db/schema'; -import { and, eq } from 'drizzle-orm'; -import createHttpError from 'http-errors'; -import HttpCode from '@server/types/HttpCode'; +import { Request } from "express"; +import { db } from "@server/db"; +import { userActions, roleActions, userOrgs } from "@server/db/schema"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; export enum ActionsEnum { createOrg = "createOrg", @@ -54,75 +54,87 @@ export enum ActionsEnum { removeUserSite = "removeUserSite", } -export async function checkUserActionPermission(actionId: string, req: Request): Promise { +export async function checkUserActionPermission( + actionId: string, + req: Request, +): Promise { const userId = req.user?.userId; - let onlyCheckUser = false; - - if (actionId = ActionsEnum.createOrg) { - onlyCheckUser = true; - } if (!userId) { - throw createHttpError(HttpCode.UNAUTHORIZED, 'User not authenticated'); + throw createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated"); } - if (!req.userOrgId && !onlyCheckUser) { - throw createHttpError(HttpCode.BAD_REQUEST, 'Organization ID is required'); + if (!req.userOrgId) { + throw createHttpError( + HttpCode.BAD_REQUEST, + "Organization ID is required", + ); } try { let userOrgRoleId = req.userOrgRoleId; // If userOrgRoleId is not available on the request, fetch it - if (userOrgRoleId === undefined && !onlyCheckUser) { - const userOrgRole = await db.select() + if (userOrgRoleId === undefined) { + const userOrgRole = await db + .select() .from(userOrgs) - .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, req.userOrgId!))) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, req.userOrgId!), + ), + ) .limit(1); if (userOrgRole.length === 0) { - throw createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization'); + throw createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization", + ); } userOrgRoleId = userOrgRole[0].roleId; } // Check if the user has direct permission for the action in the current org - const userActionPermission = await db.select() + const userActionPermission = await db + .select() .from(userActions) .where( and( eq(userActions.userId, userId), eq(userActions.actionId, actionId), - eq(userActions.orgId, req.userOrgId!) // TODO: we cant pass the org id if we are not checking the org - ) + eq(userActions.orgId, req.userOrgId!), // TODO: we cant pass the org id if we are not checking the org + ), ) .limit(1); if (userActionPermission.length > 0) { return true; } - if (!onlyCheckUser) { - // If no direct permission, check role-based permission - const roleActionPermission = await db.select() - .from(roleActions) - .where( - and( - eq(roleActions.actionId, actionId), - eq(roleActions.roleId, userOrgRoleId!), - eq(roleActions.orgId, req.userOrgId!) - ) - ) - .limit(1); + // If no direct permission, check role-based permission + const roleActionPermission = await db + .select() + .from(roleActions) + .where( + and( + eq(roleActions.actionId, actionId), + eq(roleActions.roleId, userOrgRoleId!), + eq(roleActions.orgId, req.userOrgId!), + ), + ) + .limit(1); - return roleActionPermission.length > 0; - } + return roleActionPermission.length > 0; return false; - } catch (error) { - console.error('Error checking user action permission:', error); - throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Error checking action permission'); + console.error("Error checking user action permission:", error); + throw createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error checking action permission", + ); } } diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index bbac991..b8b52a5 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,92 +1,147 @@ -import { Request, Response, NextFunction } from 'express'; -import { z } from 'zod'; -import { db } from '@server/db'; -import { sites, orgs, exitNodes, userSites, roleSites } from '@server/db/schema'; +import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; +import { db } from "@server/db"; +import { + orgs, + roleSites, + sites, + userSites, +} 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, and, or, inArray } from 'drizzle-orm'; -import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions'; -import logger from '@server/logger'; +import { and, eq, inArray, or, 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"; const listSitesParamsSchema = z.object({ - orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()), + orgId: z.string(), }); const listSitesSchema = 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 listSites(req: Request, res: Response, next: NextFunction): Promise { +function querySites(orgId: string, accessibleSiteIds: number[]) { + return db + .select({ + siteId: sites.siteId, + name: sites.name, + subdomain: sites.subdomain, + pubKey: sites.pubKey, + subnet: sites.subnet, + megabytesIn: sites.megabytesIn, + megabytesOut: sites.megabytesOut, + orgName: orgs.name, + }) + .from(sites) + .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) + .where( + and( + inArray(sites.siteId, accessibleSiteIds), + eq(sites.orgId, orgId), + ), + ); +} + +export type ListSitesResponse = { + sites: Awaited>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listSites( + req: Request, + res: Response, + next: NextFunction, +): Promise { try { const parsedQuery = listSitesSchema.safeParse(req.query); if (!parsedQuery.success) { - return next(createHttpError(HttpCode.BAD_REQUEST, parsedQuery.error.errors.map(e => e.message).join(', '))); + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error), + ), + ); } const { limit, offset } = parsedQuery.data; - const parsedParams = listSitesParamsSchema.safeParse(req.params); if (!parsedParams.success) { - return next(createHttpError(HttpCode.BAD_REQUEST, parsedParams.error.errors.map(e => e.message).join(', '))); + return next( + createHttpError( + HttpCode.BAD_REQUEST, + 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.listSites, req); + const hasPermission = await checkUserActionPermission( + ActionsEnum.listSites, + 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", + ), + ); } if (orgId && orgId !== req.userOrgId) { - return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have access to this organization')); + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization", + ), + ); } const accessibleSites = await db - .select({ siteId: sql`COALESCE(${userSites.siteId}, ${roleSites.siteId})` }) + .select({ + siteId: sql`COALESCE(${userSites.siteId}, ${roleSites.siteId})`, + }) .from(userSites) .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId)) .where( or( eq(userSites.userId, req.user!.userId), - eq(roleSites.roleId, req.userOrgRoleId!) - ) + eq(roleSites.roleId, req.userOrgRoleId!), + ), ); - const accessibleSiteIds = accessibleSites.map(site => site.siteId); + const accessibleSiteIds = accessibleSites.map((site) => site.siteId); + const baseQuery = querySites(orgId, accessibleSiteIds); - let baseQuery: any = db - .select({ - siteId: sites.siteId, - name: sites.name, - subdomain: sites.subdomain, - pubKey: sites.pubKey, - subnet: sites.subnet, - megabytesIn: sites.megabytesIn, - megabytesOut: sites.megabytesOut, - orgName: orgs.name, - exitNodeName: exitNodes.name, - }) - .from(sites) - .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) - .where(inArray(sites.siteId, accessibleSiteIds)); - - let countQuery: any = db + let countQuery = db .select({ count: sql`cast(count(*) as integer)` }) .from(sites) - .where(inArray(sites.siteId, accessibleSiteIds)); - - if (orgId) { - baseQuery = baseQuery.where(eq(sites.orgId, orgId)); - countQuery = countQuery.where(eq(sites.orgId, orgId)); - } + .where( + and( + inArray(sites.siteId, accessibleSiteIds), + eq(sites.orgId, orgId), + ), + ); const sitesList = await baseQuery.limit(limit).offset(offset); const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; - return response(res, { + return response(res, { data: { sites: sitesList, pagination: { @@ -101,7 +156,11 @@ export async function listSites(req: Request, res: Response, next: NextFunction) status: HttpCode.OK, }); } 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/src/app/[orgId]/resources/[resourceId]/layout.tsx b/src/app/[orgId]/resources/[resourceId]/layout.tsx index c2d9373..d73dfc6 100644 --- a/src/app/[orgId]/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/resources/[resourceId]/layout.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { const sidebarNavItems = [ { title: "Profile", - href: "/{orgId}/resources/{resourceId}/", + href: "/{orgId}/resources/{resourceId}", }, { title: "Appearance", diff --git a/src/app/[orgId]/sites/[siteId]/layout.tsx b/src/app/[orgId]/sites/[siteId]/layout.tsx index 5965900..973323c 100644 --- a/src/app/[orgId]/sites/[siteId]/layout.tsx +++ b/src/app/[orgId]/sites/[siteId]/layout.tsx @@ -18,7 +18,7 @@ export const metadata: Metadata = { const sidebarNavItems = [ { title: "Profile", - href: "/{orgId}/sites/{siteId}/", + href: "/{orgId}/sites/{siteId}", }, { title: "Appearance", diff --git a/src/app/[orgId]/sites/page.tsx b/src/app/[orgId]/sites/page.tsx index a01c986..5322c71 100644 --- a/src/app/[orgId]/sites/page.tsx +++ b/src/app/[orgId]/sites/page.tsx @@ -1,6 +1,23 @@ -import Link from "next/link"; +import { internal } from "@app/api"; +import { authCookieHeader } from "@app/api/cookies"; +import { ListSitesResponse } from "@server/routers/site"; +import { AxiosResponse } from "axios"; + +type SitesPageProps = { + params: { orgId: string }; +}; + +export default async function Page({ params }: SitesPageProps) { + let sites: ListSitesResponse["sites"] = []; + try { + const res = await internal.get>( + `/org/${params.orgId}/sites`, + authCookieHeader(), + ); + sites = res.data.data.sites; + } catch (e) { + } -export default async function Page() { return ( <>