Merge branch 'dev' into feat/option-to-regenerate-keys

This commit is contained in:
Owen
2025-11-09 10:43:26 -08:00
40 changed files with 1669 additions and 1313 deletions

View File

@@ -73,7 +73,7 @@ export default async function DomainSettingsPage({
<DNSRecordsTable records={dnsRecords} type={domain.type} />
{domain.type == "wildcard" && (
{domain.type == "wildcard" && !domain.configManaged && (
<DomainCertForm
orgId={orgId}
domainId={domain.domainId}

View File

@@ -68,9 +68,9 @@ export default function GeneralForm() {
const router = useRouter();
const t = useTranslations();
const [editDomainOpen, setEditDomainOpen] = useState(false);
const {licenseStatus } = useLicenseStatusContext();
const { licenseStatus } = useLicenseStatusContext();
const subscriptionStatus = useSubscriptionStatusContext();
const {user} = useUserContext();
const { user } = useUserContext();
const { env } = useEnvContext();
@@ -102,6 +102,7 @@ export default function GeneralForm() {
enabled: z.boolean(),
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
niceId: z.string().min(1).max(255).optional(),
domainId: z.string().optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
// enableProxy: z.boolean().optional()
@@ -130,6 +131,7 @@ export default function GeneralForm() {
defaultValues: {
enabled: resource.enabled,
name: resource.name,
niceId: resource.niceId,
subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined,
@@ -192,6 +194,7 @@ export default function GeneralForm() {
{
enabled: data.enabled,
name: data.name,
niceId: data.niceId,
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
domainId: data.domainId,
proxyPort: data.proxyPort,
@@ -212,16 +215,12 @@ export default function GeneralForm() {
});
if (res && res.status === 200) {
toast({
title: t("resourceUpdated"),
description: t("resourceUpdatedDescription")
});
const resource = res.data.data;
const updated = res.data.data;
updateResource({
enabled: data.enabled,
name: data.name,
niceId: data.niceId,
subdomain: data.subdomain,
fullDomain: resource.fullDomain,
proxyPort: data.proxyPort,
@@ -230,8 +229,20 @@ export default function GeneralForm() {
// })
});
router.refresh();
toast({
title: t("resourceUpdated"),
description: t("resourceUpdatedDescription")
});
if (data.niceId && data.niceId !== resource?.niceId) {
router.replace(`/${updated.orgId}/settings/resources/${data.niceId}/general`);
} else {
router.refresh();
}
setSaveLoading(false);
}
setSaveLoading(false);
}
@@ -304,6 +315,24 @@ export default function GeneralForm() {
)}
/>
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("identifier")}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t("enterIdentifier")}
className="flex-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!resource.http && (
<>
<FormField

View File

@@ -501,25 +501,6 @@ export default function ReverseProxyTargets(props: {
return;
}
// Check if target with same IP, port and method already exists
const isDuplicate = targets.some(
(t) =>
t.targetId !== target.targetId &&
t.ip === target.ip &&
t.port === target.port &&
t.method === target.method &&
t.siteId === target.siteId
);
if (isDuplicate) {
toast({
variant: "destructive",
title: t("targetErrorDuplicate"),
description: t("targetErrorDuplicateDescription")
});
return;
}
try {
setTargetsLoading(true);
@@ -585,24 +566,6 @@ export default function ReverseProxyTargets(props: {
}
async function addTarget(data: z.infer<typeof addTargetSchema>) {
// Check if target with same IP, port and method already exists
const isDuplicate = targets.some(
(target) =>
target.ip === data.ip &&
target.port === data.port &&
target.method === data.method &&
target.siteId === data.siteId
);
if (isDuplicate) {
toast({
variant: "destructive",
title: t("targetErrorDuplicate"),
description: t("targetErrorDuplicateDescription")
});
return;
}
// if (site && site.type == "wireguard" && site.subnet) {
// // make sure that the target IP is within the site subnet
// const targetIp = data.ip;
@@ -899,7 +862,7 @@ export default function ReverseProxyTargets(props: {
const healthCheckColumn: ColumnDef<LocalTarget> = {
accessorKey: "healthCheck",
header: t("healthCheck"),
header: () => (<span className="p-3">{t("healthCheck")}</span>),
cell: ({ row }) => {
const status = row.original.hcHealth || "unknown";
const isEnabled = row.original.hcEnabled;
@@ -971,7 +934,7 @@ export default function ReverseProxyTargets(props: {
const matchPathColumn: ColumnDef<LocalTarget> = {
accessorKey: "path",
header: t("matchPath"),
header: () => (<span className="p-3">{t("matchPath")}</span>),
cell: ({ row }) => {
const hasPathMatch = !!(
row.original.path || row.original.pathMatchType
@@ -1033,7 +996,7 @@ export default function ReverseProxyTargets(props: {
const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address",
header: t("address"),
header: () => (<span className="p-3">{t("address")}</span>),
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
@@ -1052,7 +1015,7 @@ export default function ReverseProxyTargets(props: {
return (
<div className="flex items-center w-full">
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs rounded-md">
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
{selectedSite &&
selectedSite.type === "newt" &&
(() => {
@@ -1247,7 +1210,7 @@ export default function ReverseProxyTargets(props: {
const rewritePathColumn: ColumnDef<LocalTarget> = {
accessorKey: "rewritePath",
header: t("rewritePath"),
header: () => (<span className="p-3">{t("rewritePath")}</span>),
cell: ({ row }) => {
const hasRewritePath = !!(
row.original.rewritePath || row.original.rewritePathType
@@ -1317,7 +1280,7 @@ export default function ReverseProxyTargets(props: {
const enabledColumn: ColumnDef<LocalTarget> = {
accessorKey: "enabled",
header: t("enabled"),
header: () => (<span className="p-3">{t("enabled")}</span>),
cell: ({ row }) => (
<div className="flex items-center justify-center w-full">
<Switch
@@ -1338,8 +1301,9 @@ export default function ReverseProxyTargets(props: {
const actionsColumn: ColumnDef<LocalTarget> = {
id: "actions",
header: () => (<span className="p-3">{t("actions")}</span>),
cell: ({ row }) => (
<div className="flex items-center justify-end w-full">
<div className="flex items-center w-full">
<Button
variant="outline"
onClick={() => removeTarget(row.original.targetId)}

View File

@@ -425,24 +425,6 @@ export default function Page() {
};
async function addTarget(data: z.infer<typeof addTargetSchema>) {
// Check if target with same IP, port and method already exists
const isDuplicate = targets.some(
(target) =>
target.ip === data.ip &&
target.port === data.port &&
target.method === data.method &&
target.siteId === data.siteId
);
if (isDuplicate) {
toast({
variant: "destructive",
title: t("targetErrorDuplicate"),
description: t("targetErrorDuplicateDescription")
});
return;
}
const site = sites.find((site) => site.siteId === data.siteId);
const isHttp = baseForm.watch("http");

View File

@@ -15,7 +15,7 @@ import {
import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form";
import { toast } from "@app/hooks/useToast";
import { toast, useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import {
SettingsContainer,
@@ -37,6 +37,7 @@ import { Tag, TagInput } from "@app/components/tags/tag-input";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"),
niceId: z.string().min(1).max(255).optional(),
dockerSocketEnabled: z.boolean().optional(),
remoteSubnets: z
.array(
@@ -55,19 +56,18 @@ export default function GeneralPage() {
const { env } = useEnvContext();
const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false);
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
null
);
const router = useRouter();
const t = useTranslations();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: site?.name,
niceId: site?.niceId || "",
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
remoteSubnets: site?.remoteSubnets
? site.remoteSubnets.split(",").map((subnet, index) => ({
@@ -82,37 +82,40 @@ export default function GeneralPage() {
async function onSubmit(data: GeneralFormValues) {
setLoading(true);
await api
.post(`/site/${site?.siteId}`, {
try {
await api.post(`/site/${site?.siteId}`, {
name: data.name,
niceId: data.niceId,
dockerSocketEnabled: data.dockerSocketEnabled,
remoteSubnets:
data.remoteSubnets
?.map((subnet) => subnet.text)
.join(",") || ""
})
.catch((e) => {
toast({
variant: "destructive",
title: t("siteErrorUpdate"),
description: formatAxiosError(
e,
t("siteErrorUpdateDescription")
)
});
?.map((subnet) => subnet.text)
.join(",") || ""
});
updateSite({
name: data.name,
dockerSocketEnabled: data.dockerSocketEnabled,
remoteSubnets:
data.remoteSubnets?.map((subnet) => subnet.text).join(",") || ""
});
updateSite({
name: data.name,
niceId: data.niceId,
dockerSocketEnabled: data.dockerSocketEnabled,
remoteSubnets:
data.remoteSubnets?.map((subnet) => subnet.text).join(",") || ""
});
toast({
title: t("siteUpdated"),
description: t("siteUpdatedDescription")
});
if (data.niceId && data.niceId !== site?.niceId) {
router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`);
}
toast({
title: t("siteUpdated"),
description: t("siteUpdatedDescription")
});
} catch (e) {
toast({
variant: "destructive",
title: t("siteErrorUpdate"),
description: formatAxiosError(e, t("siteErrorUpdateDescription"))
});
}
setLoading(false);
@@ -153,8 +156,25 @@ export default function GeneralPage() {
)}
/>
{env.flags.enableClients &&
site.type === "newt" ? (
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("identifier")}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t("enterIdentifier")}
className="flex-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{env.flags.enableClients && site.type === "newt" ? (
<FormField
control={form.control}
name="remoteSubnets"

View File

@@ -54,22 +54,7 @@ export default function BlueprintDetailsForm({
<div className="flex flex-col gap-6">
<Alert>
<AlertDescription>
<InfoSections cols={2}>
<InfoSection>
<InfoSectionTitle>
{t("appliedAt")}
</InfoSectionTitle>
<InfoSectionContent>
<time
className="text-muted-foreground"
dateTime={blueprint.createdAt.toString()}
>
{new Date(
blueprint.createdAt * 1000
).toLocaleString()}
</time>
</InfoSectionContent>
</InfoSection>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("status")}
@@ -88,16 +73,6 @@ export default function BlueprintDetailsForm({
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("message")}
</InfoSectionTitle>
<InfoSectionContent>
<p className="text-muted-foreground">
{blueprint.message}
</p>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("source")}
@@ -106,35 +81,59 @@ export default function BlueprintDetailsForm({
{blueprint.source === "API" && (
<Badge
variant="secondary"
className="-mx-2"
className="inline-flex items-center gap-1 "
>
<span className="inline-flex items-center gap-1 ">
API
<Webhook className="size-4 flex-none" />
</span>
API
<Webhook className="w-3 h-3 flex-none" />
</Badge>
)}
{blueprint.source === "NEWT" && (
<Badge variant="secondary">
<span className="inline-flex items-center gap-1 ">
Newt CLI
<Terminal className="size-4 flex-none" />
</span>
<Badge
variant="secondary"
className="inline-flex items-center gap-1 "
>
<Terminal className="w-3 h-3 flex-none" />
Newt CLI
</Badge>
)}
{blueprint.source === "UI" && (
<Badge
variant="secondary"
className="-mx-1 py-1"
className="inline-flex items-center gap-1 "
>
<span className="inline-flex items-center gap-1 ">
Dashboard{" "}
<Globe className="size-4 flex-none" />
</span>
<Globe className="w-3 h-3 flex-none" />
Dashboard
</Badge>
)}{" "}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("appliedAt")}
</InfoSectionTitle>
<InfoSectionContent>
<time
className="text-muted-foreground"
dateTime={blueprint.createdAt.toString()}
>
{new Date(
blueprint.createdAt * 1000
).toLocaleString()}
</time>
</InfoSectionContent>
</InfoSection>
{blueprint.message && (
<InfoSection>
<InfoSectionTitle>
{t("message")}
</InfoSectionTitle>
<InfoSectionContent>
<p className="text-muted-foreground">
{blueprint.message}
</p>
</InfoSectionContent>
</InfoSection>
)}
</InfoSections>
</AlertDescription>
</Alert>
@@ -169,11 +168,6 @@ export default function BlueprintDetailsForm({
<FormLabel>
{t("parsedContents")}
</FormLabel>
<FormDescription>
{t(
"blueprintContentsDescription"
)}
</FormDescription>
<FormControl>
<div
className={cn(

View File

@@ -116,10 +116,13 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
}
case "UI": {
return (
<Badge variant="secondary">
<Badge
variant="secondary"
className="inline-flex items-center gap-1"
>
<span className="inline-flex items-center gap-1 ">
Dashboard{" "}
<Globe className="size-4 flex-none" />
<Globe className="w-3 h-3" />
Dashboard
</span>
</Badge>
);
@@ -163,18 +166,14 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
cell: ({ row }) => {
return (
<div className="flex justify-end">
<Button
variant="outline"
className="items-center"
asChild
<Link
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
>
<Link
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
>
View details{" "}
<ArrowRight className="size-4 flex-none" />
</Link>
</Button>
<Button variant="outline" className="items-center">
View Details
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</div>
);
}

View File

@@ -124,7 +124,6 @@ export function DNSRecordsDataTable<TData, TValue>({
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="bg-secondary dark:bg-transparent"
>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>

View File

@@ -1,7 +1,7 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import { ShieldCheck, ShieldOff } from "lucide-react";
import { useResourceContext } from "@app/hooks/useResourceContext";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
@@ -17,21 +17,30 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
type ResourceInfoBoxType = {};
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext();
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
const { resource, authInfo, updateResource } = useResourceContext();
const { env } = useEnvContext();
const t = useTranslations();
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
return (
<Alert>
<AlertDescription>
{/* 4 cols because of the certs */}
<InfoSections
cols={resource.http && env.flags.usePangolinDns ? 4 : 3}
cols={resource.http && env.flags.usePangolinDns ? 5 : 4}
>
<InfoSection>
<InfoSectionTitle>
{t("identifier")}
</InfoSectionTitle>
<InfoSectionContent>
{resource.niceId}
</InfoSectionContent>
</InfoSection>
{resource.http ? (
<>
<InfoSection>
@@ -40,10 +49,10 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ||
authInfo.headerAuth ? (
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ||
authInfo.headerAuth ? (
<div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" />
<span>{t("protected")}</span>

View File

@@ -507,22 +507,6 @@ export default function ResourcesTable({
);
}
},
{
accessorKey: "nice",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("resource")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: "protocol",
header: t("protocol"),

View File

@@ -1,7 +1,6 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon } from "lucide-react";
import { useSiteContext } from "@app/hooks/useSiteContext";
import {
InfoSection,
@@ -12,9 +11,10 @@ import {
import { useTranslations } from "next-intl";
import { useEnvContext } from "@app/hooks/useEnvContext";
type SiteInfoCardProps = {};
export default function SiteInfoCard({}: SiteInfoCardProps) {
export default function SiteInfoCard({ }: SiteInfoCardProps) {
const { site, updateSite } = useSiteContext();
const t = useTranslations();
const { env } = useEnvContext();
@@ -31,10 +31,19 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
}
};
return (
<Alert>
<AlertDescription>
<InfoSections cols={env.flags.enableClients ? 3 : 2}>
<InfoSections cols={env.flags.enableClients ? 4 : 3}>
<InfoSection>
<InfoSectionTitle>
{t("identifier")}
</InfoSectionTitle>
<InfoSectionContent>
{site.niceId}
</InfoSectionContent>
</InfoSection>
{(site.type == "newt" || site.type == "wireguard") && (
<>
<InfoSection>

View File

@@ -164,30 +164,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
}
}
},
{
accessorKey: "nice",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
className="hidden md:flex whitespace-nowrap"
>
{t("site")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
return (
<div className="hidden md:block whitespace-nowrap">
{row.original.nice}
</div>
);
}
},
{
accessorKey: "mbIn",
header: ({ column }) => {