mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-28 07:46:36 +00:00
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
This commit is contained in:
@@ -1053,6 +1053,8 @@
|
|||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
"actionApplyBlueprint": "Apply Blueprint",
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
|
"actionListBlueprints": "List Blueprints",
|
||||||
|
"actionGetBlueprint": "Get Blueprint",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
@@ -2309,6 +2311,8 @@
|
|||||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||||
"setupSubnetAdvanced": "Subnet (Advanced)",
|
"setupSubnetAdvanced": "Subnet (Advanced)",
|
||||||
"setupSubnetDescription": "The subnet for this organization's internal network.",
|
"setupSubnetDescription": "The subnet for this organization's internal network.",
|
||||||
|
"setupUtilitySubnet": "Utility Subnet (Advanced)",
|
||||||
|
"setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.",
|
||||||
"siteRegenerateAndDisconnect": "Regenerate and Disconnect",
|
"siteRegenerateAndDisconnect": "Regenerate and Disconnect",
|
||||||
"siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?",
|
"siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?",
|
||||||
"siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.",
|
"siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.",
|
||||||
|
|||||||
@@ -1022,6 +1022,8 @@
|
|||||||
"actionGetSite": "獲取站點",
|
"actionGetSite": "獲取站點",
|
||||||
"actionListSites": "站點列表",
|
"actionListSites": "站點列表",
|
||||||
"actionApplyBlueprint": "應用藍圖",
|
"actionApplyBlueprint": "應用藍圖",
|
||||||
|
"actionListBlueprints": "藍圖列表",
|
||||||
|
"actionGetBlueprint": "獲取藍圖",
|
||||||
"setupToken": "設置令牌",
|
"setupToken": "設置令牌",
|
||||||
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
|
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
|
||||||
"setupTokenRequired": "需要設置令牌",
|
"setupTokenRequired": "需要設置令牌",
|
||||||
|
|||||||
@@ -301,6 +301,29 @@ export function isIpInCidr(ip: string, cidr: string): boolean {
|
|||||||
return ipBigInt >= range.start && ipBigInt <= range.end;
|
return ipBigInt >= range.start && ipBigInt <= range.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two CIDR ranges overlap
|
||||||
|
* @param cidr1 First CIDR string
|
||||||
|
* @param cidr2 Second CIDR string
|
||||||
|
* @returns boolean indicating if the two CIDRs overlap
|
||||||
|
*/
|
||||||
|
export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
||||||
|
const version1 = detectIpVersion(cidr1.split("/")[0]);
|
||||||
|
const version2 = detectIpVersion(cidr2.split("/")[0]);
|
||||||
|
if (version1 !== version2) {
|
||||||
|
// Different IP versions cannot overlap
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const range1 = cidrToRange(cidr1);
|
||||||
|
const range2 = cidrToRange(cidr2);
|
||||||
|
|
||||||
|
// Overlap if the ranges intersect
|
||||||
|
return (
|
||||||
|
range1.start <= range2.end &&
|
||||||
|
range2.start <= range1.end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNextAvailableClientSubnet(
|
export async function getNextAvailableClientSubnet(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
transaction: Transaction | typeof db = db
|
transaction: Transaction | typeof db = db
|
||||||
|
|||||||
@@ -255,11 +255,11 @@ export const configSchema = z
|
|||||||
orgs: z
|
orgs: z
|
||||||
.object({
|
.object({
|
||||||
block_size: z.number().positive().gt(0).optional().default(24),
|
block_size: z.number().positive().gt(0).optional().default(24),
|
||||||
subnet_group: z.string().optional().default("100.90.128.0/24"),
|
subnet_group: z.string().optional().default("100.90.128.0/20"),
|
||||||
utility_subnet_group: z
|
utility_subnet_group: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.default("100.96.128.0/24") //just hardcode this for now as well
|
.default("100.96.128.0/20") //just hardcode this for now as well
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({
|
.default({
|
||||||
|
|||||||
@@ -858,6 +858,22 @@ authenticated.put(
|
|||||||
blueprints.applyJSONBlueprint
|
blueprints.applyJSONBlueprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/blueprint/:blueprintId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getBlueprint),
|
||||||
|
blueprints.getBlueprint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/blueprints",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listBlueprints),
|
||||||
|
blueprints.listBlueprints
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/logs/request",
|
"/org/:orgId/logs/request",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { usageService } from "@server/lib/billing/usageService";
|
|||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
|
import { doCidrsOverlap } from "@server/lib/ip";
|
||||||
|
|
||||||
const createOrgSchema = z.strictObject({
|
const createOrgSchema = z.strictObject({
|
||||||
orgId: z.string(),
|
orgId: z.string(),
|
||||||
@@ -36,6 +37,11 @@ const createOrgSchema = z.strictObject({
|
|||||||
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||||
.refine((val) => isValidCIDR(val), {
|
.refine((val) => isValidCIDR(val), {
|
||||||
message: "Invalid subnet CIDR"
|
message: "Invalid subnet CIDR"
|
||||||
|
}),
|
||||||
|
utilitySubnet: z
|
||||||
|
.union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||||
|
.refine((val) => isValidCIDR(val), {
|
||||||
|
message: "Invalid utility subnet CIDR"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +90,7 @@ export async function createOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId, name, subnet } = parsedBody.data;
|
const { orgId, name, subnet, utilitySubnet } = parsedBody.data;
|
||||||
|
|
||||||
// TODO: for now we are making all of the orgs the same subnet
|
// TODO: for now we are making all of the orgs the same subnet
|
||||||
// make sure the subnet is unique
|
// make sure the subnet is unique
|
||||||
@@ -119,6 +125,15 @@ export async function createOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doCidrsOverlap(subnet, utilitySubnet)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Subnet ${subnet} overlaps with utility subnet ${utilitySubnet}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let error = "";
|
let error = "";
|
||||||
let org: Org | null = null;
|
let org: Org | null = null;
|
||||||
|
|
||||||
@@ -128,9 +143,6 @@ export async function createOrg(
|
|||||||
.from(domains)
|
.from(domains)
|
||||||
.where(eq(domains.configManaged, true));
|
.where(eq(domains.configManaged, true));
|
||||||
|
|
||||||
const utilitySubnet =
|
|
||||||
config.getRawConfig().orgs.utility_subnet_group;
|
|
||||||
|
|
||||||
const newOrg = await trx
|
const newOrg = await trx
|
||||||
.insert(orgs)
|
.insert(orgs)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import config from "@server/lib/config";
|
|||||||
|
|
||||||
export type PickOrgDefaultsResponse = {
|
export type PickOrgDefaultsResponse = {
|
||||||
subnet: string;
|
subnet: string;
|
||||||
|
utilitySubnet: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function pickOrgDefaults(
|
export async function pickOrgDefaults(
|
||||||
@@ -20,10 +21,13 @@ export async function pickOrgDefaults(
|
|||||||
// const subnet = await getNextAvailableOrgSubnet();
|
// const subnet = await getNextAvailableOrgSubnet();
|
||||||
// Just hard code the subnet for now for everyone
|
// Just hard code the subnet for now for everyone
|
||||||
const subnet = config.getRawConfig().orgs.subnet_group;
|
const subnet = config.getRawConfig().orgs.subnet_group;
|
||||||
|
const utilitySubnet =
|
||||||
|
config.getRawConfig().orgs.utility_subnet_group;
|
||||||
|
|
||||||
return response<PickOrgDefaultsResponse>(res, {
|
return response<PickOrgDefaultsResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
subnet: subnet
|
subnet: subnet,
|
||||||
|
utilitySubnet: utilitySubnet
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ export default function StepperForm() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
// Removed error state, now using toast for API errors
|
||||||
const [orgCreated, setOrgCreated] = useState(false);
|
const [orgCreated, setOrgCreated] = useState(false);
|
||||||
|
|
||||||
const orgSchema = z.object({
|
const orgSchema = z.object({
|
||||||
orgName: z.string().min(1, { message: t("orgNameRequired") }),
|
orgName: z.string().min(1, { message: t("orgNameRequired") }),
|
||||||
orgId: z.string().min(1, { message: t("orgIdRequired") }),
|
orgId: z.string().min(1, { message: t("orgIdRequired") }),
|
||||||
subnet: z.string().min(1, { message: t("subnetRequired") })
|
subnet: z.string().min(1, { message: t("subnetRequired") }),
|
||||||
|
utilitySubnet: z.string().min(1, { message: t("subnetRequired") })
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgForm = useForm({
|
const orgForm = useForm({
|
||||||
@@ -55,7 +56,8 @@ export default function StepperForm() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
orgName: "",
|
orgName: "",
|
||||||
orgId: "",
|
orgId: "",
|
||||||
subnet: ""
|
subnet: "",
|
||||||
|
utilitySubnet: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ export default function StepperForm() {
|
|||||||
const res = await api.get(`/pick-org-defaults`);
|
const res = await api.get(`/pick-org-defaults`);
|
||||||
if (res && res.data && res.data.data) {
|
if (res && res.data && res.data.data) {
|
||||||
orgForm.setValue("subnet", res.data.data.subnet);
|
orgForm.setValue("subnet", res.data.data.subnet);
|
||||||
|
orgForm.setValue("utilitySubnet", res.data.data.utilitySubnet);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch default subnet:", e);
|
console.error("Failed to fetch default subnet:", e);
|
||||||
@@ -129,7 +132,8 @@ export default function StepperForm() {
|
|||||||
const res = await api.put(`/org`, {
|
const res = await api.put(`/org`, {
|
||||||
orgId: values.orgId,
|
orgId: values.orgId,
|
||||||
name: values.orgName,
|
name: values.orgName,
|
||||||
subnet: values.subnet
|
subnet: values.subnet,
|
||||||
|
utilitySubnet: values.utilitySubnet
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.status === 201) {
|
if (res && res.status === 201) {
|
||||||
@@ -138,7 +142,11 @@ export default function StepperForm() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(formatAxiosError(e, t("orgErrorCreate")));
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e, t("orgErrorCreate")),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -320,6 +328,30 @@ export default function StepperForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={orgForm.control}
|
||||||
|
name="utilitySubnet"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("setupUtilitySubnet")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"setupUtilitySubnetDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{orgIdTaken && !orgCreated ? (
|
{orgIdTaken && !orgCreated ? (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -328,20 +360,13 @@ export default function StepperForm() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{error && (
|
{/* Error Alert removed, errors now shown as toast */}
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>
|
|
||||||
{error}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={
|
disabled={
|
||||||
error !== null ||
|
|
||||||
loading ||
|
loading ||
|
||||||
orgIdTaken
|
orgIdTaken
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionListOrgDomains")]: "listOrgDomains",
|
[t("actionListOrgDomains")]: "listOrgDomains",
|
||||||
[t("updateOrgUser")]: "updateOrgUser",
|
[t("updateOrgUser")]: "updateOrgUser",
|
||||||
[t("createOrgUser")]: "createOrgUser",
|
[t("createOrgUser")]: "createOrgUser",
|
||||||
[t("actionApplyBlueprint")]: "applyBlueprint"
|
[t("actionApplyBlueprint")]: "applyBlueprint",
|
||||||
|
[t("actionListBlueprints")]: "listBlueprints",
|
||||||
|
[t("actionGetBlueprint")]: "getBlueprint"
|
||||||
},
|
},
|
||||||
|
|
||||||
Site: {
|
Site: {
|
||||||
|
|||||||
Reference in New Issue
Block a user