mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-09 20:35:28 +02:00
Add enable rules toggle
This commit is contained in:
parent
874c67345e
commit
9694261f3e
3 changed files with 121 additions and 44 deletions
|
@ -29,7 +29,8 @@ const updateResourceBodySchema = z
|
||||||
blockAccess: z.boolean().optional(),
|
blockAccess: z.boolean().optional(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||||
emailWhitelistEnabled: z.boolean().optional(),
|
emailWhitelistEnabled: z.boolean().optional(),
|
||||||
isBaseDomain: z.boolean().optional()
|
isBaseDomain: z.boolean().optional(),
|
||||||
|
applyRules: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
|
|
@ -131,7 +131,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
resolver: zodResolver(addTargetSchema),
|
resolver: zodResolver(addTargetSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "",
|
ip: "",
|
||||||
method: resource.http ? "http" : null,
|
method: resource.http ? "http" : null
|
||||||
// protocol: "TCP",
|
// protocol: "TCP",
|
||||||
} as z.infer<typeof addTargetSchema>
|
} as z.infer<typeof addTargetSchema>
|
||||||
});
|
});
|
||||||
|
@ -316,17 +316,31 @@ export default function ReverseProxyTargets(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSsl(val: boolean) {
|
async function saveSsl(val: boolean) {
|
||||||
const res = await api.post(`/resource/${params.resourceId}`, {
|
const res = await api
|
||||||
ssl: val
|
.post(`/resource/${params.resourceId}`, {
|
||||||
});
|
ssl: val
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update SSL configuration",
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
"An error occurred while updating the SSL configuration"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
setSslEnabled(val);
|
if (res && res.status === 200) {
|
||||||
updateResource({ ssl: val });
|
setSslEnabled(val);
|
||||||
|
updateResource({ ssl: val });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "SSL Configuration",
|
title: "SSL Configuration",
|
||||||
description: "SSL configuration updated successfully"
|
description: "SSL configuration updated successfully"
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<LocalTarget>[] = [
|
const columns: ColumnDef<LocalTarget>[] = [
|
||||||
|
@ -652,7 +666,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Adding more than one target above will enable load balancing.
|
Adding more than one target above will enable load
|
||||||
|
balancing.
|
||||||
</p>
|
</p>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
|
|
|
@ -56,6 +56,7 @@ import {
|
||||||
SettingsSectionFooter
|
SettingsSectionFooter
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
|
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
|
||||||
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
|
||||||
// Schema for rule validation
|
// Schema for rule validation
|
||||||
const addRuleSchema = z.object({
|
const addRuleSchema = z.object({
|
||||||
|
@ -74,12 +75,13 @@ export default function ResourceRules(props: {
|
||||||
}) {
|
}) {
|
||||||
const params = use(props.params);
|
const params = use(props.params);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { resource } = useResourceContext();
|
const { resource, updateResource } = useResourceContext();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const [rules, setRules] = useState<LocalRule[]>([]);
|
const [rules, setRules] = useState<LocalRule[]>([]);
|
||||||
const [rulesToRemove, setRulesToRemove] = useState<number[]>([]);
|
const [rulesToRemove, setRulesToRemove] = useState<number[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pageLoading, setPageLoading] = useState(true);
|
const [pageLoading, setPageLoading] = useState(true);
|
||||||
|
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
|
||||||
|
|
||||||
const addRuleForm = useForm({
|
const addRuleForm = useForm({
|
||||||
resolver: zodResolver(addRuleSchema),
|
resolver: zodResolver(addRuleSchema),
|
||||||
|
@ -180,6 +182,34 @@ export default function ResourceRules(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveApplyRules(val: boolean) {
|
||||||
|
const res = await api
|
||||||
|
.post(`/resource/${params.resourceId}`, {
|
||||||
|
applyRules: val
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update rules",
|
||||||
|
description: formatAxiosError(
|
||||||
|
err,
|
||||||
|
"An error occurred while updating rules"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
setRulesEnabled(val);
|
||||||
|
updateResource({ applyRules: val });
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Enable Rules",
|
||||||
|
description: "Rule evaluation has been updated"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveRules() {
|
async function saveRules() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -199,7 +229,10 @@ export default function ResourceRules(props: {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rule.match === "PATH" && !isValidUrlGlobPattern(rule.value)) {
|
if (
|
||||||
|
rule.match === "PATH" &&
|
||||||
|
!isValidUrlGlobPattern(rule.value)
|
||||||
|
) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Invalid URL path",
|
title: "Invalid URL path",
|
||||||
|
@ -334,6 +367,25 @@ export default function ResourceRules(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>Enable Rules</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
Enable or disable rule evaluation for this resource
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SwitchInput
|
||||||
|
id="rules-toggle"
|
||||||
|
label="Enable Rules"
|
||||||
|
defaultChecked={resource.applyRules}
|
||||||
|
onCheckedChange={async (val) => {
|
||||||
|
await saveApplyRules(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
|
@ -400,9 +452,11 @@ export default function ResourceRules(props: {
|
||||||
<SelectItem value="CIDR">
|
<SelectItem value="CIDR">
|
||||||
CIDR
|
CIDR
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="PATH">
|
{resource.http && (
|
||||||
PATH
|
<SelectItem value="PATH">
|
||||||
</SelectItem>
|
PATH
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -421,14 +475,21 @@ export default function ResourceRules(props: {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enter CIDR or path value based
|
Enter CIDR{" "}
|
||||||
on match type
|
{resource.http
|
||||||
|
? "or path value"
|
||||||
|
: ""}{" "}
|
||||||
|
based on match type
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="outline">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
|
disabled={loading || !rulesEnabled}
|
||||||
|
>
|
||||||
Add Rule
|
Add Rule
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -518,35 +579,35 @@ function isValidCIDR(cidr: string): boolean {
|
||||||
|
|
||||||
function isValidUrlGlobPattern(pattern: string): boolean {
|
function isValidUrlGlobPattern(pattern: string): boolean {
|
||||||
// Remove leading slash if present
|
// Remove leading slash if present
|
||||||
pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
||||||
|
|
||||||
// Empty string is not valid
|
// Empty string is not valid
|
||||||
if (!pattern) {
|
if (!pattern) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split path into segments
|
// Split path into segments
|
||||||
const segments = pattern.split('/');
|
const segments = pattern.split("/");
|
||||||
|
|
||||||
// Check each segment
|
// Check each segment
|
||||||
for (let i = 0; i < segments.length; i++) {
|
for (let i = 0; i < segments.length; i++) {
|
||||||
const segment = segments[i];
|
const segment = segments[i];
|
||||||
|
|
||||||
// Empty segments are not allowed (double slashes)
|
// Empty segments are not allowed (double slashes)
|
||||||
if (!segment && i !== segments.length - 1) {
|
if (!segment && i !== segments.length - 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If segment contains *, it must be exactly *
|
// If segment contains *, it must be exactly *
|
||||||
if (segment.includes('*') && segment !== '*') {
|
if (segment.includes("*") && segment !== "*") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for invalid characters
|
// Check for invalid characters
|
||||||
if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
|
if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue