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;
+ }
+ })
+};