mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 00:36:38 +00:00
Merge branch 'dev' into refactor/save-button-positions
This commit is contained in:
@@ -1,16 +1,11 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import OrgUserProvider from "@app/providers/OrgUserProvider";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
|
||||
type BillingSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -23,8 +18,7 @@ export default async function BillingSettingsPage({
|
||||
}: BillingSettingsProps) {
|
||||
const { orgId } = await params;
|
||||
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
const user = await verifySession();
|
||||
|
||||
if (!user) {
|
||||
redirect(`/`);
|
||||
@@ -32,13 +26,7 @@ export default async function BillingSettingsPage({
|
||||
|
||||
let orgUser = null;
|
||||
try {
|
||||
const getOrgUser = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrgUser();
|
||||
const res = await getCachedOrgUser(orgId, user.userId);
|
||||
orgUser = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${orgId}`);
|
||||
@@ -46,13 +34,7 @@ export default async function BillingSettingsPage({
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
const res = await getCachedOrg(orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${orgId}`);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GetIdpResponse as GetOrgIdpResponse } from "@server/routers/idp";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
redirect(`/${params.orgId}/settings/idp`);
|
||||
}
|
||||
|
||||
const navItems: HorizontalTabs = [
|
||||
const navItems: TabItem[] = [
|
||||
{
|
||||
title: t("general"),
|
||||
href: `/${params.orgId}/settings/idp/${params.idpId}/general`
|
||||
|
||||
@@ -331,29 +331,24 @@ export default function Page() {
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("idpType")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("idpTypeDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={providerTypes}
|
||||
defaultValue={form.getValues("type")}
|
||||
onChange={(value) => {
|
||||
handleProviderChange(
|
||||
value as "oidc" | "google" | "azure"
|
||||
);
|
||||
}}
|
||||
cols={3}
|
||||
/>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t("idpType")}
|
||||
</span>
|
||||
</div>
|
||||
<StrategySelect
|
||||
options={providerTypes}
|
||||
defaultValue={form.getValues("type")}
|
||||
onChange={(value) => {
|
||||
handleProviderChange(
|
||||
value as "oidc" | "google" | "azure"
|
||||
);
|
||||
}}
|
||||
cols={3}
|
||||
/>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
createRemoteExitNode?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
export function ExitNodesDataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
createRemoteExitNode,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title={t("remoteExitNodes")}
|
||||
searchPlaceholder={t("searchRemoteExitNodes")}
|
||||
searchColumn="name"
|
||||
onAdd={createRemoteExitNode}
|
||||
addButtonText={t("remoteExitNodeAdd")}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import { ExitNodesDataTable } from "./ExitNodesDataTable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
|
||||
export type RemoteExitNodeRow = {
|
||||
id: string;
|
||||
exitNodeId: number | null;
|
||||
name: string;
|
||||
address: string;
|
||||
endpoint: string;
|
||||
orgId: string;
|
||||
type: string | null;
|
||||
online: boolean;
|
||||
dateCreated: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type ExitNodesTableProps = {
|
||||
remoteExitNodes: RemoteExitNodeRow[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function ExitNodesTable({
|
||||
remoteExitNodes,
|
||||
orgId
|
||||
}: ExitNodesTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<RemoteExitNodeRow | null>(
|
||||
null
|
||||
);
|
||||
const [rows, setRows] = useState<RemoteExitNodeRow[]>(remoteExitNodes);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
setRows(remoteExitNodes);
|
||||
}, [remoteExitNodes]);
|
||||
|
||||
const refreshData = async () => {
|
||||
console.log("Data refreshed");
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRemoteExitNode = (remoteExitNodeId: string) => {
|
||||
api.delete(`/org/${orgId}/remote-exit-node/${remoteExitNodeId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("remoteExitNodeErrorDelete"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("remoteExitNodeErrorDelete"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("remoteExitNodeErrorDelete")
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter(
|
||||
(row) => row.id !== remoteExitNodeId
|
||||
);
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ExtendedColumnDef<RemoteExitNodeRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
friendlyName: t("name"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
friendlyName: t("online"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("online")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
if (originalRow.online) {
|
||||
return (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
friendlyName: t("connectionType"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("connectionType")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
{originalRow.type === "remoteExitNode"
|
||||
? "Remote Exit Node"
|
||||
: originalRow.type}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "address",
|
||||
friendlyName: "Address",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Address
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "endpoint",
|
||||
friendlyName: "Endpoint",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Endpoint
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Version
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
{originalRow.version && originalRow.version ? (
|
||||
<Badge variant="secondary">
|
||||
{"v" + originalRow.version}
|
||||
</Badge>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
const remoteExitNodeId = nodeRow.id;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedNode(nodeRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<Button variant={"secondary"} size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedNode && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedNode(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("remoteExitNodeQuestionRemove")}</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("remoteExitNodeConfirmDelete")}
|
||||
onConfirm={async () =>
|
||||
deleteRemoteExitNode(selectedNode!.id)
|
||||
}
|
||||
string={selectedNode.name}
|
||||
title={t("remoteExitNodeDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExitNodesDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
createRemoteExitNode={() =>
|
||||
router.push(`/${orgId}/settings/remote-exit-nodes/create`)
|
||||
}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -131,19 +131,19 @@ export default function CredentialsPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
{t("credentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
{t("remoteNodeCredentialsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("endpoint") || "Endpoint"}
|
||||
{t("endpoint")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -153,8 +153,7 @@ export default function CredentialsPage() {
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("remoteExitNodeId") ||
|
||||
"Remote Exit Node ID"}
|
||||
{t("remoteExitNodeId")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displayRemoteExitNodeId ? (
|
||||
@@ -168,7 +167,7 @@ export default function CredentialsPage() {
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("secretKey") || "Secret Key"}
|
||||
{t("remoteExitNodeSecretKey")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displaySecret ? (
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={`Remote Exit Node ${remoteExitNode?.name || "Unknown"}`}
|
||||
title={`Remote Node ${remoteExitNode?.name || "Unknown"}`}
|
||||
description="Manage your remote exit node settings and configuration"
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||
import { AxiosResponse } from "axios";
|
||||
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
|
||||
import ExitNodesTable, {
|
||||
RemoteExitNodeRow
|
||||
} from "@app/components/ExitNodesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -127,7 +127,7 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
|
||||
56
src/app/[orgId]/settings/general/auth-page/page.tsx
Normal file
56
src/app/[orgId]/settings/general/auth-page/page.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm";
|
||||
import AuthPageSettings from "@app/components/private/AuthPageSettings";
|
||||
import { SettingsContainer } from "@app/components/Settings";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { getCachedSubscription } from "@app/lib/api/getCachedSubscription";
|
||||
import { build } from "@server/build";
|
||||
import type { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||
import {
|
||||
GetLoginPageBrandingResponse,
|
||||
GetLoginPageResponse
|
||||
} from "@server/routers/loginPage/types";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
export interface AuthPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function AuthPage(props: AuthPageProps) {
|
||||
const orgId = (await props.params).orgId;
|
||||
let subscriptionStatus: GetOrgTierResponse | null = null;
|
||||
try {
|
||||
const subRes = await getCachedSubscription(orgId);
|
||||
subscriptionStatus = subRes.data.data;
|
||||
} catch {}
|
||||
|
||||
let loginPage: GetLoginPageResponse | null = null;
|
||||
try {
|
||||
if (build === "saas") {
|
||||
const res = await internal.get<AxiosResponse<GetLoginPageResponse>>(
|
||||
`/org/${orgId}/login-page`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
if (res.status === 200) {
|
||||
loginPage = res.data.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
let loginPageBranding: GetLoginPageBrandingResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<GetLoginPageBrandingResponse>
|
||||
>(`/org/${orgId}/login-page-branding`, await authCookieHeader());
|
||||
if (res.status === 200) {
|
||||
loginPageBranding = res.data.data;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{build === "saas" && <AuthPageSettings loginPage={loginPage} />}
|
||||
<AuthPageBrandingForm orgId={orgId} branding={loginPageBranding} />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import OrgUserProvider from "@app/providers/OrgUserProvider";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type GeneralSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -23,8 +21,7 @@ export default async function GeneralSettingsPage({
|
||||
}: GeneralSettingsProps) {
|
||||
const { orgId } = await params;
|
||||
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
const user = await verifySession();
|
||||
|
||||
if (!user) {
|
||||
redirect(`/`);
|
||||
@@ -32,13 +29,7 @@ export default async function GeneralSettingsPage({
|
||||
|
||||
let orgUser = null;
|
||||
try {
|
||||
const getOrgUser = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrgUser();
|
||||
const res = await getCachedOrgUser(orgId, user.userId);
|
||||
orgUser = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${orgId}`);
|
||||
@@ -46,13 +37,7 @@ export default async function GeneralSettingsPage({
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
const res = await getCachedOrg(orgId);
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${orgId}`);
|
||||
@@ -60,12 +45,19 @@ export default async function GeneralSettingsPage({
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const navItems = [
|
||||
const navItems: TabItem[] = [
|
||||
{
|
||||
title: t("general"),
|
||||
href: `/{orgId}/settings/general`
|
||||
href: `/{orgId}/settings/general`,
|
||||
exact: true
|
||||
}
|
||||
];
|
||||
if (build !== "oss") {
|
||||
navItems.push({
|
||||
title: t("authPage"),
|
||||
href: `/{orgId}/settings/general/auth-page`
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -43,14 +43,13 @@ import {
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionFooter
|
||||
SettingsSectionForm
|
||||
} from "@app/components/Settings";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
@@ -113,29 +112,18 @@ const LOG_RETENTION_OPTIONS = [
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { orgUser } = userOrgUserContext();
|
||||
const router = useRouter();
|
||||
const { org } = useOrgContext();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { user } = useUserContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
// Check if security features are disabled due to licensing/subscription
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
const { isPaidUser, hasSaasSubscription } = usePaidStatus();
|
||||
|
||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||
const [loadingSave, setLoadingSave] = useState(false);
|
||||
const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] =
|
||||
useState(false);
|
||||
const authPageSettingsRef = useRef<AuthPageSettingsRef>(null);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
@@ -258,14 +246,6 @@ export default function GeneralPage() {
|
||||
// Update organization
|
||||
await api.post(`/org/${org?.org.orgId}`, reqData);
|
||||
|
||||
// Also save auth page settings if they have unsaved changes
|
||||
if (
|
||||
build === "saas" &&
|
||||
authPageSettingsRef.current?.hasUnsavedChanges()
|
||||
) {
|
||||
await authPageSettingsRef.current.saveAuthSettings();
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("orgUpdated"),
|
||||
description: t("orgUpdatedDescription")
|
||||
@@ -410,9 +390,7 @@ export default function GeneralPage() {
|
||||
{LOG_RETENTION_OPTIONS.filter(
|
||||
(option) => {
|
||||
if (
|
||||
build ==
|
||||
"saas" &&
|
||||
!subscription?.subscribed &&
|
||||
hasSaasSubscription &&
|
||||
option.value >
|
||||
30
|
||||
) {
|
||||
@@ -440,19 +418,15 @@ export default function GeneralPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{build != "oss" && (
|
||||
{build !== "oss" && (
|
||||
<>
|
||||
<SecurityFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAccess"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
(build == "saas" &&
|
||||
!subscription?.subscribed) ||
|
||||
(build == "enterprise" &&
|
||||
!isUnlocked());
|
||||
const isDisabled = !isPaidUser;
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -518,11 +492,7 @@ export default function GeneralPage() {
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAction"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
(build == "saas" &&
|
||||
!subscription?.subscribed) ||
|
||||
(build == "enterprise" &&
|
||||
!isUnlocked());
|
||||
const isDisabled = !isPaidUser;
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -590,8 +560,7 @@ export default function GeneralPage() {
|
||||
</SettingsSectionBody>
|
||||
|
||||
{build !== "oss" && (
|
||||
<>
|
||||
<hr className="my-10 max-w-xl" />
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("securitySettings")}
|
||||
@@ -601,14 +570,13 @@ export default function GeneralPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm className="mb-4">
|
||||
<SecurityFeaturesAlert />
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireTwoFactor"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
const isDisabled = !isPaidUser;
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -655,8 +623,7 @@ export default function GeneralPage() {
|
||||
control={form.control}
|
||||
name="maxSessionLengthHours"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
const isDisabled = !isPaidUser;
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -744,8 +711,7 @@ export default function GeneralPage() {
|
||||
control={form.control}
|
||||
name="passwordExpiryDays"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
const isDisabled = !isPaidUser;
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -831,7 +797,7 @@ export default function GeneralPage() {
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
@@ -848,8 +814,6 @@ export default function GeneralPage() {
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||
|
||||
{build !== "saas" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
|
||||
@@ -67,7 +67,10 @@ export default async function ClientResourcesPage(
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
alias: siteResource.alias || null,
|
||||
siteNiceId: siteResource.siteNiceId,
|
||||
niceId: siteResource.niceId
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||
disableIcmp: siteResource.disableIcmp || false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -12,10 +12,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -41,7 +37,7 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||
import { AxiosResponse } from "axios";
|
||||
@@ -51,6 +47,8 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import { toASCII, toUnicode } from "punycode";
|
||||
import { useActionState, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
|
||||
export default function GeneralForm() {
|
||||
const params = useParams();
|
||||
@@ -69,28 +67,14 @@ export default function GeneralForm() {
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
||||
);
|
||||
|
||||
console.log({ resource });
|
||||
|
||||
const [defaultSubdomain, defaultBaseDomain] = useMemo(() => {
|
||||
const resourceUrl = new URL(resourceFullDomain);
|
||||
const domain = resourceUrl.hostname;
|
||||
|
||||
const allDomainParts = domain.split(".");
|
||||
let sub = undefined;
|
||||
let base = domain;
|
||||
|
||||
if (allDomainParts.length >= 3) {
|
||||
// 3 parts: [subdomain, domain, tld]
|
||||
const [first, ...rest] = allDomainParts;
|
||||
sub = first;
|
||||
base = rest.join(".");
|
||||
}
|
||||
|
||||
return [sub, base];
|
||||
const resourceFullDomainName = useMemo(() => {
|
||||
const url = new URL(resourceFullDomain);
|
||||
return url.hostname;
|
||||
}, [resourceFullDomain]);
|
||||
|
||||
const [selectedDomain, setSelectedDomain] = useState<{
|
||||
domainId: string;
|
||||
domainNamespaceId?: string;
|
||||
subdomain?: string;
|
||||
fullDomain: string;
|
||||
baseDomain: string;
|
||||
@@ -177,7 +161,11 @@ export default function GeneralForm() {
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: updated.fullDomain,
|
||||
proxyPort: data.proxyPort
|
||||
proxyPort: data.proxyPort,
|
||||
domainId: data.domainId
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
});
|
||||
|
||||
toast({
|
||||
@@ -359,9 +347,6 @@ export default function GeneralForm() {
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
console.log(form.getValues());
|
||||
}}
|
||||
loading={saveLoading}
|
||||
disabled={saveLoading}
|
||||
form="general-settings-form"
|
||||
@@ -387,15 +372,26 @@ export default function GeneralForm() {
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
cols={1}
|
||||
defaultSubdomain={defaultSubdomain}
|
||||
defaultBaseDomain={defaultBaseDomain}
|
||||
defaultSubdomain={
|
||||
form.watch("subdomain") ?? resource.subdomain
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch("domainId") ?? resource.domainId
|
||||
}
|
||||
defaultFullDomain={resourceFullDomainName}
|
||||
onDomainChange={(res) => {
|
||||
const selected = {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain
|
||||
};
|
||||
const selected =
|
||||
res === null
|
||||
? null
|
||||
: {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain,
|
||||
domainNamespaceId:
|
||||
res.domainNamespaceId
|
||||
};
|
||||
|
||||
setSelectedDomain(selected);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1396,6 +1396,8 @@ export default function Page() {
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
onDomainChange={(res) => {
|
||||
if (!res) return;
|
||||
|
||||
httpForm.setValue(
|
||||
"subdomain",
|
||||
res.subdomain
|
||||
@@ -1848,7 +1850,7 @@ export default function Page() {
|
||||
|
||||
<Link
|
||||
className="text-sm text-primary flex items-center gap-1"
|
||||
href="https://docs.pangolin.net/manage/resources/tcp-udp-resources"
|
||||
href="https://docs.pangolin.net/manage/resources/public/raw-resources"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -23,7 +23,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
generateObfuscatedWireGuardConfig
|
||||
} from "@app/lib/wireguard";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -203,7 +203,7 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SecurityFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={3}>
|
||||
@@ -300,7 +300,7 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SecurityFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<SettingsSectionBody>
|
||||
{!loadingDefaults && (
|
||||
|
||||
@@ -53,7 +53,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
newtVersion: site.newtVersion || undefined,
|
||||
newtUpdateAvailable: site.newtUpdateAvailable || false,
|
||||
exitNodeName: site.exitNodeName || undefined,
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined,
|
||||
remoteExitNodeId: (site as any).remoteExitNodeId || undefined
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user