Pull request 1907: 951-blocked-services-schedule-api
Updates #951. Squashed commit of the following: commit 6b840fd516f5a87fde0420e3aceb9c239b22c974 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Aug 29 19:53:03 2023 +0300 client: imp docs more commit 7fc8f0363fbe4c4266cb0f67428fe4d18c351d2d Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Aug 29 19:40:00 2023 +0300 client: imp docs commit 00bc14d5760614f2797714cdc2c4c19b1a94b86e Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 18:43:49 2023 +0300 try to fix lock file commit d749df74b576091e0b58928d86ea8b3b49f919da Merge: c69f9230be1f6229e5
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Aug 28 18:14:02 2023 +0300 Merge branch 'master' into 951-blocked-services-schedule-api commit c69f9230b12f7c983db06b74324b3df77d74b32b Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 17:16:20 2023 +0300 revert eslintrc commit b37916c2dff0ddea5293d87570bb58e3443d2d21 Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 12:02:39 2023 +0300 fix translations commit f5bb67d81506c687d0abd580049a3eee0af808e0 Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 11:43:57 2023 +0300 fix helpers commit 13ec6a8b3a0acfb62762ae7e46c6e98eb7c82212 Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 11:24:57 2023 +0300 remove todo commit 23724ec2fd683ed17b9f1cee841ad9aaf4c9d04f Author: Ildar Kamalov <ik@adguard.com> Date: Mon Aug 28 09:56:56 2023 +0300 add clients schedule form commit 84d29e558a329068e64e7a95ee183946aa4515b5 Author: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 25 17:44:40 2023 +0300 fix schedule form commit 83e4017688082e9eb670091d5a24d98157050502 Author: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 18 12:58:16 2023 +0300 remove unused commit ef2b68e138da382e3cf42586ae604e12d9493504 Author: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 18 12:57:37 2023 +0300 client: fix translation string commit 32ea80c968f52f18adbc811b2f06874644cdfe20 Author: Ildar Kamalov <ik@adguard.com> Date: Fri Aug 18 12:26:26 2023 +0300 wip schedule commit 9b770873859186c9424c8d108812e32ddff33bad Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jul 21 14:29:50 2023 +0300 all: imp naming commit ea4e9514ea3b264bcce7f2a301db817de4e87059 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jul 19 18:09:27 2023 +0300 all: imp code commit 98a705bdaa5c1e79394c73e5d75af2416fe9f297 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jul 18 18:23:26 2023 +0300 all: imp naming commit 4f84b55c7bfc9f7b680feac0ec45f5ea9189299a Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jul 14 15:01:17 2023 +0300 all: add global schedule api commit 87cf1646869ee9138964b47a27b7493674c8854a Merge: cabb80ac12adc8624c
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jul 14 12:09:29 2023 +0300 Merge branch 'master' into 951-blocked-services-schedule-api commit cabb80ac16de437a8118bb0166479574379c97a3 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jul 13 13:37:23 2023 +0300 openapi: fix typo commit 2279b03acbcfc3d76216f8aaf30ae1c7894127bc Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jul 13 12:26:19 2023 +0300 all: imp docs ... and 3 more commits
This commit is contained in:
parent
e1f6229e56
commit
aac36a2d2f
|
@ -81,6 +81,19 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/prefer-default-export": "off",
|
"import/prefer-default-export": "off",
|
||||||
"no-alert": "off"
|
"no-alert": "off",
|
||||||
|
"arrow-body-style": "off",
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
120,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"ignoreUrls": true,
|
||||||
|
"ignoreComments": false,
|
||||||
|
"ignoreRegExpLiterals": true,
|
||||||
|
"ignoreStrings": true,
|
||||||
|
"ignoreTemplateLiterals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": ".eslintrc",
|
"extends": ".eslintrc",
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-debugger":"warn",
|
"no-debugger":"warn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15094,6 +15094,11 @@
|
||||||
"setimmediate": "^1.0.4"
|
"setimmediate": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"timezones-list": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/timezones-list/-/timezones-list-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg=="
|
||||||
|
},
|
||||||
"tiny-invariant": {
|
"tiny-invariant": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"redux-form": "^8.3.5",
|
"redux-form": "^8.3.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"string-length": "^5.0.1",
|
"string-length": "^5.0.1",
|
||||||
|
"timezones-list": "^3.0.2",
|
||||||
"url-polyfill": "^1.1.9"
|
"url-polyfill": "^1.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -680,5 +680,37 @@
|
||||||
"protection_section_label": "Protection",
|
"protection_section_label": "Protection",
|
||||||
"log_and_stats_section_label": "Query log and statistics",
|
"log_and_stats_section_label": "Query log and statistics",
|
||||||
"ignore_query_log": "Ignore this client in query log",
|
"ignore_query_log": "Ignore this client in query log",
|
||||||
"ignore_statistics": "Ignore this client in statistics"
|
"ignore_statistics": "Ignore this client in statistics",
|
||||||
|
"schedule_services": "Pause service blocking",
|
||||||
|
"schedule_services_desc": "Configure the pause schedule of the service-blocking filter",
|
||||||
|
"schedule_services_desc_client": "Configure the pause schedule of the service-blocking filter for this client",
|
||||||
|
"schedule_desc": "Set inactivity periods for blocked services",
|
||||||
|
"schedule_invalid_select": "Start time must be before end time",
|
||||||
|
"schedule_select_days": "Select days",
|
||||||
|
"schedule_timezone": "Select a time zone",
|
||||||
|
"schedule_current_timezone": "Current time zone: {{value}}",
|
||||||
|
"schedule_time_all_day": "All day",
|
||||||
|
"schedule_modal_description": "This schedule will replace any existing schedules for the same day of the week. Each day of the week can have only one inactivity period.",
|
||||||
|
"schedule_modal_time_off": "No service blocking:",
|
||||||
|
"schedule_new": "New schedule",
|
||||||
|
"schedule_edit": "Edit schedule",
|
||||||
|
"schedule_save": "Save schedule",
|
||||||
|
"schedule_add": "Add schedule",
|
||||||
|
"schedule_remove": "Remove schedule",
|
||||||
|
"schedule_from": "From",
|
||||||
|
"schedule_to": "To",
|
||||||
|
"sunday": "Sunday",
|
||||||
|
"monday": "Monday",
|
||||||
|
"tuesday": "Tuesday",
|
||||||
|
"wednesday": "Wednesday",
|
||||||
|
"thursday": "Thursday",
|
||||||
|
"friday": "Friday",
|
||||||
|
"saturday": "Saturday",
|
||||||
|
"sunday_short": "Sun",
|
||||||
|
"monday_short": "Mon",
|
||||||
|
"tuesday_short": "Tue",
|
||||||
|
"wednesday_short": "Wed",
|
||||||
|
"thursday_short": "Thu",
|
||||||
|
"friday_short": "Fri",
|
||||||
|
"saturday_short": "Sat"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,19 +32,19 @@ export const getAllBlockedServices = () => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
|
export const updateBlockedServicesRequest = createAction('UPDATE_BLOCKED_SERVICES_REQUEST');
|
||||||
export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
|
export const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE');
|
||||||
export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');
|
export const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');
|
||||||
|
|
||||||
export const setBlockedServices = (values) => async (dispatch) => {
|
export const updateBlockedServices = (values) => async (dispatch) => {
|
||||||
dispatch(setBlockedServicesRequest());
|
dispatch(updateBlockedServicesRequest());
|
||||||
try {
|
try {
|
||||||
await apiClient.setBlockedServices(values);
|
await apiClient.updateBlockedServices(values);
|
||||||
dispatch(setBlockedServicesSuccess());
|
dispatch(updateBlockedServicesSuccess());
|
||||||
dispatch(getBlockedServices());
|
dispatch(getBlockedServices());
|
||||||
dispatch(addSuccessToast('blocked_services_saved'));
|
dispatch(addSuccessToast('blocked_services_saved'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(setBlockedServicesFailure());
|
dispatch(updateBlockedServicesFailure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -489,9 +489,9 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocked services
|
// Blocked services
|
||||||
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };
|
||||||
|
|
||||||
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' };
|
||||||
|
|
||||||
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
|
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
|
||||||
|
|
||||||
|
@ -501,12 +501,12 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockedServices() {
|
getBlockedServices() {
|
||||||
const { path, method } = this.BLOCKED_SERVICES_LIST;
|
const { path, method } = this.BLOCKED_SERVICES_GET;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlockedServices(config) {
|
updateBlockedServices(config) {
|
||||||
const { path, method } = this.BLOCKED_SERVICES_SET;
|
const { path, method } = this.BLOCKED_SERVICES_UPDATE;
|
||||||
const parameters = {
|
const parameters = {
|
||||||
data: config,
|
data: config,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
|
import { Timezone } from './Timezone';
|
||||||
|
import { TimeSelect } from './TimeSelect';
|
||||||
|
import { TimePeriod } from './TimePeriod';
|
||||||
|
import { getFullDayName, getShortDayName } from './helpers';
|
||||||
|
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||||
|
|
||||||
|
export const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||||
|
|
||||||
|
const INITIAL_START_TIME_MS = 0;
|
||||||
|
const INITIAL_END_TIME_MS = 86340000;
|
||||||
|
|
||||||
|
export const Modal = ({
|
||||||
|
isOpen,
|
||||||
|
currentDay,
|
||||||
|
schedule,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const intialTimezone = schedule.time_zone === LOCAL_TIMEZONE_VALUE
|
||||||
|
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
: schedule.time_zone;
|
||||||
|
|
||||||
|
const [timezone, setTimezone] = useState(intialTimezone);
|
||||||
|
const [days, setDays] = useState(new Set());
|
||||||
|
|
||||||
|
const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);
|
||||||
|
const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);
|
||||||
|
|
||||||
|
const [wrongPeriod, setWrongPeriod] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentDay) {
|
||||||
|
const newDays = new Set([currentDay]);
|
||||||
|
setDays(newDays);
|
||||||
|
|
||||||
|
setStartTime(schedule[currentDay].start);
|
||||||
|
setEndTime(schedule[currentDay].end);
|
||||||
|
}
|
||||||
|
}, [currentDay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startTime >= endTime) {
|
||||||
|
setWrongPeriod(true);
|
||||||
|
} else {
|
||||||
|
setWrongPeriod(false);
|
||||||
|
}
|
||||||
|
}, [startTime, endTime]);
|
||||||
|
|
||||||
|
const addDays = (day) => {
|
||||||
|
const newDays = new Set(days);
|
||||||
|
|
||||||
|
if (newDays.has(day)) {
|
||||||
|
newDays.delete(day);
|
||||||
|
} else {
|
||||||
|
newDays.add(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDays(newDays);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeDay = (day) => {
|
||||||
|
return days.has(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (currentDay) {
|
||||||
|
const newSchedule = schedule;
|
||||||
|
|
||||||
|
Array.from(days).forEach((day) => {
|
||||||
|
newSchedule[day] = {
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onSubmit(newSchedule);
|
||||||
|
} else {
|
||||||
|
const newSchedule = {
|
||||||
|
time_zone: timezone,
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.from(days).forEach((day) => {
|
||||||
|
newSchedule[day] = {
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onSubmit(newSchedule);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule"
|
||||||
|
closeTimeoutMS={0}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h4 className="modal-title">
|
||||||
|
{currentDay ? t('schedule_edit') : t('schedule_new')}
|
||||||
|
</h4>
|
||||||
|
<button type="button" className="close" onClick={onClose}>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={onFormSubmit}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<Timezone
|
||||||
|
timezone={timezone}
|
||||||
|
setTimezone={setTimezone}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="schedule__days">
|
||||||
|
{DAYS_OF_WEEK.map((day) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={day}
|
||||||
|
className="btn schedule__button-day"
|
||||||
|
data-active={activeDay(day)}
|
||||||
|
onClick={() => addDays(day)}
|
||||||
|
>
|
||||||
|
{getShortDayName(t, day)}
|
||||||
|
</button>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="schedule__time-wrap">
|
||||||
|
<div className="schedule__time-row">
|
||||||
|
<TimeSelect
|
||||||
|
value={startTime}
|
||||||
|
onChange={(v) => setStartTime(v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeSelect
|
||||||
|
value={endTime}
|
||||||
|
onChange={(v) => setEndTime(v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wrongPeriod && (
|
||||||
|
<div className="schedule__error">
|
||||||
|
{t('schedule_invalid_select')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="schedule__info">
|
||||||
|
<div className="schedule__info-title">
|
||||||
|
{t('schedule_modal_time_off')}
|
||||||
|
</div>
|
||||||
|
<div className="schedule__info-row">
|
||||||
|
<svg className="icons schedule__info-icon">
|
||||||
|
<use xlinkHref="#calendar" />
|
||||||
|
</svg>
|
||||||
|
{days.size ? (
|
||||||
|
Array.from(days).map((day) => getFullDayName(t, day)).join(', ')
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="schedule__info-row">
|
||||||
|
<svg className="icons schedule__info-icon">
|
||||||
|
<use xlinkHref="#watch" />
|
||||||
|
</svg>
|
||||||
|
{wrongPeriod ? (
|
||||||
|
<span>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<TimePeriod
|
||||||
|
startTimeMs={startTime}
|
||||||
|
endTimeMs={endTime}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="schedule__notice">
|
||||||
|
{t('schedule_modal_description')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={days.size === 0 || wrongPeriod}
|
||||||
|
onClick={onFormSubmit}
|
||||||
|
>
|
||||||
|
{currentDay ? t('schedule_save') : t('schedule_add')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ReactModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
schedule: PropTypes.object.isRequired,
|
||||||
|
currentDay: PropTypes.string,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { getTimeFromMs } from './helpers';
|
||||||
|
|
||||||
|
export const TimePeriod = ({
|
||||||
|
startTimeMs,
|
||||||
|
endTimeMs,
|
||||||
|
}) => {
|
||||||
|
const startTime = getTimeFromMs(startTimeMs);
|
||||||
|
const endTime = getTimeFromMs(endTimeMs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="schedule__time">
|
||||||
|
<time>{startTime.hours}:{startTime.minutes}</time>
|
||||||
|
–
|
||||||
|
<time>{endTime.hours}:{endTime.minutes}</time>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TimePeriod.propTypes = {
|
||||||
|
startTimeMs: PropTypes.number.isRequired,
|
||||||
|
endTimeMs: PropTypes.number.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { getTimeFromMs, convertTimeToMs } from './helpers';
|
||||||
|
|
||||||
|
export const TimeSelect = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);
|
||||||
|
|
||||||
|
const [hours, setHours] = useState(initialHours);
|
||||||
|
const [minutes, setMinutes] = useState(initialMinutes);
|
||||||
|
|
||||||
|
const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
||||||
|
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
|
||||||
|
|
||||||
|
const onHourChange = (event) => {
|
||||||
|
setHours(event.target.value);
|
||||||
|
onChange(convertTimeToMs(event.target.value, minutes));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMinuteChange = (event) => {
|
||||||
|
setMinutes(event.target.value);
|
||||||
|
onChange(convertTimeToMs(hours, event.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="schedule__time-select">
|
||||||
|
<select
|
||||||
|
value={hours}
|
||||||
|
onChange={onHourChange}
|
||||||
|
className="form-control custom-select"
|
||||||
|
>
|
||||||
|
{hourOptions.map((hour) => (
|
||||||
|
<option key={hour} value={hour}>
|
||||||
|
{hour}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
:
|
||||||
|
<select
|
||||||
|
value={minutes}
|
||||||
|
onChange={onMinuteChange}
|
||||||
|
className="form-control custom-select"
|
||||||
|
>
|
||||||
|
{minuteOptions.map((minute) => (
|
||||||
|
<option key={minute} value={minute}>
|
||||||
|
{minute}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TimeSelect.propTypes = {
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react';
|
||||||
|
import timezones from 'timezones-list';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||||
|
|
||||||
|
export const Timezone = ({
|
||||||
|
timezone,
|
||||||
|
setTimezone,
|
||||||
|
}) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
const onTimeZoneChange = (event) => {
|
||||||
|
setTimezone(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="schedule__timezone">
|
||||||
|
<label className="form__label form__label--with-desc mb-2">
|
||||||
|
{t('schedule_timezone')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="form-control custom-select"
|
||||||
|
value={timezone}
|
||||||
|
onChange={onTimeZoneChange}
|
||||||
|
>
|
||||||
|
<option value={LOCAL_TIMEZONE_VALUE}>
|
||||||
|
{t('schedule_timezone')}
|
||||||
|
</option>
|
||||||
|
{/* TODO: get timezones from backend method when the method is ready */}
|
||||||
|
{timezones.map((zone) => (
|
||||||
|
<option key={zone.name} value={zone.tzCode}>
|
||||||
|
{zone.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Timezone.propTypes = {
|
||||||
|
timezone: PropTypes.string.isRequired,
|
||||||
|
setTimezone: PropTypes.func.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
export const getFullDayName = (t, abbreviation) => {
|
||||||
|
const dayMap = {
|
||||||
|
sun: t('sunday'),
|
||||||
|
mon: t('monday'),
|
||||||
|
tue: t('tuesday'),
|
||||||
|
wed: t('wednesday'),
|
||||||
|
thu: t('thursday'),
|
||||||
|
fri: t('friday'),
|
||||||
|
sat: t('saturday'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return dayMap[abbreviation] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShortDayName = (t, abbreviation) => {
|
||||||
|
const dayMap = {
|
||||||
|
sun: t('sunday_short'),
|
||||||
|
mon: t('monday_short'),
|
||||||
|
tue: t('tuesday_short'),
|
||||||
|
wed: t('wednesday_short'),
|
||||||
|
thu: t('thursday_short'),
|
||||||
|
fri: t('friday_short'),
|
||||||
|
sat: t('saturday_short'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return dayMap[abbreviation] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimeFromMs = (value) => {
|
||||||
|
const selectedTime = new Date(value);
|
||||||
|
const hours = selectedTime.getUTCHours();
|
||||||
|
const minutes = selectedTime.getUTCMinutes();
|
||||||
|
|
||||||
|
return {
|
||||||
|
hours: hours.toString().padStart(2, '0'),
|
||||||
|
minutes: minutes.toString().padStart(2, '0'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertTimeToMs = (hours, minutes) => {
|
||||||
|
const selectedTime = new Date(0);
|
||||||
|
selectedTime.setUTCHours(parseInt(hours, 10));
|
||||||
|
selectedTime.setUTCMinutes(parseInt(minutes, 10));
|
||||||
|
|
||||||
|
return selectedTime.getTime();
|
||||||
|
};
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cn from 'classnames';
|
||||||
|
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { getFullDayName, getShortDayName } from './helpers';
|
||||||
|
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||||
|
import { TimePeriod } from './TimePeriod';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export const ScheduleForm = ({
|
||||||
|
schedule,
|
||||||
|
onScheduleSubmit,
|
||||||
|
clientForm,
|
||||||
|
}) => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [currentDay, setCurrentDay] = useState();
|
||||||
|
|
||||||
|
const onModalOpen = () => setModalOpen(true);
|
||||||
|
const onModalClose = () => setModalOpen(false);
|
||||||
|
|
||||||
|
const filteredScheduleKeys = useMemo(() => (
|
||||||
|
schedule ? Object.keys(schedule).filter((v) => v !== 'time_zone') : []
|
||||||
|
), [schedule]);
|
||||||
|
|
||||||
|
const scheduleMap = new Map();
|
||||||
|
filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));
|
||||||
|
|
||||||
|
const onSubmit = (values) => {
|
||||||
|
onScheduleSubmit(values);
|
||||||
|
onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (day) => {
|
||||||
|
scheduleMap.delete(day);
|
||||||
|
|
||||||
|
const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));
|
||||||
|
|
||||||
|
onScheduleSubmit({
|
||||||
|
time_zone: schedule.time_zone,
|
||||||
|
...scheduleWeek,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEdit = (day) => {
|
||||||
|
setCurrentDay(day);
|
||||||
|
onModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setCurrentDay(undefined);
|
||||||
|
onModalOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="schedule__current-timezone">
|
||||||
|
{t('schedule_current_timezone', { value: schedule?.time_zone || LOCAL_TIMEZONE_VALUE })}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="schedule__rows">
|
||||||
|
{filteredScheduleKeys.map((day) => {
|
||||||
|
const data = schedule[day];
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={day} className="schedule__row">
|
||||||
|
<div className="schedule__day">
|
||||||
|
{getFullDayName(t, day)}
|
||||||
|
</div>
|
||||||
|
<div className="schedule__day schedule__day--mobile">
|
||||||
|
{getShortDayName(t, day)}
|
||||||
|
</div>
|
||||||
|
<TimePeriod
|
||||||
|
startTimeMs={data.start}
|
||||||
|
endTimeMs={data.end}
|
||||||
|
/>
|
||||||
|
<div className="schedule__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-primary btn-sm schedule__button"
|
||||||
|
title={t('edit_table_action')}
|
||||||
|
onClick={() => onEdit(day)}
|
||||||
|
>
|
||||||
|
<svg className="icons icon12">
|
||||||
|
<use xlinkHref="#edit" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-secondary btn-sm schedule__button"
|
||||||
|
title={t('delete_table_action')}
|
||||||
|
onClick={() => onDelete(day)}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#delete" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'btn',
|
||||||
|
{ 'btn-outline-success btn-sm': clientForm },
|
||||||
|
{ 'btn-success btn-standard': !clientForm },
|
||||||
|
)}
|
||||||
|
onClick={onAdd}
|
||||||
|
>
|
||||||
|
{t('schedule_new')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{modalOpen && (
|
||||||
|
<Modal
|
||||||
|
isOpen={modalOpen}
|
||||||
|
onClose={onModalClose}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
schedule={schedule}
|
||||||
|
currentDay={currentDay}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ScheduleForm.propTypes = {
|
||||||
|
schedule: PropTypes.object,
|
||||||
|
onScheduleSubmit: PropTypes.func.isRequired,
|
||||||
|
clientForm: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1,134 @@
|
||||||
|
.schedule__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__rows {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__day {
|
||||||
|
display: none;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__day--mobile {
|
||||||
|
display: block;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 767px) {
|
||||||
|
.schedule__row {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__day {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__day--mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__actions {
|
||||||
|
margin-left: 32px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__time {
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__button {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__days {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__button-day {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
color: #495057;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__button-day[data-active="true"] {
|
||||||
|
color: var(--btn-success-bgcolor);
|
||||||
|
border-color: var(--btn-success-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__time-wrap {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__time-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__time-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__error {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cd201f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__timezone {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__current-timezone {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__info {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__notice {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__info-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__info-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #495057;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
|
@ -4,9 +4,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import { getBlockedServices, getAllBlockedServices, setBlockedServices } from '../../../actions/services';
|
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
|
||||||
import PageTitle from '../../ui/PageTitle';
|
import PageTitle from '../../ui/PageTitle';
|
||||||
|
|
||||||
|
import { ScheduleForm } from './ScheduleForm';
|
||||||
|
|
||||||
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
|
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
|
||||||
(acc, service) => {
|
(acc, service) => {
|
||||||
acc.blocked_services[service] = true;
|
acc.blocked_services[service] = true;
|
||||||
|
@ -33,10 +35,24 @@ const Services = () => {
|
||||||
.keys(values.blocked_services)
|
.keys(values.blocked_services)
|
||||||
.filter((service) => values.blocked_services[service]);
|
.filter((service) => values.blocked_services[service]);
|
||||||
|
|
||||||
dispatch(setBlockedServices(blocked_services));
|
dispatch(updateBlockedServices({
|
||||||
|
ids: blocked_services,
|
||||||
|
schedule: services.list.schedule,
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValues = getInitialDataForServices(services.list);
|
const handleScheduleSubmit = (values) => {
|
||||||
|
dispatch(updateBlockedServices({
|
||||||
|
ids: services.list.ids,
|
||||||
|
schedule: values,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = getInitialDataForServices(services.list.ids);
|
||||||
|
|
||||||
|
if (!initialValues) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -57,6 +73,17 @@ const Services = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title={t('schedule_services')}
|
||||||
|
subtitle={t('schedule_services_desc')}
|
||||||
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
|
<ScheduleForm
|
||||||
|
schedule={services.list.schedule}
|
||||||
|
onScheduleSubmit={handleScheduleSubmit}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { getAllBlockedServices } from '../../../../actions/services';
|
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
|
||||||
import { initSettings } from '../../../../actions';
|
import { initSettings } from '../../../../actions';
|
||||||
import {
|
import {
|
||||||
splitByNewLine,
|
splitByNewLine,
|
||||||
|
@ -14,7 +14,7 @@ import {
|
||||||
sortIp,
|
sortIp,
|
||||||
getService,
|
getService,
|
||||||
} from '../../../../helpers/helpers';
|
} from '../../../../helpers/helpers';
|
||||||
import { MODAL_TYPE } from '../../../../helpers/constants';
|
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||||
import Card from '../../../ui/Card';
|
import Card from '../../../ui/Card';
|
||||||
import CellWrap from '../../../ui/CellWrap';
|
import CellWrap from '../../../ui/CellWrap';
|
||||||
import LogsSearchLink from '../../../ui/LogsSearchLink';
|
import LogsSearchLink from '../../../ui/LogsSearchLink';
|
||||||
|
@ -45,6 +45,7 @@ const ClientsTable = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllBlockedServices());
|
dispatch(getAllBlockedServices());
|
||||||
|
dispatch(getBlockedServices());
|
||||||
dispatch(initSettings());
|
dispatch(initSettings());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -112,6 +113,9 @@ const ClientsTable = ({
|
||||||
tags: [],
|
tags: [],
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
use_global_blocked_services: true,
|
use_global_blocked_services: true,
|
||||||
|
blocked_services_schedule: {
|
||||||
|
time_zone: LOCAL_TIMEZONE_VALUE,
|
||||||
|
},
|
||||||
safe_search: { ...(safesearch || {}) },
|
safe_search: { ...(safesearch || {}) },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Select from 'react-select';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Tabs from '../../ui/Tabs';
|
import Tabs from '../../ui/Tabs';
|
||||||
import Examples from '../Dns/Upstream/Examples';
|
import Examples from '../Dns/Upstream/Examples';
|
||||||
|
import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
|
||||||
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
|
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
renderInputField,
|
renderInputField,
|
||||||
|
@ -137,10 +138,10 @@ let Form = (props) => {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
change,
|
change,
|
||||||
pristine,
|
|
||||||
submitting,
|
submitting,
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
useGlobalServices,
|
useGlobalServices,
|
||||||
|
blockedServicesSchedule,
|
||||||
toggleClientModal,
|
toggleClientModal,
|
||||||
processingAdding,
|
processingAdding,
|
||||||
processingUpdating,
|
processingUpdating,
|
||||||
|
@ -155,6 +156,10 @@ let Form = (props) => {
|
||||||
|
|
||||||
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
||||||
|
|
||||||
|
const handleScheduleSubmit = (values) => {
|
||||||
|
change('blocked_services_schedule', values);
|
||||||
|
};
|
||||||
|
|
||||||
const tabs = {
|
const tabs = {
|
||||||
settings: {
|
settings: {
|
||||||
title: 'settings',
|
title: 'settings',
|
||||||
|
@ -269,6 +274,21 @@ let Form = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
},
|
},
|
||||||
|
schedule_services: {
|
||||||
|
title: 'schedule_services',
|
||||||
|
component: (
|
||||||
|
<>
|
||||||
|
<div className="form__desc mb-4">
|
||||||
|
<Trans>schedule_services_desc_client</Trans>
|
||||||
|
</div>
|
||||||
|
<ScheduleForm
|
||||||
|
schedule={blockedServicesSchedule}
|
||||||
|
onScheduleSubmit={handleScheduleSubmit}
|
||||||
|
clientForm
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
upstream_dns: {
|
upstream_dns: {
|
||||||
title: 'upstream_dns',
|
title: 'upstream_dns',
|
||||||
component: <div label="upstream" title={props.t('upstream_dns')}>
|
component: <div label="upstream" title={props.t('upstream_dns')}>
|
||||||
|
@ -355,8 +375,12 @@ let Form = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs controlClass="form" tabs={tabs} activeTabLabel={activeTabLabel}
|
<Tabs
|
||||||
setActiveTabLabel={setActiveTabLabel}>
|
controlClass="form"
|
||||||
|
tabs={tabs}
|
||||||
|
activeTabLabel={activeTabLabel}
|
||||||
|
setActiveTabLabel={setActiveTabLabel}
|
||||||
|
>
|
||||||
{activeTab}
|
{activeTab}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
@ -380,7 +404,6 @@ let Form = (props) => {
|
||||||
disabled={
|
disabled={
|
||||||
submitting
|
submitting
|
||||||
|| invalid
|
|| invalid
|
||||||
|| pristine
|
|
||||||
|| processingAdding
|
|| processingAdding
|
||||||
|| processingUpdating
|
|| processingUpdating
|
||||||
}
|
}
|
||||||
|
@ -402,6 +425,7 @@ Form.propTypes = {
|
||||||
toggleClientModal: PropTypes.func.isRequired,
|
toggleClientModal: PropTypes.func.isRequired,
|
||||||
useGlobalSettings: PropTypes.bool,
|
useGlobalSettings: PropTypes.bool,
|
||||||
useGlobalServices: PropTypes.bool,
|
useGlobalServices: PropTypes.bool,
|
||||||
|
blockedServicesSchedule: PropTypes.object,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
processingAdding: PropTypes.bool.isRequired,
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
processingUpdating: PropTypes.bool.isRequired,
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
|
@ -415,9 +439,11 @@ const selector = formValueSelector(FORM_NAME.CLIENT);
|
||||||
Form = connect((state) => {
|
Form = connect((state) => {
|
||||||
const useGlobalSettings = selector(state, 'use_global_settings');
|
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||||
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
||||||
|
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
|
||||||
return {
|
return {
|
||||||
useGlobalSettings,
|
useGlobalSettings,
|
||||||
useGlobalServices,
|
useGlobalServices,
|
||||||
|
blockedServicesSchedule,
|
||||||
};
|
};
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|
||||||
|
|
|
@ -225,6 +225,20 @@ const Icons = () => (
|
||||||
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
||||||
</g>
|
</g>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="calendar" fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="4" y="5.5" width="16" height="14" rx="3" />
|
||||||
|
<path d="M12 4V7" />
|
||||||
|
<path d="M8 4L8 7" />
|
||||||
|
<path d="M16 4V7" />
|
||||||
|
<path d="M9.7397 15.5V11L8 13" />
|
||||||
|
<path d="M14.7397 15.5V11L13 13" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="watch" fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M16.1215 12.1213H11.8789V7.87866" />
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
.modal-dialog--clients {
|
.modal-dialog--clients,
|
||||||
|
.modal-dialog--schedule {
|
||||||
max-width: 650px;
|
max-width: 650px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -470,6 +470,10 @@ hr {
|
||||||
border-top: 1px solid rgba(0, 40, 100, 0.12);
|
border-top: 1px solid rgba(0, 40, 100, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] hr {
|
||||||
|
border-color: var(--card-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
small,
|
small,
|
||||||
.small {
|
.small {
|
||||||
font-size: 87.5%;
|
font-size: 87.5%;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { initSettings, toggleSetting } from '../actions';
|
import { initSettings, toggleSetting } from '../actions';
|
||||||
import { getBlockedServices, setBlockedServices } from '../actions/services';
|
import { getBlockedServices, updateBlockedServices } from '../actions/services';
|
||||||
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
|
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
|
||||||
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
|
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
|
||||||
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
|
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
|
||||||
|
@ -24,7 +24,7 @@ const mapDispatchToProps = {
|
||||||
initSettings,
|
initSettings,
|
||||||
toggleSetting,
|
toggleSetting,
|
||||||
getBlockedServices,
|
getBlockedServices,
|
||||||
setBlockedServices,
|
updateBlockedServices,
|
||||||
getStatsConfig,
|
getStatsConfig,
|
||||||
setStatsConfig,
|
setStatsConfig,
|
||||||
resetStats,
|
resetStats,
|
||||||
|
|
|
@ -552,3 +552,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
|
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
|
||||||
|
|
||||||
|
export const LOCAL_TIMEZONE_VALUE = 'Local';
|
||||||
|
|
|
@ -20,9 +20,9 @@ const services = handleActions(
|
||||||
processingAll: false,
|
processingAll: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.setBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
|
[actions.updateBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
|
||||||
[actions.setBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
|
[actions.updateBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
|
||||||
[actions.setBlockedServicesSuccess]: (state) => ({
|
[actions.updateBlockedServicesSuccess]: (state) => ({
|
||||||
...state,
|
...state,
|
||||||
processingSet: false,
|
processingSet: false,
|
||||||
}),
|
}),
|
||||||
|
@ -31,7 +31,7 @@ const services = handleActions(
|
||||||
processing: true,
|
processing: true,
|
||||||
processingAll: true,
|
processingAll: true,
|
||||||
processingSet: false,
|
processingSet: false,
|
||||||
list: [],
|
list: {},
|
||||||
allServices: [],
|
allServices: [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
package aghhttp
|
package aghhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -61,23 +60,3 @@ func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainTe
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteJSONResponse sets the content-type header in w.Header() to
|
|
||||||
// "application/json", writes a header with a "200 OK" status, encodes resp to
|
|
||||||
// w, calls [Error] on any returned error, and returns it as well.
|
|
||||||
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
|
|
||||||
return WriteJSONResponseCode(w, r, http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
|
|
||||||
// redefine the status code.
|
|
||||||
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
|
||||||
w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
|
|
||||||
w.WriteHeader(code)
|
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package websvc
|
package aghhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -7,7 +7,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/golibs/httphdr"
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
@ -87,30 +86,29 @@ func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
// WriteJSONResponse writes headers with the code, encodes resp into w, and logs
|
||||||
// and logs any errors it encounters. r is used to get additional information
|
// any errors it encounters. r is used to get additional information from the
|
||||||
// from the request.
|
|
||||||
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
|
||||||
writeJSONResponse(w, r, v, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
|
||||||
// errors it encounters. r is used to get additional information from the
|
|
||||||
// request.
|
// request.
|
||||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, code int, resp any) {
|
||||||
// TODO(a.garipov): Put some of these to a middleware.
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
|
h.Set(httphdr.ContentType, HdrValApplicationJSON)
|
||||||
h.Set(httphdr.Server, aghhttp.UserAgent())
|
h.Set(httphdr.Server, UserAgent())
|
||||||
|
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(v)
|
err := json.NewEncoder(w).Encode(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
log.Error("aghhttp: writing json resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteJSONResponseOK writes headers with the code 200 OK, encodes v into w,
|
||||||
|
// and logs any errors it encounters. r is used to get additional information
|
||||||
|
// from the request.
|
||||||
|
func WriteJSONResponseOK(w http.ResponseWriter, r *http.Request, v any) {
|
||||||
|
WriteJSONResponse(w, r, http.StatusOK, v)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||||
// definition in the OpenAPI specification.
|
// definition in the OpenAPI specification.
|
||||||
type ErrorCode string
|
type ErrorCode string
|
||||||
|
@ -131,14 +129,14 @@ type HTTPAPIErrorResp struct {
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
// WriteJSONResponseError encodes err as a JSON error into w, and logs any
|
||||||
// errors it encounters. r is used to get additional information from the
|
// errors it encounters. r is used to get additional information from the
|
||||||
// request.
|
// request.
|
||||||
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
func WriteJSONResponseError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
log.Error("aghhttp: writing json error to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
|
||||||
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
WriteJSONResponse(w, r, http.StatusUnprocessableEntity, &HTTPAPIErrorResp{
|
||||||
Code: ErrorCodeTMP000,
|
Code: ErrorCodeTMP000,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
}, http.StatusUnprocessableEntity)
|
})
|
||||||
}
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
package websvc_test
|
package aghhttp_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testJSONTime is the JSON time for tests.
|
// testJSONTime is the JSON time for tests.
|
||||||
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
var testJSONTime = aghhttp.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||||
|
|
||||||
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||||
const testJSONTimeStr = "1234567890123.456"
|
const testJSONTimeStr = "1234567890123.456"
|
||||||
|
@ -21,17 +21,17 @@ func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
in websvc.JSONTime
|
in aghhttp.JSONTime
|
||||||
want []byte
|
want []byte
|
||||||
}{{
|
}{{
|
||||||
name: "unix_zero",
|
name: "unix_zero",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
in: websvc.JSONTime(time.Unix(0, 0)),
|
in: aghhttp.JSONTime(time.Unix(0, 0)),
|
||||||
want: []byte("0"),
|
want: []byte("0"),
|
||||||
}, {
|
}, {
|
||||||
name: "empty",
|
name: "empty",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
in: websvc.JSONTime{},
|
in: aghhttp.JSONTime{},
|
||||||
want: []byte("-6795364578871.345"),
|
want: []byte("-6795364578871.345"),
|
||||||
}, {
|
}, {
|
||||||
name: "time",
|
name: "time",
|
||||||
|
@ -51,7 +51,7 @@ func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
t.Run("json", func(t *testing.T) {
|
||||||
in := &struct {
|
in := &struct {
|
||||||
A websvc.JSONTime
|
A aghhttp.JSONTime
|
||||||
}{
|
}{
|
||||||
A: testJSONTime,
|
A: testJSONTime,
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
want websvc.JSONTime
|
want aghhttp.JSONTime
|
||||||
data []byte
|
data []byte
|
||||||
}{{
|
}{{
|
||||||
name: "time",
|
name: "time",
|
||||||
|
@ -78,13 +78,13 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
name: "bad",
|
name: "bad",
|
||||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||||
`invalid syntax`,
|
`invalid syntax`,
|
||||||
want: websvc.JSONTime{},
|
want: aghhttp.JSONTime{},
|
||||||
data: []byte(`{}`),
|
data: []byte(`{}`),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var got websvc.JSONTime
|
var got aghhttp.JSONTime
|
||||||
err := got.UnmarshalJSON(tc.data)
|
err := got.UnmarshalJSON(tc.data)
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
t.Run("nil", func(t *testing.T) {
|
||||||
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
err := (*aghhttp.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
|
@ -103,7 +103,7 @@ func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
t.Run("json", func(t *testing.T) {
|
t.Run("json", func(t *testing.T) {
|
||||||
want := testJSONTime
|
want := testJSONTime
|
||||||
var got struct {
|
var got struct {
|
||||||
A websvc.JSONTime
|
A aghhttp.JSONTime
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
|
@ -146,7 +146,7 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
status.Leases = leasesToDynamic(s.Leases(LeasesDynamic))
|
status.Leases = leasesToDynamic(s.Leases(LeasesDynamic))
|
||||||
status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic))
|
status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic))
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, status)
|
aghhttp.WriteJSONResponseOK(w, r, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||||
|
@ -395,7 +395,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newNetInterfaceJSON creates a JSON object from a [net.Interface] iface.
|
// newNetInterfaceJSON creates a JSON object from a [net.Interface] iface.
|
||||||
|
@ -547,7 +547,7 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
setOtherDHCPResult(ifaceName, result)
|
setOtherDHCPResult(ifaceName, result)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, result)
|
aghhttp.WriteJSONResponseOK(w, r, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setOtherDHCPResult sets the results of the check for another DHCP server in
|
// setOtherDHCPResult sets the results of the check for another DHCP server in
|
||||||
|
|
|
@ -24,7 +24,7 @@ type jsonError struct {
|
||||||
// TODO(a.garipov): Either take the logger from the server after we've
|
// TODO(a.garipov): Either take the logger from the server after we've
|
||||||
// refactored logging or make this not a method of *Server.
|
// refactored logging or make this not a method of *Server.
|
||||||
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
|
aghhttp.WriteJSONResponse(w, r, http.StatusNotImplemented, &jsonError{
|
||||||
Message: aghos.Unsupported("dhcp").Error(),
|
Message: aghos.Unsupported("dhcp").Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,7 +182,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
|
aghhttp.WriteJSONResponseOK(w, r, s.accessListJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAccessSet checks the internal accessListJSON lists. To search for
|
// validateAccessSet checks the internal accessListJSON lists. To search for
|
||||||
|
|
|
@ -169,7 +169,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||||
// handleGetConfig handles requests to the GET /control/dns_info endpoint.
|
// handleGetConfig handles requests to the GET /control/dns_info endpoint.
|
||||||
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := s.getDNSConfig()
|
resp := s.getDNSConfig()
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
||||||
|
@ -758,7 +758,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, result)
|
aghhttp.WriteJSONResponseOK(w, r, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
|
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
|
||||||
|
|
|
@ -50,10 +50,10 @@ func initBlockedServices() {
|
||||||
// BlockedServices is the configuration of blocked services.
|
// BlockedServices is the configuration of blocked services.
|
||||||
type BlockedServices struct {
|
type BlockedServices struct {
|
||||||
// Schedule is blocked services schedule for every day of the week.
|
// Schedule is blocked services schedule for every day of the week.
|
||||||
Schedule *schedule.Weekly `yaml:"schedule"`
|
Schedule *schedule.Weekly `json:"schedule" yaml:"schedule"`
|
||||||
|
|
||||||
// IDs is the names of blocked services.
|
// IDs is the names of blocked services.
|
||||||
IDs []string `yaml:"ids"`
|
IDs []string `json:"ids" yaml:"ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns a deep copy of blocked services.
|
// Clone returns a deep copy of blocked services.
|
||||||
|
@ -114,25 +114,33 @@ func (d *DNSFilter) ApplyBlockedServicesList(setts *Settings, list []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesIDs(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesIDs(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
|
aghhttp.WriteJSONResponseOK(w, r, serviceIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, struct {
|
aghhttp.WriteJSONResponseOK(w, r, struct {
|
||||||
BlockedServices []blockedService `json:"blocked_services"`
|
BlockedServices []blockedService `json:"blocked_services"`
|
||||||
}{
|
}{
|
||||||
BlockedServices: blockedServices,
|
BlockedServices: blockedServices,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleBlockedServicesList is the handler for the GET
|
||||||
|
// /control/blocked_services/list HTTP API.
|
||||||
|
//
|
||||||
|
// Deprecated: Use handleBlockedServicesGet.
|
||||||
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||||
d.confLock.RLock()
|
d.confLock.RLock()
|
||||||
list := d.Config.BlockedServices.IDs
|
list := d.Config.BlockedServices.IDs
|
||||||
d.confLock.RUnlock()
|
d.confLock.RUnlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, list)
|
aghhttp.WriteJSONResponseOK(w, r, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleBlockedServicesSet is the handler for the POST
|
||||||
|
// /control/blocked_services/set HTTP API.
|
||||||
|
//
|
||||||
|
// Deprecated: Use handleBlockedServicesUpdate.
|
||||||
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
||||||
list := []string{}
|
list := []string{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&list)
|
err := json.NewDecoder(r.Body).Decode(&list)
|
||||||
|
@ -150,3 +158,51 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
|
||||||
|
|
||||||
d.Config.ConfigModified()
|
d.Config.ConfigModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleBlockedServicesGet is the handler for the GET
|
||||||
|
// /control/blocked_services/get HTTP API.
|
||||||
|
func (d *DNSFilter) handleBlockedServicesGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var bsvc *BlockedServices
|
||||||
|
func() {
|
||||||
|
d.confLock.RLock()
|
||||||
|
defer d.confLock.RUnlock()
|
||||||
|
|
||||||
|
bsvc = d.Config.BlockedServices.Clone()
|
||||||
|
}()
|
||||||
|
|
||||||
|
aghhttp.WriteJSONResponseOK(w, r, bsvc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBlockedServicesUpdate is the handler for the PUT
|
||||||
|
// /control/blocked_services/update HTTP API.
|
||||||
|
func (d *DNSFilter) handleBlockedServicesUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bsvc := &BlockedServices{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(bsvc)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bsvc.Validate()
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "validating: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bsvc.Schedule == nil {
|
||||||
|
bsvc.Schedule = schedule.EmptyWeekly()
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
d.confLock.Lock()
|
||||||
|
defer d.confLock.Unlock()
|
||||||
|
|
||||||
|
d.Config.BlockedServices = bsvc
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Debug("updated blocked services schedule: %d", len(bsvc.IDs))
|
||||||
|
|
||||||
|
d.Config.ConfigModified()
|
||||||
|
}
|
||||||
|
|
|
@ -301,7 +301,7 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type filterJSON struct {
|
type filterJSON struct {
|
||||||
|
@ -354,7 +354,7 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
|
||||||
resp.UserRules = d.UserRules
|
resp.UserRules = d.UserRules
|
||||||
d.filtersMu.RUnlock()
|
d.filtersMu.RUnlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set filtering configuration
|
// Set filtering configuration
|
||||||
|
@ -456,7 +456,7 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setProtectedBool sets the value of a boolean pointer under a lock. l must
|
// setProtectedBool sets the value of a boolean pointer under a lock. l must
|
||||||
|
@ -504,7 +504,7 @@ func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Requ
|
||||||
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
|
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleParentalEnable is the handler for the POST /control/parental/enable
|
// handleParentalEnable is the handler for the POST /control/parental/enable
|
||||||
|
@ -530,7 +530,7 @@ func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request)
|
||||||
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
|
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterFilteringHandlers - register handlers
|
// RegisterFilteringHandlers - register handlers
|
||||||
|
@ -560,9 +560,14 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
|
||||||
|
|
||||||
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
|
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
|
||||||
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)
|
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)
|
||||||
|
|
||||||
|
// Deprecated handlers.
|
||||||
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
|
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
|
||||||
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
|
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
|
||||||
|
|
||||||
|
registerHTTP(http.MethodGet, "/control/blocked_services/get", d.handleBlockedServicesGet)
|
||||||
|
registerHTTP(http.MethodPut, "/control/blocked_services/update", d.handleBlockedServicesUpdate)
|
||||||
|
|
||||||
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
|
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
|
||||||
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
|
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
|
||||||
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
|
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
|
||||||
|
|
|
@ -28,7 +28,7 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
d.confLock.Unlock()
|
d.confLock.Unlock()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, arr)
|
aghhttp.WriteJSONResponseOK(w, r, arr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Reques
|
||||||
resp = d.Config.SafeSearchConf
|
resp = d.Config.SafeSearchConf
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings
|
// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings
|
||||||
|
|
|
@ -34,8 +34,12 @@ type clientJSON struct {
|
||||||
WHOIS *whois.Info `json:"whois_info,omitempty"`
|
WHOIS *whois.Info `json:"whois_info,omitempty"`
|
||||||
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
|
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
|
||||||
|
|
||||||
|
// Schedule is blocked services schedule for every day of the week.
|
||||||
|
Schedule *schedule.Weekly `json:"blocked_services_schedule"`
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// BlockedServices is the names of blocked services.
|
||||||
BlockedServices []string `json:"blocked_services"`
|
BlockedServices []string `json:"blocked_services"`
|
||||||
IDs []string `json:"ids"`
|
IDs []string `json:"ids"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
|
@ -53,6 +57,34 @@ type clientJSON struct {
|
||||||
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
|
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copySettings returns a copy of specific settings from JSON or a previous
|
||||||
|
// client.
|
||||||
|
func (j *clientJSON) copySettings(
|
||||||
|
prev *Client,
|
||||||
|
) (weekly *schedule.Weekly, ignoreQueryLog, ignoreStatistics bool) {
|
||||||
|
if j.Schedule != nil {
|
||||||
|
weekly = j.Schedule.Clone()
|
||||||
|
} else if prev != nil && prev.BlockedServices != nil {
|
||||||
|
weekly = prev.BlockedServices.Schedule.Clone()
|
||||||
|
} else {
|
||||||
|
weekly = schedule.EmptyWeekly()
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.IgnoreQueryLog != aghalg.NBNull {
|
||||||
|
ignoreQueryLog = j.IgnoreQueryLog == aghalg.NBTrue
|
||||||
|
} else if prev != nil {
|
||||||
|
ignoreQueryLog = prev.IgnoreQueryLog
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.IgnoreStatistics != aghalg.NBNull {
|
||||||
|
ignoreStatistics = j.IgnoreStatistics == aghalg.NBTrue
|
||||||
|
} else if prev != nil {
|
||||||
|
ignoreStatistics = prev.IgnoreStatistics
|
||||||
|
}
|
||||||
|
|
||||||
|
return weekly, ignoreQueryLog, ignoreStatistics
|
||||||
|
}
|
||||||
|
|
||||||
type runtimeClientJSON struct {
|
type runtimeClientJSON struct {
|
||||||
WHOIS *whois.Info `json:"whois_info"`
|
WHOIS *whois.Info `json:"whois_info"`
|
||||||
|
|
||||||
|
@ -93,7 +125,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
||||||
|
|
||||||
data.Tags = clientTags
|
data.Tags = clientTags
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsonToClient converts JSON object to Client object.
|
// jsonToClient converts JSON object to Client object.
|
||||||
|
@ -119,9 +151,15 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
weekly := schedule.EmptyWeekly()
|
weekly, ignoreQueryLog, ignoreStatistics := cj.copySettings(prev)
|
||||||
if prev != nil {
|
|
||||||
weekly = prev.BlockedServices.Schedule.Clone()
|
bs := &filtering.BlockedServices{
|
||||||
|
Schedule: weekly,
|
||||||
|
IDs: cj.BlockedServices,
|
||||||
|
}
|
||||||
|
err = bs.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("validating blocked services: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = &Client{
|
c = &Client{
|
||||||
|
@ -129,10 +167,7 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
|
||||||
|
|
||||||
Name: cj.Name,
|
Name: cj.Name,
|
||||||
|
|
||||||
BlockedServices: &filtering.BlockedServices{
|
BlockedServices: bs,
|
||||||
Schedule: weekly,
|
|
||||||
IDs: cj.BlockedServices,
|
|
||||||
},
|
|
||||||
|
|
||||||
IDs: cj.IDs,
|
IDs: cj.IDs,
|
||||||
Tags: cj.Tags,
|
Tags: cj.Tags,
|
||||||
|
@ -143,18 +178,8 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
|
||||||
ParentalEnabled: cj.ParentalEnabled,
|
ParentalEnabled: cj.ParentalEnabled,
|
||||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||||
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||||
}
|
IgnoreQueryLog: ignoreQueryLog,
|
||||||
|
IgnoreStatistics: ignoreStatistics,
|
||||||
if cj.IgnoreQueryLog != aghalg.NBNull {
|
|
||||||
c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue
|
|
||||||
} else if prev != nil {
|
|
||||||
c.IgnoreQueryLog = prev.IgnoreQueryLog
|
|
||||||
}
|
|
||||||
|
|
||||||
if cj.IgnoreStatistics != aghalg.NBNull {
|
|
||||||
c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue
|
|
||||||
} else if prev != nil {
|
|
||||||
c.IgnoreStatistics = prev.IgnoreStatistics
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if safeSearchConf.Enabled {
|
if safeSearchConf.Enabled {
|
||||||
|
@ -191,6 +216,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
|
||||||
|
|
||||||
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
UseGlobalBlockedServices: !c.UseOwnBlockedServices,
|
||||||
|
|
||||||
|
Schedule: c.BlockedServices.Schedule,
|
||||||
BlockedServices: c.BlockedServices.IDs,
|
BlockedServices: c.BlockedServices.IDs,
|
||||||
|
|
||||||
Upstreams: c.Upstreams,
|
Upstreams: c.Upstreams,
|
||||||
|
@ -338,7 +364,7 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findRuntime looks up the IP in runtime and temporary storages, like
|
// findRuntime looks up the IP in runtime and temporary storages, like
|
||||||
|
|
|
@ -170,7 +170,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
resp.IsDHCPAvailable = Context.dhcpServer != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
|
|
@ -59,7 +59,7 @@ func (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Requ
|
||||||
data.Interfaces[iface.Name] = iface
|
data.Interfaces[iface.Name] = iface
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkConfReqEnt struct {
|
type checkConfReqEnt struct {
|
||||||
|
@ -190,7 +190,7 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque
|
||||||
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
|
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStaticIP - handles static IP request
|
// handleStaticIP - handles static IP request
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := &versionResponse{}
|
resp := &versionResponse{}
|
||||||
if web.conf.disableUpdate {
|
if web.conf.disableUpdate {
|
||||||
resp.Disabled = true
|
resp.Disabled = true
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ func (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestVersionInfo sets the VersionInfo field of resp if it can reach the
|
// requestVersionInfo sets the VersionInfo field of resp if it can reach the
|
||||||
|
|
|
@ -58,7 +58,7 @@ type languageJSON struct {
|
||||||
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("home: language is %s", config.Language)
|
log.Printf("home: language is %s", config.Language)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, &languageJSON{
|
aghhttp.WriteJSONResponseOK(w, r, &languageJSON{
|
||||||
Language: config.Language,
|
Language: config.Language,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlePutProfile is the handler for PUT /control/profile/update endpoint.
|
// handlePutProfile is the handler for PUT /control/profile/update endpoint.
|
||||||
|
|
|
@ -770,7 +770,7 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
||||||
data.PrivateKey = ""
|
data.PrivateKey = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerWebHandlers registers HTTP handlers for TLS configuration.
|
// registerWebHandlers registers HTTP handlers for TLS configuration.
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ type ReqPatchSettingsDNS struct {
|
||||||
BootstrapServers []string `json:"bootstrap_servers"`
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
UpstreamServers []string `json:"upstream_servers"`
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
|
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
|
||||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
|
||||||
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
|
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
|
||||||
UseDNS64 bool `json:"use_dns64"`
|
UseDNS64 bool `json:"use_dns64"`
|
||||||
}
|
}
|
||||||
|
@ -35,7 +36,7 @@ type HTTPAPIDNSSettings struct {
|
||||||
BootstrapServers []string `json:"bootstrap_servers"`
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
UpstreamServers []string `json:"upstream_servers"`
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
|
DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"`
|
||||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"`
|
||||||
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
|
BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"`
|
||||||
UseDNS64 bool `json:"use_dns64"`
|
UseDNS64 bool `json:"use_dns64"`
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("updating: %w", err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -79,17 +80,17 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
|
||||||
newSvc := svc.confMgr.DNS()
|
newSvc := svc.confMgr.DNS()
|
||||||
err = newSvc.Start()
|
err = newSvc.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIDNSSettings{
|
||||||
Addresses: newConf.Addresses,
|
Addresses: newConf.Addresses,
|
||||||
BootstrapServers: newConf.BootstrapServers,
|
BootstrapServers: newConf.BootstrapServers,
|
||||||
UpstreamServers: newConf.UpstreamServers,
|
UpstreamServers: newConf.UpstreamServers,
|
||||||
DNS64Prefixes: newConf.DNS64Prefixes,
|
DNS64Prefixes: newConf.DNS64Prefixes,
|
||||||
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
UpstreamTimeout: aghhttp.JSONDuration(newConf.UpstreamTimeout),
|
||||||
BootstrapPreferIPv6: newConf.BootstrapPreferIPv6,
|
BootstrapPreferIPv6: newConf.BootstrapPreferIPv6,
|
||||||
UseDNS64: newConf.UseDNS64,
|
UseDNS64: newConf.UseDNS64,
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
@ -24,7 +25,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
BootstrapServers: []string{"1.0.0.1"},
|
BootstrapServers: []string{"1.0.0.1"},
|
||||||
UpstreamServers: []string{"1.1.1.1"},
|
UpstreamServers: []string{"1.1.1.1"},
|
||||||
DNS64Prefixes: []netip.Prefix{netip.MustParsePrefix("1234::/64")},
|
DNS64Prefixes: []netip.Prefix{netip.MustParsePrefix("1234::/64")},
|
||||||
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
UpstreamTimeout: aghhttp.JSONDuration(2 * time.Second),
|
||||||
BootstrapPreferIPv6: true,
|
BootstrapPreferIPv6: true,
|
||||||
UseDNS64: true,
|
UseDNS64: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
@ -23,7 +24,7 @@ type ReqPatchSettingsHTTP struct {
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
Timeout JSONDuration `json:"timeout"`
|
Timeout aghhttp.JSONDuration `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||||
|
@ -33,7 +34,7 @@ type HTTPAPIHTTPSettings struct {
|
||||||
|
|
||||||
Addresses []netip.AddrPort `json:"addresses"`
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
Timeout JSONDuration `json:"timeout"`
|
Timeout aghhttp.JSONDuration `json:"timeout"`
|
||||||
ForceHTTPS bool `json:"force_https"`
|
ForceHTTPS bool `json:"force_https"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -65,10 +66,10 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||||
ForceHTTPS: svc.forceHTTPS,
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{
|
||||||
Addresses: newConf.Addresses,
|
Addresses: newConf.Addresses,
|
||||||
SecureAddresses: newConf.SecureAddresses,
|
SecureAddresses: newConf.SecureAddresses,
|
||||||
Timeout: JSONDuration(newConf.Timeout),
|
Timeout: aghhttp.JSONDuration(newConf.Timeout),
|
||||||
ForceHTTPS: newConf.ForceHTTPS,
|
ForceHTTPS: newConf.ForceHTTPS,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -20,7 +21,7 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||||
Timeout: websvc.JSONDuration(10 * time.Second),
|
Timeout: aghhttp.JSONDuration(10 * time.Second),
|
||||||
ForceHTTPS: false,
|
ForceHTTPS: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package websvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// All Settings Handlers
|
// All Settings Handlers
|
||||||
|
@ -25,20 +27,20 @@ func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request)
|
||||||
httpConf := webSvc.Config()
|
httpConf := webSvc.Config()
|
||||||
|
|
||||||
// TODO(a.garipov): Add all currently supported parameters.
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SettingsAll{
|
||||||
DNS: &HTTPAPIDNSSettings{
|
DNS: &HTTPAPIDNSSettings{
|
||||||
Addresses: dnsConf.Addresses,
|
Addresses: dnsConf.Addresses,
|
||||||
BootstrapServers: dnsConf.BootstrapServers,
|
BootstrapServers: dnsConf.BootstrapServers,
|
||||||
UpstreamServers: dnsConf.UpstreamServers,
|
UpstreamServers: dnsConf.UpstreamServers,
|
||||||
DNS64Prefixes: dnsConf.DNS64Prefixes,
|
DNS64Prefixes: dnsConf.DNS64Prefixes,
|
||||||
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
UpstreamTimeout: aghhttp.JSONDuration(dnsConf.UpstreamTimeout),
|
||||||
BootstrapPreferIPv6: dnsConf.BootstrapPreferIPv6,
|
BootstrapPreferIPv6: dnsConf.BootstrapPreferIPv6,
|
||||||
UseDNS64: dnsConf.UseDNS64,
|
UseDNS64: dnsConf.UseDNS64,
|
||||||
},
|
},
|
||||||
HTTP: &HTTPAPIHTTPSettings{
|
HTTP: &HTTPAPIHTTPSettings{
|
||||||
Addresses: httpConf.Addresses,
|
Addresses: httpConf.Addresses,
|
||||||
SecureAddresses: httpConf.SecureAddresses,
|
SecureAddresses: httpConf.SecureAddresses,
|
||||||
Timeout: JSONDuration(httpConf.Timeout),
|
Timeout: aghhttp.JSONDuration(httpConf.Timeout),
|
||||||
ForceHTTPS: httpConf.ForceHTTPS,
|
ForceHTTPS: httpConf.ForceHTTPS,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
@ -23,14 +24,14 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||||
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||||
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||||
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
UpstreamTimeout: aghhttp.JSONDuration(1 * time.Second),
|
||||||
BootstrapPreferIPv6: true,
|
BootstrapPreferIPv6: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
Timeout: websvc.JSONDuration(5 * time.Second),
|
Timeout: aghhttp.JSONDuration(5 * time.Second),
|
||||||
ForceHTTPS: true,
|
ForceHTTPS: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,20 +17,20 @@ type RespGetV1SystemInfo struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
NewVersion string `json:"new_version,omitempty"`
|
NewVersion string `json:"new_version,omitempty"`
|
||||||
Start JSONTime `json:"start"`
|
Start aghhttp.JSONTime `json:"start"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||||
// API.
|
// API.
|
||||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SystemInfo{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Channel: version.Channel(),
|
Channel: version.Channel(),
|
||||||
OS: runtime.GOOS,
|
OS: runtime.GOOS,
|
||||||
// TODO(a.garipov): Fill this when we have an updater.
|
// TODO(a.garipov): Fill this when we have an updater.
|
||||||
NewVersion: "",
|
NewVersion: "",
|
||||||
Start: JSONTime(svc.start),
|
Start: aghhttp.JSONTime(svc.start),
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
resp := entriesToJSON(entries, oldest, l.anonymizer.Load())
|
resp := entriesToJSON(entries, oldest, l.anonymizer.Load())
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
||||||
|
@ -118,7 +118,7 @@ func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
ivl = timeutil.Day * 90
|
ivl = timeutil.Day * 90
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, configJSON{
|
aghhttp.WriteJSONResponseOK(w, r, configJSON{
|
||||||
Enabled: aghalg.BoolToNullBool(l.conf.Enabled),
|
Enabled: aghalg.BoolToNullBool(l.conf.Enabled),
|
||||||
Interval: ivl.Hours() / 24,
|
Interval: ivl.Hours() / 24,
|
||||||
AnonymizeClientIP: aghalg.BoolToNullBool(l.conf.AnonymizeClientIP),
|
AnonymizeClientIP: aghalg.BoolToNullBool(l.conf.AnonymizeClientIP),
|
||||||
|
@ -143,7 +143,7 @@ func (l *queryLog) handleGetQueryLogConfig(w http.ResponseWriter, r *http.Reques
|
||||||
|
|
||||||
slices.Sort(resp.Ignored)
|
slices.Sort(resp.Ignored)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
package schedule
|
package schedule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
@ -50,6 +52,10 @@ func FullWeekly() (w *Weekly) {
|
||||||
|
|
||||||
// Clone returns a deep copy of a weekly.
|
// Clone returns a deep copy of a weekly.
|
||||||
func (w *Weekly) Clone() (c *Weekly) {
|
func (w *Weekly) Clone() (c *Weekly) {
|
||||||
|
if w == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: Do not use time.LoadLocation, because the results will be
|
// NOTE: Do not use time.LoadLocation, because the results will be
|
||||||
// different on time zone database update.
|
// different on time zone database update.
|
||||||
return &Weekly{
|
return &Weekly{
|
||||||
|
@ -75,12 +81,62 @@ func (w *Weekly) Contains(t time.Time) (ok bool) {
|
||||||
return dr.contains(offset)
|
return dr.contains(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*Weekly)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the [json.Unmarshaler] interface for *Weekly.
|
||||||
|
func (w *Weekly) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
conf := &weeklyConfigJSON{}
|
||||||
|
err = json.Unmarshal(data, conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
weekly := Weekly{}
|
||||||
|
|
||||||
|
weekly.location, err = time.LoadLocation(conf.TimeZone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
days := []*dayConfigJSON{
|
||||||
|
time.Sunday: conf.Sunday,
|
||||||
|
time.Monday: conf.Monday,
|
||||||
|
time.Tuesday: conf.Tuesday,
|
||||||
|
time.Wednesday: conf.Wednesday,
|
||||||
|
time.Thursday: conf.Thursday,
|
||||||
|
time.Friday: conf.Friday,
|
||||||
|
time.Saturday: conf.Saturday,
|
||||||
|
}
|
||||||
|
for i, d := range days {
|
||||||
|
var r dayRange
|
||||||
|
|
||||||
|
if d != nil {
|
||||||
|
r = dayRange{
|
||||||
|
start: time.Duration(d.Start),
|
||||||
|
end: time.Duration(d.End),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.validate(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
weekly.days[i] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
*w = weekly
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ yaml.Unmarshaler = (*Weekly)(nil)
|
var _ yaml.Unmarshaler = (*Weekly)(nil)
|
||||||
|
|
||||||
// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.
|
// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.
|
||||||
func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
|
func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
conf := &weeklyConfig{}
|
conf := &weeklyConfigYAML{}
|
||||||
|
|
||||||
err = value.Decode(conf)
|
err = value.Decode(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -96,7 +152,7 @@ func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
days := []dayConfig{
|
days := []dayConfigYAML{
|
||||||
time.Sunday: conf.Sunday,
|
time.Sunday: conf.Sunday,
|
||||||
time.Monday: conf.Monday,
|
time.Monday: conf.Monday,
|
||||||
time.Tuesday: conf.Tuesday,
|
time.Tuesday: conf.Tuesday,
|
||||||
|
@ -124,24 +180,24 @@ func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// weeklyConfig is the YAML configuration structure of Weekly.
|
// weeklyConfigYAML is the YAML configuration structure of Weekly.
|
||||||
type weeklyConfig struct {
|
type weeklyConfigYAML struct {
|
||||||
// TimeZone is the local time zone.
|
// TimeZone is the local time zone.
|
||||||
TimeZone string `yaml:"time_zone"`
|
TimeZone string `yaml:"time_zone"`
|
||||||
|
|
||||||
// Days of the week.
|
// Days of the week.
|
||||||
|
|
||||||
Sunday dayConfig `yaml:"sun,omitempty"`
|
Sunday dayConfigYAML `yaml:"sun,omitempty"`
|
||||||
Monday dayConfig `yaml:"mon,omitempty"`
|
Monday dayConfigYAML `yaml:"mon,omitempty"`
|
||||||
Tuesday dayConfig `yaml:"tue,omitempty"`
|
Tuesday dayConfigYAML `yaml:"tue,omitempty"`
|
||||||
Wednesday dayConfig `yaml:"wed,omitempty"`
|
Wednesday dayConfigYAML `yaml:"wed,omitempty"`
|
||||||
Thursday dayConfig `yaml:"thu,omitempty"`
|
Thursday dayConfigYAML `yaml:"thu,omitempty"`
|
||||||
Friday dayConfig `yaml:"fri,omitempty"`
|
Friday dayConfigYAML `yaml:"fri,omitempty"`
|
||||||
Saturday dayConfig `yaml:"sat,omitempty"`
|
Saturday dayConfigYAML `yaml:"sat,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// dayConfig is the YAML configuration structure of dayRange.
|
// dayConfigYAML is the YAML configuration structure of dayRange.
|
||||||
type dayConfig struct {
|
type dayConfigYAML struct {
|
||||||
Start timeutil.Duration `yaml:"start"`
|
Start timeutil.Duration `yaml:"start"`
|
||||||
End timeutil.Duration `yaml:"end"`
|
End timeutil.Duration `yaml:"end"`
|
||||||
}
|
}
|
||||||
|
@ -172,38 +228,57 @@ func (w *Weekly) validate(r dayRange) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = (*Weekly)(nil)
|
||||||
|
|
||||||
|
// MarshalJSON implements the [json.Marshaler] interface for *Weekly.
|
||||||
|
func (w *Weekly) MarshalJSON() (data []byte, err error) {
|
||||||
|
c := &weeklyConfigJSON{
|
||||||
|
TimeZone: w.location.String(),
|
||||||
|
Sunday: w.days[time.Sunday].toDayConfigJSON(),
|
||||||
|
Monday: w.days[time.Monday].toDayConfigJSON(),
|
||||||
|
Tuesday: w.days[time.Tuesday].toDayConfigJSON(),
|
||||||
|
Wednesday: w.days[time.Wednesday].toDayConfigJSON(),
|
||||||
|
Thursday: w.days[time.Thursday].toDayConfigJSON(),
|
||||||
|
Friday: w.days[time.Friday].toDayConfigJSON(),
|
||||||
|
Saturday: w.days[time.Saturday].toDayConfigJSON(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(c)
|
||||||
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ yaml.Marshaler = (*Weekly)(nil)
|
var _ yaml.Marshaler = (*Weekly)(nil)
|
||||||
|
|
||||||
// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.
|
// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.
|
||||||
func (w *Weekly) MarshalYAML() (v any, err error) {
|
func (w *Weekly) MarshalYAML() (v any, err error) {
|
||||||
return weeklyConfig{
|
return weeklyConfigYAML{
|
||||||
TimeZone: w.location.String(),
|
TimeZone: w.location.String(),
|
||||||
Sunday: dayConfig{
|
Sunday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Sunday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Sunday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Sunday].end},
|
End: timeutil.Duration{Duration: w.days[time.Sunday].end},
|
||||||
},
|
},
|
||||||
Monday: dayConfig{
|
Monday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Monday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Monday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Monday].end},
|
End: timeutil.Duration{Duration: w.days[time.Monday].end},
|
||||||
},
|
},
|
||||||
Tuesday: dayConfig{
|
Tuesday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Tuesday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Tuesday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Tuesday].end},
|
End: timeutil.Duration{Duration: w.days[time.Tuesday].end},
|
||||||
},
|
},
|
||||||
Wednesday: dayConfig{
|
Wednesday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Wednesday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Wednesday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Wednesday].end},
|
End: timeutil.Duration{Duration: w.days[time.Wednesday].end},
|
||||||
},
|
},
|
||||||
Thursday: dayConfig{
|
Thursday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Thursday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Thursday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Thursday].end},
|
End: timeutil.Duration{Duration: w.days[time.Thursday].end},
|
||||||
},
|
},
|
||||||
Friday: dayConfig{
|
Friday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Friday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Friday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Friday].end},
|
End: timeutil.Duration{Duration: w.days[time.Friday].end},
|
||||||
},
|
},
|
||||||
Saturday: dayConfig{
|
Saturday: dayConfigYAML{
|
||||||
Start: timeutil.Duration{Duration: w.days[time.Saturday].start},
|
Start: timeutil.Duration{Duration: w.days[time.Saturday].start},
|
||||||
End: timeutil.Duration{Duration: w.days[time.Saturday].end},
|
End: timeutil.Duration{Duration: w.days[time.Saturday].end},
|
||||||
},
|
},
|
||||||
|
@ -248,3 +323,38 @@ func (r dayRange) validate() (err error) {
|
||||||
func (r *dayRange) contains(offset time.Duration) (ok bool) {
|
func (r *dayRange) contains(offset time.Duration) (ok bool) {
|
||||||
return r.start <= offset && offset < r.end
|
return r.start <= offset && offset < r.end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toDayConfigJSON returns nil if the day range is empty, otherwise returns
|
||||||
|
// initialized JSON configuration of the day range.
|
||||||
|
func (r dayRange) toDayConfigJSON() (j *dayConfigJSON) {
|
||||||
|
if (r == dayRange{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dayConfigJSON{
|
||||||
|
Start: aghhttp.JSONDuration(r.start),
|
||||||
|
End: aghhttp.JSONDuration(r.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// weeklyConfigJSON is the JSON configuration structure of Weekly.
|
||||||
|
type weeklyConfigJSON struct {
|
||||||
|
// TimeZone is the local time zone.
|
||||||
|
TimeZone string `json:"time_zone"`
|
||||||
|
|
||||||
|
// Days of the week.
|
||||||
|
|
||||||
|
Sunday *dayConfigJSON `json:"sun,omitempty"`
|
||||||
|
Monday *dayConfigJSON `json:"mon,omitempty"`
|
||||||
|
Tuesday *dayConfigJSON `json:"tue,omitempty"`
|
||||||
|
Wednesday *dayConfigJSON `json:"wed,omitempty"`
|
||||||
|
Thursday *dayConfigJSON `json:"thu,omitempty"`
|
||||||
|
Friday *dayConfigJSON `json:"fri,omitempty"`
|
||||||
|
Saturday *dayConfigJSON `json:"sat,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// dayConfigJSON is the JSON configuration structure of dayRange.
|
||||||
|
type dayConfigJSON struct {
|
||||||
|
Start aghhttp.JSONDuration `json:"start"`
|
||||||
|
End aghhttp.JSONDuration `json:"end"`
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package schedule
|
package schedule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -122,7 +123,7 @@ func TestWeekly_Contains(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const brusselsSunday = `
|
const brusselsSundayYAML = `
|
||||||
sun:
|
sun:
|
||||||
start: 12h
|
start: 12h
|
||||||
end: 14h
|
end: 14h
|
||||||
|
@ -179,7 +180,7 @@ yaml: "bad"
|
||||||
}, {
|
}, {
|
||||||
name: "brussels_sunday",
|
name: "brussels_sunday",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
data: []byte(brusselsSunday),
|
data: []byte(brusselsSundayYAML),
|
||||||
want: brusselsWeekly,
|
want: brusselsWeekly,
|
||||||
}, {
|
}, {
|
||||||
name: "start_equal_end",
|
name: "start_equal_end",
|
||||||
|
@ -240,7 +241,7 @@ func TestWeekly_MarshalYAML(t *testing.T) {
|
||||||
want: &Weekly{},
|
want: &Weekly{},
|
||||||
}, {
|
}, {
|
||||||
name: "brussels_sunday",
|
name: "brussels_sunday",
|
||||||
data: []byte(brusselsSunday),
|
data: []byte(brusselsSundayYAML),
|
||||||
want: brusselsWeekly,
|
want: brusselsWeekly,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -369,3 +370,142 @@ func TestDayRange_Validate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const brusselsSundayJSON = `{
|
||||||
|
"sun": {
|
||||||
|
"end": 50400000,
|
||||||
|
"start": 43200000
|
||||||
|
},
|
||||||
|
"time_zone": "Europe/Brussels"
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestWeekly_UnmarshalJSON(t *testing.T) {
|
||||||
|
const (
|
||||||
|
sameTime = `{
|
||||||
|
"sun": {
|
||||||
|
"end": 32400000,
|
||||||
|
"start": 32400000
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
negativeStart = `{
|
||||||
|
"sun": {
|
||||||
|
"end": 3600000,
|
||||||
|
"start": -3600000
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
badTZ = `{
|
||||||
|
"time_zone": "bad_timezone"
|
||||||
|
}`
|
||||||
|
badJSON = `{
|
||||||
|
"bad": "json",
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
brusseltsTZ, err := time.LoadLocation("Europe/Brussels")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
brusselsWeekly := &Weekly{
|
||||||
|
days: [7]dayRange{{
|
||||||
|
start: time.Hour * 12,
|
||||||
|
end: time.Hour * 14,
|
||||||
|
}},
|
||||||
|
location: brusseltsTZ,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
data []byte
|
||||||
|
want *Weekly
|
||||||
|
}{{
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "unexpected end of JSON input",
|
||||||
|
data: []byte(""),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "null",
|
||||||
|
wantErrMsg: "",
|
||||||
|
data: []byte("null"),
|
||||||
|
want: &Weekly{location: time.UTC},
|
||||||
|
}, {
|
||||||
|
name: "brussels_sunday",
|
||||||
|
wantErrMsg: "",
|
||||||
|
data: []byte(brusselsSundayJSON),
|
||||||
|
want: brusselsWeekly,
|
||||||
|
}, {
|
||||||
|
name: "start_equal_end",
|
||||||
|
wantErrMsg: "weekday Sunday: bad day range: start 9h0m0s is greater or equal to end 9h0m0s",
|
||||||
|
data: []byte(sameTime),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "start_negative",
|
||||||
|
wantErrMsg: "weekday Sunday: bad day range: start -1h0m0s is negative",
|
||||||
|
data: []byte(negativeStart),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "bad_time_zone",
|
||||||
|
wantErrMsg: "unknown time zone bad_timezone",
|
||||||
|
data: []byte(badTZ),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "bad_json",
|
||||||
|
wantErrMsg: "invalid character '}' looking for beginning of object key string",
|
||||||
|
data: []byte(badJSON),
|
||||||
|
want: &Weekly{},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
w := &Weekly{}
|
||||||
|
err = json.Unmarshal(tc.data, w)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekly_MarshalJSON(t *testing.T) {
|
||||||
|
brusselsTZ, err := time.LoadLocation("Europe/Brussels")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
brusselsWeekly := &Weekly{
|
||||||
|
days: [7]dayRange{time.Sunday: {
|
||||||
|
start: time.Hour * 12,
|
||||||
|
end: time.Hour * 14,
|
||||||
|
}},
|
||||||
|
location: brusselsTZ,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
want *Weekly
|
||||||
|
}{{
|
||||||
|
name: "empty",
|
||||||
|
data: []byte(""),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "null",
|
||||||
|
data: []byte("null"),
|
||||||
|
want: &Weekly{},
|
||||||
|
}, {
|
||||||
|
name: "brussels_sunday",
|
||||||
|
data: []byte(brusselsSundayJSON),
|
||||||
|
want: brusselsWeekly,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data, err = json.Marshal(brusselsWeekly)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w := &Weekly{}
|
||||||
|
err = json.Unmarshal(data, w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, brusselsWeekly, w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// configResp is the response to the GET /control/stats_info.
|
// configResp is the response to the GET /control/stats_info.
|
||||||
|
@ -122,7 +122,7 @@ func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
resp.IntervalDays = 0
|
resp.IntervalDays = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetStatsConfig is the handler for the GET /control/stats/config HTTP
|
// handleGetStatsConfig is the handler for the GET /control/stats/config HTTP
|
||||||
|
@ -142,7 +142,7 @@ func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
slices.Sort(resp.Ignored)
|
slices.Sort(resp.Ignored)
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStatsConfig is the handler for the POST /control/stats_config HTTP API.
|
// handleStatsConfig is the handler for the POST /control/stats_config HTTP API.
|
||||||
|
|
|
@ -4,6 +4,86 @@
|
||||||
|
|
||||||
## v0.108.0: API changes
|
## v0.108.0: API changes
|
||||||
|
|
||||||
|
## v0.107.37: API changes
|
||||||
|
|
||||||
|
### Deprecated blocked services APIs
|
||||||
|
|
||||||
|
* The `GET /control/blocked_services/list` HTTP API; use the new `GET
|
||||||
|
/control/blocked_services/get` API instead.
|
||||||
|
|
||||||
|
* The `POST /control/blocked_services/set` HTTP API; use the new `PUT
|
||||||
|
/control/blocked_services/update` API instead.
|
||||||
|
|
||||||
|
### New blocked services APIs
|
||||||
|
|
||||||
|
* The new `GET /control/blocked_services/get` HTTP API.
|
||||||
|
|
||||||
|
* The new `PUT /control/blocked_services/update` HTTP API allows config
|
||||||
|
updates.
|
||||||
|
|
||||||
|
These APIs accept and return a JSON object with the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schedule": {
|
||||||
|
"time_zone": "Local",
|
||||||
|
"sun": {
|
||||||
|
"start": 46800000,
|
||||||
|
"end": 82800000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ids": [
|
||||||
|
"vk"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/control/clients` HTTP APIs
|
||||||
|
|
||||||
|
The following HTTP APIs have been changed:
|
||||||
|
|
||||||
|
* `GET /control/clients`;
|
||||||
|
* `GET /control/clients/find?ip0=...&ip1=...&ip2=...`;
|
||||||
|
* `POST /control/clients/add`;
|
||||||
|
* `POST /control/clients/update`;
|
||||||
|
|
||||||
|
The new field `blocked_services_schedule` has been added to JSON objects. It
|
||||||
|
has the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_zone": "Local",
|
||||||
|
"sun": {
|
||||||
|
"start": 0,
|
||||||
|
"end": 86400000
|
||||||
|
},
|
||||||
|
"mon": {
|
||||||
|
"start": 60000,
|
||||||
|
"end": 82800000
|
||||||
|
},
|
||||||
|
"thu": {
|
||||||
|
"start": 120000,
|
||||||
|
"end": 79200000
|
||||||
|
},
|
||||||
|
"tue": {
|
||||||
|
"start": 180000,
|
||||||
|
"end": 75600000
|
||||||
|
},
|
||||||
|
"wed": {
|
||||||
|
"start": 240000,
|
||||||
|
"end": 72000000
|
||||||
|
},
|
||||||
|
"fri": {
|
||||||
|
"start": 300000,
|
||||||
|
"end": 68400000
|
||||||
|
},
|
||||||
|
"sat": {
|
||||||
|
"start": 360000,
|
||||||
|
"end": 64800000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## v0.107.36: API changes
|
## v0.107.36: API changes
|
||||||
|
|
||||||
### The new fields `"top_upstreams_responses"` and `"top_upstreams_avg_time"` in `Stats` object
|
### The new fields `"top_upstreams_responses"` and `"top_upstreams_avg_time"` in `Stats` object
|
||||||
|
|
|
@ -1001,6 +1001,9 @@
|
||||||
'$ref': '#/components/schemas/BlockedServicesAll'
|
'$ref': '#/components/schemas/BlockedServicesAll'
|
||||||
'/blocked_services/list':
|
'/blocked_services/list':
|
||||||
'get':
|
'get':
|
||||||
|
'deprecated': true
|
||||||
|
'description': >
|
||||||
|
Deprecated: Use `GET /blocked_services/get` instead.
|
||||||
'tags':
|
'tags':
|
||||||
- 'blocked_services'
|
- 'blocked_services'
|
||||||
'operationId': 'blockedServicesList'
|
'operationId': 'blockedServicesList'
|
||||||
|
@ -1014,6 +1017,9 @@
|
||||||
'$ref': '#/components/schemas/BlockedServicesArray'
|
'$ref': '#/components/schemas/BlockedServicesArray'
|
||||||
'/blocked_services/set':
|
'/blocked_services/set':
|
||||||
'post':
|
'post':
|
||||||
|
'deprecated': true
|
||||||
|
'description': >
|
||||||
|
Deprecated: Use `PUT /blocked_services/update` instead.
|
||||||
'tags':
|
'tags':
|
||||||
- 'blocked_services'
|
- 'blocked_services'
|
||||||
'operationId': 'blockedServicesSet'
|
'operationId': 'blockedServicesSet'
|
||||||
|
@ -1026,6 +1032,34 @@
|
||||||
'responses':
|
'responses':
|
||||||
'200':
|
'200':
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
|
'/blocked_services/get':
|
||||||
|
'get':
|
||||||
|
'tags':
|
||||||
|
- 'blocked_services'
|
||||||
|
'operationId': 'blockedServicesSchedule'
|
||||||
|
'summary': 'Get blocked services'
|
||||||
|
'responses':
|
||||||
|
'200':
|
||||||
|
'description': 'OK.'
|
||||||
|
'content':
|
||||||
|
'application/json':
|
||||||
|
'schema':
|
||||||
|
'$ref': '#/components/schemas/BlockedServicesSchedule'
|
||||||
|
'/blocked_services/update':
|
||||||
|
'put':
|
||||||
|
'tags':
|
||||||
|
- 'blocked_services'
|
||||||
|
'operationId': 'blockedServicesScheduleUpdate'
|
||||||
|
'summary': 'Update blocked services'
|
||||||
|
'requestBody':
|
||||||
|
'content':
|
||||||
|
'application/json':
|
||||||
|
'schema':
|
||||||
|
'$ref': '#/components/schemas/BlockedServicesSchedule'
|
||||||
|
'required': true
|
||||||
|
'responses':
|
||||||
|
'200':
|
||||||
|
'description': 'OK.'
|
||||||
'/rewrite/list':
|
'/rewrite/list':
|
||||||
'get':
|
'get':
|
||||||
'tags':
|
'tags':
|
||||||
|
@ -2485,6 +2519,54 @@
|
||||||
'type': 'boolean'
|
'type': 'boolean'
|
||||||
'youtube':
|
'youtube':
|
||||||
'type': 'boolean'
|
'type': 'boolean'
|
||||||
|
'Schedule':
|
||||||
|
'type': 'object'
|
||||||
|
'description': >
|
||||||
|
Sets periods of inactivity for filtering blocked services. The
|
||||||
|
schedule contains 7 days (Sunday to Saturday) and a time zone.
|
||||||
|
'properties':
|
||||||
|
'time_zone':
|
||||||
|
'description': >
|
||||||
|
Time zone name according to IANA time zone database. For example
|
||||||
|
`Europe/Brussels`. `Local` represents the system's local time
|
||||||
|
zone.
|
||||||
|
'type': 'string'
|
||||||
|
'sun':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'mon':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'tue':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'wed':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'thu':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'fri':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'sat':
|
||||||
|
'$ref': '#/components/schemas/DayRange'
|
||||||
|
'DayRange':
|
||||||
|
'type': 'object'
|
||||||
|
'description': >
|
||||||
|
The single interval within a day. It begins at the `start` and ends
|
||||||
|
before the `end`.
|
||||||
|
'properties':
|
||||||
|
'start':
|
||||||
|
'type': 'number'
|
||||||
|
'description': >
|
||||||
|
The number of milliseconds elapsed from the start of a day. It
|
||||||
|
must be less than `end` and is expected to be rounded to minutes.
|
||||||
|
So the maximum value is `86340000` (23 hours and 59 minutes).
|
||||||
|
'minimum': 0
|
||||||
|
'maximum': 86340000
|
||||||
|
'end':
|
||||||
|
'type': 'number'
|
||||||
|
'description': >
|
||||||
|
The number of milliseconds elapsed from the start of a day. It is
|
||||||
|
expected to be rounded to minutes. The maximum value is `86400000`
|
||||||
|
(24 hours).
|
||||||
|
'minimum': 0
|
||||||
|
'maximum': 86400000
|
||||||
'Client':
|
'Client':
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
'description': 'Client information.'
|
'description': 'Client information.'
|
||||||
|
@ -2513,6 +2595,8 @@
|
||||||
'$ref': '#/components/schemas/SafeSearchConfig'
|
'$ref': '#/components/schemas/SafeSearchConfig'
|
||||||
'use_global_blocked_services':
|
'use_global_blocked_services':
|
||||||
'type': 'boolean'
|
'type': 'boolean'
|
||||||
|
'blocked_services_schedule':
|
||||||
|
'$ref': '#/components/schemas/Schedule'
|
||||||
'blocked_services':
|
'blocked_services':
|
||||||
'type': 'array'
|
'type': 'array'
|
||||||
'items':
|
'items':
|
||||||
|
@ -2793,6 +2877,17 @@
|
||||||
- 'name'
|
- 'name'
|
||||||
- 'rules'
|
- 'rules'
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
|
'BlockedServicesSchedule':
|
||||||
|
'type': 'object'
|
||||||
|
'properties':
|
||||||
|
'schedule':
|
||||||
|
'$ref': '#/components/schemas/Schedule'
|
||||||
|
'ids':
|
||||||
|
'description': >
|
||||||
|
The names of the blocked services.
|
||||||
|
'type': 'array'
|
||||||
|
'items':
|
||||||
|
'type': 'string'
|
||||||
'CheckConfigRequest':
|
'CheckConfigRequest':
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
'description': 'Configuration to be checked'
|
'description': 'Configuration to be checked'
|
||||||
|
|
Loading…
Reference in New Issue