浏览代码

fix list resources endpoint and more color tweaks

Milo Schwartz 8 月之前
父节点
当前提交
d677d9c8ea

+ 1 - 0
server/index.ts

@@ -91,6 +91,7 @@ declare global {
         interface Request {
             user?: User;
             userOrgRoleId?: number;
+            orgId?: string;
             userOrgId?: string;
             userOrgIds?: string[];
         }

+ 1 - 1
server/routers/org/listOrgs.ts

@@ -91,4 +91,4 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
         logger.error(error);
         return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
     }
-}
+}

+ 133 - 54
server/routers/resource/listResources.ts

@@ -1,99 +1,173 @@
-import { Request, Response, NextFunction } from 'express';
-import { z } from 'zod';
-import { db } from '@server/db';
-import { resources, sites, userResources, roleResources } from '@server/db/schema';
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import {
+    resources,
+    sites,
+    userResources,
+    roleResources,
+} from "@server/db/schema";
 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 HttpCode from "@server/types/HttpCode";
+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";
 
-const listResourcesParamsSchema = z.object({
-    siteId: z.coerce.number().int().positive().optional(),
-    orgId: z.coerce.number().int().positive().optional(),
-}).refine(data => !!data.siteId !== !!data.orgId, {
-    message: "Either siteId or orgId must be provided, but not both",
-});
+const listResourcesParamsSchema = z
+    .object({
+        siteId: z.number().int().positive().optional(),
+        orgId: z.string().optional(),
+    })
+    .refine((data) => !!data.siteId !== !!data.orgId, {
+        message: "Either siteId or orgId must be provided, but not both",
+    });
 
 const listResourcesSchema = z.object({
-    limit: z.coerce.number().int().positive().default(10),
-    offset: z.coerce.number().int().nonnegative().default(0),
+    limit: z
+        .string()
+        .optional()
+        .default("0")
+        .transform(Number)
+        .pipe(z.number().int().nonnegative()),
+
+    offset: z
+        .string()
+        .optional()
+        .default("0")
+        .transform(Number)
+        .pipe(z.number().int().nonnegative()),
 });
 
-interface RequestWithOrgAndRole extends Request {
-    userOrgRoleId?: number;
-    orgId?: number;
+function queryResources(
+    accessibleResourceIds: string[],
+    siteId?: number,
+    orgId?: string,
+) {
+    if (siteId) {
+        return db
+            .select({
+                resourceId: resources.resourceId,
+                name: resources.name,
+                subdomain: resources.subdomain,
+                siteName: sites.name,
+            })
+            .from(resources)
+            .leftJoin(sites, eq(resources.siteId, sites.siteId))
+            .where(
+                and(
+                    inArray(resources.resourceId, accessibleResourceIds),
+                    eq(resources.siteId, siteId),
+                ),
+            );
+    } else if (orgId) {
+        return db
+            .select({
+                resourceId: resources.resourceId,
+                name: resources.name,
+                subdomain: resources.subdomain,
+                siteName: sites.name,
+            })
+            .from(resources)
+            .leftJoin(sites, eq(resources.siteId, sites.siteId))
+            .where(
+                and(
+                    inArray(resources.resourceId, accessibleResourceIds),
+                    eq(resources.orgId, orgId),
+                ),
+            );
+    }
 }
 
-export async function listResources(req: RequestWithOrgAndRole, res: Response, next: NextFunction): Promise<any> {
+export type ListSitesResponse = {
+    resources: NonNullable<Awaited<ReturnType<typeof queryResources>>>;
+    pagination: { total: number; limit: number; offset: number };
+};
+
+export async function listResources(
+    req: Request,
+    res: Response,
+    next: NextFunction,
+): Promise<any> {
     try {
         const parsedQuery = listResourcesSchema.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,
+                    parsedQuery.error.errors.map((e) => e.message).join(", "),
+                ),
+            );
         }
         const { limit, offset } = parsedQuery.data;
 
         const parsedParams = listResourcesParamsSchema.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 { siteId, orgId } = parsedParams.data;
 
         // Check if the user has permission to list sites
-        const hasPermission = await checkUserActionPermission(ActionsEnum.listResources, req);
+        const hasPermission = await checkUserActionPermission(
+            ActionsEnum.listResources,
+            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.orgId) {
-            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",
+                ),
+            );
         }
 
         // Get the list of resources the user has access to
         const accessibleResources = await db
-            .select({ resourceId: sql<string>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` })
+            .select({
+                resourceId: sql<string>`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})`,
+            })
             .from(userResources)
-            .fullJoin(roleResources, eq(userResources.resourceId, roleResources.resourceId))
+            .fullJoin(
+                roleResources,
+                eq(userResources.resourceId, roleResources.resourceId),
+            )
             .where(
                 or(
                     eq(userResources.userId, req.user!.userId),
-                    eq(roleResources.roleId, req.userOrgRoleId!)
-                )
+                    eq(roleResources.roleId, req.userOrgRoleId!),
+                ),
             );
 
-        const accessibleResourceIds = accessibleResources.map(resource => resource.resourceId);
-
-        let baseQuery: any = db
-            .select({
-                resourceId: resources.resourceId,
-                name: resources.name,
-                subdomain: resources.subdomain,
-                siteName: sites.name,
-            })
-            .from(resources)
-            .leftJoin(sites, eq(resources.siteId, sites.siteId))
-            .where(inArray(resources.resourceId, accessibleResourceIds));
+        const accessibleResourceIds = accessibleResources.map(
+            (resource) => resource.resourceId,
+        );
 
         let countQuery: any = db
-            .select({ count: sql<number>`cast(count(*) as integer)` })
+            .select({ count: count() })
             .from(resources)
             .where(inArray(resources.resourceId, accessibleResourceIds));
 
-        if (siteId) {
-            baseQuery = baseQuery.where(eq(resources.siteId, siteId));
-            countQuery = countQuery.where(eq(resources.siteId, siteId));
-        } else {
-            // If orgId is provided, it's already checked to match req.orgId
-            baseQuery = baseQuery.where(eq(resources.orgId, req.orgId!));
-            countQuery = countQuery.where(eq(resources.orgId, req.orgId!));
-        }
+        const baseQuery = queryResources(accessibleResourceIds, siteId, orgId);
 
-        const resourcesList = await baseQuery.limit(limit).offset(offset);
+        const resourcesList = await baseQuery!.limit(limit).offset(offset);
         const totalCountResult = await countQuery;
         const totalCount = totalCountResult[0].count;
 
-        return response(res, {
+        return response<ListSitesResponse>(res, {
             data: {
                 resources: resourcesList,
                 pagination: {
@@ -109,6 +183,11 @@ export async function listResources(req: RequestWithOrgAndRole, res: Response, n
         });
     } 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...",
+            ),
+        );
     }
 }

+ 2 - 2
server/routers/site/listSites.ts

@@ -3,7 +3,7 @@ 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 { and, eq, inArray, or, sql } from "drizzle-orm";
+import { and, count, eq, inArray, or, sql } from "drizzle-orm";
 import { NextFunction, Request, Response } from "express";
 import createHttpError from "http-errors";
 import { z } from "zod";
@@ -123,7 +123,7 @@ export async function listSites(
         const baseQuery = querySites(orgId, accessibleSiteIds);
 
         let countQuery = db
-            .select({ count: sql<number>`cast(count(*) as integer)` })
+            .select({ count: count() })
             .from(sites)
             .where(
                 and(

+ 1 - 1
src/app/[orgId]/sites/components/DataTable.tsx

@@ -69,7 +69,7 @@ export function DataTable<TData, TValue>({
                             .getColumn("name")
                             ?.setFilterValue(event.target.value)
                     }
-                    className="max-w-sm"
+                    className="max-w-sm mr-2"
                 />
                 <Button onClick={() => {
                     if (addSite) {

+ 12 - 12
src/app/globals.css

@@ -4,32 +4,32 @@
 
 @layer base {
     :root {
-        --background: 0 0% 100%;
+        --background: 0 0% 96.47%;
         --foreground: 30 28.57% 2.75%;
 
         --primary: 23.64 23.74% 27.25%;
-        --primary-foreground: 20 8.11% 92.75%;
+        --primary-foreground: 0 0% 100%;
 
-        --card: 30 11.11% 96.47%;
+        --card: 210 20% 98.04%;
         --card-foreground: 30 28.57% 2.75%;
 
         --popover: 0 0% 100%;
         --popover-foreground: 30 28.57% 2.75%;
 
-        --secondary: 24 9.09% 89.22%;
-        --secondary-foreground: 0 0% 0%;
+        --secondary: 26.67 14.88% 47.45%;
+        --secondary-foreground: 0 0% 100%;
 
-        --muted: 20 8.11% 92.75%;
+        --muted: 0 0% 90.98%;
         --muted-foreground: 0 0% 40%;
 
-        --accent: 20 8.11% 92.75%;
+        --accent: 0 0% 86.67%;
         --accent-foreground: 24 23.81% 4.12%;
 
         --destructive: 0 84.2% 60.2%;
         --destructive-foreground: 210 40% 98%;
 
         --border: 20 8.11% 85.49%;
-        --input: 30 3.64% 78.43%;
+        --input: 20 8.11% 85.49%;
         --ring: 23.64 23.74% 27.25%;
 
         --chart-1: 23.64 23.74% 27.25%;
@@ -38,7 +38,7 @@
         --chart-4: 23.33 8.82% 60%;
         --chart-5: 24 8.98% 67.25%;
 
-        --radius: 0.25rem;
+        --radius: 0.5rem;
     }
     .dark {
         --background: 0 0% 10.98%;
@@ -47,7 +47,7 @@
         --primary: 23.64 23.74% 27.25%;
         --primary-foreground: 20 8.11% 92.75%;
 
-        --card: 0 0% 8.63%;
+        --card: 0 0% 9.8%;
         --card-foreground: 24 9.09% 89.22%;
 
         --popover: 0 0% 10.98%;
@@ -66,7 +66,7 @@
         --destructive-foreground: 210 40% 98%;
 
         --border: 20 21.43% 5.49%;
-        --input: 0 0% 27.06%;
+        --input: 0 0% 23.53%;
         --ring: 23.64 23.74% 27.25%;
 
         --chart-1: 23.64 23.74% 27.25%;
@@ -75,7 +75,7 @@
         --chart-4: 23.33 23.68% 14.9%;
         --chart-5: 24 23.81% 12.35%;
 
-        --radius: 0.25rem;
+        --radius: 0.5rem;
     }
 }