diff --git a/messages/en-US.json b/messages/en-US.json index 01613779..e5cafc97 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -452,6 +452,12 @@ "selectDuration": "Select duration", "selectResource": "Select Resource", "filterByResource": "Filter By Resource", + "selectApprovalState": "Select Approval State", + "filterByApprovalState": "Filter By Approval State", + "approve": "Approve", + "deny": "Deny", + "viewDetails": "View Details", + "requestingNewDeviceApproval": "is requesting new device", "resetFilters": "Reset Filters", "totalBlocked": "Requests Blocked By Pangolin", "totalRequests": "Total Requests", diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index fb7b4fed..05456b1a 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -308,10 +308,12 @@ export const approvals = pgTable("approvals", { clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }), // clients reference user devices (in this case) - userId: varchar("userId").references(() => users.userId, { - // optionally tied to a user and in this case delete when the user deletes - onDelete: "cascade" - }), + userId: varchar("userId") + .references(() => users.userId, { + // optionally tied to a user and in this case delete when the user deletes + onDelete: "cascade" + }) + .notNull(), decision: varchar("decision") .$type<"approved" | "denied" | "pending">() .default("pending") diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts index 305785dc..a54b134b 100644 --- a/server/private/routers/approvals/listApprovals.ts +++ b/server/private/routers/approvals/listApprovals.ts @@ -50,13 +50,16 @@ async function queryApprovals(orgId: string, limit: number, offset: number) { approvalId: approvals.id, orgId: approvals.orgId, clientId: approvals.clientId, - userId: users.userId, - username: users.username, - name: users.name, decision: approvals.decision, - type: approvals.type + type: approvals.type, + user: { + name: users.name, + userId: users.userId, + username: users.username + } }) .from(approvals) + .innerJoin(users, and(eq(approvals.userId, users.userId))) .leftJoin( clients, and( @@ -64,7 +67,6 @@ async function queryApprovals(orgId: string, limit: number, offset: number) { not(isNull(clients.userId)) // only user devices ) ) - .leftJoin(users, and(eq(approvals.userId, users.userId))) .where(eq(approvals.orgId, orgId)) .orderBy(desc(approvals.timestamp)) .limit(limit) diff --git a/src/app/[orgId]/settings/access/approvals/page.tsx b/src/app/[orgId]/settings/access/approvals/page.tsx index b5369782..ce871719 100644 --- a/src/app/[orgId]/settings/access/approvals/page.tsx +++ b/src/app/[orgId]/settings/access/approvals/page.tsx @@ -1,3 +1,4 @@ +import { ApprovalFeed } from "@app/components/ApprovalFeed"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; @@ -42,7 +43,9 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) { description={t("accessApprovalsDescription")} /> -

Orgs

+
+ +
); diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 132f0c05..85f0e2b1 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -2,10 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; -import SitesTable, { SiteRow } from "../../../../components/SitesTable"; +import SitesTable, { SiteRow } from "@app/components/SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SitesBanner from "@app/components/SitesBanner"; -import SitesSplashCard from "../../../../components/SitesSplashCard"; import { getTranslations } from "next-intl/server"; type SitesPageProps = { diff --git a/src/components/ApprovalFeed.tsx b/src/components/ApprovalFeed.tsx new file mode 100644 index 00000000..469762b4 --- /dev/null +++ b/src/components/ApprovalFeed.tsx @@ -0,0 +1,183 @@ +"use client"; +import { useTranslations } from "next-intl"; +import { useSearchParams, usePathname, useRouter } from "next/navigation"; +import { Card, CardHeader } from "./ui/card"; +import { Button } from "./ui/button"; +import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react"; +import { cn } from "@app/lib/cn"; +import { Label } from "./ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "./ui/select"; +import { approvalFiltersSchema, approvalQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import { Fragment } from "react"; +import { Separator } from "./ui/separator"; +import type { ListApprovalsResponse } from "@server/private/routers/approvals"; +import Link from "next/link"; + +export type ApprovalFeedProps = { + orgId: string; +}; + +export function ApprovalFeed({ orgId }: ApprovalFeedProps) { + const searchParams = useSearchParams(); + const path = usePathname(); + const t = useTranslations(); + + const router = useRouter(); + + const filters = approvalFiltersSchema.parse( + Object.fromEntries(searchParams.entries()) + ); + + const { data, isFetching, refetch } = useQuery( + approvalQueries.listApprovals(orgId) + ); + + const approvals = data?.approvals ?? []; + + console.log({ + approvals + }); + + return ( +
+ + +
+ + +
+ + +
+
+ + +
    + {approvals.map((approval, index) => ( + +
  • + +
  • + {/* */} + {index < approvals.length - 1 && } + {/*
  • + +
  • + +
  • + +
  • */} +
    + ))} +
+
+
+
+ ); +} + +type ApprovalRequestProps = { + approval: ListApprovalsResponse["approvals"][number]; +}; + +function ApprovalRequest({ approval }: ApprovalRequestProps) { + const t = useTranslations(); + return ( +
+
+ + + + {approval.user.username} + +   + {approval.type === "user_device" && ( + {t("requestingNewDeviceApproval")} + )} + +
+
+ + + +
+
+ ); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 5ea3c2f2..4bdd2310 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -21,6 +21,7 @@ import type { } from "@server/routers/resource"; import type { ListTargetsResponse } from "@server/routers/target"; import type { ListDomainsResponse } from "@server/routers/domain"; +import type { ListApprovalsResponse } from "@server/private/routers/approvals"; export type ProductUpdate = { link: string | null; @@ -311,3 +312,25 @@ export const resourceQueries = { } }) }; + +export const approvalFiltersSchema = z.object({ + approvalState: z + .enum(["pending", "approved", "denied", "all"]) + .optional() + .catch("pending") +}); + +export const approvalQueries = { + listApprovals: (orgId: string) => + queryOptions({ + queryKey: ["APPROVALS", orgId] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/approvals`, { + signal + }); + return res.data.data; + } + }) +};