Add enable rules toggle

This commit is contained in:
Owen 2025-02-09 11:02:40 -05:00
parent 874c67345e
commit 9694261f3e
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
3 changed files with 121 additions and 44 deletions

View file

@ -29,7 +29,8 @@ const updateResourceBodySchema = z
blockAccess: z.boolean().optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
emailWhitelistEnabled: z.boolean().optional(),
isBaseDomain: z.boolean().optional()
isBaseDomain: z.boolean().optional(),
applyRules: z.boolean().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {

View file

@ -131,7 +131,7 @@ export default function ReverseProxyTargets(props: {
resolver: zodResolver(addTargetSchema),
defaultValues: {
ip: "",
method: resource.http ? "http" : null,
method: resource.http ? "http" : null
// protocol: "TCP",
} as z.infer<typeof addTargetSchema>
});
@ -316,17 +316,31 @@ export default function ReverseProxyTargets(props: {
}
async function saveSsl(val: boolean) {
const res = await api.post(`/resource/${params.resourceId}`, {
ssl: val
});
const res = await api
.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);
updateResource({ ssl: val });
if (res && res.status === 200) {
setSslEnabled(val);
updateResource({ ssl: val });
toast({
title: "SSL Configuration",
description: "SSL configuration updated successfully"
});
toast({
title: "SSL Configuration",
description: "SSL configuration updated successfully"
});
}
}
const columns: ColumnDef<LocalTarget>[] = [
@ -652,7 +666,8 @@ export default function ReverseProxyTargets(props: {
</Table>
</TableContainer>
<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>
</SettingsSectionBody>
<SettingsSectionFooter>

View file

@ -56,6 +56,7 @@ import {
SettingsSectionFooter
} from "@app/components/Settings";
import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules";
import { SwitchInput } from "@app/components/SwitchInput";
// Schema for rule validation
const addRuleSchema = z.object({
@ -74,12 +75,13 @@ export default function ResourceRules(props: {
}) {
const params = use(props.params);
const { toast } = useToast();
const { resource } = useResourceContext();
const { resource, updateResource } = useResourceContext();
const api = createApiClient(useEnvContext());
const [rules, setRules] = useState<LocalRule[]>([]);
const [rulesToRemove, setRulesToRemove] = useState<number[]>([]);
const [loading, setLoading] = useState(false);
const [pageLoading, setPageLoading] = useState(true);
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
const addRuleForm = useForm({
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() {
try {
setLoading(true);
@ -199,7 +229,10 @@ export default function ResourceRules(props: {
setLoading(false);
return;
}
if (rule.match === "PATH" && !isValidUrlGlobPattern(rule.value)) {
if (
rule.match === "PATH" &&
!isValidUrlGlobPattern(rule.value)
) {
toast({
variant: "destructive",
title: "Invalid URL path",
@ -334,6 +367,25 @@ export default function ResourceRules(props: {
return (
<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>
<SettingsSectionHeader>
<SettingsSectionTitle>
@ -400,9 +452,11 @@ export default function ResourceRules(props: {
<SelectItem value="CIDR">
CIDR
</SelectItem>
<SelectItem value="PATH">
PATH
</SelectItem>
{resource.http && (
<SelectItem value="PATH">
PATH
</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
@ -421,14 +475,21 @@ export default function ResourceRules(props: {
</FormControl>
<FormMessage />
<FormDescription>
Enter CIDR or path value based
on match type
Enter CIDR{" "}
{resource.http
? "or path value"
: ""}{" "}
based on match type
</FormDescription>
</FormItem>
)}
/>
</div>
<Button type="submit" variant="outline">
<Button
type="submit"
variant="outline"
disabled={loading || !rulesEnabled}
>
Add Rule
</Button>
</form>
@ -518,35 +579,35 @@ function isValidCIDR(cidr: string): boolean {
function isValidUrlGlobPattern(pattern: string): boolean {
// Remove leading slash if present
pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
// Empty string is not valid
if (!pattern) {
return false;
return false;
}
// Split path into segments
const segments = pattern.split('/');
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;
}
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;
}
}