瀏覽代碼

fixed listSites endpoint

Milo Schwartz 9 月之前
父節點
當前提交
3bb7efb7c6

+ 51 - 39
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 {
 export enum ActionsEnum {
     createOrg = "createOrg",
     createOrg = "createOrg",
@@ -54,75 +54,87 @@ export enum ActionsEnum {
     removeUserSite = "removeUserSite",
     removeUserSite = "removeUserSite",
 }
 }
 
 
-export async function checkUserActionPermission(actionId: string, req: Request): Promise<boolean> {
+export async function checkUserActionPermission(
+    actionId: string,
+    req: Request,
+): Promise<boolean> {
     const userId = req.user?.userId;
     const userId = req.user?.userId;
-    let onlyCheckUser = false;
-
-    if (actionId = ActionsEnum.createOrg) {
-        onlyCheckUser = true;
-    }
 
 
     if (!userId) {
     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 {
     try {
         let userOrgRoleId = req.userOrgRoleId;
         let userOrgRoleId = req.userOrgRoleId;
 
 
         // If userOrgRoleId is not available on the request, fetch it
         // 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)
                 .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);
                 .limit(1);
 
 
             if (userOrgRole.length === 0) {
             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;
             userOrgRoleId = userOrgRole[0].roleId;
         }
         }
 
 
         // Check if the user has direct permission for the action in the current org
         // 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)
             .from(userActions)
             .where(
             .where(
                 and(
                 and(
                     eq(userActions.userId, userId),
                     eq(userActions.userId, userId),
                     eq(userActions.actionId, actionId),
                     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);
             .limit(1);
 
 
         if (userActionPermission.length > 0) {
         if (userActionPermission.length > 0) {
             return true;
             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;
         return false;
-
     } catch (error) {
     } 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",
+        );
     }
     }
 }
 }

+ 108 - 49
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 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({
 const listSitesParamsSchema = z.object({
-    orgId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
+    orgId: z.string(),
 });
 });
 
 
 const listSitesSchema = z.object({
 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<any> {
+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<ReturnType<typeof querySites>>;
+    pagination: { total: number; limit: number; offset: number };
+};
+
+export async function listSites(
+    req: Request,
+    res: Response,
+    next: NextFunction,
+): Promise<any> {
     try {
     try {
         const parsedQuery = listSitesSchema.safeParse(req.query);
         const parsedQuery = listSitesSchema.safeParse(req.query);
         if (!parsedQuery.success) {
         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 { limit, offset } = parsedQuery.data;
 
 
-
         const parsedParams = listSitesParamsSchema.safeParse(req.params);
         const parsedParams = listSitesParamsSchema.safeParse(req.params);
         if (!parsedParams.success) {
         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;
         const { orgId } = parsedParams.data;
 
 
         // Check if the user has permission to list sites
         // 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) {
         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) {
         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
         const accessibleSites = await db
-            .select({ siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})` })
+            .select({
+                siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`,
+            })
             .from(userSites)
             .from(userSites)
             .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
             .fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
             .where(
             .where(
                 or(
                 or(
                     eq(userSites.userId, req.user!.userId),
                     eq(userSites.userId, req.user!.userId),
-                    eq(roleSites.roleId, req.userOrgRoleId!)
-                )
+                    eq(roleSites.roleId, req.userOrgRoleId!),
+                ),
             );
             );
 
 
-        const accessibleSiteIds = accessibleSites.map(site => site.siteId);
-
-        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));
+        const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
+        const baseQuery = querySites(orgId, accessibleSiteIds);
 
 
-        let countQuery: any = db
+        let countQuery = db
             .select({ count: sql<number>`cast(count(*) as integer)` })
             .select({ count: sql<number>`cast(count(*) as integer)` })
             .from(sites)
             .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 sitesList = 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<ListSitesResponse>(res, {
             data: {
             data: {
                 sites: sitesList,
                 sites: sitesList,
                 pagination: {
                 pagination: {
@@ -101,7 +156,11 @@ export async function listSites(req: Request, res: Response, next: NextFunction)
             status: HttpCode.OK,
             status: HttpCode.OK,
         });
         });
     } catch (error) {
     } 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...",
+            ),
+        );
     }
     }
 }
 }

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

@@ -12,7 +12,7 @@ export const metadata: Metadata = {
 const sidebarNavItems = [
 const sidebarNavItems = [
     {
     {
         title: "Profile",
         title: "Profile",
-        href: "/{orgId}/resources/{resourceId}/",
+        href: "/{orgId}/resources/{resourceId}",
     },
     },
     {
     {
         title: "Appearance",
         title: "Appearance",

+ 1 - 1
src/app/[orgId]/sites/[siteId]/layout.tsx

@@ -18,7 +18,7 @@ export const metadata: Metadata = {
 const sidebarNavItems = [
 const sidebarNavItems = [
     {
     {
         title: "Profile",
         title: "Profile",
-        href: "/{orgId}/sites/{siteId}/",
+        href: "/{orgId}/sites/{siteId}",
     },
     },
     {
     {
         title: "Appearance",
         title: "Appearance",

+ 19 - 2
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<AxiosResponse<ListSitesResponse>>(
+            `/org/${params.orgId}/sites`,
+            authCookieHeader(),
+        );
+        sites = res.data.data.sites;
+    } catch (e) {
+    }
 
 
-export default async function Page() {
     return (
     return (
         <>
         <>
             <div className="space-y-0.5 select-none">
             <div className="space-y-0.5 select-none">