Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
commit
b3d371c01e
6 changed files with 141 additions and 17 deletions
|
@ -130,8 +130,10 @@ export const userOrgs = sqliteTable("userOrgs", {
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId),
|
.references(() => users.userId),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.references(() => orgs.orgId, {
|
||||||
.references(() => orgs.orgId),
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
|
|
|
@ -27,7 +27,6 @@ export async function verifyUserIsOrgOwner(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!req.userOrg) {
|
if (!req.userOrg) {
|
||||||
const res = await db
|
const res = await db
|
||||||
|
@ -56,6 +55,8 @@ export async function verifyUserIsOrgOwner(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|
|
@ -24,6 +24,10 @@ const deleteOrgSchema = z
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
export type DeleteOrgResponse = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteOrg(
|
export async function deleteOrg(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -41,7 +45,6 @@ export async function deleteOrg(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
// Check if the user has permission to list sites
|
// Check if the user has permission to list sites
|
||||||
const hasPermission = await checkUserActionPermission(
|
const hasPermission = await checkUserActionPermission(
|
||||||
ActionsEnum.deleteOrg,
|
ActionsEnum.deleteOrg,
|
||||||
|
@ -55,7 +58,6 @@ export async function deleteOrg(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [org] = await db
|
const [org] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
|
@ -70,7 +72,6 @@ export async function deleteOrg(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to handle deleting each site
|
// we need to handle deleting each site
|
||||||
const orgSites = await db
|
const orgSites = await db
|
||||||
.select()
|
.select()
|
||||||
|
@ -97,20 +98,20 @@ export async function deleteOrg(
|
||||||
sendToClient(deletedNewt.newtId, payload);
|
sendToClient(deletedNewt.newtId, payload);
|
||||||
|
|
||||||
// delete all of the sessions for the newt
|
// delete all of the sessions for the newt
|
||||||
db.delete(newtSessions)
|
await db.delete(newtSessions)
|
||||||
.where(
|
.where(
|
||||||
eq(newtSessions.newtId, deletedNewt.newtId)
|
eq(newtSessions.newtId, deletedNewt.newtId)
|
||||||
)
|
);
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.delete(sites).where(eq(sites.siteId, site.siteId)).run();
|
logger.info(`Deleting site ${site.siteId}`);
|
||||||
|
await db.delete(sites).where(eq(sites.siteId, site.siteId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(orgs).where(eq(orgs.orgId, orgId)).returning();
|
await db.delete(orgs).where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
|
|
@ -28,6 +28,7 @@ export type GetSiteResponse = {
|
||||||
name: string;
|
name: string;
|
||||||
subdomain: string;
|
subdomain: string;
|
||||||
subnet: string;
|
subnet: string;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getSite(
|
export async function getSite(
|
||||||
|
@ -81,7 +82,8 @@ export async function getSite(
|
||||||
siteId: site[0].siteId,
|
siteId: site[0].siteId,
|
||||||
niceId: site[0].niceId,
|
niceId: site[0].niceId,
|
||||||
name: site[0].name,
|
name: site[0].name,
|
||||||
subnet: site[0].subnet
|
subnet: site[0].subnet,
|
||||||
|
type: site[0].type
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|
|
@ -30,6 +30,9 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { DeleteOrgResponse, ListOrgsResponse } from "@server/routers/org";
|
||||||
|
import { redirect, useRouter } from "next/navigation";
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string()
|
name: z.string()
|
||||||
|
@ -41,6 +44,7 @@ export default function GeneralPage() {
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
|
||||||
const { orgUser } = userOrgUserContext();
|
const { orgUser } = userOrgUserContext();
|
||||||
|
const router = useRouter();
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
@ -54,16 +58,54 @@ export default function GeneralPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function deleteOrg() {
|
async function deleteOrg() {
|
||||||
await api.delete(`/org/${org?.org.orgId}`).catch((e) => {
|
try {
|
||||||
|
const res = await api.delete<AxiosResponse<DeleteOrgResponse>>(
|
||||||
|
`/org/${org?.org.orgId}`
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
pickNewOrgAndNavigate();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Failed to delete org",
|
title: "Failed to delete org",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
err,
|
||||||
"An error occurred while deleting the org."
|
"An error occurred while deleting the org."
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickNewOrgAndNavigate() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res = await api.get<AxiosResponse<ListOrgsResponse>>(
|
||||||
|
`/orgs`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (res.data.data.orgs.length > 0) {
|
||||||
|
const orgId = res.data.data.orgs[0].orgId;
|
||||||
|
// go to `/${orgId}/settings`);
|
||||||
|
router.push(`/${orgId}/settings`);
|
||||||
|
} else {
|
||||||
|
// go to `/setup`
|
||||||
|
router.push("/setup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to fetch orgs",
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
"An error occurred while listing your orgs"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmit(data: GeneralFormValues) {
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
|
|
|
@ -51,6 +51,7 @@ import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
import { formatAxiosError } from "@app/lib/utils";
|
import { formatAxiosError } from "@app/lib/utils";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { createApiClient } from "@app/api";
|
import { createApiClient } from "@app/api";
|
||||||
|
import { GetSiteResponse } from "@server/routers/site";
|
||||||
|
|
||||||
const addTargetSchema = z.object({
|
const addTargetSchema = z.object({
|
||||||
ip: z.string().ip(),
|
ip: z.string().ip(),
|
||||||
|
@ -85,6 +86,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||||
|
const [site, setSite] = useState<GetSiteResponse>();
|
||||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
||||||
const [sslEnabled, setSslEnabled] = useState(resource.ssl);
|
const [sslEnabled, setSslEnabled] = useState(resource.ssl);
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSites = async () => {
|
const fetchTargets = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
||||||
`/resource/${params.resourceId}/targets`,
|
`/resource/${params.resourceId}/targets`,
|
||||||
|
@ -126,7 +128,30 @@ export default function ReverseProxyTargets(props: {
|
||||||
setPageLoading(false);
|
setPageLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchSites();
|
fetchTargets();
|
||||||
|
|
||||||
|
const fetchSite = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get<AxiosResponse<GetSiteResponse>>(
|
||||||
|
`/site/${resource.siteId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setSite(res.data.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to fetch resource",
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
"An error occurred while fetching resource",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchSite();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function addTarget(data: AddTargetFormValues) {
|
async function addTarget(data: AddTargetFormValues) {
|
||||||
|
@ -146,6 +171,20 @@ export default function ReverseProxyTargets(props: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (site && site.type == "wireguard" && site.subnet) {
|
||||||
|
// make sure that the target IP is within the site subnet
|
||||||
|
const targetIp = data.ip;
|
||||||
|
const subnet = site.subnet;
|
||||||
|
if (!isIPInSubnet(targetIp, subnet)) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Invalid target IP",
|
||||||
|
description: "Target IP must be within the site subnet",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -602,3 +641,40 @@ export default function ReverseProxyTargets(props: {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isIPInSubnet(subnet: string, ip: string): boolean {
|
||||||
|
// Split subnet into IP and mask parts
|
||||||
|
const [subnetIP, maskBits] = subnet.split('/');
|
||||||
|
const mask = parseInt(maskBits);
|
||||||
|
|
||||||
|
if (mask < 0 || mask > 32) {
|
||||||
|
throw new Error('Invalid subnet mask. Must be between 0 and 32.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IP addresses to binary numbers
|
||||||
|
const subnetNum = ipToNumber(subnetIP);
|
||||||
|
const ipNum = ipToNumber(ip);
|
||||||
|
|
||||||
|
// Calculate subnet mask
|
||||||
|
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
|
||||||
|
|
||||||
|
// Check if the IP is in the subnet
|
||||||
|
return (subnetNum & maskNum) === (ipNum & maskNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipToNumber(ip: string): number {
|
||||||
|
// Validate IP address format
|
||||||
|
const parts = ip.split('.');
|
||||||
|
if (parts.length !== 4) {
|
||||||
|
throw new Error('Invalid IP address format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IP octets to 32-bit number
|
||||||
|
return parts.reduce((num, octet) => {
|
||||||
|
const oct = parseInt(octet);
|
||||||
|
if (isNaN(oct) || oct < 0 || oct > 255) {
|
||||||
|
throw new Error('Invalid IP address octet');
|
||||||
|
}
|
||||||
|
return (num << 8) + oct;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue