mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-17 16:01:22 +02:00
Add remote subnets to ui
This commit is contained in:
parent
1466788f77
commit
15adfcca8c
5 changed files with 92 additions and 14 deletions
|
@ -1312,5 +1312,8 @@
|
||||||
"sitesFetchFailed": "Failed to fetch sites",
|
"sitesFetchFailed": "Failed to fetch sites",
|
||||||
"sitesFetchError": "An error occurred while fetching sites.",
|
"sitesFetchError": "An error occurred while fetching sites.",
|
||||||
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release."
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
|
"remoteSubnets": "Remote Subnets",
|
||||||
|
"enterCidrRange": "Enter CIDR range",
|
||||||
|
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24."
|
||||||
}
|
}
|
|
@ -59,7 +59,8 @@ export const sites = pgTable("sites", {
|
||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||||
|
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
|
@ -542,7 +543,7 @@ export const olmSessions = pgTable("clientSession", {
|
||||||
olmId: varchar("olmId")
|
olmId: varchar("olmId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => olms.olmId, { onDelete: "cascade" }),
|
.references(() => olms.olmId, { onDelete: "cascade" }),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userClients = pgTable("userClients", {
|
export const userClients = pgTable("userClients", {
|
||||||
|
@ -565,9 +566,11 @@ export const roleClients = pgTable("roleClients", {
|
||||||
|
|
||||||
export const securityKeys = pgTable("webauthnCredentials", {
|
export const securityKeys = pgTable("webauthnCredentials", {
|
||||||
credentialId: varchar("credentialId").primaryKey(),
|
credentialId: varchar("credentialId").primaryKey(),
|
||||||
userId: varchar("userId").notNull().references(() => users.userId, {
|
userId: varchar("userId")
|
||||||
onDelete: "cascade"
|
.notNull()
|
||||||
}),
|
.references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
publicKey: varchar("publicKey").notNull(),
|
publicKey: varchar("publicKey").notNull(),
|
||||||
signCount: integer("signCount").notNull(),
|
signCount: integer("signCount").notNull(),
|
||||||
transports: varchar("transports"),
|
transports: varchar("transports"),
|
||||||
|
|
|
@ -65,7 +65,8 @@ export const sites = sqliteTable("sites", {
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true)
|
.default(true),
|
||||||
|
remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { isValidCIDR } from "@server/lib/validators";
|
||||||
|
|
||||||
const updateSiteParamsSchema = z
|
const updateSiteParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -20,6 +21,9 @@ const updateSiteBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
dockerSocketEnabled: z.boolean().optional(),
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
|
remoteSubnets: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
// subdomain: z
|
// subdomain: z
|
||||||
// .string()
|
// .string()
|
||||||
// .min(1)
|
// .min(1)
|
||||||
|
@ -85,6 +89,21 @@ export async function updateSite(
|
||||||
const { siteId } = parsedParams.data;
|
const { siteId } = parsedParams.data;
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
// if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
||||||
|
if (updateData.remoteSubnets) {
|
||||||
|
const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim());
|
||||||
|
for (const subnet of subnets) {
|
||||||
|
if (!isValidCIDR(subnet)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Invalid CIDR format: ${subnet}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedSite = await db
|
const updatedSite = await db
|
||||||
.update(sites)
|
.update(sites)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
|
|
|
@ -33,10 +33,17 @@ import { useState } from "react";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string().nonempty("Name is required"),
|
name: z.string().nonempty("Name is required"),
|
||||||
dockerSocketEnabled: z.boolean().optional()
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
|
remoteSubnets: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string()
|
||||||
|
})
|
||||||
|
).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
@ -44,9 +51,11 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const { site, updateSite } = useSiteContext();
|
const { site, updateSite } = useSiteContext();
|
||||||
|
|
||||||
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
@ -55,7 +64,13 @@ export default function GeneralPage() {
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: site?.name,
|
name: site?.name,
|
||||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||||
|
remoteSubnets: site?.remoteSubnets
|
||||||
|
? site.remoteSubnets.split(',').map((subnet, index) => ({
|
||||||
|
id: subnet.trim(),
|
||||||
|
text: subnet.trim()
|
||||||
|
}))
|
||||||
|
: []
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
@ -66,7 +81,8 @@ export default function GeneralPage() {
|
||||||
await api
|
await api
|
||||||
.post(`/site/${site?.siteId}`, {
|
.post(`/site/${site?.siteId}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || ''
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
|
@ -81,7 +97,8 @@ export default function GeneralPage() {
|
||||||
|
|
||||||
updateSite({
|
updateSite({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
@ -124,12 +141,47 @@ export default function GeneralPage() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
|
||||||
{t("siteNameDescription")}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="remoteSubnets"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("remoteSubnets")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={activeCidrTagIndex}
|
||||||
|
setActiveTagIndex={setActiveCidrTagIndex}
|
||||||
|
placeholder={t("enterCidrRange")}
|
||||||
|
size="sm"
|
||||||
|
tags={form.getValues().remoteSubnets || []}
|
||||||
|
setTags={(newSubnets) => {
|
||||||
|
form.setValue(
|
||||||
|
"remoteSubnets",
|
||||||
|
newSubnets as Tag[]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
validateTag={(tag) => {
|
||||||
|
// Basic CIDR validation regex
|
||||||
|
const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
|
||||||
|
return cidrRegex.test(tag);
|
||||||
|
}}
|
||||||
|
allowDuplicates={false}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("remoteSubnetsDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{site && site.type === "newt" && (
|
{site && site.type === "newt" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue