浏览代码

lighten dark background, add more info to resources table

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

+ 1 - 0
config.example.yml

@@ -14,6 +14,7 @@ traefik:
     cert_resolver: letsencrypt
     http_entrypoint: web
     https_entrypoint: websecure
+    prefer_wildcard_cert: true
 
 badger:
     session_query_parameter: __pang_sess

+ 2 - 2
server/routers/badger/verifySession.ts

@@ -165,7 +165,7 @@ function notAllowed(res: Response, redirectUrl?: string) {
         error: false,
         message: "Access denied",
         status: HttpCode.OK,
-    }
+    };
     logger.debug(JSON.stringify(data));
     return response<VerifyUserResponse>(res, data);
 }
@@ -177,7 +177,7 @@ function allowed(res: Response) {
         error: false,
         message: "Access allowed",
         status: HttpCode.OK,
-    }
+    };
     logger.debug(JSON.stringify(data));
     return response<VerifyUserResponse>(res, data);
 }

+ 1 - 0
server/routers/resource/createResource.ts

@@ -94,6 +94,7 @@ export async function createResource(
                 orgId,
                 name,
                 subdomain,
+                ssl: true,
             })
             .returning();
 

+ 48 - 19
server/routers/resource/listResources.ts

@@ -6,6 +6,8 @@ import {
     sites,
     userResources,
     roleResources,
+    resourcePassword,
+    resourcePincode,
 } from "@server/db/schema";
 import response from "@server/utils/response";
 import HttpCode from "@server/types/HttpCode";
@@ -46,39 +48,63 @@ const listResourcesSchema = z.object({
 function queryResources(
     accessibleResourceIds: number[],
     siteId?: number,
-    orgId?: string
+    orgId?: string,
 ) {
     if (siteId) {
         return db
             .select({
                 resourceId: resources.resourceId,
                 name: resources.name,
-                subdomain: resources.subdomain,
+                fullDomain: resources.fullDomain,
+                ssl: resources.ssl,
                 siteName: sites.name,
+                passwordId: resourcePassword.passwordId,
+                pincodeId: resourcePincode.pincodeId,
+                sso: resources.sso,
             })
             .from(resources)
             .leftJoin(sites, eq(resources.siteId, sites.siteId))
+            .leftJoin(
+                resourcePassword,
+                eq(resourcePassword.resourceId, resources.resourceId),
+            )
+            .leftJoin(
+                resourcePincode,
+                eq(resourcePincode.resourceId, resources.resourceId),
+            )
             .where(
                 and(
                     inArray(resources.resourceId, accessibleResourceIds),
-                    eq(resources.siteId, siteId)
-                )
+                    eq(resources.siteId, siteId),
+                ),
             );
     } else if (orgId) {
         return db
             .select({
                 resourceId: resources.resourceId,
                 name: resources.name,
-                subdomain: resources.subdomain,
+                ssl: resources.ssl,
+                fullDomain: resources.fullDomain,
                 siteName: sites.name,
+                passwordId: resourcePassword.passwordId,
+                sso: resources.sso,
+                pincodeId: resourcePincode.pincodeId,
             })
             .from(resources)
             .leftJoin(sites, eq(resources.siteId, sites.siteId))
+            .leftJoin(
+                resourcePassword,
+                eq(resourcePassword.resourceId, resources.resourceId),
+            )
+            .leftJoin(
+                resourcePincode,
+                eq(resourcePincode.resourceId, resources.resourceId),
+            )
             .where(
                 and(
                     inArray(resources.resourceId, accessibleResourceIds),
-                    eq(resources.orgId, orgId)
-                )
+                    eq(resources.orgId, orgId),
+                ),
             );
     }
 }
@@ -91,7 +117,7 @@ export type ListResourcesResponse = {
 export async function listResources(
     req: Request,
     res: Response,
-    next: NextFunction
+    next: NextFunction,
 ): Promise<any> {
     try {
         const parsedQuery = listResourcesSchema.safeParse(req.query);
@@ -99,8 +125,8 @@ export async function listResources(
             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;
@@ -110,8 +136,8 @@ export async function listResources(
             return next(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
-                    parsedParams.error.errors.map((e) => e.message).join(", ")
-                )
+                    parsedParams.error.errors.map((e) => e.message).join(", "),
+                ),
             );
         }
         const { siteId, orgId } = parsedParams.data;
@@ -120,8 +146,8 @@ export async function listResources(
             return next(
                 createHttpError(
                     HttpCode.FORBIDDEN,
-                    "User does not have access to this organization"
-                )
+                    "User does not have access to this organization",
+                ),
             );
         }
 
@@ -132,17 +158,17 @@ export async function listResources(
             .from(userResources)
             .fullJoin(
                 roleResources,
-                eq(userResources.resourceId, roleResources.resourceId)
+                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
+            (resource) => resource.resourceId,
         );
 
         let countQuery: any = db
@@ -173,7 +199,10 @@ export async function listResources(
     } catch (error) {
         logger.error(error);
         return next(
-            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "An error occurred",
+            ),
         );
     }
 }

+ 1 - 1
server/routers/traefik/getTraefikConfig.ts

@@ -117,7 +117,7 @@ export async function traefikConfigProvider(
                         ? config.traefik.https_entrypoint
                         : config.traefik.http_entrypoint,
                 ],
-                middlewares: resource.ssl ? [badgerMiddlewareName] : [],
+                middlewares: [badgerMiddlewareName],
                 service: serviceName,
                 rule: `Host(\`${fullDomain}\`)`,
                 ...(resource.ssl ? { tls } : {}),

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

@@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
 
     return (
         <>
-            <div className="w-full border-b bg-neutral-100 dark:bg-neutral-900 mb-6 select-none sm:px-0 px-3 pt-3">
+            <div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 mb-6 select-none sm:px-0 px-3 pt-3">
                 <div className="container mx-auto flex flex-col content-between gap-4 ">
                     <Header
                         email={user.email}

+ 103 - 4
src/app/[orgId]/settings/resources/components/ResourcesTable.tsx

@@ -9,7 +9,16 @@ import {
     DropdownMenuTrigger,
 } from "@app/components/ui/dropdown-menu";
 import { Button } from "@app/components/ui/button";
-import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
+import {
+    Copy,
+    ArrowRight,
+    ArrowUpDown,
+    MoreHorizontal,
+    Check,
+    ArrowUpRight,
+    ShieldOff,
+    ShieldCheck,
+} from "lucide-react";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import api from "@app/api";
@@ -26,6 +35,7 @@ export type ResourceRow = {
     orgId: string;
     domain: string;
     site: string;
+    hasAuth: boolean;
 };
 
 type ResourcesTableProps = {
@@ -91,10 +101,99 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                     </Button>
                 );
             },
+            cell: ({ row }) => {
+                const resourceRow = row.original;
+                return (
+                    <Button variant="outline">
+                        <Link
+                            href={`/${resourceRow.orgId}/settings/sites/${resourceRow.site}`}
+                        >
+                            {resourceRow.site}
+                        </Link>
+                        <ArrowUpRight className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            },
         },
         {
             accessorKey: "domain",
             header: "Domain",
+            cell: ({ row }) => {
+                const resourceRow = row.original;
+                return (
+                    <div className="flex items-center">
+                        <Link
+                            href={`https://${resourceRow.domain}`}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="hover:underline mr-2"
+                        >
+                            {resourceRow.domain}
+                        </Link>
+                        <Button
+                            variant="ghost"
+                            className="h-6 w-6 p-0"
+                            onClick={() => {
+                                navigator.clipboard.writeText(
+                                    resourceRow.domain,
+                                );
+                                const originalIcon = document.querySelector(
+                                    `#icon-${resourceRow.id}`,
+                                );
+                                if (originalIcon) {
+                                    originalIcon.classList.add("hidden");
+                                }
+                                const checkIcon = document.querySelector(
+                                    `#check-icon-${resourceRow.id}`,
+                                );
+                                if (checkIcon) {
+                                    checkIcon.classList.remove("hidden");
+                                    setTimeout(() => {
+                                        checkIcon.classList.add("hidden");
+                                        if (originalIcon) {
+                                            originalIcon.classList.remove(
+                                                "hidden",
+                                            );
+                                        }
+                                    }, 2000);
+                                }
+                            }}
+                        >
+                            <Copy
+                                id={`icon-${resourceRow.id}`}
+                                className="h-4 w-4"
+                            />
+                            <Check
+                                id={`check-icon-${resourceRow.id}`}
+                                className="hidden text-green-500 h-4 w-4"
+                            />
+                            <span className="sr-only">Copy domain</span>
+                        </Button>
+                    </div>
+                );
+            },
+        },
+        {
+            accessorKey: "hasAuth",
+            header: "Authentication",
+            cell: ({ row }) => {
+                const resourceRow = row.original;
+                return (
+                    <div>
+                        {resourceRow.hasAuth ? (
+                            <span className="text-green-500 flex items-center space-x-2">
+                                <ShieldCheck className="w-4 h-4" />
+                                <span>Protected</span>
+                            </span>
+                        ) : (
+                            <span className="text-yellow-500 flex items-center space-x-2">
+                                <ShieldOff className="w-4 h-4" />
+                                <span>Not Protected</span>
+                            </span>
+                        )}
+                    </div>
+                );
+            },
         },
         {
             id: "actions",
@@ -130,11 +229,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                                         <button
                                             onClick={() => {
                                                 setSelectedResource(
-                                                    resourceRow
+                                                    resourceRow,
                                                 );
                                                 setIsDeleteModalOpen(true);
                                             }}
-                                            className="text-red-600 hover:text-red-800 hover:underline cursor-pointer"
+                                            className="text-red-500"
                                         >
                                             Delete
                                         </button>
@@ -146,7 +245,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                                 className="ml-2"
                                 onClick={() =>
                                     router.push(
-                                        `/${resourceRow.orgId}/settings/resources/${resourceRow.id}`
+                                        `/${resourceRow.orgId}/settings/resources/${resourceRow.id}`,
                                     )
                                 }
                             >

+ 8 - 4
src/app/[orgId]/settings/resources/page.tsx

@@ -19,7 +19,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
     try {
         const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
             `/org/${params.orgId}/resources`,
-            await authCookieHeader()
+            await authCookieHeader(),
         );
         resources = res.data.data.resources;
     } catch (e) {
@@ -31,8 +31,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
         const getOrg = cache(async () =>
             internal.get<AxiosResponse<GetOrgResponse>>(
                 `/org/${params.orgId}`,
-                await authCookieHeader()
-            )
+                await authCookieHeader(),
+            ),
         );
         const res = await getOrg();
         org = res.data.data;
@@ -49,8 +49,12 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
             id: resource.resourceId,
             name: resource.name,
             orgId: params.orgId,
-            domain: resource.subdomain || "",
+            domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
             site: resource.siteName || "None",
+            hasAuth:
+                resource.sso ||
+                resource.pincodeId !== null ||
+                resource.pincodeId !== null,
         };
     });
 

+ 2 - 2
src/app/[orgId]/settings/sites/components/SitesTable.tsx

@@ -24,8 +24,8 @@ export type SiteRow = {
     id: number;
     nice: string;
     name: string;
-    mbIn: number;
-    mbOut: number;
+    mbIn: string;
+    mbOut: string;
     orgId: string;
 };
 

+ 13 - 3
src/app/[orgId]/settings/sites/page.tsx

@@ -15,20 +15,30 @@ export default async function SitesPage(props: SitesPageProps) {
     try {
         const res = await internal.get<AxiosResponse<ListSitesResponse>>(
             `/org/${params.orgId}/sites`,
-            await authCookieHeader()
+            await authCookieHeader(),
         );
         sites = res.data.data.sites;
     } catch (e) {
         console.error("Error fetching sites", e);
     }
 
+    function formatSize(mb: number): string {
+        if (mb >= 1024 * 1024) {
+            return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
+        } else if (mb >= 1024) {
+            return `${(mb / 1024).toFixed(2)} GB`;
+        } else {
+            return `${mb.toFixed(2)} MB`;
+        }
+    }
+
     const siteRows: SiteRow[] = sites.map((site) => {
         return {
             name: site.name,
             id: site.siteId,
             nice: site.niceId.toString(),
-            mbIn: site.megabytesIn || 0,
-            mbOut: site.megabytesOut || 0,
+            mbIn: formatSize(site.megabytesIn || 0),
+            mbOut: formatSize(site.megabytesOut || 0),
             orgId: params.orgId,
         };
     });

+ 11 - 11
src/app/globals.css

@@ -6,11 +6,11 @@
 @layer base {
   :root {
     --background: 0 0% 100%;
-    --foreground: 20 14.3% 4.1%;
+    --foreground: 20 5.0% 10.0%;
     --card: 0 0% 100%;
-    --card-foreground: 20 14.3% 4.1%;
+    --card-foreground: 20 5.0% 10.0%;
     --popover: 0 0% 100%;
-    --popover-foreground: 20 14.3% 4.1%;
+    --popover-foreground: 20 5.0% 10.0%;
     --primary: 24.6 95% 53.1%;
     --primary-foreground: 60 9.1% 97.8%;
     --secondary: 60 4.8% 95.9%;
@@ -33,24 +33,24 @@
   }
 
   .dark {
-    --background: 20 14.3% 4.1%;
+    --background: 20 5.0% 10.0%;
     --foreground: 60 9.1% 97.8%;
-    --card: 20 14.3% 4.1%;
+    --card: 20 5.0% 10.0%;
     --card-foreground: 60 9.1% 97.8%;
-    --popover: 20 14.3% 4.1%;
+    --popover: 20 5.0% 10.0%;
     --popover-foreground: 60 9.1% 97.8%;
     --primary: 20.5 90.2% 48.2%;
     --primary-foreground: 60 9.1% 97.8%;
-    --secondary: 12 6.5% 15.1%;
+    --secondary: 12 6.5% 25.0%;
     --secondary-foreground: 60 9.1% 97.8%;
-    --muted: 12 6.5% 15.1%;
+    --muted: 12 6.5% 25.0%;
     --muted-foreground: 24 5.4% 63.9%;
-    --accent: 12 6.5% 15.1%;
+    --accent: 12 6.5% 25.0%;
     --accent-foreground: 60 9.1% 97.8%;
     --destructive: 0 72.2% 50.6%;
     --destructive-foreground: 60 9.1% 97.8%;
-    --border: 12 6.5% 15.1%;
-    --input: 12 6.5% 15.1%;
+    --border: 12 6.5% 25.0%;
+    --input: 12 6.5% 25.0%;
     --ring: 20.5 90.2% 48.2%;
     --chart-1: 220 70% 50%;
     --chart-2: 160 60% 45%;