feat: add dns disable timer (#2029)

This commit is contained in:
Yossi Hillali
2024-05-23 19:18:06 +03:00
committed by GitHub
parent cb2b28ca4b
commit afdca50bd2
8 changed files with 273 additions and 78 deletions

View File

@@ -17,6 +17,7 @@
"disabled": "Disabled", "disabled": "Disabled",
"enableAll": "Enable all", "enableAll": "Enable all",
"disableAll": "Disable all", "disableAll": "Disable all",
"setTimer": "Set timer",
"version": "Version", "version": "Version",
"changePosition": "Change position", "changePosition": "Change position",
"remove": "Remove", "remove": "Remove",

View File

@@ -14,5 +14,12 @@
"text": "There was a problem connecting to your DNS Hole(s). Please verify your configuration/integration(s)." "text": "There was a problem connecting to your DNS Hole(s). Please verify your configuration/integration(s)."
} }
} }
},
"durationModal": {
"title": "Set disable duration time",
"hours": "Hours",
"minutes": "Minutes",
"unlimited": "leave empty for unlimited",
"set": "Set"
} }
} }

View File

@@ -14,6 +14,7 @@ export const dnsHoleRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
action: z.enum(['enable', 'disable']), action: z.enum(['enable', 'disable']),
duration: z.number(),
configName: z.string(), configName: z.string(),
appsToChange: z.optional(z.array(z.string())), appsToChange: z.optional(z.array(z.string())),
}) })
@@ -32,12 +33,12 @@ export const dnsHoleRouter = createTRPCRouter({
await Promise.all( await Promise.all(
applicableApps.map(async (app) => { applicableApps.map(async (app) => {
if (app.integration?.type === 'pihole') { if (app.integration?.type === 'pihole') {
await processPiHole(app, input.action === 'enable'); await processPiHole(app, input.action === 'enable', input.duration);
return; return;
} }
await processAdGuard(app, input.action === 'enable'); await processAdGuard(app, input.action === 'enable', input.duration);
}) })
); );
}), }),
@@ -89,7 +90,7 @@ export const dnsHoleRouter = createTRPCRouter({
}), }),
}); });
const processAdGuard = async (app: ConfigAppType, enable: boolean) => { const processAdGuard = async (app: ConfigAppType, enable: boolean, duration: number = 0) => {
const adGuard = new AdGuard( const adGuard = new AdGuard(
app.url, app.url,
findAppProperty(app, 'username'), findAppProperty(app, 'username'),
@@ -106,13 +107,13 @@ const processAdGuard = async (app: ConfigAppType, enable: boolean) => {
} }
try { try {
await adGuard.disable(); await adGuard.disable(duration);
} catch (error) { } catch (error) {
Consola.error((error as Error).message); Consola.error((error as Error).message);
} }
}; };
const processPiHole = async (app: ConfigAppType, enable: boolean) => { const processPiHole = async (app: ConfigAppType, enable: boolean, duration: number = 0) => {
const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey')); const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
if (enable) { if (enable) {
@@ -125,7 +126,7 @@ const processPiHole = async (app: ConfigAppType, enable: boolean) => {
} }
try { try {
await pihole.disable(); await pihole.disable(duration);
} catch (error) { } catch (error) {
Consola.error((error as Error).message); Consola.error((error as Error).message);
} }

View File

@@ -59,8 +59,8 @@ export class AdGuard {
.reduce((sum, filter) => filter.rules_count + sum, 0); .reduce((sum, filter) => filter.rules_count + sum, 0);
} }
async disable() { async disable(duration: number) {
await this.changeProtectionStatus(false); await this.changeProtectionStatus(false, duration);
} }
async enable() { async enable() {
await this.changeProtectionStatus(true); await this.changeProtectionStatus(true);
@@ -69,7 +69,7 @@ export class AdGuard {
/** /**
* Make a post request to the AdGuard API to change the protection status based on the value of newStatus * Make a post request to the AdGuard API to change the protection status based on the value of newStatus
* @param {boolean} newStatus - The new status of the protection * @param {boolean} newStatus - The new status of the protection
* @param {number} duration - Duration of a pause, in milliseconds. Enabled should be false. * @param {number} duration - Duration of a pause, in seconds. Enabled should be false.
* @returns {string} - The response from the AdGuard API * @returns {string} - The response from the AdGuard API
*/ */
private async changeProtectionStatus(newStatus: boolean, duration = 0) { private async changeProtectionStatus(newStatus: boolean, duration = 0) {
@@ -78,7 +78,7 @@ export class AdGuard {
`${this.baseHostName}/control/protection`, `${this.baseHostName}/control/protection`,
{ {
enabled: newStatus, enabled: newStatus,
duration, duration: duration * 1000,
}, },
{ {
headers: { headers: {

View File

@@ -37,16 +37,19 @@ export class PiHoleClient {
return response.status === 'enabled'; return response.status === 'enabled';
} }
async disable() { async disable(duration: number) {
const response = await this.sendStatusChangeRequest('disable'); const response = await this.sendStatusChangeRequest('disable', duration);
return response.status === 'disabled'; return response.status === 'disabled';
} }
private async sendStatusChangeRequest( private async sendStatusChangeRequest(
action: 'enable' | 'disable' action: 'enable' | 'disable',
duration = 0
): Promise<PiHoleApiStatusChangeResponse> { ): Promise<PiHoleApiStatusChangeResponse> {
const response = await fetch( const response = await fetch(
`${this.baseHostName}/admin/api.php?${action}&auth=${this.apiToken}` duration !== 0
? `${this.baseHostName}/admin/api.php?${action}=${duration}&auth=${this.apiToken}`
: `${this.baseHostName}/admin/api.php?${action}&auth=${this.apiToken}`
); );
if (response.status !== 200) { if (response.status !== 200) {

View File

@@ -1,21 +1,29 @@
import { import {
ActionIcon,
Badge, Badge,
Box, Box,
Button, Button,
Card, Card,
Center, Center,
Flex,
Group, Group,
Image, Image,
SimpleGrid,
Stack, Stack,
Text, Text,
Title, Title,
Tooltip,
UnstyledButton, UnstyledButton,
} from '@mantine/core'; } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; import {
IconClockPause,
IconDeviceGamepad,
IconPlayerPlay,
IconPlayerStop,
} from '@tabler/icons-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useConfigContext } from '~/config/provider'; import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
@@ -23,6 +31,7 @@ import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading'; import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
import { useDnsHoleSummeryQuery } from './DnsHoleSummary'; import { useDnsHoleSummeryQuery } from './DnsHoleSummary';
import { TimerModal } from './TimerModal';
const definition = defineWidget({ const definition = defineWidget({
id: 'dns-hole-controls', id: 'dns-hole-controls',
@@ -69,9 +78,10 @@ const dnsLightStatus = (
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
const [opened, { close, open }] = useDisclosure(false);
const [appId, setAppId] = useState('');
const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery(); const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery();
const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation(); const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation();
const { width, ref } = useElementSize();
const { t } = useTranslation(['common', 'modules/dns-hole-controls']); const { t } = useTranslation(['common', 'modules/dns-hole-controls']);
const enableControls = sessionData?.user.isAdmin ?? false; const enableControls = sessionData?.user.isAdmin ?? false;
@@ -124,10 +134,17 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
return dnsList; return dnsList;
}; };
const toggleDns = async (action: 'enable' | 'disable', appsToChange?: string[]) => { const toggleDns = async (
action: 'enable' | 'disable',
appsToChange?: string[],
hours: number = 0,
minutes: number = 0
) => {
const duration = hours * 3600 + minutes * 60;
await mutateAsync( await mutateAsync(
{ {
action, action,
duration,
configName, configName,
appsToChange, appsToChange,
}, },
@@ -137,40 +154,68 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
}, },
} }
); );
setAppId('');
}; };
return ( return (
<Stack justify="space-between" h={'100%'} spacing="0.25rem"> <Stack h="100%" spacing="0.25rem">
{enableControls && widget.properties.showToggleAllButtons && ( {enableControls && widget.properties.showToggleAllButtons && (
<SimpleGrid <Flex gap="xs">
ref={ref} <Tooltip label={t('enableAll')}>
cols={width > 275 ? 2 : 1} <Button
verticalSpacing="0.25rem" onClick={() => toggleDns('enable', getDnsStatus()?.disabled)}
spacing="0.25rem" disabled={
> getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus
<Button }
onClick={() => toggleDns('enable', getDnsStatus()?.disabled)} variant="light"
disabled={getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus} color="green"
leftIcon={<IconPlayerPlay size={20} />} fullWidth
variant="light" h="2rem"
color="green" >
h="2rem" <IconPlayerPlay size={20} />
> </Button>
{t('enableAll')} </Tooltip>
</Button>
<Button <Tooltip label={t('setTimer')}>
onClick={() => toggleDns('disable', getDnsStatus()?.enabled)} <Button
disabled={getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus} onClick={open}
leftIcon={<IconPlayerStop size={20} />} disabled={
variant="light" getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus
color="red" }
h="2rem" variant="light"
> color="yellow"
{t('disableAll')} fullWidth
</Button> h="2rem"
</SimpleGrid> >
<IconClockPause size={20} />
</Button>
</Tooltip>
<Tooltip label={t('disableAll')}>
<Button
onClick={() => toggleDns('disable', getDnsStatus()?.enabled)}
disabled={
getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus
}
variant="light"
color="red"
fullWidth
h="2rem"
>
<IconPlayerStop size={20} />
</Button>
</Tooltip>
</Flex>
)} )}
<TimerModal
toggleDns={toggleDns}
getDnsStatus={getDnsStatus}
opened={opened}
close={close}
appId={appId}
/>
<Stack <Stack
spacing="0.25rem" spacing="0.25rem"
display="flex" display="flex"
@@ -203,36 +248,50 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
</Box> </Box>
<Stack spacing="0rem"> <Stack spacing="0rem">
<Text>{app.name}</Text> <Text>{app.name}</Text>
<UnstyledButton <Flex direction="row" gap="md">
onClick={() => <UnstyledButton
toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id]) onClick={() =>
} toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id])
disabled={fetchingDnsSummary || changingStatus} }
style={{ pointerEvents: enableControls ? 'auto' : 'none' }} disabled={fetchingDnsSummary || changingStatus}
> style={{ pointerEvents: enableControls ? 'auto' : 'none' }}
<Badge
variant="dot"
color={dnsLightStatus(fetchingDnsSummary || changingStatus, dnsHole.status)}
styles={(theme) => ({
root: {
'&:hover': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2],
},
'&:active': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[3],
},
},
})}
> >
{t(dnsHole.status)} <Badge
</Badge> variant="dot"
</UnstyledButton> color={dnsLightStatus(fetchingDnsSummary || changingStatus, dnsHole.status)}
styles={(theme) => ({
root: {
'&:hover': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2],
},
'&:active': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[3],
},
},
})}
>
{t(dnsHole.status)}
</Badge>
</UnstyledButton>
<ActionIcon
size={20}
radius="xl"
top="2.67px"
variant="default"
onClick={() => {
setAppId(app.id);
open();
}}
>
<IconClockPause size={20} color="red" />
</ActionIcon>
</Flex>
</Stack> </Stack>
</Group> </Group>
</Card> </Card>

View File

@@ -1,4 +1,4 @@
import { Box, Card, Center, Container, Flex, Text } from '@mantine/core'; import { Card, Center, Container, Flex, Text } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
import { import {
IconAd, IconAd,
@@ -112,7 +112,7 @@ export const useDnsHoleSummeryQuery = () => {
configName: configName!, configName: configName!,
}, },
{ {
staleTime: 1000 * 60 * 2, refetchInterval: 1000 * 60 * 2,
} }
); );
}; };

View File

@@ -0,0 +1,124 @@
import {
ActionIcon,
Button,
Flex,
Group,
Modal,
NumberInput,
NumberInputHandlers,
Stack,
Text,
rem,
} from '@mantine/core';
import { IconClockPause } from '@tabler/icons-react';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface TimerModalProps {
toggleDns: any;
getDnsStatus(): any;
opened: boolean;
close(): any;
appId: string;
}
export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: TimerModalProps) {
const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(0);
const hoursHandlers = useRef<NumberInputHandlers>();
const minutesHandlers = useRef<NumberInputHandlers>();
const { t } = useTranslation('modules/dns-hole-controls');
return (
<Modal
withinPortal
radius="lg"
shadow="sm"
size="sm"
opened={opened}
onClose={() => {
close();
setHours(0);
setMinutes(0);
}}
title={t('modules/dns-hole-controls:durationModal.title')}
>
<Flex direction="column" align="center" justify="center">
<Stack align="flex-end">
<Group spacing={5}>
<Text>{t('modules/dns-hole-controls:durationModal.hours')}</Text>
<ActionIcon
size={35}
variant="default"
onClick={() => hoursHandlers.current?.decrement()}
>
</ActionIcon>
<NumberInput
hideControls
value={hours}
onChange={(val) => setHours(Number(val))}
handlersRef={hoursHandlers}
max={999}
min={0}
step={1}
styles={{ input: { width: rem(54), textAlign: 'center' } }}
/>
<ActionIcon
size={35}
variant="default"
onClick={() => hoursHandlers.current?.increment()}
>
+
</ActionIcon>
</Group>
<Group spacing={5}>
<Text>{t('modules/dns-hole-controls:durationModal.minutes')}</Text>
<ActionIcon
size={35}
variant="default"
onClick={() => minutesHandlers.current?.decrement()}
>
</ActionIcon>
<NumberInput
hideControls
value={minutes}
onChange={(val) => setMinutes(Number(val))}
handlersRef={minutesHandlers}
max={59}
min={0}
step={1}
styles={{ input: { width: rem(54), textAlign: 'center' } }}
/>
<ActionIcon
size={35}
variant="default"
onClick={() => minutesHandlers.current?.increment()}
>
+
</ActionIcon>
</Group>
</Stack>
<Text ta="center" c="dimmed" my={5}>
{t('modules/dns-hole-controls:durationModal.unlimited')}
</Text>
<Button
variant="light"
color="red"
leftIcon={<IconClockPause size={20} />}
h="2rem"
w="12rem"
onClick={() => {
toggleDns('disable', appId !== '' ? [appId] : getDnsStatus()?.enabled, hours, minutes);
setHours(0);
setMinutes(0);
close();
}}
>
{t('modules/dns-hole-controls:durationModal.set')}
</Button>
</Flex>
</Modal>
);
}