|
@@ -93,9 +93,9 @@ export default function ResourceRules(props: {
|
|
|
useEffect(() => {
|
|
|
const fetchRules = async () => {
|
|
|
try {
|
|
|
- const res = await api.get<AxiosResponse<ListResourceRulesResponse>>(
|
|
|
- `/resource/${params.resourceId}/rules`
|
|
|
- );
|
|
|
+ const res = await api.get<
|
|
|
+ AxiosResponse<ListResourceRulesResponse>
|
|
|
+ >(`/resource/${params.resourceId}/rules`);
|
|
|
if (res.status === 200) {
|
|
|
setRules(res.data.data.rules);
|
|
|
}
|
|
@@ -133,6 +133,25 @@ export default function ResourceRules(props: {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ if (data.match === "CIDR" && !isValidCIDR(data.value)) {
|
|
|
+ toast({
|
|
|
+ variant: "destructive",
|
|
|
+ title: "Invalid CIDR",
|
|
|
+ description: "Please enter a valid CIDR value"
|
|
|
+ });
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
|
|
|
+ toast({
|
|
|
+ variant: "destructive",
|
|
|
+ title: "Invalid URL path",
|
|
|
+ description: "Please enter a valid URL path value"
|
|
|
+ });
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const newRule: LocalRule = {
|
|
|
...data,
|
|
|
ruleId: new Date().getTime(),
|
|
@@ -171,11 +190,27 @@ export default function ResourceRules(props: {
|
|
|
value: rule.value
|
|
|
};
|
|
|
|
|
|
+ if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
|
|
+ toast({
|
|
|
+ variant: "destructive",
|
|
|
+ title: "Invalid CIDR",
|
|
|
+ description: "Please enter a valid CIDR value"
|
|
|
+ });
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (rule.match === "PATH" && !isValidUrlGlobPattern(rule.value)) {
|
|
|
+ toast({
|
|
|
+ variant: "destructive",
|
|
|
+ title: "Invalid URL path",
|
|
|
+ description: "Please enter a valid URL path value"
|
|
|
+ });
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
if (rule.new) {
|
|
|
- await api.put(
|
|
|
- `/resource/${params.resourceId}/rule`,
|
|
|
- data
|
|
|
- );
|
|
|
+ await api.put(`/resource/${params.resourceId}/rule`, data);
|
|
|
} else if (rule.updated) {
|
|
|
await api.post(
|
|
|
`/resource/${params.resourceId}/rule/${rule.ruleId}`,
|
|
@@ -190,7 +225,9 @@ export default function ResourceRules(props: {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- setRules(rules.map(rule => ({ ...rule, new: false, updated: false })));
|
|
|
+ setRules(
|
|
|
+ rules.map((rule) => ({ ...rule, new: false, updated: false }))
|
|
|
+ );
|
|
|
setRulesToRemove([]);
|
|
|
|
|
|
toast({
|
|
@@ -322,7 +359,9 @@ export default function ResourceRules(props: {
|
|
|
<FormControl>
|
|
|
<Select
|
|
|
value={field.value}
|
|
|
- onValueChange={field.onChange}
|
|
|
+ onValueChange={
|
|
|
+ field.onChange
|
|
|
+ }
|
|
|
>
|
|
|
<SelectTrigger>
|
|
|
<SelectValue />
|
|
@@ -350,7 +389,9 @@ export default function ResourceRules(props: {
|
|
|
<FormControl>
|
|
|
<Select
|
|
|
value={field.value}
|
|
|
- onValueChange={field.onChange}
|
|
|
+ onValueChange={
|
|
|
+ field.onChange
|
|
|
+ }
|
|
|
>
|
|
|
<SelectTrigger>
|
|
|
<SelectValue />
|
|
@@ -380,7 +421,8 @@ export default function ResourceRules(props: {
|
|
|
</FormControl>
|
|
|
<FormMessage />
|
|
|
<FormDescription>
|
|
|
- Enter CIDR or path value based on match type
|
|
|
+ Enter CIDR or path value based
|
|
|
+ on match type
|
|
|
</FormDescription>
|
|
|
</FormItem>
|
|
|
)}
|
|
@@ -401,7 +443,8 @@ export default function ResourceRules(props: {
|
|
|
{header.isPlaceholder
|
|
|
? null
|
|
|
: flexRender(
|
|
|
- header.column.columnDef.header,
|
|
|
+ header.column
|
|
|
+ .columnDef.header,
|
|
|
header.getContext()
|
|
|
)}
|
|
|
</TableHead>
|
|
@@ -413,14 +456,17 @@ export default function ResourceRules(props: {
|
|
|
{table.getRowModel().rows?.length ? (
|
|
|
table.getRowModel().rows.map((row) => (
|
|
|
<TableRow key={row.id}>
|
|
|
- {row.getVisibleCells().map((cell) => (
|
|
|
- <TableCell key={cell.id}>
|
|
|
- {flexRender(
|
|
|
- cell.column.columnDef.cell,
|
|
|
- cell.getContext()
|
|
|
- )}
|
|
|
- </TableCell>
|
|
|
- ))}
|
|
|
+ {row
|
|
|
+ .getVisibleCells()
|
|
|
+ .map((cell) => (
|
|
|
+ <TableCell key={cell.id}>
|
|
|
+ {flexRender(
|
|
|
+ cell.column
|
|
|
+ .columnDef.cell,
|
|
|
+ cell.getContext()
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ ))}
|
|
|
</TableRow>
|
|
|
))
|
|
|
) : (
|
|
@@ -449,4 +495,58 @@ export default function ResourceRules(props: {
|
|
|
</SettingsSection>
|
|
|
</SettingsContainer>
|
|
|
);
|
|
|
-}
|
|
|
+}
|
|
|
+
|
|
|
+function isValidCIDR(cidr: string): boolean {
|
|
|
+ // Match CIDR pattern (e.g., "192.168.0.0/24")
|
|
|
+ const cidrPattern =
|
|
|
+ /^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
|
+
|
|
|
+ if (!cidrPattern.test(cidr)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate IP address part
|
|
|
+ const ipPart = cidr.split("/")[0];
|
|
|
+ const octets = ipPart.split(".");
|
|
|
+
|
|
|
+ return octets.every((octet) => {
|
|
|
+ const num = parseInt(octet, 10);
|
|
|
+ return num >= 0 && num <= 255;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function isValidUrlGlobPattern(pattern: string): boolean {
|
|
|
+ // Remove leading slash if present
|
|
|
+ pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
|
|
+
|
|
|
+ // Empty string is not valid
|
|
|
+ if (!pattern) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Split path into segments
|
|
|
+ const segments = pattern.split('/');
|
|
|
+
|
|
|
+ // Check each segment
|
|
|
+ for (let i = 0; i < segments.length; i++) {
|
|
|
+ const segment = segments[i];
|
|
|
+
|
|
|
+ // Empty segments are not allowed (double slashes)
|
|
|
+ if (!segment && i !== segments.length - 1) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If segment contains *, it must be exactly *
|
|
|
+ if (segment.includes('*') && segment !== '*') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for invalid characters
|
|
|
+ if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|