Merge branch 'feat/resource-policies' into resource-policies

This commit is contained in:
Owen
2026-05-04 14:40:44 -07:00
66 changed files with 11356 additions and 1169 deletions

View File

@@ -0,0 +1,23 @@
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
import OrgProvider from "@app/providers/OrgProvider";
import type { GetOrgResponse } from "@server/routers/org";
import { redirect } from "next/navigation";
export interface PolicyLayoutPageProps {
params: Promise<{ orgId: string }>;
children: React.ReactNode;
}
export default async function PolicyLayoutPage(props: PolicyLayoutPageProps) {
const params = await props.params;
let org: GetOrgResponse | null = null;
try {
const res = await getCachedOrg(params.orgId);
org = res.data.data;
} catch {
redirect(`/${params.orgId}/settings`);
}
return <OrgProvider org={org}>{props.children}</OrgProvider>;
}

View File

@@ -0,0 +1,60 @@
import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider";
import type { GetResourcePolicyResponse } from "@server/routers/policy";
import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { redirect } from "next/navigation";
export interface EditPolicyPageProps {
params: Promise<{ niceId: string; orgId: string }>;
}
export default async function EditPolicyPage(props: EditPolicyPageProps) {
const params = await props.params;
const t = await getTranslations();
let policyResponse: GetResourcePolicyResponse | null = null;
try {
const res = await internal.get<
AxiosResponse<GetResourcePolicyResponse>
>(
`/org/${params.orgId}/resource-policy/${params.niceId}`,
await authCookieHeader()
);
policyResponse = res.data.data;
} catch {
redirect(`/${params.orgId}/settings/policies/resource`);
}
if (!policyResponse) {
redirect(`/${params.orgId}/settings/policies/resource`);
}
return (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={t("resourcePolicySetting", {
policyName: policyResponse.name
})}
description={t("resourcePolicySettingDescription")}
/>
<Button asChild variant="outline">
<Link href={`/${params.orgId}/settings/policies/resource`}>
{t("resourcePoliciesSeeAll")}
</Link>
</Button>
</div>
<ResourcePolicyProvider policy={policyResponse}>
<EditPolicyForm />
</ResourcePolicyProvider>
</>
);
}

View File

@@ -0,0 +1,35 @@
import { CreatePolicyForm } from "@app/components/resource-policy/CreatePolicyForm";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
export interface CreateResourcePolicyPageProps {
params: Promise<{ orgId: string }>;
}
export default async function CreateResourcePolicyPage(
props: CreateResourcePolicyPageProps
) {
const params = await props.params;
const t = await getTranslations();
return (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={t("resourcePoliciesCreate")}
description={t("resourcePoliciesCreateDescription")}
/>
<Button asChild variant="outline">
<Link href={`/${params.orgId}/settings/policies/resource`}>
{t("resourcePoliciesSeeAll")}
</Link>
</Button>
</div>
<CreatePolicyForm />
</>
);
}

View File

@@ -0,0 +1,68 @@
import { ResourcePoliciesTable } from "@app/components/ResourcePoliciesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
import type { GetOrgResponse } from "@server/routers/org";
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export interface ResourcePoliciesPageProps {
params: Promise<{ orgId: string }>;
searchParams: Promise<Record<string, string>>;
}
export default async function ResourcePoliciesPage(
props: ResourcePoliciesPageProps
) {
const params = await props.params;
const t = await getTranslations();
const searchParams = new URLSearchParams(await props.searchParams);
let org: GetOrgResponse | null = null;
try {
const res = await getCachedOrg(params.orgId);
org = res.data.data;
} catch {
redirect(`/${params.orgId}/settings/resources`);
}
let policies: ListResourcePoliciesResponse["policies"] = [];
let pagination: ListResourcePoliciesResponse["pagination"] = {
total: 0,
page: 1,
pageSize: 20
};
try {
const res = await internal.get<
AxiosResponse<ListResourcePoliciesResponse>
>(
`/org/${params.orgId}/resource-policies?${searchParams.toString()}`,
await authCookieHeader()
);
const responseData = res.data.data;
policies = responseData.policies;
pagination = responseData.pagination;
} catch (e) {}
return (
<>
<SettingsSectionTitle
title={t("resourcePoliciesTitle")}
description={t("resourcePoliciesDescription")}
/>
<ResourcePoliciesTable
policies={policies}
orgId={params.orgId}
rowCount={pagination.total}
pagination={{
pageIndex: pagination.page - 1,
pageSize: pagination.pageSize
}}
/>
</>
);
}

View File

@@ -13,6 +13,7 @@ import { Layout } from "@app/components/Layout";
import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv";
import { orgNavSections } from "@app/app/navigation";
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
export const dynamic = "force-dynamic";
@@ -48,13 +49,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const t = await getTranslations();
try {
const getOrgUser = cache(() =>
internal.get<AxiosResponse<GetOrgUserResponse>>(
`/org/${params.orgId}/user/${user.userId}`,
cookie
)
);
const orgUser = await getOrgUser();
const orgUser = await getCachedOrgUser(params.orgId, user.userId);
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
throw new Error(t("userErrorNotAdminOrOwner"));

View File

@@ -96,10 +96,10 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
title: t("authentication"),
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
});
navItems.push({
title: t("rules"),
href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
});
// navItems.push({
// title: t("rules"),
// href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
// });
}
return (

View File

@@ -92,7 +92,13 @@ import { useTranslations } from "next-intl";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { toASCII } from "punycode";
import { useEffect, useMemo, useState, useCallback } from "react";
import {
useMemo,
useState,
useCallback,
useTransition,
useEffect
} from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@@ -218,7 +224,7 @@ export default function Page() {
>([]);
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
const [createLoading, setCreateLoading] = useState(false);
const [createLoading, startTransition] = useTransition();
const [showSnippets, setShowSnippets] = useState(false);
const [niceId, setNiceId] = useState<string>("");
@@ -328,7 +334,7 @@ export default function Page() {
id: "raw" as ResourceType,
title: t("resourceRaw"),
description:
build == "saas"
build === "saas"
? t("resourceRawDescriptionCloud")
: t("resourceRawDescription")
}
@@ -473,8 +479,6 @@ export default function Page() {
);
async function onSubmit() {
setCreateLoading(true);
const baseData = baseForm.getValues();
const isHttp = baseData.http;
@@ -610,8 +614,6 @@ export default function Page() {
)
});
}
setCreateLoading(false);
}
useEffect(() => {
@@ -1465,7 +1467,7 @@ export default function Page() {
console.log(httpForm.getValues());
if (baseValid && settingsValid) {
onSubmit();
startTransition(onSubmit);
}
}}
loading={createLoading}

View File

@@ -11,6 +11,7 @@ import {
CreditCard,
Fingerprint,
Globe,
GlobeIcon,
GlobeLock,
KeyRound,
Laptop,
@@ -22,6 +23,7 @@ import {
ScanEye,
Server,
Settings,
ShieldIcon,
SquareMousePointer,
TicketCheck,
Unplug,
@@ -99,7 +101,7 @@ export const orgNavSections = (
href: "/{orgId}/settings/domains",
icon: <Globe className="size-4 flex-none" />
},
...(build == "saas"
...(build === "saas"
? [
{
title: "sidebarRemoteExitNodes",
@@ -134,6 +136,24 @@ export const orgNavSections = (
}
]
},
...(build !== "oss"
? [
{
title: "sidebarPolicies",
icon: <ShieldIcon className="size-4 flex-none" />,
items: [
{
title: "sidebarResourcePolicies",
href: "/{orgId}/settings/policies/resource",
icon: (
<GlobeIcon className="size-4 flex-none" />
)
}
]
}
]
: []),
// PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" ||