lighten dark background, add more info to resources table

This commit is contained in:
Milo Schwartz 2024-11-24 22:34:11 -05:00
parent 658a6ca7bb
commit ce2bfcddd5
No known key found for this signature in database
11 changed files with 191 additions and 47 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return ( 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 "> <div className="container mx-auto flex flex-col content-between gap-4 ">
<Header <Header
email={user.email} email={user.email}

View file

@ -9,7 +9,16 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; 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 Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import api from "@app/api"; import api from "@app/api";
@ -26,6 +35,7 @@ export type ResourceRow = {
orgId: string; orgId: string;
domain: string; domain: string;
site: string; site: string;
hasAuth: boolean;
}; };
type ResourcesTableProps = { type ResourcesTableProps = {
@ -91,10 +101,99 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
</Button> </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", accessorKey: "domain",
header: "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", id: "actions",
@ -130,11 +229,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<button <button
onClick={() => { onClick={() => {
setSelectedResource( setSelectedResource(
resourceRow resourceRow,
); );
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}} }}
className="text-red-600 hover:text-red-800 hover:underline cursor-pointer" className="text-red-500"
> >
Delete Delete
</button> </button>
@ -146,7 +245,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
className="ml-2" className="ml-2"
onClick={() => onClick={() =>
router.push( router.push(
`/${resourceRow.orgId}/settings/resources/${resourceRow.id}` `/${resourceRow.orgId}/settings/resources/${resourceRow.id}`,
) )
} }
> >

View file

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

View file

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

View file

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

View file

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