diff --git a/src/server/api/routers/dns-hole/router.ts b/src/server/api/routers/dns-hole/router.ts index 4d8bb8a76..6de3f91a3 100644 --- a/src/server/api/routers/dns-hole/router.ts +++ b/src/server/api/routers/dns-hole/router.ts @@ -1,3 +1,4 @@ +import Consola from 'consola'; import { z } from 'zod'; import { findAppProperty } from '~/tools/client/app-properties'; import { getConfig } from '~/tools/config/getConfig'; @@ -14,19 +15,25 @@ export const dnsHoleRouter = createTRPCRouter({ z.object({ action: z.enum(['enable', 'disable']), configName: z.string(), + appsToChange: z.optional(z.array(z.string())), }) ) .mutation(async ({ input }) => { const config = getConfig(input.configName); const applicableApps = config.apps.filter( - (x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type) + (app) => + app.id && + app.integration?.type && + input.appsToChange?.includes(app.id) && + ['pihole', 'adGuardHome'].includes(app.integration?.type) ); await Promise.all( applicableApps.map(async (app) => { if (app.integration?.type === 'pihole') { await processPiHole(app, input.action === 'enable'); + return; } @@ -72,8 +79,6 @@ export const dnsHoleRouter = createTRPCRouter({ } ); - //const data: AdStatistics = ; - data.adsBlockedTodayPercentage = data.adsBlockedToday / data.dnsQueriesToday; if (Number.isNaN(data.adsBlockedTodayPercentage)) { data.adsBlockedTodayPercentage = 0; @@ -90,22 +95,38 @@ const processAdGuard = async (app: ConfigAppType, enable: boolean) => { ); if (enable) { - await adGuard.disable(); + try { + await adGuard.enable(); + } catch (error) { + Consola.error((error as Error).message); + } return; } - await adGuard.enable(); + try { + await adGuard.disable(); + } catch (error) { + Consola.error((error as Error).message); + } }; const processPiHole = async (app: ConfigAppType, enable: boolean) => { const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey')); if (enable) { - await pihole.enable(); + try { + await pihole.enable(); + } catch (error) { + Consola.error((error as Error).message); + } return; } - await pihole.disable(); + try { + await pihole.disable(); + } catch (error) { + Consola.error((error as Error).message); + } }; const collectPiHoleSummary = async (app: ConfigAppType) => { diff --git a/src/tools/server/sdk/adGuard/adGuard.ts b/src/tools/server/sdk/adGuard/adGuard.ts index 38a05102c..c6e0d4620 100644 --- a/src/tools/server/sdk/adGuard/adGuard.ts +++ b/src/tools/server/sdk/adGuard/adGuard.ts @@ -1,4 +1,7 @@ +import axios from 'axios'; +import Consola from 'consola'; import { z } from 'zod'; + import { trimStringEnding } from '../../../shared/strings'; import { adGuardApiFilteringStatusSchema, @@ -60,19 +63,41 @@ export class AdGuard { await this.changeProtectionStatus(false); } async enable() { - await this.changeProtectionStatus(false); + await this.changeProtectionStatus(true); } + /** + * 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 {number} duration - Duration of a pause, in milliseconds. Enabled should be false. + * @returns {string} - The response from the AdGuard API + */ private async changeProtectionStatus(newStatus: boolean, duration = 0) { - await fetch(`${this.baseHostName}/control/protection`, { - method: 'POST', - body: JSON.stringify({ - enabled: newStatus, - duration, - }), - }); + try { + const { data }: { data: string } = await axios.post( + `${this.baseHostName}/control/protection`, + { + enabled: newStatus, + duration, + }, + { + headers: { + Authorization: `Basic ${this.getAuthorizationHeaderValue()}`, + }, + } + ); + return data; + } catch (error) { + if (axios.isAxiosError(error)) { + Consola.error(error.message); + } + } } + /** + * It return a base64 username:password string + * @returns {string} The base64 encoded username and password + */ private getAuthorizationHeaderValue() { return Buffer.from(`${this.username}:${this.password}`).toString('base64'); } diff --git a/src/tools/server/sdk/pihole/piHole.ts b/src/tools/server/sdk/pihole/piHole.ts index 13ffd2b35..fcd22b605 100644 --- a/src/tools/server/sdk/pihole/piHole.ts +++ b/src/tools/server/sdk/pihole/piHole.ts @@ -62,6 +62,18 @@ export class PiHoleClient { ); } - return json as PiHoleApiStatusChangeResponse; + for(let loops = 0; loops < 10; loops++){ + const summary = await this.getSummary() + if (summary.status === action + 'd'){ + return json as PiHoleApiStatusChangeResponse; + } + await new Promise ((resolve) => { setTimeout(resolve, 50)}); + } + + return Promise.reject( + new Error( + `Although PiHole received the command, it failed to update it's status: ${json}` + ) + ) } } diff --git a/src/widgets/dnshole/DnsHoleControls.tsx b/src/widgets/dnshole/DnsHoleControls.tsx index dbd7a6b19..7f2835035 100644 --- a/src/widgets/dnshole/DnsHoleControls.tsx +++ b/src/widgets/dnshole/DnsHoleControls.tsx @@ -1,16 +1,25 @@ -import { Badge, Box, Button, Card, Group, Image, SimpleGrid, Stack, Text } from '@mantine/core'; +import { + Badge, + Box, + Button, + Card, + Group, + Image, + SimpleGrid, + Stack, + Text, + UnstyledButton, +} from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { api } from '~/utils/api'; import { useConfigContext } from '../../config/provider'; -import { queryClient } from '../../tools/server/configurations/tanstack/queryClient.tool'; import { defineWidget } from '../helper'; import { WidgetLoading } from '../loading'; import { IWidget } from '../widgets'; import { useDnsHoleSummeryQuery } from './DnsHoleSummary'; -import { PiholeApiSummaryType } from './type'; const definition = defineWidget({ id: 'dns-hole-controls', @@ -31,29 +40,83 @@ interface DnsHoleControlsWidgetProps { widget: IDnsHoleControlsWidget; } +/** + * + * @param fetching - a expression that return a boolean if the data is been fetched + * @param currentStatus the current status of the dns integration, either enabled or disabled + * @returns + */ +const dnsLightStatus = ( + fetching: boolean, + currentStatus: 'enabled' | 'disabled' +): 'blue' | 'green' | 'red' => { + if (fetching) { + return 'blue'; + } + if (currentStatus === 'enabled') { + return 'green'; + } + return 'red'; +}; + function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { - const { isInitialLoading, data } = useDnsHoleSummeryQuery(); - const { mutateAsync } = useDnsHoleControlMutation(); + const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery(); + const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation(); const { width, ref } = useElementSize(); const { t } = useTranslation('common'); const { name: configName, config } = useConfigContext(); + const trpcUtils = api.useContext(); + if (isInitialLoading || !data || !configName) { return ; } + type getDnsStatusAcc = { + enabled: string[]; + disabled: string[]; + }; + + const getDnsStatus = () => { + const dnsList = data?.status.reduce( + (acc: getDnsStatusAcc, dns) => { + if (dns.status === 'enabled') { + acc.enabled.push(dns.appId); + } else if (dns.status === 'disabled') { + acc.disabled.push(dns.appId); + } + return acc; + }, + { enabled: [], disabled: [] } + ); + + if (dnsList.enabled.length === 0 && dnsList.disabled.length === 0) { + return undefined; + } + return dnsList; + }; + + const reFetchSummaryDns = () => { + trpcUtils.dnsHole.summary.invalidate(); + }; + return ( - 275 ? 2 : 1} verticalSpacing="0.25rem" spacing="0.25rem"> + 275 ? 2 : 1} spacing="0.25rem">