Add remote subnets to ui

This commit is contained in:
Owen 2025-07-24 22:01:22 -07:00
parent 1466788f77
commit 15adfcca8c
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
5 changed files with 92 additions and 14 deletions

View file

@ -1312,5 +1312,8 @@
"sitesFetchFailed": "Failed to fetch sites",
"sitesFetchError": "An error occurred while fetching sites.",
"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."
}

View file

@ -59,7 +59,8 @@ export const sites = pgTable("sites", {
publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
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", {
@ -542,7 +543,7 @@ export const olmSessions = pgTable("clientSession", {
olmId: varchar("olmId")
.notNull()
.references(() => olms.olmId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const userClients = pgTable("userClients", {
@ -565,9 +566,11 @@ export const roleClients = pgTable("roleClients", {
export const securityKeys = pgTable("webauthnCredentials", {
credentialId: varchar("credentialId").primaryKey(),
userId: varchar("userId").notNull().references(() => users.userId, {
onDelete: "cascade"
}),
userId: varchar("userId")
.notNull()
.references(() => users.userId, {
onDelete: "cascade"
}),
publicKey: varchar("publicKey").notNull(),
signCount: integer("signCount").notNull(),
transports: varchar("transports"),

View file

@ -65,7 +65,8 @@ export const sites = sqliteTable("sites", {
listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull()
.default(true)
.default(true),
remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access
});
export const resources = sqliteTable("resources", {

View file

@ -9,6 +9,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { isValidCIDR } from "@server/lib/validators";
const updateSiteParamsSchema = z
.object({
@ -20,6 +21,9 @@ const updateSiteBodySchema = z
.object({
name: z.string().min(1).max(255).optional(),
dockerSocketEnabled: z.boolean().optional(),
remoteSubnets: z
.string()
.optional()
// subdomain: z
// .string()
// .min(1)
@ -85,6 +89,21 @@ export async function updateSite(
const { siteId } = parsedParams.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
.update(sites)
.set(updateData)

View file

@ -33,10 +33,17 @@ import { useState } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { Tag, TagInput } from "@app/components/tags/tag-input";
const GeneralFormSchema = z.object({
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>;
@ -44,9 +51,11 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export default function GeneralPage() {
const { site, updateSite } = useSiteContext();
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();
@ -55,7 +64,13 @@ export default function GeneralPage() {
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
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"
});
@ -66,7 +81,8 @@ export default function GeneralPage() {
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
dockerSocketEnabled: data.dockerSocketEnabled
dockerSocketEnabled: data.dockerSocketEnabled,
remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || ''
})
.catch((e) => {
toast({
@ -81,7 +97,8 @@ export default function GeneralPage() {
updateSite({
name: data.name,
dockerSocketEnabled: data.dockerSocketEnabled
dockerSocketEnabled: data.dockerSocketEnabled,
remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || ''
});
toast({
@ -124,12 +141,47 @@ export default function GeneralPage() {
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{t("siteNameDescription")}
</FormDescription>
</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" && (
<FormField
control={form.control}