feat(cloudron): add tirreno package artifacts

- Add CloudronStack/output/CloudronPackages-Artifacts/tirreno/ directory and its contents
- Includes package manifest, Dockerfile, source code, documentation, and build artifacts
- Add tirreno-1761840148.tar.gz as a build artifact
- Add tirreno-cloudron-package-1761841304.tar.gz as the Cloudron package
- Include all necessary files for the tirreno Cloudron package

This adds the complete tirreno Cloudron package artifacts to the repository.
This commit is contained in:
2025-10-30 11:43:06 -05:00
parent 0ce353ea9d
commit 91d52d2de5
1692 changed files with 202851 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import {ApiPage} from '../pages/Api.js';
new ApiPage();

View File

@@ -0,0 +1,3 @@
import {BlacklistPage} from '../pages/Blacklist.js';
new BlacklistPage();

View File

@@ -0,0 +1,3 @@
import {BotPage} from '../pages/Bot.js';
new BotPage();

View File

@@ -0,0 +1,3 @@
import {BotsPage} from '../pages/Bots.js';
new BotsPage();

View File

@@ -0,0 +1,3 @@
import {CountriesPage} from '../pages/Countries.js';
new CountriesPage();

View File

@@ -0,0 +1,3 @@
import {CountryPage} from '../pages/Country.js';
new CountryPage();

View File

@@ -0,0 +1,3 @@
import {DashboardPage} from '../pages/Dashboard.js';
new DashboardPage();

View File

@@ -0,0 +1,3 @@
import {DomainPage} from '../pages/Domain.js';
new DomainPage();

View File

@@ -0,0 +1,3 @@
import {DomainsPage} from '../pages/Domains.js';
new DomainsPage();

View File

@@ -0,0 +1,3 @@
import {EventsPage} from '../pages/Events.js';
new EventsPage();

View File

@@ -0,0 +1,3 @@
import {IpPage} from '../pages/Ip.js';
new IpPage();

View File

@@ -0,0 +1,3 @@
import {IpsPage} from '../pages/Ips.js';
new IpsPage();

View File

@@ -0,0 +1,3 @@
import {IspPage} from '../pages/Isp.js';
new IspPage();

View File

@@ -0,0 +1,3 @@
import {IspsPage} from '../pages/Isps.js';
new IspsPage();

View File

@@ -0,0 +1,3 @@
import {LogbookPage} from '../pages/Logbook.js';
new LogbookPage();

View File

@@ -0,0 +1,3 @@
import {ManualCheckPage} from '../pages/ManualCheck.js';
new ManualCheckPage();

View File

@@ -0,0 +1,3 @@
import {ResourcePage} from '../pages/Resource.js';
new ResourcePage();

View File

@@ -0,0 +1,3 @@
import {ResourcesPage} from '../pages/Resources.js';
new ResourcesPage();

View File

@@ -0,0 +1,3 @@
import {ReviewQueuePage} from '../pages/ReviewQueue.js';
new ReviewQueuePage();

View File

@@ -0,0 +1,3 @@
import {RulesPage} from '../pages/Rules.js';
new RulesPage();

View File

@@ -0,0 +1,3 @@
import {SettingsPage} from '../pages/Settings.js';
new SettingsPage();

View File

@@ -0,0 +1,3 @@
import {UserPage} from '../pages/User.js';
new UserPage();

View File

@@ -0,0 +1,3 @@
import {UsersPage} from '../pages/Users.js';
new UsersPage();

View File

@@ -0,0 +1,3 @@
import {WatchlistPage} from '../pages/Watchlist.js';
new WatchlistPage();

View File

@@ -0,0 +1,20 @@
(function() {
const emulateButtonClick = () => {
let button = document.getElementById('submit-button');
button.classList.add('clicked');
button.click();
setTimeout(() => {
button.classList.remove('clicked');
}, 200);
};
const onEnterKeyDown = e => {
if (e.keyCode === 13) {
emulateButtonClick();
}
};
document.addEventListener('keydown', onEnterKeyDown, false);
})();

View File

@@ -0,0 +1,65 @@
import {BasePage} from './Base.js';
import {UsageStatsGrid} from '../parts/grid/UsageStats.js?v=2';
import {EnrichAllPopUp} from '../parts/EnrichAllPopUp.js?v=2';
export class ApiPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
const onSelectChange = this.onSelectChange.bind(this);
this.versionSelect.addEventListener('change', onSelectChange, false);
const onTextAreaClick = this.onTextAreaClick.bind(this);
this.snippetTextareas.forEach(txt => txt.addEventListener('click', onTextAreaClick, false));
const gridParams = {
url: '/admin/loadUsageStats',
tableId: 'usage-stats-table',
tileId: 'totalUsageStats',
isSortable: false,
getParams: function() {
return {};
}
};
new UsageStatsGrid(gridParams);
new EnrichAllPopUp();
}
onTextAreaClick(e) {
const txt = e.target;
const value = txt.value;
txt.setSelectionRange(0, value.length);
}
onSelectChange(e) {
const value = event.target.value;
this.snippetTextareas.forEach(txt => {
const container = txt.closest('div');
const isHidden = container.classList.contains('is-hidden');
if (!isHidden) {
container.classList.add('is-hidden');
}
});
const textarea = document.getElementById(value);
textarea.closest('div').classList.remove('is-hidden');
}
get versionSelect() {
return document.querySelector('select[name=version]');
}
get snippetTextareas() {
return document.querySelectorAll('.code-snippet');
}
}

View File

@@ -0,0 +1,168 @@
import {SearchLine} from '../parts/SearchLine.js';
import {Tooltip} from '../parts/Tooltip.js?v=2';
export class BasePage {
constructor(name, single = false) {
this.initCommonUi();
if (name) {
this.name = name;
if (single) {
let path = (this.name !== 'user') ? this.name : 'id';
this.id = parseInt(window.location.pathname.replace('/' + path + '/', ''), 10);
const key = this.name + 'Id';
this.getParams = () => {
return {[key]: this.id};
};
}
}
}
initCommonUi() {
new SearchLine();
document.addEventListener('keyup', e => {
if (e.key !== '/' || e.ctrlKey || e.metaKey) return;
if (/^(?:input|textarea|select|button)$/i.test(e.target.tagName)) return;
e.preventDefault();
document.getElementById('auto-complete').focus();
});
const initTooltip = this.initTooltip;
if (initTooltip) {
Tooltip.init();
}
const closeNotificationButtons = this.closeNotificationButtons;
if (closeNotificationButtons && closeNotificationButtons.length) {
const onCloseNotificationButtonClick = this.onCloseNotificationButtonClick.bind(this);
closeNotificationButtons.forEach(
button => button.addEventListener('click', onCloseNotificationButtonClick, false)
);
}
this.procedureNotifications.forEach(notification => {
const btn = notification.querySelector('.delete');
if (!btn) return;
btn.addEventListener('click', () => {notification.remove();}, false);
});
}
onCloseNotificationButtonClick() {
const notification = event.target.closest('.notification.system');
if (notification) {
notification.remove();
}
}
getDevicesGridParams() {
return {
url: '/admin/loadDevices',
tileId: 'totalDevices',
tableId: 'devices-table',
panelType: 'device',
isSortable: false,
getParams: this.getParams,
};
}
getIpsGridParams() {
return {
url: '/admin/loadIps',
tileId: 'totalIps',
tableId: 'ips-table',
isSortable: false,
orderByLastseen: true,
getParams: this.getParams,
};
}
getEventsGridParams() {
return {
url: '/admin/loadEvents',
tileId: 'totalEvents',
tableId: 'user-events-table',
panelType: 'event',
isSortable: false,
getParams: this.getParams,
};
}
getUsersGridParams() {
return {
url: '/admin/loadUsers',
tileId: 'totalUsers',
tableId: 'users-table',
isSortable: false,
getParams: this.getParams,
};
}
getIspsGridParams() {
return {
url: '/admin/loadIsps',
tableId: 'isps-table',
isSortable: false,
getParams: this.getParams,
};
}
getMapParams() {
return {
getParams: this.getParams,
tooltipString: 'event',
tooltipField: 'total_visit',
tileId: 'totalCountries',
};
}
getBarChartParams() {
return {
getParams: () => ({
mode: this.name,
id: this.id,
}),
};
}
getChartParams(datesFilter, searchFilter) {
return {
getParams: () => {
const mode = this.name;
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {mode, dateRange, searchValue};
},
};
}
getSelfDetails() {
return {
getParams: this.getParams,
};
}
get initTooltip() {
return true;
}
get closeNotificationButtons() {
return document.querySelectorAll('.notification.system:not(.is-hidden) .delete');
}
get procedureNotifications() {
return document.querySelectorAll('#success-procedure-notification, #error-procedure-notification');
}
}

View File

@@ -0,0 +1,56 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {EntityTypeFilter} from '../parts/choices/EntityTypeFilter.js?v=2';
import {BlacklistGridActionButtons} from '../parts/BlacklistGridActionButtons.js?v=2';
import {BlacklistChart} from '../parts/chart/Blacklist.js?v=2';
import {BlacklistGrid} from '../parts/grid/Blacklist.js?v=2';
export class BlacklistPage extends BasePage {
constructor() {
super('blacklist');
this.tableId = 'blacklist-table';
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadBlacklist',
tileId: 'totalBlacklist',
tableId: 'blacklist-table',
dateRangeGrid: true,
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
},
};
if (document.getElementById('entity-type-selectors')) {
const entityTypeFilter = new EntityTypeFilter();
gridParams.choicesFilterEvents = [entityTypeFilter.getEventType()];
gridParams.getParams = function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
const entityTypeIds = entityTypeFilter.getValues();
return {dateRange, searchValue, entityTypeIds};
};
}
const chartParams = this.getChartParams(datesFilter, searchFilter);
new BlacklistChart(chartParams);
new BlacklistGrid(gridParams);
new BlacklistGridActionButtons(this.tableId);
}
}

View File

@@ -0,0 +1,44 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {Map} from '../parts/Map.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {BotTiles} from '../parts/details/BotTiles.js?v=2';
import {ReenrichmentButton} from '../parts/ReenrichmentButton.js?v=2';
export class BotPage extends BasePage {
constructor() {
super('bot', true);
this.initUi();
}
initUi() {
const usersGridParams = this.getUsersGridParams();
const eventsGridParams = this.getEventsGridParams();
const ipsGridParams = this.getIpsGridParams();
const mapParams = this.getMapParams();
const botDetailsTiles = this.getSelfDetails();
const chartParams = this.getBarChartParams();
new EventPanel();
new ReenrichmentButton();
const elements = [
[BotTiles, botDetailsTiles],
[Map, mapParams],
[IpsGrid, ipsGridParams],
[UsersGrid, usersGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,40 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {BotsChart} from '../parts/chart/Bots.js?v=2';
import {BotsGrid} from '../parts/grid/Bots.js?v=2';
export class BotsPage extends BasePage {
constructor() {
super('bots');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadBots',
// tileId: 'totalDevices',
tableId: 'bots-table',
dateRangeGrid: true,
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
},
};
const chartParams = this.getChartParams(datesFilter, searchFilter);
new BotsChart(chartParams);
new BotsGrid(gridParams);
}
}

View File

@@ -0,0 +1,54 @@
import {BasePage} from './Base.js';
import {Map} from '../parts/Map.js?v=2';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {CountriesGrid} from '../parts/grid/Countries.js?v=2';
export class CountriesPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const getMapParams = () => {
const dateRange = datesFilter.getValue();
return {dateRange};
};
const gridParams = {
url: '/admin/loadCountries',
tileId: 'totalCountries',
tableId: 'countries-table',
dateRangeGrid: true,
calculateTotals: true,
totals: {
type: 'country',
columns: ['total_visit', 'total_account', 'total_ip'],
},
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
}
};
const mapParams = {
getParams: getMapParams,
tooltipString: 'user',
tooltipField: 'total_account'
};
new Map(mapParams);
new CountriesGrid(gridParams);
}
}

View File

@@ -0,0 +1,44 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
import {IspsGrid} from '../parts/grid/Isps.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {StaticTiles} from '../parts/StaticTiles.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
export class CountryPage extends BasePage {
constructor() {
super('country', true);
this.initUi();
}
initUi() {
const usersGridParams = this.getUsersGridParams();
const eventsGridParams = this.getEventsGridParams();
const ispsGridParams = this.getIspsGridParams();
const ipsGridParams = this.getIpsGridParams();
const chartParams = this.getBarChartParams();
const tilesParams = {
elems: ['totalUsers', 'totalIps', 'totalEvents']
};
new StaticTiles(tilesParams);
new EventPanel();
const elements = [
[UsersGrid, usersGridParams],
[IpsGrid, ipsGridParams],
[IspsGrid, ispsGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,95 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {DashboardTile} from '../parts/DashboardTile.js?v=2';
import {TopTenGrid} from '../parts/grid/TopTen.js?v=2';
import {
renderClickableImportantUserWithScoreTile,
renderClickableCountry,
renderClickableResourceWithoutQuery,
renderClickableIpWithCountry,
} from '../parts/DataRenderers.js?v=2';
export class DashboardPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter(true);
const getParams = () => {
const dateRange = datesFilter.getValue();
return {dateRange};
};
const topTenUsersGridParams = {
getParams: getParams,
mode: 'mostActiveUsers',
tableId: 'most-active-users-table',
dateRangeGrid: true,
renderItemColumn: renderClickableImportantUserWithScoreTile,
};
const topTenCountriesGridParams = {
getParams: getParams,
mode: 'mostActiveCountries',
tableId: 'most-active-countries-table',
dateRangeGrid: true,
renderItemColumn: renderClickableCountry,
};
const topTenResourcesGridParams = {
getParams: getParams,
mode: 'mostActiveUrls',
tableId: 'most-active-urls-table',
dateRangeGrid: true,
renderItemColumn: renderClickableResourceWithoutQuery,
};
const topTenIpsWithMostUsersGridParams = {
getParams: getParams,
mode: 'ipsWithTheMostUsers',
tableId: 'ips-with-the-most-users-table',
dateRangeGrid: true,
renderItemColumn: renderClickableIpWithCountry,
};
const topTenUsersWithMostLoginFailGridParams = {
getParams: getParams,
mode: 'usersWithMostLoginFail',
tableId: 'users-with-most-login-fail-table',
dateRangeGrid: true,
renderItemColumn: renderClickableImportantUserWithScoreTile,
};
const topTenUsersWithMostIpsGridParams = {
getParams: getParams,
mode: 'usersWithMostIps',
tableId: 'users-with-most-ips-table',
dateRangeGrid: true,
renderItemColumn: renderClickableImportantUserWithScoreTile,
};
const elements = [
//[DashboardTile, {getParams: getParams, mode: 'totalEvents'}],
[DashboardTile, {getParams: getParams, mode: 'totalUsers'}],
[DashboardTile, {getParams: getParams, mode: 'totalIps'}],
[DashboardTile, {getParams: getParams, mode: 'totalCountries'}],
[DashboardTile, {getParams: getParams, mode: 'totalUrls'}],
[DashboardTile, {getParams: getParams, mode: 'totalUsersForReview'}],
[DashboardTile, {getParams: getParams, mode: 'totalBlockedUsers'}],
[TopTenGrid, topTenUsersGridParams],
[TopTenGrid, topTenCountriesGridParams],
[TopTenGrid, topTenResourcesGridParams],
[TopTenGrid, topTenIpsWithMostUsersGridParams],
[TopTenGrid, topTenUsersWithMostLoginFailGridParams],
[TopTenGrid, topTenUsersWithMostIpsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,57 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {Map} from '../parts/Map.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {IspsGrid} from '../parts/grid/Isps.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {DomainsGrid} from '../parts/grid/Domains.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {DomainTiles} from '../parts/details/DomainTiles.js?v=2';
import {ReenrichmentButton} from '../parts/ReenrichmentButton.js?v=2';
export class DomainPage extends BasePage {
constructor() {
super('domain', true);
this.initUi();
}
initUi() {
const usersGridParams = this.getUsersGridParams();
const eventsGridParams = this.getEventsGridParams();
const ipsGridParams = this.getIpsGridParams();
const ispsGridParams = this.getIspsGridParams();
const mapParams = this.getMapParams();
const domainDetailsTiles = this.getSelfDetails();
const chartParams = this.getBarChartParams();
const domainsGridParams = {
url: '/admin/loadDomains',
tileId: 'totalDomains',
tableId: 'domains-table',
isSortable: false,
getParams: this.getParams,
};
new EventPanel();
new ReenrichmentButton();
const elements = [
[DomainTiles, domainDetailsTiles],
[UsersGrid, usersGridParams],
[DomainsGrid, domainsGridParams],
[Map, mapParams],
[IpsGrid, ipsGridParams],
[IspsGrid, ispsGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,45 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {DomainsChart} from '../parts/chart/Domains.js?v=2';
import {DomainsGrid} from '../parts/grid/Domains.js?v=2';
export class DomainsPage extends BasePage {
constructor() {
super('domains');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadDomains',
tileId: 'totalDomains',
tableId: 'domains-table',
dateRangeGrid: true,
calculateTotals: true,
totals: {
type: 'domain',
columns: ['total_account'],
},
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
}
};
const chartParams = this.getChartParams(datesFilter, searchFilter);
new DomainsChart(chartParams);
new DomainsGrid(gridParams);
}
}

View File

@@ -0,0 +1,61 @@
import {BasePage} from './Base.js';
import {EventsChart} from '../parts/chart/Events.js?v=2';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {EventTypeFilter} from '../parts/choices/EventTypeFilter.js?v=2';
import {DeviceTypeFilter} from '../parts/choices/DeviceTypeFilter.js?v=2';
import {RulesFilter} from '../parts/choices/RulesFilter.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
export class EventsPage extends BasePage {
constructor() {
super('events');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const eventTypeFilter = new EventTypeFilter();
const deviceTypeFilter = new DeviceTypeFilter();
const rulesFilter = new RulesFilter();
const chartParams = this.getChartParams(datesFilter, searchFilter);
const gridParams = {
url: '/admin/loadEvents',
tileId: 'totalEvents',
tableId: 'user-events-table',
panelType: 'event',
dateRangeGrid: true,
sessionGroup: true,
singleUser: false,
isSortable: true,
choicesFilterEvents: [
eventTypeFilter.getEventType(),
rulesFilter.getEventType(),
deviceTypeFilter.getEventType(),
],
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
const eventTypeIds = eventTypeFilter.getValues();
const ruleUids = rulesFilter.getValues();
const deviceTypes = deviceTypeFilter.getValues();
return {dateRange, searchValue, eventTypeIds, ruleUids, deviceTypes};
}
};
new EventPanel();
new EventsChart(chartParams);
new EventsGrid(gridParams);
}
}

View File

@@ -0,0 +1,41 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {DevicesGrid} from '../parts/grid/Devices.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {DevicePanel} from '../parts/panel/DevicePanel.js?v=2';
import {IpTiles} from '../parts/details/IpTiles.js?v=2';
import {ReenrichmentButton} from '../parts/ReenrichmentButton.js?v=2';
export class IpPage extends BasePage {
constructor() {
super('ip', true);
this.initUi();
}
initUi() {
const usersGridParams = this.getUsersGridParams();
const devicesGridParams = this.getDevicesGridParams();
const eventsGridParams = this.getEventsGridParams();
const ipDetailsTiles = this.getSelfDetails();
const chartParams = this.getBarChartParams();
new EventPanel();
new DevicePanel();
new ReenrichmentButton();
const elements = [
[IpTiles, ipDetailsTiles],
[UsersGrid, usersGridParams],
[DevicesGrid, devicesGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,53 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {IpTypeFilter} from '../parts/choices/IpTypeFilter.js?v=2';
import {IpsChart} from '../parts/chart/Ips.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
export class IpsPage extends BasePage {
constructor() {
super('ips');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const ipTypeFilter = new IpTypeFilter();
const gridParams = {
url: '/admin/loadIps',
tileId: 'totalIps',
tableId: 'ips-table',
dateRangeGrid: true,
calculateTotals: true,
totals: {
type: 'ip',
columns: ['total_visit'],
},
isSortable: true,
orderByLastseen: false,
choicesFilterEvents: [ipTypeFilter.getEventType()],
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
const ipTypeIds = ipTypeFilter.getValues();
return {dateRange, searchValue, ipTypeIds};
}
};
const chartParams = this.getChartParams(datesFilter, searchFilter);
new IpsChart(chartParams);
new IpsGrid(gridParams);
}
}

View File

@@ -0,0 +1,40 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {Map} from '../parts/Map.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {IspTiles} from '../parts/details/IspTiles.js?v=2';
export class IspPage extends BasePage {
constructor() {
super('isp', true);
this.initUi();
}
initUi() {
const ispDetailsTiles = this.getSelfDetails();
const usersGridParams = this.getUsersGridParams();
const eventsGridParams = this.getEventsGridParams();
const ipsGridParams = this.getIpsGridParams();
const mapParams = this.getMapParams();
const chartParams = this.getBarChartParams();
new EventPanel();
const elements = [
[IspTiles, ispDetailsTiles],
[UsersGrid, usersGridParams],
[Map, mapParams],
[IpsGrid, ipsGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,45 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {IspsChart} from '../parts/chart/Isps.js?v=2';
import {IspsGrid} from '../parts/grid/Isps.js?v=2';
export class IspsPage extends BasePage {
constructor() {
super('isps');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadIsps',
tileId: 'totalIsps',
tableId: 'isps-table',
dateRangeGrid: true,
calculateTotals: true,
totals: {
type: 'isp',
columns: ['total_visit', 'total_account', 'total_ip'],
},
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
}
};
const chartParams = this.getChartParams(datesFilter, searchFilter);
new IspsChart(chartParams);
new IspsGrid(gridParams);
}
}

View File

@@ -0,0 +1,46 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {LogbookPanel} from '../parts/panel/LogbookPanel.js?v=2';
import {LogbookGrid} from '../parts/grid/Logbook.js?v=2';
import {LogbookChart} from '../parts/chart/Logbook.js?v=2';
export class LogbookPage extends BasePage {
constructor() {
super('logbook');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const chartParams = this.getChartParams(datesFilter, searchFilter);
const gridParams = {
url: '/admin/loadLogbook',
tileId: 'totalLogbook',
tableId: 'logbook-table',
panelType: 'logbook',
dateRangeGrid: true,
sessionGroup: false,
singleUser: false,
isSortable: true,
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
}
};
new LogbookChart(chartParams);
new LogbookPanel();
new LogbookGrid(gridParams);
}
}

View File

@@ -0,0 +1,28 @@
import {BasePage} from './Base.js';
import {ManualCheckItems} from '../parts/ManualCheckItems.js?v=2';
export class ManualCheckPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
new ManualCheckItems();
const onTableLinkClick = e => {
e.preventDefault();
const f = e.target.closest('form');
f.submit();
return false;
};
const historyTableLinks = document.querySelectorAll('[data-item-id="manual-check-history-item"]');
historyTableLinks.forEach(link => link.addEventListener('click', onTableLinkClick, false));
}
}

View File

@@ -0,0 +1,51 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {Map} from '../parts/Map.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js';
import {IspsGrid} from '../parts/grid/Isps.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {DevicesGrid} from '../parts/grid/Devices.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {StaticTiles} from '../parts/StaticTiles.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {DevicePanel} from '../parts/panel/DevicePanel.js?v=2';
export class ResourcePage extends BasePage {
constructor() {
super('resource', true);
this.initUi();
}
initUi() {
const devicesGridParams = this.getDevicesGridParams();
const eventsGridParams = this.getEventsGridParams();
const ipsGridParams = this.getIpsGridParams();
const usersGridParams = this.getUsersGridParams();
const ispsGridParams = this.getIspsGridParams();
const mapParams = this.getMapParams();
const chartParams = this.getBarChartParams();
const tilesParams = {
elems: ['totalUsers', 'totalCountries', 'totalIps', 'totalEvents']
};
new StaticTiles(tilesParams);
new EventPanel();
new DevicePanel();
const elements = [
[UsersGrid, usersGridParams],
[Map, mapParams],
[IpsGrid, ipsGridParams],
[IspsGrid, ispsGridParams],
[DevicesGrid, devicesGridParams],
[BaseBarChart, chartParams],
[EventsGrid, eventsGridParams],
];
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,45 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {ResourcesChart} from '../parts/chart/Resources.js?v=2';
import {ResourcesGrid} from '../parts/grid/Resources.js?v=2';
export class ResourcesPage extends BasePage {
constructor() {
super('resources');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadResources',
tileId: 'totalResources',
tableId: 'resources-table',
dateRangeGrid: true,
calculateTotals: true,
totals: {
type: 'resource',
columns: ['total_visit', 'total_account', 'total_ip', 'total_country'],
},
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
}
};
const chartParams = this.getChartParams(datesFilter, searchFilter);
new ResourcesChart(chartParams);
new ResourcesGrid(gridParams);
}
}

View File

@@ -0,0 +1,46 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {RulesFilter} from '../parts/choices/RulesFilter.js?v=2';
import {UserGridActionButtons} from '../parts/UserGridActionButtons.js?v=2';
import {ReviewQueueGrid} from '../parts/grid/ReviewQueue.js?v=2';
import {ReviewQueueChart} from '../parts/chart/ReviewQueue.js?v=2';
export class ReviewQueuePage extends BasePage {
constructor() {
super('review-queue');
this.tableId = 'review-queue-table';
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const rulesFilter = new RulesFilter();
const chartParams = this.getChartParams(datesFilter, searchFilter);
const gridParams = {
url: '/admin/loadReviewQueue',
tileId: 'totalUsers',
tableId: 'review-queue-table',
dateRangeGrid: true,
choicesFilterEvents: [rulesFilter.getEventType()],
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
const ruleUids = rulesFilter.getValues();
return {dateRange, searchValue, ruleUids};
}
};
new ReviewQueueChart(chartParams);
new ReviewQueueGrid(gridParams);
new UserGridActionButtons(this.tableId);
}
}

View File

@@ -0,0 +1,197 @@
import {BasePage} from './Base.js';
import {Tooltip} from '../parts/Tooltip.js?v=2';
import {handleAjaxError} from '../parts/utils/ErrorHandler.js?v=2';
import {getRuleClass} from '../parts/utils/String.js?v=2';
import {ThresholdsForm} from '../parts/ThresholdsForm.js?v=2';
import {
renderClickableUser,
renderProportion,
renderRulePlayResult,
} from '../parts/DataRenderers.js?v=2';
export class RulesPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
new ThresholdsForm();
const searchTable = this.searchTable.bind(this);
this.searchInput.addEventListener('keyup', searchTable, false);
const onPlayButtonClick = this.onPlayButtonClick.bind(this);
this.playButtons.forEach(button => button.addEventListener('click', onPlayButtonClick, false));
const onSaveButtonClick = this.onSaveButtonClick.bind(this);
this.saveButtons.forEach(button => button.addEventListener('click', onSaveButtonClick, false));
const onSelectChange = this.onSelectChange.bind(this);
this.selects.forEach(select => select.addEventListener('change', onSelectChange, false));
}
onPlayButtonClick(e) {
e.preventDefault();
this.updateDisabled(true);
const currentPlayButton = e.target.closest('button');
currentPlayButton.classList.add('is-loading');
const ruleUid = currentPlayButton.dataset.ruleUid;
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const params = {ruleUid: currentPlayButton.dataset.ruleUid, token: token};
$.ajax({
url: '/admin/checkRule',
type: 'get',
context: {currentPlayButton: currentPlayButton, ruleUid: ruleUid},
data: params,
success: this.onCheckRuleLoad, // without binding to keep simultaneous calls scopes separate
error: handleAjaxError,
complete: this.updateDisabled.bind(this, false)
});
return false;
}
onCheckRuleLoad(data, status) {
if ('success' !== status || 0 === data.length) {
return;
}
this.currentPlayButton.classList.remove('is-loading');
let row = document.querySelector(`tr[data-rule-uid="${this.ruleUid}"]`);
let nextRow = row.nextElementSibling;
if (!nextRow || nextRow.dataset.ruleUid) {
const ex = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 6;
ex.replaceChildren(td);
nextRow = row.parentNode.insertBefore(ex, row.nextSibling);
}
nextRow.querySelector('td').replaceChildren(renderRulePlayResult(data.users, data.count, this.ruleUid));
// 3 is index of proportion column
row.children[3].replaceChildren(renderProportion(data.proportion, data.proportion_updated_at));
Tooltip.addTooltipsToRulesProportion();
}
updateDisabled(disabled) {
this.playButtons.forEach(button => button.disabled = disabled);
}
onSelectChange(e) {
e.preventDefault();
const field = e.target;
const parentRow = field.closest('tr');
const saveButton = parentRow.querySelector('button[type="button"]');
const value = field.value;
const cls = getRuleClass(parseInt(value, 10));
const newClassName = `ruleHighlight ${cls}`;
parentRow.querySelector('h3').className = newClassName;
if (field.dataset.initialValue == value) {
parentRow.classList.remove('input-field-changed');
saveButton.classList.add('is-hidden');
} else {
parentRow.classList.add('input-field-changed');
saveButton.classList.remove('is-hidden');
}
return false;
}
onSaveButtonClick(e) {
e.preventDefault();
const currentSaveButton = e.target.closest('button');
currentSaveButton.classList.add('is-loading');
const select = currentSaveButton.closest('tr').querySelector('select');
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const params = {
rule: select.name,
value: select.value,
token: token,
};
$.ajax({
url: '/admin/saveRule',
type: 'post',
data: params,
context: {currentSaveButton: currentSaveButton},
error: handleAjaxError,
success: this.onSaveLoaded, // without binding to keep simultaneous calls scopes separate
});
return false;
}
onSaveLoaded(data, status) {
if ('success' !== status) {
return;
}
this.currentSaveButton.classList.remove('is-loading');
const parentRow = this.currentSaveButton.closest('tr');
const saveButton = parentRow.querySelector('button[type="button"]');
parentRow.classList.remove('input-field-changed');
saveButton.classList.add('is-hidden');
}
searchTable() {
let td, i, txtValue;
const input = document.getElementById('search');
const filter = input.value.toLowerCase();
const table = document.getElementById('rules-table');
const tr = table.getElementsByTagName('tr');
// i = 1 because search must skip first line with column names
for (i = 1; i < tr.length; i++) {
td = tr[i].getElementsByTagName('td');
let found = false;
for (let j = 0; j < Math.min(td.length, 3); j++) {
if (td[j]) {
txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toLowerCase().indexOf(filter) > -1) {
found = true;
break;
}
}
}
tr[i].style.display = found ? '' : 'none';
}
}
get selects() {
return document.querySelectorAll('td select');
}
get saveButtons() {
return document.querySelectorAll('td button[type="button"]');
}
get playButtons() {
return document.querySelectorAll('td button[data-rule-uid]');
}
get searchInput() {
return document.getElementById('search');
}
}

View File

@@ -0,0 +1,15 @@
import {BasePage} from './Base.js';
import {DeleteAccountPopUp} from '../parts/DeleteAccountPopUp.js?v=2';
export class SettingsPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
new DeleteAccountPopUp();
}
}

View File

@@ -0,0 +1,121 @@
import {BasePage} from './Base.js';
import {SequentialLoad} from '../parts/SequentialLoad.js?v=2';
import {Map} from '../parts/Map.js?v=2';
import {EmailsGrid} from '../parts/grid/Emails.js?v=2';
import {IpsGrid} from '../parts/grid/Ips.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
import {DevicesGrid} from '../parts/grid/Devices.js?v=2';
import {BaseBarChart} from '../parts/chart/BaseBar.js?v=2';
import {BaseSparklineChart} from '../parts/chart/BaseSparkline.js?v=2';
import {UserTiles} from '../parts/details/UserTiles.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {SingleReviewButton} from '../parts/SingleReviewButton.js?v=2';
import {ScoreDetails} from '../parts/ScoreDetails.js?v=2';
import {PhonesGrid} from '../parts/grid/Phones.js?v=2';
import {FieldAuditTrailGrid} from '../parts/grid/payloads/FieldAuditTrail.js?v=2';
import {IspsGrid} from '../parts/grid/Isps.js?v=2';
import {EmailPanel} from '../parts/panel/EmailPanel.js?v=2';
import {PhonePanel} from '../parts/panel/PhonePanel.js?v=2';
import {DevicePanel} from '../parts/panel/DevicePanel.js?v=2';
import {ReenrichmentButton} from '../parts/ReenrichmentButton.js?v=2';
export class UserPage extends BasePage {
constructor() {
super('user', true);
this.initUi();
}
initUi() {
const userDetailsTiles = this.getSelfDetails();
const devicesGridParams = this.getDevicesGridParams();
const ipsGridParams = this.getIpsGridParams();
const ispsGridParams = this.getIspsGridParams();
const eventsGridParams = this.getEventsGridParams();
eventsGridParams.sessionGroup = true;
eventsGridParams.singleUser = true;
const mapParams = this.getMapParams();
const chartParams = this.getBarChartParams();
const statsChartParams = {
getParams: () => ({
mode: 'stats',
id: this.id,
}),
};
ipsGridParams.tileId = null;
eventsGridParams.tileId = null;
devicesGridParams.tileId = null;
mapParams.tileId = null;
const emailsGridParams = {
url: '/admin/loadEmails',
tableId: 'emails-table',
panelType: 'email',
isSortable: false,
getParams: this.getParams,
};
const phonesGridParams = {
url: '/admin/loadPhones',
tableId: 'phones-table',
panelType: 'phone',
isSortable: false,
getParams: this.getParams,
};
const fieldAuditTrailGridParams = {
url: '/admin/loadFieldAuditTrail',
tableId: 'field-audit-trail-table',
isSortable: false,
getParams: this.getParams,
};
const userScoreDetails = {
userId: this.id,
};
new ScoreDetails(userScoreDetails);
new SingleReviewButton(this.id);
new EventPanel();
new DevicePanel();
new ReenrichmentButton();
const isEmailPhone = !!document.getElementById('email-card');
if (isEmailPhone) {
new EmailPanel();
new PhonePanel();
}
const elements = [
[UserTiles, userDetailsTiles],
[BaseSparklineChart, statsChartParams],
[Map, mapParams],
[IpsGrid, ipsGridParams],
[IspsGrid, ispsGridParams],
[DevicesGrid, devicesGridParams],
];
if (isEmailPhone) {
elements.push([EmailsGrid, emailsGridParams]);
elements.push([PhonesGrid, phonesGridParams]);
}
elements.push([FieldAuditTrailGrid, fieldAuditTrailGridParams]);
elements.push([BaseBarChart, chartParams]);
elements.push([EventsGrid, eventsGridParams]);
new SequentialLoad(elements);
}
}

View File

@@ -0,0 +1,48 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {RulesFilter} from '../parts/choices/RulesFilter.js?v=2';
import {ScoresRangeFilter} from '../parts/choices/ScoresRangeFilter.js?v=2';
import {UsersGrid} from '../parts/grid/Users.js?v=2';
import {UsersChart} from '../parts/chart/Users.js?v=2';
export class UsersPage extends BasePage {
constructor() {
super('users');
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const rulesFilter = new RulesFilter();
const scoresRangeFilter = new ScoresRangeFilter();
const chartParams = this.getChartParams(datesFilter, searchFilter);
const gridParams = {
url: '/admin/loadUsers',
tileId: 'totalUsers',
tableId: 'users-table',
dateRangeGrid: true,
choicesFilterEvents: [rulesFilter.getEventType(), scoresRangeFilter.getEventType()],
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
const ruleUids = rulesFilter.getValues();
const scoresRange = scoresRangeFilter.getValues();
return {dateRange, searchValue, ruleUids, scoresRange};
},
};
new UsersGrid(gridParams);
new UsersChart(chartParams);
}
}

View File

@@ -0,0 +1,42 @@
import {BasePage} from './Base.js';
import {DatesFilter} from '../parts/DatesFilter.js?v=2';
import {SearchFilter} from '../parts/SearchFilter.js?v=2';
import {EventPanel} from '../parts/panel/EventPanel.js?v=2';
import {WatchlistTags} from '../parts/WatchlistTags.js?v=2';
import {EventsGrid} from '../parts/grid/Events.js?v=2';
export class WatchlistPage extends BasePage {
constructor() {
super();
this.initUi();
}
initUi() {
const datesFilter = new DatesFilter();
const searchFilter = new SearchFilter();
const gridParams = {
url: '/admin/loadEvents?watchlist=true',
tileId: 'totalEvents',
tableId: 'user-events-table',
panelType: 'event',
dateRangeGrid: true,
isSortable: false,
getParams: function() {
const dateRange = datesFilter.getValue();
const searchValue = searchFilter.getValue();
return {dateRange, searchValue};
},
};
new EventPanel();
new WatchlistTags();
new EventsGrid(gridParams);
}
}

View File

@@ -0,0 +1,83 @@
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
export class BlacklistGridActionButtons {
constructor(tableId) {
this.tableId = tableId;
const onTableLoaded = this.onTableLoaded.bind(this);
window.addEventListener('tableLoaded', onTableLoaded, false);
}
onTableLoaded(e) {
const tableId = e.detail.tableId;
const buttons = document.querySelectorAll(`#${tableId} button`);
const onButtonClick = this.onButtonClick.bind(this);
buttons.forEach(button => button.addEventListener('click', onButtonClick, false));
}
onButtonClick(e) {
e.preventDefault();
e.stopPropagation();
const me = this;
const target = e.target;
const url = '/admin/removeBlacklisted';
const data = {
id: target.dataset.itemId,
type: target.dataset.itemType,
token: me.csrf
};
target.classList.add('is-loading');
$.ajax({
type: 'POST',
url: url,
data: data,
scope: me,
target: target,
success: me.onSuccess,
error: handleAjaxError,
dataType: 'json'
});
return false;
}
onSuccess(dta, status) {
if ('success' !== status) {
return;
}
const me = this.scope;
const target = this.target;
const tableRow = target.closest('tr');
target.classList.remove('is-loading');
target.setAttribute('disabled', '');
target.textContent = 'Removed';
const card = target.closest('.card');
const span = card.querySelector('.card-header-title span');
let total = parseInt(span.innerHTML, 10);
if (total > 0) {
total -= 1;
}
span.textContent = total;
if (tableRow) {
const dataTable = $(`#${me.tableId}`).DataTable();
dataTable.row(tableRow).remove().draw(false);
}
}
get csrf() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
}

View File

@@ -0,0 +1,84 @@
import {Loader} from './Loader.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {fireEvent} from './utils/Event.js?v=2';
export class DashboardTile {
constructor(tilesParams) {
const me = this;
this.config = tilesParams;
this.loader = new Loader();
if (!this.config.sequential) {
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
this.initLoad();
}
}
startLoader() {
const el = document.querySelector(`.${this.config.mode} .title`);
this.loader.start(el);
}
loadData() {
const me = this;
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
let params = this.config.getParams().dateRange;
params.mode = this.config.mode;
if (!this.config.sequential) {
this.startLoader();
}
fireEvent('dateFilterChangedCaught');
$.ajax({
url: `/admin/loadDashboardStat?token=${token}`,
type: 'get',
scope: me,
data: params,
success: me.onLoad,
error: handleAjaxError,
complete: function() {
fireEvent('dateFilterChangedCompleted');
},
});
}
onLoad(data, status) {
if ('success' == status) {
this.scope.loader.stop();
const frag = document.createDocumentFragment();
const period = document.createElement('p');
if (this.scope.config.mode === 'totalUsersForReview') {
period.className = 'periodTotalYellow';
} else if (this.scope.config.mode === 'totalBlockedUsers') {
period.className = 'periodTotalRed';
} else {
period.className = 'periodTotal';
}
period.textContent = data.total;
const total = document.createElement('p');
total.className = 'allTimeTotal';
total.textContent = data.allTimeTotal;
frag.appendChild(period);
frag.appendChild(total);
const el = document.querySelector(`.${this.scope.config.mode} .title`);
el.replaceChildren(frag);
}
}
onDateFilterChanged() {
this.loadData();
}
}

View File

@@ -0,0 +1,261 @@
import {fireEvent} from './utils/Event.js?v=2';
import {
formatStringTime,
addDays,
addHours,
} from './utils/Date.js?v=2';
import {debounce} from './utils/Functions.js?v=2';
import {DAYS_IN_RANGE} from './utils/Constants.js?v=2';
export class DatesFilter {
constructor(sequential=false) {
this.setupXhrPool();
this.offset = (this.offsetField) ? parseInt(this.offsetField.value, 10) : 0;
this.ajaxCount = 0;
this.sequential = sequential;
if (this.isDateFilterUnavailable) {
return true;
}
if (this.dateToLocalField !== null && this.dateFromLocalField !== null) {
// visible fields change should set invisible fields
this.onTimestampFieldChange = this.onTimestampFieldChange.bind(this);
const debouncedOnTimestampFieldChange = debounce(this.onTimestampFieldChange);
this.dateToLocalField.addEventListener('change', debouncedOnTimestampFieldChange, false);
this.dateFromLocalField.addEventListener('change', debouncedOnTimestampFieldChange, false);
this.setDefaultLocalDates();
this.setDefaultDates();
} else if (!this.dateFromField.value && !this.dateToField.value) {
this.setDefaultDates();
}
const onDateFilterChange = this.onDateFilterChange.bind(this);
this.dateToField.addEventListener('change', onDateFilterChange, false);
this.dateFromField.addEventListener('change', onDateFilterChange, false);
const onIntervalLinkClick = this.onIntervalLinkClick.bind(this);
this.intervalLinks.forEach(item => item.addEventListener('click', onIntervalLinkClick, false));
const onDateFilterChangedCaught = this.onDateFilterChangedCaught.bind(this);
window.addEventListener('dateFilterChangedCaught', onDateFilterChangedCaught, false);
const onDateFilterChangedCompleted = this.onDateFilterChangedCompleted.bind(this);
window.addEventListener('dateFilterChangedCompleted', onDateFilterChangedCompleted, false);
const onSequentialLoadCompleted = this.onSequentialLoadCompleted.bind(this);
window.addEventListener('sequentialLoadCompleted', onSequentialLoadCompleted, false);
}
setupXhrPool() {
// https://gist.github.com/msankhala/3fa2844c1fbad1f4c0185a8e3ef09aed
// Stop all ajax request by http://tjrus.com/blog/stop-all-active-ajax-requests
$.xhrPool = []; // array of uncompleted requests
$.xhrPool.abortAll = function() { // our abort function
$(this).each((idx, jqXHR) => {
jqXHR.abort();
});
$.xhrPool.length = 0;
};
$.ajaxSetup({
beforeSend: function(jqXHR) { // before jQuery send the request we will push it to our array
$.xhrPool.push(jqXHR);
},
complete: function(jqXHR) { // when some of the requests completed it will splice from the array
const index = $.xhrPool.indexOf(jqXHR);
if (index > -1) {
$.xhrPool.splice(index, 1);
}
}
});
}
setDefaultDates() {
this.setDateRangeFromNow(DAYS_IN_RANGE * 24);
}
setDefaultLocalDates() {
let dateTo = new Date();
dateTo = new Date(dateTo.getTime() + (dateTo.getTimezoneOffset() * 60 + this.offset) * 1000); // now time in op tz
const dateFrom = addDays(dateTo, -DAYS_IN_RANGE); // dateFrom in op tz
dateFrom.setHours(24, 0, 0, 0);
this.dateToLocalField.value = formatStringTime(dateTo);
this.dateFromLocalField.value = formatStringTime(dateFrom);
}
onTimestampFieldChange(e) {
// get value (with offset)
// set normal input exluding offset
e.preventDefault();
const input = e.target;
$.xhrPool.abortAll();
const value = input.value;
const name = input.name;
let target = null;
if (name === 'date_from_local') {
target = this.dateFromField;
} else if (name === 'date_to_local') {
target = this.dateToField;
}
let dt = new Date(value);
dt = new Date(dt.getTime() - this.offset * 1000); // shift fom op tz to utc
target.value = formatStringTime(dt);
this.onDateFilterChange(e);
return false;
}
onDateFilterChange() {
$.xhrPool.abortAll();
fireEvent('dateFilterChanged');
}
getValue() {
const data = {
dateTo: null,
dateFrom: null
};
if (this.isDateFilterUnavailable) {
return data;
}
data['dateTo'] = this.dateToField.value;
data['dateFrom'] = this.dateFromField.value;
/*const rangeWasChanged = (1 == this.dateFromField.dataset.changed) || (1 == this.dateToField.dataset.changed);
if (rangeWasChanged) {
data['keepDates'] = 1;
}*/
return data;
}
onIntervalLinkClick(e) {
e.preventDefault();
const link = e.target;
if (link.classList.contains('active') || link.classList.contains('blocked')) {
return false;
}
this.updateDisabled(true);
$.xhrPool.abortAll();
const value = parseInt(link.dataset.value, 10) || 0;
if (value === 0) {
this.clearDateRange();
} else {
this.setDateRangeFromNow(value);
}
this.intervalLinks.forEach(item => item.classList.remove('active'));
link.classList.add('active');
this.onDateFilterChange();
return false;
}
updateDisabled(disabled) {
let m = disabled ? 'true' : 'false';
this.intervalLinks.forEach(item => {
if (disabled) {
if (!item.classList.contains('active')) {
item.setAttribute('tabindex', '-1');
item.classList.add('blocked');
item.removeAttribute('href');
}
} else {
item.setAttribute('tabindex', '0');
item.classList.remove('blocked');
item.setAttribute('href', 'javascript:void(0);');
}
});
}
// with op tz and utc shift for calculation request
setDateRangeFromNow(hoursDiff) {
let dateTo = new Date();
dateTo = new Date(dateTo.getTime() + (dateTo.getTimezoneOffset() * 60 + this.offset) * 1000); // now time in op tz
let dateFrom = addHours(dateTo, -hoursDiff); // dateFrom in op tz
// floor to not miss data in group
if (hoursDiff < 24 && hoursDiff > -24) {
dateFrom.setMinutes(0, 0, 0);
} else {
dateFrom.setHours(24, 0, 0, 0);
}
dateTo = new Date(dateTo.getTime() - (this.offset * 1000)); // dateTo at utc
dateFrom = new Date(dateFrom.getTime() - (this.offset * 1000)); // dateFrom at utc
this.dateToField.value = formatStringTime(dateTo);
this.dateFromField.value = formatStringTime(dateFrom);
}
clearDateRange() {
this.dateToField.value = null;
this.dateFromField.value = null;
}
onDateFilterChangedCaught() {
this.ajaxCount++;
}
onDateFilterChangedCompleted() {
this.ajaxCount--;
if (this.ajaxCount <= 0 && !this.sequential) {
this.updateDisabled(false);
}
}
onSequentialLoadCompleted() {
this.updateDisabled(false);
}
get isDateFilterUnavailable() {
return this.dateFromField === null || this.dateToField === null;
}
get intervalLinks() {
return this.navbar.querySelectorAll('a');
}
get navbar() {
return document.querySelector('nav.filtersForm.daterange');
}
get offsetField() {
return document.querySelector('input[name="offset"]');
}
get dateToField() {
return document.querySelector('input[name="date_to"]');
}
get dateFromField() {
return document.querySelector('input[name="date_from"]');
}
get dateToLocalField() {
return document.querySelector('input[name="date_to_local"]');
}
get dateFromLocalField() {
return document.querySelector('input[name="date_from_local"]');
}
}

View File

@@ -0,0 +1,95 @@
import {fireEvent} from './utils/Event.js?v=2';
export class DeleteAccountPopUp {
constructor() {
const onDeleteAccountButtonClicked = this.onDeleteAccountButtonClicked.bind(this);
this.closeAccountButton.addEventListener('click', onDeleteAccountButtonClicked, false);
const onConfirmDeleteAccountButton = this.onConfirmDeleteAccountButton.bind(this);
this.confirmButton.addEventListener('click', onConfirmDeleteAccountButton, false);
const onKeydown = this.onKeydown.bind(this);
window.addEventListener('keydown', onKeydown, false);
const onCloseButtonClick = this.onCloseButtonClick.bind(this);
this.closePopUpButton.addEventListener('click', onCloseButtonClick, false);
}
//https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
onKeydown(e) {
if (e.defaultPrevented) {
return; // Do nothing if the event was already processed
}
switch (e.key) {
case 'Esc': // IE/Edge specific value
case 'Escape':
this.close();
break;
default:
return;
}
// Cancel the default action to avoid it being handled twice
e.preventDefault();
}
onConfirmDeleteAccountButton(e) {
e.preventDefault();
this.accountForm.submit();
this.card.classList.add('is-hidden');
this.contentDiv.classList.add('is-hidden');
}
onDeleteAccountButtonClicked(e) {
e.preventDefault();
// close other panels
const card = document.querySelector('.details-card#enrich-all-popup');
if (card && !card.classList.contains('is-hidden')) {
fireEvent('enrichAllPopUpClosed');
card.classList.add('is-hidden');
}
this.card.classList.remove('is-hidden');
this.contentDiv.classList.remove('is-hidden');
}
onCloseButtonClick(e) {
e.preventDefault();
this.close();
}
close() {
fireEvent('closeAccountPopUpClosed');
this.card.classList.add('is-hidden');
return false;
}
get contentDiv() {
return this.card.querySelector('div.content');
}
get card() {
return document.querySelector('.details-card#close-account-popup');
}
get closePopUpButton() {
return this.card.querySelector('.delete');
}
get accountForm() {
return document.getElementById('close-account-form');
}
get confirmButton() {
return document.getElementById('confirm-close-account-button');
}
get closeAccountButton() {
return document.getElementById('close-account-btn');
}
}

View File

@@ -0,0 +1,154 @@
import {Loader} from './Loader.js?v=2';
import {fireEvent} from './utils/Event.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {renderEnrichmentCalculation} from './DataRenderers.js?v=2';
export class EnrichAllPopUp {
constructor() {
this.loader = new Loader();
const onEnrichAllButtonClicked = this.onEnrichAllButtonClicked.bind(this);
this.enrichAllButton.addEventListener('click', onEnrichAllButtonClicked, false);
const onConfirmEnrichAllButton = this.onConfirmEnrichAllButton.bind(this);
this.confirmButton.addEventListener('click', onConfirmEnrichAllButton, false);
const onKeydown = this.onKeydown.bind(this);
window.addEventListener('keydown', onKeydown, false);
const onCloseButtonClick = this.onCloseButtonClick.bind(this);
this.closePopUpButton.addEventListener('click', onCloseButtonClick, false);
}
//https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
onKeydown(e) {
if (e.defaultPrevented) {
return; // Do nothing if the event was already processed
}
switch (e.key) {
case 'Esc': // IE/Edge specific value
case 'Escape':
this.close();
break;
default:
return;
}
// Cancel the default action to avoid it being handled twice
e.preventDefault();
}
onConfirmEnrichAllButton(e) {
e.preventDefault();
this.accountForm.submit();
this.card.classList.add('is-hidden');
this.contentDiv.classList.add('is-hidden');
}
onEnrichAllButtonClicked(e) {
e.preventDefault();
// close other panels
const card = document.querySelector('.details-card#close-account-popup');
if (card && !card.classList.contains('is-hidden')) {
fireEvent('closeAccountPopUpClosed');
card.classList.add('is-hidden');
}
// call ajax
this.loadData();
}
loadData(id) {
this.contentDiv.classList.add('is-hidden');
this.loaderDiv.classList.remove('is-hidden');
this.card.classList.remove('is-hidden');
const el = this.loaderDiv;
this.loader.start(el);
const onDetailsLoaded = this.onDetailsLoaded.bind(this);
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
$.ajax({
url: '/admin/enrichmentDetails',
type: 'get',
data: {token: token},
success: onDetailsLoaded,
error: handleAjaxError,
});
}
onDetailsLoaded(data, status) {
if ('success' !== status || 0 === data.length) {
return;
}
data = this.proceedData(data);
this.loader.stop();
this.contentDiv.classList.remove('is-hidden');
this.loaderDiv.classList.add('is-hidden');
let span = null;
//todo: foreach and arrow fn ?
for (const key in data) {
span = this.card.querySelector(`#details_${key}`);
if (span) {
if (data[key] instanceof Node) {
span.replaceChildren(data[key]);
} else {
span.innerHTML = data[key];
}
}
}
}
proceedData(data) {
data.calculation = renderEnrichmentCalculation(data);
return data;
}
onCloseButtonClick(e) {
e.preventDefault();
this.close();
}
close() {
fireEvent('enrichAllPopUpClosed');
this.card.classList.add('is-hidden');
return false;
}
get loaderDiv() {
return this.card.querySelector('div.text-loader');
}
get contentDiv() {
return this.card.querySelector('div.content');
}
get card() {
return document.querySelector('.details-card#enrich-all-popup');
}
get closePopUpButton() {
return this.card.querySelector('.delete');
}
get accountForm() {
return document.getElementById('enrich-all-form');
}
get confirmButton() {
return document.getElementById('confirm-enrich-all-button');
}
get enrichAllButton() {
return document.getElementById('enrich-all-btn');
}
}

View File

@@ -0,0 +1,50 @@
export class Loader {
constructor() {
this.symbols = [
this.el('⣾'),
this.el('⣷'),
this.el('⣯'),
this.el('⣟'),
this.el('⡿'),
this.el('⢿'),
this.el('⣻'),
this.el('⣽'),
];
}
start(loaderEl) {
this.loaderEl = loaderEl;
let me = this;
let counter = 0;
this.loaderEl.classList.add('loading');
this.loaderEl.classList.remove('loaded');
let timerId = setInterval(() => {
if (me.loaderEl.classList.contains('loaded')) {
clearInterval(timerId);
return;
}
let symbol = me.symbols[counter % me.symbols.length];
me.loaderEl.replaceChildren(symbol);
counter++;
}, 85);
}
stop() {
this.loaderEl.classList.add('loaded');
this.loaderEl.classList.remove('loading');
}
el(c) {
const node = document.createElement('p');
node.textContent = c;
return node;
}
}

View File

@@ -0,0 +1,318 @@
import {
renderDefaultIfEmptyElement,
renderBoolean,
renderDate,
renderCountryIso,
renderHttpCode,
renderPhoneType,
renderPhoneCarrierName,
renderAsn,
} from '../parts/DataRenderers.js?v=2';
export class ManualCheckItems {
constructor() {
const table = document.querySelector('.events-card.is-hidden');
if (!table) return;
table.classList.remove('is-hidden');
const itemType = table.dataset.itemType;
if ('ip' == itemType) {
this.enrichIpDetails();
}
if ('email' == itemType) {
this.enrichEmailDetails();
}
if ('domain' == itemType) {
this.enrichDomainDetails();
}
if ('phone' == itemType) {
this.enrichPhoneDetails();
}
}
enrichPhoneDetails() {
let item = null;
item = 'iso_country_code';
this.renderCountryIso(item);
item = 'type';
this.renderPhoneType(item);
item = 'invalid';
this.renderBoolean(item);
item = 'profiles';
this.renderProfiles(item);
item = 'carrier_name';
this.renderPhoneCarrierName(item);
}
enrichDomainDetails() {
let item = null;
item = 'blockdomains';
this.renderBoolean(item);
item = 'disposable_domains';
this.renderBoolean(item);
item = 'free_email_provider';
this.renderBoolean(item);
item = 'geo_ip';
this.renderCountryIso(item);
item = 'geo_html';
this.renderCountryIso(item);
item = 'web_server';
this.renderDefaultIfEmptyElement(item);
item = 'hostname';
this.renderDefaultIfEmptyElement(item);
item = 'emails';
this.renderDefaultIfEmptyElement(item);
item = 'phone';
this.renderDefaultIfEmptyElement(item);
item = 'discovery_date';
this.renderDate(item);
item = 'creation_date';
this.renderDate(item);
item = 'expiration_date';
this.renderDate(item);
item = 'mx_record';
this.renderBoolean(item);
item = 'return_code';
this.renderHttpCode(item);
item = 'disabled';
this.renderBoolean(item);
item = 'closest_snapshot';
this.renderDate(item);
}
enrichIpDetails() {
let item = null;
let value = null;
item = 'country';
this.renderCountryIso(item);
item = 'asn';
value = this.getItem(item);
value = {asn: value};
value = renderAsn(value);
this.setItem(item, value);
item = 'hosting';
this.renderBoolean(item);
item = 'vpn';
this.renderBoolean(item);
item = 'tor';
this.renderBoolean(item);
item = 'relay';
this.renderBoolean(item);
item = 'starlink';
this.renderBoolean(item);
item = 'description';
this.renderDefaultIfEmptyElement(item);
item = 'blocklist';
this.renderBoolean(item);
item = 'domains_count';
value = this.getItem(item);
if (value) {
value = JSON.parse(value);
if (Array.isArray(value)) {
value = value.length;
} else {
value = parseInt(value, 10);
}
if (isNaN(value)) {
value = renderDefaultIfEmptyElement(value);
} else {
value = !!value;
value = renderBoolean(value);
}
} else {
value = renderDefaultIfEmptyElement(value);
}
this.setItem(item, value);
}
enrichEmailDetails() {
let item = null;
item = 'blockemails';
this.renderBoolean(item);
item = 'data_breach';
this.renderDataBreach(item);
item = 'earliest_breach';
this.renderDate(item);
item = 'domain_contact_email';
this.renderBoolean(item);
item = 'profiles';
this.renderProfiles(item);
}
renderDataBreach(itemId) {
let value;
value = this.getItem(itemId);
if (null === value) {
value = renderDefaultIfEmptyElement(value);
} else {
//Revert databreach to "No databreach"
value = !value;
value = renderBoolean(value);
}
this.setItem(itemId, value);
}
renderProfiles(itemId) {
let value;
value = this.getItem(itemId);
value = parseInt(value, 10);
if (isNaN(value)) {
value = renderDefaultIfEmptyElement(value);
} else {
//Convert to boolean
value = !!value;
//Revert profiles to "No profiles"
value = !value;
value = renderBoolean(value);
}
this.setItem(itemId, value);
}
renderDate(itemId) {
let value;
value = this.getItem(itemId);
value = renderDate(value);
this.setItem(itemId, value);
}
renderCountryIso(itemId) {
let value;
value = this.getItem(itemId);
value = {country_iso: value, full_country: value};
value = renderCountryIso(value);
this.setItem(itemId, value);
}
renderHttpCode(itemId) {
let value;
value = this.getItem(itemId);
value = {http_code: value};
value = renderHttpCode(value);
this.setItem(itemId, value);
}
renderPhoneType(itemId) {
let value;
value = this.getItem(itemId);
value = {type: value};
value = renderPhoneType(value);
this.setItem(itemId, value);
}
renderPhoneCarrierName(itemId) {
let value;
value = this.getItem(itemId);
value = {carrier_name: value};
value = renderPhoneCarrierName(value);
this.setItem(itemId, value);
}
renderBoolean(itemId) {
let value;
value = this.getItem(itemId);
value = renderBoolean(value);
this.setItem(itemId, value);
}
renderDefaultIfEmptyElement(itemId) {
let value;
value = this.getItem(itemId);
value = renderDefaultIfEmptyElement(value);
this.setItem(itemId, value);
}
getItem(itemId, returnNode = false) {
const td = document.querySelector(`td[data-item-id="${itemId}"]`);
if (!td) return null;
const tr = td.closest('tr');
const valueTd = tr.lastElementChild;
if (returnNode) {
return valueTd;
} else {
let text = valueTd.innerText;
let value = text;
if ('false' === text) value = false;
if ('true' === text) value = true;
if ('null' === text) value = null;
return value;
}
}
setItem(itemId, value) {
const item = this.getItem(itemId, true);
if (item) {
if (item instanceof Node) {
item.replaceChildren(value);
} else {
item.innerHTML = value;
}
}
}
}

View File

@@ -0,0 +1,191 @@
import {TotalTile} from './TotalTile.js?v=2';
import {getQueryParams} from './utils/DataSource.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {fireEvent} from './utils/Event.js?v=2';
export class Map {
constructor(mapParams) {
this.config = mapParams;
this.totalTile = new TotalTile();
this.regions = {};
const onRegionTipShow = this.onRegionTipShow.bind(this);
const onRegionClick = this.onRegionClick.bind(this);
$('#world-map-markers').vectorMap({
map: 'world_mill_en',
normalizeFunction: 'polynomial',
hoverOpacity: 0.7,
regionsSelectable: false,
markersSelectable: false,
zoomOnScroll: false,
hoverColor: false,
series: {
regions: [
{
values: {},
scale: ['#4e6964', '#01EE99'],
normalizeFunction: 'polynomial'
}
]
},
regionStyle: {
initial: {
fill: '#575678'
},
selected: {
fill: '#01EE99'
}
},
onRegionTipShow: function(e, el, code) {
onRegionTipShow(el, code);
},
onRegionClick: function(e, code) {
onRegionClick(code);
},
backgroundColor: '#131220'
});
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
if (!this.config.sequential) {
this.loadData();
}
}
startLoader() {
}
onRegionTipShow(tipEl, value) {
const regionValue = this.mapObject.series.regions[0].values[value];
const phrase = this.getTooltipString(regionValue);
tipEl.html(`${tipEl.html()} - ${phrase}`);
}
onRegionClick(value) {
if (this.regions[value] !== undefined && this.regions[value][this.config.tooltipField] > 0) {
const url = `/country/${this.regions[value].id}`;
if (event.ctrlKey || event.metaKey) {
window.open(url, '_blank');
} else {
window.location.href = url;
}
}
}
getCountriesRegionsFromResponse(records) {
const me = this;
const regions = {};
this.regions = {};
records.forEach(rec => {
const country = rec.iso;
if (!regions[country]) {
regions[country] = 0;
this.regions[country] = 0;
}
const value = me.getRegionValue(rec);
regions[country] = value;
this.regions[country] = {
[this.config.tooltipField]: value,
id: rec.id,
};
});
return regions;
}
getRegionValue(record) {
const field = this.config.tooltipField;
const value = record[field];
return value;
}
selectRegions(regions) {
const map = this.mapObject;
//Remove countries which does not exist in the vectormap: MU, BH, etc...
for (const [key, value] of Object.entries(regions)) {
if (!map.regions.hasOwnProperty(key)) {
delete regions[key];
}
}
//https://github.com/bjornd/jvectormap/issues/376
map.series.regions[0].params.min = undefined;
map.series.regions[0].params.max = undefined;
map.series.regions[0].clear();
map.series.regions[0].setValues(regions);
}
onDateFilterChanged() {
this.loadData();
}
loadData() {
const me = this;
const params = this.config.getParams();
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const data = getQueryParams(params);
fireEvent('dateFilterChangedCaught');
$.ajax({
type: 'get',
url: `/admin/loadMap?token=${token}`,
data: data,
scope: me,
success: me.onCountriesListLoaded,
error: handleAjaxError,
complete: function() {
fireEvent('dateFilterChangedCompleted');
},
});
}
onCountriesListLoaded(data, status) {
if ('success' == status) {
const me = this.scope;
const tableId = 'countries-table';
me.totalTile.update(tableId, me.config.tileId, data.length);
const regions = me.getCountriesRegionsFromResponse(data);
me.selectRegions(regions);
}
}
getTooltipString(value) {
value = value ? value : 0;
let string = this.config.tooltipString;
if (1 !== value) {
string += 's';
}
const tooltipPhrase = `${value} ${string}`;
return tooltipPhrase;
}
get mapObject() {
return $('#world-map-markers').vectorMap('get', 'mapObject');
}
}

View File

@@ -0,0 +1,23 @@
// should be used for POST forms
export class ReenrichmentButton {
constructor() {
if (this.reenrichmentButton) {
const onButtonClick = this.onButtonClick.bind(this);
this.reenrichmentButton.addEventListener('click', onButtonClick, false);
}
}
onButtonClick(e) {
e.preventDefault();
this.reenrichmentButton.setAttribute('disabled', '');
this.form.submit();
}
get reenrichmentButton() {
return document.querySelector('#reenrichment-form .reenrichment-button');
}
get form() {
return document.querySelector('#reenrichment-form');
}
}

View File

@@ -0,0 +1,51 @@
import {renderScoreDetails} from './DataRenderers.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {Tooltip} from './Tooltip.js?v=2';
export class ScoreDetails {
constructor(scoreParams) {
this.config = scoreParams;
this.loadData();
}
loadData() {
const onScoreDetailsLoaded = this.onScoreDetailsLoaded.bind(this);
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
$.ajax({
url: '/admin/scoreDetails',
type: 'get',
data: {userId: this.config.userId, token: token},
success: onScoreDetailsLoaded,
error: handleAjaxError,
});
}
onScoreDetailsLoaded(data, status) {
if ('success' !== status || 0 === data.length) {
return;
}
let el = this.contentDiv;
if (el) {
el.replaceChildren(renderScoreDetails(data));
}
this.initTooltips();
}
get card() {
return document.querySelector('.score-details');
}
get contentDiv() {
return this.card.querySelector('div.score-details-content');
}
initTooltips() {
Tooltip.addTooltipsToScoreDetails();
}
}

View File

@@ -0,0 +1,30 @@
import {fireEvent} from './utils/Event.js?v=2';
import {debounce} from './utils/Functions.js?v=2';
export class SearchFilter {
constructor() {
const onSearchInputChange = this.onSearchInputChange.bind(this);
const debouncedOnSearchInputChange = debounce(onSearchInputChange);
this.searchField.addEventListener('input', debouncedOnSearchInputChange, false);
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
}
onSearchInputChange({target}) {
const value = target.value;
fireEvent('searchFilterChanged', {query: value});
}
onDateFilterChanged() {
this.searchField.value = '';
}
getValue() {
return this.searchField.value;
}
get searchField() {
return document.getElementById('search');
}
}

View File

@@ -0,0 +1,120 @@
import {Loader} from './Loader.js?v=2';
import {Tooltip} from './Tooltip.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {padZero} from './utils/Date.js?v=2';
export class SearchLine {
constructor() {
this.loader = new Loader();
Tooltip.addTooltipsToClock();
const me = this;
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const url = `/admin/search?token=${token}`;
$('#auto-complete').autocomplete({
serviceUrl: url,
deferRequestBy: 300,
minChars: 3,
groupBy: 'category',
showNoSuggestionNotice: true,
noSuggestionNotice: 'Sorry, no matching results',
onSelect: function(suggestion) {
window.open(`/${suggestion.entityId}/${suggestion.id}`, '_self');
},
onSearchStart: function(params) {
params.query = params.query.trim();
me.loaderDiv.classList.remove('is-hidden');
me.loader.start(me.loaderDiv);
},
onSearchComplete: function(query, suggestions) {
me.loader.stop();
me.loaderDiv.classList.add('is-hidden');
},
onSearchError: handleAjaxError,
});
setInterval(this.updateTime.bind(this), 1000);
}
updateTime() {
let [time, tz] = this.timeInput.placeholder.split(' ');
let [h, m, s] = time.split(':').map(x => parseInt(x, 10));
let d = this.dayInput.placeholder;
s += 1;
if (s >= 60) {
s = 0;
m += 1;
}
if (m >= 60) {
m = 0;
h += 1;
}
if (h >= 24) {
h = 0;
d = parseInt(d, 10) + 1;
if (d >= 366) {
const now = new Date();
const year = now.getFullYear() - (now.getMonth() === 0 ? 1 : 0);
const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
if (d > 366 || (d === 366 && !isLeap)) {
d = 0;
}
}
this.dayInput.placeholder = (d < 10 ? '00' : (d < 100 ? '0' : '')) + d.toString();
}
h = padZero(h);
m = padZero(m);
s = padZero(s);
this.timeInput.placeholder = `${h}:${m}:${s} ${tz}`;
}
onTypeLinkClick(e) {
e.preventDefault();
this.queryTypeLinks.forEach(link => link.classList.remove('active'));
e.target.classList.add('active');
return false;
}
getActiveQueryTypeItem() {
const activeLink = this.queryTypeControl.querySelector('a.active');
const activeType = activeLink.dataset.value;
return activeType;
}
get loaderDiv() {
return document.querySelector('.searchline').querySelector('div.text-loader');
}
get timeInput() {
return document.getElementById('clock-time');
}
get dayInput() {
return document.getElementById('clock-day');
}
get queryTypeLinks() {
return this.queryTypeControl.querySelectorAll('A');
}
get queryTypeControl() {
return document.querySelector('nav.filtersForm.search');
}
}

View File

@@ -0,0 +1,70 @@
import {fireEvent} from './utils/Event.js?v=2';
export class SequentialLoad {
constructor(data, eventName = 'dateFilterChangedCompleted') {
this.objects = [];
this.eventName = eventName;
for (let i = 0; i < data.length; i++) {
data[i][1].sequential = true;
this.objects.push(new (data[i][0])(data[i][1]));
}
const me = this;
$(document).ready(() => {
me.startLoaders();
let i = 0;
const onReady = () => {
if (i >= me.objects.length) {
window.removeEventListener(eventName, onReady);
fireEvent('sequentialLoadCompleted');
return;
}
me.objects[i].loadData();
i++;
};
window.addEventListener(eventName, onReady);
onReady();
});
// catching filters change
const onFilterChanged = this.onFilterChanged.bind(this);
window.addEventListener('searchFilterChanged', onFilterChanged, false);
window.addEventListener('dateFilterChanged', onFilterChanged, false);
}
onFilterChanged() {
this.startLoaders();
let i = 0;
const onLoad = () => {
if (i >= this.objects.length) {
fireEvent('sequentialLoadCompleted');
window.removeEventListener(this.eventName, onLoad);
return;
}
this.objects[i].loadData();
i++;
};
window.addEventListener(this.eventName, onLoad);
onLoad();
}
startLoaders() {
this.objects.forEach(item => {
item.startLoader();
});
}
};

View File

@@ -0,0 +1,142 @@
import {
renderUserActionButtons,
renderUserReviewedStatus,
} from './DataRenderers.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {replaceAll} from './utils/String.js?v=2';
export class SingleReviewButton {
constructor(userId) {
this.userId = userId;
const me = this;
const onButtonClick = this.onButtonClick.bind(this);
if (me.legitFraudButtonsBlock) {
//Get HTML w/ new fraud&legit buttons
let fraud = null;
if ('true' == me.legitFraudButtonsBlock.dataset.userFraud) fraud = true;
if ('false' == me.legitFraudButtonsBlock.dataset.userFraud) fraud = false;
const record = {reviewed: true, accountid: me.userId, fraud: fraud};
me.legitFraudButtonsBlock.replaceChildren(renderUserActionButtons(record, false));
}
if (me.reviewedButton) {
this.reviewedButton.addEventListener('click', onButtonClick, false);
}
if (me.legitButton) {
this.legitButton.addEventListener('click', onButtonClick, false);
}
if (me.fraudButton) {
this.fraudButton.addEventListener('click', onButtonClick, false);
}
}
onButtonClick(e) {
e.preventDefault();
const me = this;
const target = e.target;
const url = '/admin/manageUser';
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const data = {userId: this.userId, type: target.dataset.type, token: token};
target.classList.add('is-loading');
$.ajax({
type: 'POST',
url: url,
data: data,
scope: me,
target: target,
success: me.onSuccess,
error: handleAjaxError,
dataType: 'json'
});
return false;
}
onSuccess() {
const me = this.scope;
const target = this.target;
const type = target.dataset.type;
target.classList.remove('is-loading');
if ('reviewed-button' === target.id) {
//Get HTML w/ new fraud&legit buttons
const record = {reviewed: true, accountid: me.userId};
const div = target.closest('div.head-button');
div.replaceChildren(renderUserActionButtons(record, false));
const onButtonClick = me.onButtonClick.bind(me);
if (me.legitButton) {
me.legitButton.addEventListener('click', onButtonClick, false);
}
if (me.fraudButton) {
me.fraudButton.addEventListener('click', onButtonClick, false);
}
}
const buttonType = target.dataset.buttonType;
if ('fraudButton' === buttonType) {
let reviewStatus = '';
if ('fraud' === type) {
reviewStatus = 'Blacklisted';
me.fraudButton.classList.replace('is-neutral', 'is-highlighted');
me.fraudButton.setAttribute('disabled', '');
me.legitButton.classList.replace('is-highlighted', 'is-neutral');
me.legitButton.removeAttribute('disabled');
} else {
reviewStatus = 'Whitelisted';
me.legitButton.classList.replace('is-neutral', 'is-highlighted');
me.legitButton.setAttribute('disabled', '');
me.fraudButton.classList.replace('is-highlighted', 'is-neutral');
me.fraudButton.removeAttribute('disabled');
}
const tile = document.querySelector('#user-id-tile');
const title = tile.querySelector('#review-status span').title;
const record = {
fraud: (reviewStatus === 'Blacklisted'),
latest_decision: title,
};
tile.querySelector('#review-status').replaceChildren(renderUserReviewedStatus(record));
const userTitleSpan = document.querySelector('h1 span');
userTitleSpan.textContent = (reviewStatus === 'Blacklisted') ? 'X' : 'OK';
userTitleSpan.classList.remove('high', 'medium', 'low', 'empty');
userTitleSpan.classList.add((reviewStatus === 'Blacklisted') ? 'low' : 'high');
}
}
get legitFraudButtonsBlock() {
return document.getElementById('legit-fraud-buttons-block');
}
get fraudButton() {
return document.querySelector('[data-type="fraud"]');
}
get legitButton() {
return document.querySelector('[data-type="legit"]');
}
get reviewedButton() {
return document.getElementById('reviewed-button');
}
}

View File

@@ -0,0 +1,30 @@
import {Loader} from './Loader.js?v=2';
export class StaticTiles {
constructor(tilesParams) {
const me = this;
this.config = tilesParams;
this.loaders = {};
this.config.elems.forEach(elem => {
me.loaders[elem] = new Loader();
});
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
this.runLoaders();
}
onDateFilterChanged() {
this.runLoaders();
}
runLoaders() {
for (const property in this.loaders) {
const el = document.querySelector(`.${property} .title`);
this.loaders[property].start(el);
}
}
}

View File

@@ -0,0 +1,48 @@
export class ThresholdsForm {
constructor() {
const updateReviewQueueOptions = this.updateReviewQueueOptions.bind(this);
this.blacklistOptions.forEach(radio => {
radio.addEventListener('change', updateReviewQueueOptions, false);
});
const updateBlacklistOptions = this.updateBlacklistOptions.bind(this);
this.reviewQueueOptions.forEach(radio => {
radio.addEventListener('change', updateBlacklistOptions, false);
});
}
updateReviewQueueOptions(e) {
const blacklistValue = this.blacklistVal;
this.reviewQueueOptions.forEach(radio => {
const value = parseInt(radio.value, 10);
radio.disabled = value <= blacklistValue;
});
}
updateBlacklistOptions(e) {
const reviewValue = this.reviewQueueVal;
this.blacklistOptions.forEach(radio => {
const value = parseInt(radio.value, 10);
radio.disabled = value >= reviewValue;
});
}
get reviewQueueOptions() {
return document.querySelectorAll('input[name="review-queue-threshold"]');
}
get blacklistOptions() {
return document.querySelectorAll('input[name="blacklist-threshold"]');
}
get reviewQueueVal() {
return parseInt(document.querySelector('input[name="review-queue-threshold"]:checked').value || 100, 10);
}
get blacklistVal() {
return parseInt(document.querySelector('input[name="blacklist-threshold"]:checked').value || -1, 10);
}
}

View File

@@ -0,0 +1,70 @@
export class Tooltip {
static init() {
this.addTooltipToSpans();
this.addTooltipToParagraphs();
this.addTooltipToTableHeaders();
}
static addTooltipsToEventDetailsPanel() {
this.baseTooltips('.details-card .tooltip', true);
}
static addTooltipsToScoreDetails() {
this.baseTooltips('.score-details-content .tooltip', true);
}
static addTooltipsToTiles() {
this.baseTooltips('span.detailsTileValue .tooltip', false);
}
static addTooltipsToGridRecords(tableId) {
this.baseTooltips(`#${tableId} td .tooltip.tooltipster-word-break`, true, true);
this.baseTooltips(`#${tableId} td .tooltip:not(.tooltipster-word-break)`, true);
}
static addTooltipToSpans() {
this.baseTooltips('span.tooltip', true);
}
static addTooltipToTableHeaders() {
this.baseTooltips('th.tooltip', true);
}
static addTooltipToParagraphs() {
this.baseTooltips('p.tooltip', true);
}
static addTooltipsToRulesProportion() {
this.baseTooltips('td .tooltip', true);
}
static addTooltipsToClock() {
this.baseTooltips('div .day-tile.tooltip, div .time-tile.tooltip', true);
}
static baseTooltips(path, useMaxWidth = true, wordBreak = false) {
$(document.querySelectorAll(path)).tooltipster(this.getConfig(useMaxWidth, wordBreak));
}
static getConfig(useMaxWidth, wordBreak) {
const config = {
delay: 0,
delayTouch: 0,
debug: false,
side: 'bottom',
animationDuration: 0,
theme: ['tooltipster-borderless'],
};
if (wordBreak) {
config.theme.push('tooltipster-word-break');
}
if (useMaxWidth) {
config['maxWidth'] = 250;
}
return config;
}
}

View File

@@ -0,0 +1,56 @@
import {
USER_IPS_CRITICAL_VALUE,
USER_EVENTS_CRITICAL_VALUE,
USER_DEVICES_CRITICAL_VALUE,
USER_COUNTRIES_CRITICAL_VALUE,
} from './utils/Constants.js?v=2';
export class TotalTile {
constructor() {
this.criticalValues = {
totalIps: USER_IPS_CRITICAL_VALUE,
totalEvents: USER_EVENTS_CRITICAL_VALUE,
totalDevices: USER_DEVICES_CRITICAL_VALUE,
totalCountries: USER_COUNTRIES_CRITICAL_VALUE
};
}
update(tableId, tileId, value) {
const tileCls = this.getTileClass(tableId, tileId, value);
const path = `.${tileId} .title`;
const el = document.querySelector(path);
if (el) {
el.classList.add('loaded');
el.classList.remove('loading');
//Remove previous class if exists
el.classList.remove('low');
el.classList.remove('medium');
el.classList.remove('high');
//Add new color class
el.classList.add(tileCls);
el.textContent = value;
}
}
getTileClass(tableId, tileId, value) {
const litmus = this.criticalValues[tileId];
const USER_ID = parseInt(window.location.pathname.replace('/id/', ''), 10);
const isUserPage = () => !isNaN(USER_ID);
if (!litmus || !isUserPage()) return;
let cls = null;
if (value >= litmus) {
cls = 'medium';
}
return cls;
}
}

View File

@@ -0,0 +1,134 @@
import {renderUserActionButtons} from './DataRenderers.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
export class UserGridActionButtons {
constructor(tableId) {
this.tableId = tableId;
const onTableLoaded = this.onTableLoaded.bind(this);
window.addEventListener('tableLoaded', onTableLoaded, false);
}
onTableLoaded(e) {
const tableId = e.detail.tableId;
const buttons = document.querySelectorAll(`#${tableId} button`);
const onButtonClick = this.onButtonClick.bind(this);
buttons.forEach(button => button.addEventListener('click', onButtonClick, false));
}
onButtonClick(e) {
e.preventDefault();
e.stopPropagation();
const me = this;
const target = e.target;
const url = '/admin/manageUser';
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const data = {userId: target.dataset.userId, type: target.dataset.type, token: token};
target.classList.add('is-loading');
$.ajax({
type: 'POST',
url: url,
data: data,
scope: me,
target: target,
success: me.onSuccess,
error: handleAjaxError,
dataType: 'json'
});
return false;
}
onSuccessCount(data) {
const span = document.querySelector('span.reviewed-users-tile');
span.textContent = data.total;
}
setMenuCount() {
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
$.ajax({
type: 'GET',
url: '/admin/loadReviewQueueCount',
data: {token: token},
success: this.onSuccessCount,
error: handleAjaxError,
});
}
onSuccess() {
const me = this.scope;
const target = this.target;
const type = target.dataset.type;
const buttonType = target.dataset.buttonType;
const accountId = target.dataset.userId;
const tableRow = target.closest('tr');
target.classList.remove('is-loading');
const twoButtonsContainer = target.closest('.legitfraud');
if (twoButtonsContainer && !twoButtonsContainer.hasAttribute('counterUpdated')) {
twoButtonsContainer.setAttribute('counterUpdated', 0);
}
if ('fraudButton' === buttonType) {
const td = target.closest('td');
const fraudButton = td.querySelector('[data-type="fraud"]');
const legitButton = td.querySelector('[data-type="legit"]');
if ('fraud' === type) {
fraudButton.classList.replace('is-neutral', 'is-highlighted');
fraudButton.setAttribute('disabled', '');
legitButton.classList.replace('is-highlighted', 'is-neutral');
legitButton.removeAttribute('disabled');
} else {
legitButton.classList.replace('is-neutral', 'is-highlighted');
legitButton.setAttribute('disabled', '');
fraudButton.classList.replace('is-highlighted', 'is-neutral');
fraudButton.removeAttribute('disabled');
}
const counterUpdated = twoButtonsContainer.getAttribute('counterUpdated');
const wasCounterUpdated = parseInt(counterUpdated, 10);
if (!wasCounterUpdated) {
const card = target.closest('.card');
const span = card.querySelector('.card-header-title span');
let total = parseInt(span.innerHTML, 10);
if (total > 0) {
total -= 1;
}
span.textContent = total;
twoButtonsContainer.setAttribute('counterUpdated', 1);
}
if (tableRow) {
const dataTable = $(`#${me.tableId}`).DataTable();
dataTable.row(tableRow).remove().draw(false);
me.setMenuCount();
}
}
if ('reviewedButton' === buttonType) {
//Get HTML w/ new fraud&legit buttons
const record = {reviewed: true, accountid: accountId};
const html = renderUserActionButtons(record);
const td = target.closest('td');
td.replaceChildren(html);
//Add event listener to newly created buttons
const buttons = td.querySelectorAll('button');
const onButtonClick = me.onButtonClick.bind(me);
buttons.forEach(button => button.addEventListener('click', onButtonClick, false));
}
}
}

View File

@@ -0,0 +1,142 @@
import {renderUserActionButtons} from './DataRenderers.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
import {replaceAll} from './utils/String.js?v=2';
export class WatchlistBlock {
constructor(userId) {
this.userId = userId;
const me = this;
const onButtonClick = this.onButtonClick.bind(this);
if (me.legitFraudButtonsBlock) {
//Get HTML w/ new fraud&legit buttons
let fraud = null;
if ('true' == me.legitFraudButtonsBlock.dataset.userFraud) fraud = true;
if ('false' == me.legitFraudButtonsBlock.dataset.userFraud) fraud = false;
const record = {reviewed: true, accountid: me.userId, fraud: fraud};
let html = renderUserActionButtons(record);
html = replaceAll(html, 'is-small', '');
me.legitFraudButtonsBlock.innerHTML = html;
}
if (me.reviewedButton) {
this.reviewedButton.addEventListener('click', onButtonClick, false);
}
if (me.legitButton) {
this.legitButton.addEventListener('click', onButtonClick, false);
}
if (me.fraudButton) {
this.fraudButton.addEventListener('click', onButtonClick, false);
}
this.watchListButton.addEventListener('click', onButtonClick, false);
}
onButtonClick(e) {
e.preventDefault();
const me = this;
const target = e.target;
const url = `/id/${this.userId}`;
const data = {type: target.dataset.type};
target.classList.add('is-loading');
$.ajax({
type: 'POST',
url: url,
data: data,
scope: me,
target: target,
success: me.onSuccess,
error: handleAjaxError,
dataType: 'json'
});
return false;
}
onSuccess() {
const me = this.scope;
const target = this.target;
const type = target.dataset.type;
target.classList.remove('is-loading');
if ('watchlist-button' === target.id) {
const oldClass = ('add' === type)? 'is-warning' : 'is-primary';
const newClass = ('add' === type)? 'is-primary' : 'is-warning';
target.classList.replace(oldClass, newClass);
target.dataset.type = ('add' === type)? 'remove' : 'add';
target.innerText = ('add' === type)? 'Remove from watchlist':'Add to watchlist';
}
if ('reviewed-button' === target.id) {
//Get HTML w/ new fraud&legit buttons
const record = {reviewed: true, accountid: me.userId};
let html = renderUserActionButtons(record);
html = replaceAll(html, 'is-small', '');
const div = target.closest('div.head-button');
div.innerHTML = html;
const onButtonClick = me.onButtonClick.bind(me);
if (me.legitButton) {
me.legitButton.addEventListener('click', onButtonClick, false);
}
if (me.fraudButton) {
me.fraudButton.addEventListener('click', onButtonClick, false);
}
}
const buttonType = target.dataset.buttonType;
if ('fraudButton' === buttonType) {
if ('fraud' === type) {
me.fraudButton.classList.replace('is-grey', 'is-warning');
me.fraudButton.setAttribute('disabled', '');
me.legitButton.classList.replace('is-warning', 'is-grey');
me.legitButton.removeAttribute('disabled');
} else {
me.fraudButton.classList.replace('is-warning', 'is-grey');
me.fraudButton.removeAttribute('disabled');
me.legitButton.classList.replace('is-grey', 'is-warning');
me.legitButton.setAttribute('disabled', '');
}
}
}
get legitFraudButtonsBlock() {
return document.getElementById('legit-fraud-buttons-block');
}
get watchListButton() {
return document.getElementById('watchlist-button');
}
get fraudButton() {
return document.querySelector('[data-type="fraud"]');
}
get legitButton() {
return document.querySelector('[data-type="legit"]');
}
get reviewedButton() {
return document.getElementById('reviewed-button');
}
}

View File

@@ -0,0 +1,53 @@
import {fireEvent} from './utils/Event.js?v=2';
import {handleAjaxError} from './utils/ErrorHandler.js?v=2';
export class WatchlistTags {
constructor() {
this.onRemoveUserTagClick = this.onRemoveUserTagClick.bind(this);
this.onUserTagRemoveCallback = this.onUserTagRemoveCallback.bind(this);
this.tags.forEach(tag => tag.addEventListener('click', this.onRemoveUserTagClick, false));
}
onRemoveUserTagClick(e) {
const wrapper = e.target.closest('.control');
const id = wrapper.querySelector('[data-id]').dataset.id;
const data = {
'userId': id,
'token': this.csrf,
};
const me = this;
$.ajax({
type: 'POST',
url: '/admin/removeWatchlisted',
data: data,
scope: me,
success: me.onUserTagRemoveCallback,
error: handleAjaxError,
dataType: 'json'
});
}
onUserTagRemoveCallback(response) {
if (response.success) {
const userId = response.userId;
const tag = document.querySelector(`[data-id="${userId}"]`);
const wrapper = tag.closest('.control');
wrapper.remove();
//TODO: fireevent reloadData();
fireEvent('watchlistTagRemoved', {});
}
}
get tags() {
return document.querySelectorAll('#important-users a.is-delete');
}
get csrf() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
}

View File

@@ -0,0 +1,210 @@
import {BaseChart} from './BaseChart.js?v=2';
import {COLOR_MAP} from '../utils/Constants.js?v=2';
import {formatIntTimeUtc} from '../utils/Date.js?v=2';
import {renderChartTooltipPart} from '../DataRenderers.js?v=2';
export class BaseBarChart extends BaseChart {
getSeries() {
return [
this.getDaySeries(),
{
width: -1,
paths: uPlot.paths.bars({size: [0.6, 100]}),
points: {show: false},
},
this.getSingleSeries('Regular events', 'green'),
this.getSingleSeries('Warning events', 'yellow'),
this.getSingleSeries('Alert events', 'red'),
];
}
getSingleSeries(label, color) {
return {
label: label,
width: -1,
drawStyle: 1,
fill: COLOR_MAP[color].main,
stroke: COLOR_MAP[color].main,
paths: uPlot.paths.bars({size: [0.6, 100]}),
points: {show: false},
};
}
// dataset adaption for bands instead of regular bars
getData(data) {
let stacked = [data[0]];
let sums = new Array(data[0].length).fill(0);
stacked.push(new Array(data[0].length).fill(0));
let maxLvl = data.length - 1;
for (let i = 1; i < data.length; i++) {
let series = [];
for (let j = 0; j < data[0].length; j++) {
sums[j] += +data[i][j];
series.push(sums[j]);
}
stacked.push(series);
}
maxLvl = data.length;
for (let i = 0; i < data[0].length; i++) {
let topMet = false;
for (let j = maxLvl; j > 1; j--) {
if (stacked[j][i] <= stacked[j-1][i] && !topMet) {
stacked[j][i] = null;
} else {
topMet = true;
}
}
}
this.data = stacked;
return stacked;
}
stack(data, omit) {
let data2 = [];
let bands = [];
let d0Len = data ? data[0].length : 0;
let accum = Array(d0Len);
let i;
for (i = 0; i < d0Len; i++) {
accum[i] = 0;
}
let el;
for (i = 1; i < data.length; i++) {
el = data[i];
if (!omit(i)) {
el = el.map((v, j) => {
let val = accum[j] + +v;
accum[j] = val;
return val;
});
}
data2.push(el);
}
for (i = 1; i < data.length; i++) {
!omit(i) && bands.push({
series: [
data.findIndex((s, j) => j > i && !omit(j)),
i,
],
});
}
bands = bands.filter(b => b.series[1] > -1);
return {
data: [data[0]].concat(data2),
bands,
};
}
getOptions(resolution = 'day', nullChar = '0') {
const opts = super.getOptions(resolution, nullChar);
let stacked = this.stack(this.data, i => false);
opts.bands = stacked.bands;
opts.series.forEach((s, sIdx) => {
if (s) {
s.value = (u, v, si, i) => u.data[sIdx][i];
s.points = s.points || {};
s.points.filter = (u, seriesIdx, show, gaps) => {
if (show) {
let pts = [];
u.data[seriesIdx].forEach((v, i) => {
v != null && pts.push(i);
});
return pts;
}
};
}
});
return opts;
}
tooltipCursor(u, seriestt, opts, resolution, defaultVal) {
const left = u.cursor.left;
const idx = u.cursor.idx;
const col = [];
if (opts && opts.cursorMemo) {
opts.cursorMemo.set(left, top);
}
seriestt.style.display = 'none';
if (left >= 0 && u.data) {
let xVal = u.data[0][idx];
let maxLvl = u.data.length - 1;
for (let i = 0; i <= maxLvl; i++) {
col.push(u.data[i][idx]);
}
const vtp = (resolution === 'day') ? 'DAY' : ((resolution === 'hour') ? 'HOUR' : 'MINUTE');
let ts = '';
if (Number.isInteger(xVal)) {
const useTime = resolution === 'hour' || resolution === 'minute';
ts = formatIntTimeUtc(xVal * 1000, useTime);
}
let frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode(ts.replace(/\./g, '/')));
let prev = null;
let maxVal = 0;
let maxIdx = 0;
for (let i = maxLvl; i >= 1; i--) {
if (col[i] === null) {
col[i] = 0;
} else {
if (maxVal < col[i]) {
maxVal = col[i];
maxIdx = i;
}
if (prev !== null) {
col[prev] -= col[i];
}
prev = i;
}
}
for (let i = 2; i <= maxLvl; i++) {
frag = this.extendTooltipFragment(i, null, col, defaultVal, u, frag);
}
if (frag.children.length > 1) {
seriestt.replaceChildren(frag);
seriestt.style.top = Math.round(u.valToPos(maxVal, u.series[maxIdx].scale)) + 'px';
seriestt.style.left = Math.round(u.valToPos(xVal, vtp)) + 'px';
seriestt.style.display = null;
}
}
return [seriestt, opts];
}
}

View File

@@ -0,0 +1,380 @@
import {Loader} from '../Loader.js?v=2';
import {getQueryParams} from '../utils/DataSource.js?v=2';
import {handleAjaxError} from '../utils/ErrorHandler.js?v=2';
import {formatIntTimeUtc} from '../utils/Date.js?v=2';
import {fireEvent} from '../utils/Event.js?v=2';
import {renderChartTooltipPart} from '../DataRenderers.js?v=2';
import {
MAX_HOURS_CHART,
MIN_HOURS_CHART,
X_AXIS_SERIFS,
} from '../utils/Constants.js?v=2';
export class BaseChart {
constructor(chartParams) {
this.config = chartParams;
this.cursLeft = -10;
this.cursTop = -10;
this.cursorMemo = {
set: (left, top) => {
this.cursLeft = left;
this.cursTop = top;
},
get: () => ({
left: this.cursLeft,
top: this.cursTop,
y: false,
drag: {
x: false,
y: false
}
})
};
this.timeLabelColor = '#d7e6e1';
this.loader = new Loader();
const loaderDiv = document.createElement('div');
loaderDiv.id = 'loader';
this.chartBlock.appendChild(loaderDiv);
this.chart = null;
if (!this.config.sequential) {
this.loadData();
}
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
}
onDateFilterChanged() {
this.loadData();
}
stopAnimation() {
this.loaderBlock.classList.add('is-hidden');
this.loader.stop();
}
startLoader() {
const el = document.createElement('p');
el.className = 'text-loader';
this.loaderBlock.classList.remove('is-hidden');
this.loaderBlock.replaceChildren(el);
const p = this.loaderBlock.querySelector('p');
this.loader.start(p);
}
loadData() {
if (!this.config.sequential) {
this.startLoader();
}
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const params = this.config.getParams();
const data = getQueryParams(params);
data['mode'] = params.mode;
data['token'] = token;
data['resolution'] = 'day';
if (data['dateFrom']) {
const diff = new Date(data['dateTo']) - new Date(data['dateFrom']);
const hours = diff/(60 * 60 * 1000);
if (hours <= MAX_HOURS_CHART && hours > MIN_HOURS_CHART) {
data['resolution'] = 'hour';
} else if (hours <= MIN_HOURS_CHART) {
data['resolution'] = 'minute';
}
}
fireEvent('dateFilterChangedCaught');
$.ajax({
url: '/admin/loadChart',
type: 'get',
data: data,
success: (responseData, status) => this.onChartLoaded(responseData, status, data['resolution']),
error: handleAjaxError,
complete: function() {
fireEvent('dateFilterChangedCompleted');
},
});
}
onChartLoaded(data, status, resolution) {
if ('success' == status) {
if (this.chart) {
this.chart.destroy();
}
const prepData = this.getData(data);
this.chart = new uPlot(this.getOptions(resolution), prepData, this.chartBlock);
this.stopAnimation();
}
}
seriesResolutionShift(series, resolution) {
if (resolution === 'hour') {
series[0].label = 'Hour';
series[0].scale = 'HOUR';
series[0].value = '{YYYY}-{MM}-{DD} {HH}:{mm}';
} else if (resolution === 'minute') {
series[0].label = 'Minute';
series[0].scale = 'MINUTE';
series[0].value = '{YYYY}-{MM}-{DD} {HH}:{mm}';
}
return series;
}
getDaySeries() {
return {
label: 'Day',
scale: 'DAY',
value: '{YYYY}-{MM}-{DD}'
};
}
getAxisConfig() {
const xAxis = {
scale: 'DAY',
stroke: '#8180a0',
grid: {
width: 1 / devicePixelRatio,
stroke: '#2b2a3d',
},
ticks: {
width: 1 / devicePixelRatio,
stroke: '#2b2a3d',
},
values: [
//Copied from https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
// tick incr default year month day hour min sec mode
[3600 * 24, '{DD}/{MM}', '\n{YYYY}', null, null, null, null, null, 1],
],
};
const yAxis = {
stroke: '#8180a0',
values: (u, vals, space) => vals.map(v => this.formatKiloValue(u, v)),
grid: {
width: 1 / devicePixelRatio,
stroke: '#2b2a3d',
},
ticks: {
width: 1 / devicePixelRatio,
stroke: '#2b2a3d',
},
};
return {
x: xAxis,
y: yAxis,
};
}
getData(data) {
return data;
}
getOptions(resolution = 'day', nullChar = '0') {
const tooltipsPlugin = this.tooltipsPlugin({cursorMemo: this.cursorMemo}, resolution, nullChar);
const axes = this.getAxisConfig();
const series = this.seriesResolutionShift(this.getSeries(), resolution);
const xAxis = this.xAxisResolutionShift(axes.x, resolution);
const yAxis = axes.y;
const opts = {
width: 995,
height: 200,
tzDate: ts => uPlot.tzDate(new Date(ts * 1000), 'Etc/UTC'),
series: series,
legend: {
show: false
},
cursor: this.cursorMemo.get(),
plugins: [tooltipsPlugin],
scales: {
x: {time: false},
},
axes: [
xAxis,
yAxis,
]
};
return opts;
}
xAxisResolutionShift(xAxis, resolution) {
if (resolution === 'hour') {
xAxis.scale = 'HOUR';
xAxis.values = [
// tick incr default year month day hour min sec mode
[3600, '{HH}:{mm}', '\n{DD}/{MM}/{YYYY}', null, '\n{DD}/{MM}', null, null, null, 1]
];
xAxis.space = function(self, axisIdx, scaleMin, scaleMax, plotDim) {
let rangeHours = (scaleMax - scaleMin) / 3600;
if (rangeHours > X_AXIS_SERIFS) rangeHours = X_AXIS_SERIFS;
const pxPerHour = plotDim / rangeHours;
return pxPerHour;
};
} else if (resolution === 'minute') {
xAxis.scale = 'MINUTE';
xAxis.values = [
// tick incr default year month day hour min sec mode
[60, '{HH}:{mm}', '\n{DD}/{MM}/{YYYY}', null, '\n{DD}/{MM}', null, null, null, 1]
];
xAxis.space = function(self, axisIdx, scaleMin, scaleMax, plotDim) {
let rangeMinutes = (scaleMax - scaleMin) / 60;
if (rangeMinutes > X_AXIS_SERIFS) rangeMinutes = X_AXIS_SERIFS;
const pxPerMinute = plotDim / rangeMinutes;
return pxPerMinute;
};
}
return xAxis;
}
formatKiloValue(u, value) {
if (value === 0) {
return value;
}
if (value % 1000000 === 0) {
return Math.round(value / 1000000) + 'M';
}
if (value % 1000 === 0) {
return Math.round(value / 1000) + 'k';
}
return value;
}
get loaderBlock() {
return document.getElementById('loader');
}
get chartBlock() {
return document.querySelector('.stat-chart:not(#session-stat)');
}
tooltipsPlugin(opts, resolution = 'day', defaultVal = '0') {
let self = this;
let seriestt;
function init(u, options, data) {
seriestt = self.tooltipInit(u, options, data);
}
function setCursor(u) {
[seriestt, opts] = self.tooltipCursor(u, seriestt, opts, resolution, defaultVal);
}
return {
hooks: {
init,
setCursor,
},
};
}
tooltipCursor(u, seriestt, opts, resolution, defaultVal) {
const left = u.cursor.left;
const idx = u.cursor.idx;
if (opts && opts.cursorMemo) {
opts.cursorMemo.set(left, top);
}
seriestt.style.display = 'none';
if (left >= 0) {
let xVal = u.data[0][idx];
const vtp = (resolution === 'day') ? 'DAY' : ((resolution === 'hour') ? 'HOUR' : 'MINUTE');
let ts = '';
if (Number.isInteger(xVal)) {
const useTime = resolution === 'hour' || resolution === 'minute';
ts = formatIntTimeUtc(xVal * 1000, useTime);
}
let frag = document.createDocumentFragment();
frag.appendChild(renderChartTooltipPart(this.timeLabelColor, null, ts.replace(/\./g, '/')));
for (let i = 1; i <= 12; i++) {
frag = this.extendTooltipFragment(i, idx, u.data, defaultVal, u, frag);
}
if (frag.children.length > 1) {
seriestt.replaceChildren(frag);
let val = null;
let lvl = 1;
const lim = Math.min(u.data.length - 1, 12);
for (let i = 1; i <= lim; i++) {
if (u.data[i][idx] > val) {
val = u.data[i][idx];
lvl = i;
}
}
val = (val !== null && val != undefined) ? val : defaultVal;
seriestt.style.top = Math.round(u.valToPos(val, u.series[lvl].scale)) + 'px';
seriestt.style.left = Math.round(u.valToPos(xVal, vtp)) + 'px';
seriestt.style.display = null;
}
}
return [seriestt, opts];
}
tooltipInit(u, options, data) {
let over = u.over;
let tt = document.createElement('div');
tt.className = 'tooltipline';
tt.textContent = '';
tt.style.pointerEvents = 'none';
tt.style.position = 'absolute';
tt.style.background = 'rgba(0,0,0,1)';
over.appendChild(tt);
over.addEventListener('mouseleave', () => {
if (!u.cursor._lock) {
tt.style.display = 'none';
}
});
over.addEventListener('mouseenter', () => {
tt.style.display = u.data.length > 1 ? null : 'none';
});
tt.style.display = (u.cursor.left < 0) ? 'none' : null;
return tt;
}
extendTooltipFragment(lvl, idx, data, defaultVal, u, frag) {
if (data.length > lvl) {
let series = u.series[lvl];
let val = (idx !== null) ? data[lvl][idx] : data[lvl];
val = (val !== null && val != undefined) ? val : defaultVal;
frag.appendChild(document.createElement('br'));
frag.appendChild(renderChartTooltipPart(series.stroke(), series.label, val));
}
return frag;
}
}

View File

@@ -0,0 +1,74 @@
import {BaseChart} from './BaseChart.js?v=2';
import {
COLOR_MAP,
X_AXIS_SERIFS,
} from '../utils/Constants.js?v=2';
export class BaseLineChart extends BaseChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Total events', 'green'),
];
}
getSingleSeries(label, color) {
return {
label: label,
scale: 'EVENTS',
value: (u, v) => Number(v.toFixed(0)).toLocaleString(),
points: {
space: 0,
fill: COLOR_MAP[color].main,
},
stroke: COLOR_MAP[color].main,
fill: COLOR_MAP[color].light,
};
}
getAxisConfig() {
const axes = super.getAxisConfig();
axes.x.space = function(self, axisIdx, scaleMin, scaleMax, plotDim) {
let rangeDays = (scaleMax - scaleMin) / 86400;
if (rangeDays > X_AXIS_SERIFS) rangeDays = X_AXIS_SERIFS;
const pxPerDay = plotDim / rangeDays;
return pxPerDay;
};
axes.y.scale = 'EVENTS';
axes.y.side = 3;
axes.y.split = u => [
u.series[1].min,
u.series[1].max,
];
return axes;
}
getOptions(resolution = 'day') {
return super.getOptions(resolution, '—');
}
// invert lines order to keep originally first line on top layer
seriesResolutionShift(series, resolution) {
if (resolution === 'hour') {
series[0].label = 'Hour';
series[0].scale = 'HOUR';
series[0].value = '{YYYY}-{MM}-{DD} {HH}:{mm}';
} else if (resolution === 'minute') {
series[0].label = 'Minute';
series[0].scale = 'MINUTE';
series[0].value = '{YYYY}-{MM}-{DD} {HH}:{mm}';
}
const inverted = [series[0]].concat(series.slice(1).reverse());
return inverted;
}
getData(data) {
return [data[0]].concat(data.slice(1).reverse());
}
}

View File

@@ -0,0 +1,137 @@
import {Loader} from '../Loader.js?v=2';
import {BaseChart} from './BaseChart.js?v=2';
import {
COLOR_LIGHT_GREEN,
COLOR_GREEN,
X_AXIS_SERIFS,
} from '../utils/Constants.js?v=2';
export class BaseSparklineChart extends BaseChart {
constructor(chartParams) {
super(chartParams);
this.charts = null;
if (!this.loaders) {
this.loaders = [];
this.elems.forEach(el => {this.loaders[el] = new Loader();});
}
}
getOptions() {
const tooltipsPlugin = this.tooltipsPlugin({cursorMemo: this.cursorMemo}, 'day', '0');
return {
width: 200,
height: 30,
pxAlign: false,
cursor: {
show: false
},
select: {
show: false,
},
legend: {
show: false,
},
scales: {
x: {time: false},
},
axes: [
{show: false},
{show: false}
],
cursor: this.cursorMemo.get(),
plugins: [tooltipsPlugin],
series: [
{
label: 'Day',
scale: 'DAY',
value: '{YYYY}-{MM}-{DD}',
stroke: '#8180a0',
},
{
label: 'This week',
stroke: COLOR_GREEN,
fill: COLOR_LIGHT_GREEN,
points: {show: false}
},
{
label: 'Previous week',
stroke: 'rgba(129,128,160,0.7)',
fill: 'rgba(129,128,160,0.03)',
points: {show: false}
},
],
};
}
onChartLoaded(data, status, resolution) {
if ('success' == status) {
data = this.getData(data);
this.stopLoader();
this.charts = [];
this.elems.forEach(el => {
const lines = [data.time, data[el], data[el + 'Prev']];
this.charts.push(new uPlot(this.getOptions(), lines, this.getChartBlock(el)));
});
}
}
startLoader() {
if (!this.loaders) {
this.loaders = [];
this.elems.forEach(el => this.loaders[el] = new Loader());
}
this.elems.forEach(name => {
const el = document.createElement('p');
const block = this.getChartBlock(name);
block.classList.remove('is-hidden');
block.replaceChildren(el);
const p = block.querySelector('p');
this.loaders[name].start(p);
});
}
stopLoader() {
this.elems.forEach(el => {
this.loaders[el].stop();
this.getChartBlock(el).querySelector('p').classList.add('is-hidden');
});
}
getData(data) {
return {
'time': data[0],
'totalDevices': data[1],
'totalIps': data[2],
'totalSessions': data[3],
'totalEvents': data[4],
'totalDevicesPrev': data[5],
'totalIpsPrev': data[6],
'totalSessionsPrev': data[7],
'totalEventsPrev': data[8],
};
}
getChartBlock(cls) {
return document.querySelector(`td.${cls} p.session-stat`);
}
get chartBlocks() {
const result = {};
this.elems.forEach(el => {result[el] = this.getChartBlock(el);});
return result;
}
get elems() {
return ['totalDevices', 'totalIps', 'totalSessions', 'totalEvents'];
}
}

View File

@@ -0,0 +1,10 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class BlacklistChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Blacklisted identities', 'red'),
];
}
}

View File

@@ -0,0 +1,10 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class BotsChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Bots', 'red'),
];
}
}

View File

@@ -0,0 +1,11 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class DomainsChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Total domains', 'green'),
this.getSingleSeries('New domains', 'yellow'),
];
}
}

View File

@@ -0,0 +1,13 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class EventsChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Regular events', 'green'),
this.getSingleSeries('Warning events', 'yellow'),
this.getSingleSeries('Alert events', 'red'),
this.getSingleSeries('Unauthenticated events', 'purple'),
];
}
}

View File

@@ -0,0 +1,12 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class IpsChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Residential', 'green'),
this.getSingleSeries('Privacy', 'yellow'),
this.getSingleSeries('Suspicious', 'red'),
];
}
}

View File

@@ -0,0 +1,11 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class IspsChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Total ISPs', 'green'),
this.getSingleSeries('New ISPs', 'yellow'),
];
}
}

View File

@@ -0,0 +1,12 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class LogbookChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Success', 'green'),
this.getSingleSeries('Validation issues', 'yellow'),
this.getSingleSeries('Failed', 'red'),
];
}
}

View File

@@ -0,0 +1,12 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class ResourcesChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('200', 'green'),
this.getSingleSeries('404', 'yellow'),
this.getSingleSeries('403 & 500', 'red'),
];
}
}

View File

@@ -0,0 +1,12 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class ReviewQueueChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('Whitelisted', 'green'),
this.getSingleSeries('In review', 'yellow'),
this.getSingleSeries('Blacklisted', 'red'),
];
}
}

View File

@@ -0,0 +1,12 @@
import {BaseLineChart} from './BaseLine.js?v=2';
export class UsersChart extends BaseLineChart {
getSeries() {
return [
this.getDaySeries(),
this.getSingleSeries('High trust', 'green'),
this.getSingleSeries('Average trust', 'yellow'),
this.getSingleSeries('In review', 'red'),
];
}
}

View File

@@ -0,0 +1,43 @@
import {fireEvent} from '../utils/Event.js?v=2';
export class BaseFilter {
constructor(selectorId, renderItemFn, renderChoiceFn, eventType) {
this.selectorId = selectorId;
this.renderItemFn = renderItemFn;
this.renderChoiceFn = renderChoiceFn;
this.eventType = eventType;
const renderItem = renderItemFn;
const renderChoice = renderChoiceFn;
const choices = new Choices(`${this.selectorId} select`, {
removeItemButton: true,
allowHTML: true,
callbackOnCreateTemplates: function(strToEl) {
const {classNames, itemSelectText} = this.config;
return {
item: function({classNames}, data) {
return strToEl(renderItem(classNames, data));
},
choice: function({classNames}, data) {
return strToEl(renderChoice(classNames, data, itemSelectText));
},
};
}
});
choices.passedElement.element.addEventListener(
'change',
() => fireEvent(this.eventType)
);
}
getValues() {
return Array.from(document.querySelector(`${this.selectorId} select`).options)
.filter(option => option.selected)
.map(option => option.value);
}
getEventType() {
return this.eventType;
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderDeviceTypeSelectorItem,
renderDeviceTypeSelectorChoice,
} from '../DataRenderers.js?v=2';
export class DeviceTypeFilter extends BaseFilter {
constructor() {
super(
'#device-type-selectors',
renderDeviceTypeSelectorItem,
renderDeviceTypeSelectorChoice,
'deviceTypeFilterChanged'
);
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderEntityTypeSelectorItem,
renderEntityTypeSelectorChoice,
} from '../DataRenderers.js?v=2';
export class EntityTypeFilter extends BaseFilter {
constructor() {
super(
'#entity-type-selectors',
renderEntityTypeSelectorItem,
renderEntityTypeSelectorChoice,
'entityTypeFilterChanged'
);
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderEventTypeSelectorItem,
renderEventTypeSelectorChoice,
} from '../DataRenderers.js?v=2';
export class EventTypeFilter extends BaseFilter {
constructor() {
super(
'#event-type-selectors',
renderEventTypeSelectorItem,
renderEventTypeSelectorChoice,
'eventTypeFilterChanged'
);
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderIpTypeSelectorItem,
renderIpTypeSelectorChoice,
} from '../DataRenderers.js?v=2';
export class IpTypeFilter extends BaseFilter {
constructor() {
super(
'#ip-type-selectors',
renderIpTypeSelectorItem,
renderIpTypeSelectorChoice,
'ipTypeFilterChanged'
);
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderRuleSelectorItem,
renderRuleSelectorChoice,
} from '../DataRenderers.js?v=2';
export class RulesFilter extends BaseFilter {
constructor() {
super(
'#rule-selectors',
renderRuleSelectorItem,
renderRuleSelectorChoice,
'rulesFilterChanged'
);
}
}

View File

@@ -0,0 +1,16 @@
import {BaseFilter} from './BaseFilter.js?v=2';
import {
renderScoresRangeSelectorItem,
renderScoresRangeSelectorChoice,
} from '../DataRenderers.js?v=2';
export class ScoresRangeFilter extends BaseFilter {
constructor() {
super(
'#scores-range-selectors',
renderScoresRangeSelectorItem,
renderScoresRangeSelectorChoice,
'scoresRangeFilterChanged'
);
}
}

View File

@@ -0,0 +1,77 @@
import {Loader} from '../Loader.js?v=2';
import {Tooltip} from '../Tooltip.js?v=2';
import {handleAjaxError} from '../utils/ErrorHandler.js?v=2';
import {fireEvent} from '../utils/Event.js?v=2';
export class BaseTiles {
constructor(tilesParams) {
const me = this;
this.config = tilesParams;
this.loaders = {};
this.elems.forEach(elem => {
me.loaders[elem] = new Loader();
});
if (!this.config.sequential) {
this.loadData();
}
}
loadData() {
const me = this;
const url = this.url;
const params = this.config.getParams();
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
if (!this.config.sequential) {
this.startLoaders();
}
fireEvent('dateFilterChangedCaught');
$.ajax({
url: `${url}?token=${token}`,
type: 'get',
scope: me,
data: params,
success: function(response) {
me.onLoad(response, 'success');
me.initTooltips();
},
error: handleAjaxError,
});
}
stopLoaders() {
for (const property in this.loaders) {
this.loaders[property].stop();
}
}
startLoaders() {
for (const property in this.loaders) {
const el = document.querySelector(`#${property}`);
this.loaders[property].start(el);
}
}
startLoader() {
this.startLoaders();
}
onLoad(data, status) {
if ('success' == status) {
this.stopLoaders();
this.updateTiles(data);
fireEvent('dateFilterChangedCompleted');
}
}
initTooltips() {
Tooltip.addTooltipsToTiles();
}
updateTiles(data) {}
}

View File

@@ -0,0 +1,41 @@
import {BaseTiles} from './BaseTiles.js?v=2';
import {
renderBoolean,
renderDefaultIfEmptyElement,
renderBrowser,
renderOs,
} from '../DataRenderers.js?v=2';
const URL = '/admin/loadBotDetails';
const ELEMS = ['title', 'os', 'browser', 'modified'];
export class BotTiles extends BaseTiles {
updateTiles(data) {
const os = [];
if (data.os_name) os.push(data.os_name);
if (data.os_version) os.push(data.os_version);
const browser = [];
if (data.browser_name) browser.push(data.browser_name);
if (data.browser_version) browser.push(data.browser_version);
const record = {
os: os.join(' '),
browser: browser.join(' ')
};
document.getElementById('title').replaceChildren(renderDefaultIfEmptyElement(data.title));
document.getElementById('os').replaceChildren(renderOs(record));
document.getElementById('browser').replaceChildren(renderBrowser(record));
document.getElementById('modified').replaceChildren(renderBoolean(data.modified));
}
get elems() {
return ELEMS;
}
get url() {
return URL;
}
}

View File

@@ -0,0 +1,33 @@
import {BaseTiles} from './BaseTiles.js?v=2';
import {
renderBoolean,
renderDefaultIfEmptyElement,
renderDate,
} from '../DataRenderers.js?v=2';
const URL = '/admin/loadDomainDetails';
const ELEMS = [
'free-email', 'tranco-rank', 'unavailable', 'disposable',
'creation-date', 'expiration-date', 'total-account', 'fraud'];
export class DomainTiles extends BaseTiles {
updateTiles(data) {
document.getElementById('free-email').replaceChildren(renderBoolean(data.free_email_provider));
document.getElementById('tranco-rank').replaceChildren(renderDefaultIfEmptyElement(data.tranco_rank));
document.getElementById('unavailable').replaceChildren(renderBoolean(data.disabled));
document.getElementById('disposable').replaceChildren(renderBoolean(data.disposable_domains));
document.getElementById('creation-date').replaceChildren(renderDate(data.creation_date));
document.getElementById('expiration-date').replaceChildren(renderDate(data.expiration_date));
document.getElementById('total-account').replaceChildren(data.total_account);
document.getElementById('fraud').replaceChildren(data.fraud);
}
get elems() {
return ELEMS;
}
get url() {
return URL;
}
}

View File

@@ -0,0 +1,44 @@
import {BaseTiles} from './BaseTiles.js?v=2';
import {Tooltip} from '../Tooltip.js?v=2';
import {
renderBoolean,
renderClickableCountryTruncated,
renderClickableAsn,
} from '../DataRenderers.js?v=2';
const URL = '/admin/loadIpDetails';
const ELEMS = ['country', 'asn', 'blocklist', 'blacklist', 'dc', 'vpn', 'tor', 'ar'];
export class IpTiles extends BaseTiles {
updateTiles(data) {
const record = {
full_country: data.full_country,
country_id: data.country_id,
country_iso: data.country_iso,
asn: data.asn,
ispid: data.ispid,
};
document.getElementById('country').replaceChildren(renderClickableCountryTruncated(record));
document.getElementById('asn').replaceChildren(renderClickableAsn(record));
document.getElementById('blocklist').replaceChildren(renderBoolean(data.blocklist));
document.getElementById('blacklist').replaceChildren(renderBoolean(data.fraud_detected));
document.getElementById('dc').replaceChildren(renderBoolean(data.data_center));
document.getElementById('vpn').replaceChildren(renderBoolean(data.vpn));
document.getElementById('tor').replaceChildren(renderBoolean(data.tor));
document.getElementById('ar').replaceChildren(renderBoolean(data.relay));
}
initTooltips() {
super.initTooltips();
Tooltip.addTooltipToSpans();
}
get elems() {
return ELEMS;
}
get url() {
return URL;
}
}

View File

@@ -0,0 +1,23 @@
import {BaseTiles} from './BaseTiles.js?v=2';
import {renderAsn} from '../DataRenderers.js?v=2';
const URL = '/admin/loadIspDetails';
const ELEMS = ['asn', 'total-ips', 'total-visits', 'total-accounts', 'total-fraud'];
export class IspTiles extends BaseTiles {
updateTiles(data) {
document.getElementById('asn').replaceChildren(renderAsn(data));
document.getElementById('total-accounts').replaceChildren(data.total_account);
document.getElementById('total-visits').replaceChildren(data.total_visit);
document.getElementById('total-fraud').replaceChildren(data.total_fraud);
document.getElementById('total-ips').replaceChildren(data.total_ip);
}
get elems() {
return ELEMS;
}
get url() {
return URL;
}
}

View File

@@ -0,0 +1,133 @@
import {BaseTiles} from './BaseTiles.js?v=2';
import {
renderDate,
renderBoolean,
renderUserCounter,
renderReputation,
renderUserId,
renderUserFirstname,
renderUserLastname,
renderUserReviewedStatus,
} from '../DataRenderers.js?v=2';
const URL = '/admin/loadUserDetails';
export class UserTiles extends BaseTiles {
updateTiles(data) {
this.updateIdDetails(data);
this.updateIpDetails(data);
this.updateDayDetails(data);
this.updateWeekDetails(data);
}
updateIdDetails(data) {
const tile = document.querySelector('#user-id-tile');
if (!tile) {
return;
}
const record = data.userDetails;
this.removeLoaderBackground(tile);
tile.querySelector('#signup-date').replaceChildren(renderDate(record.created));
tile.querySelector('#lastseen').replaceChildren(renderDate(record.lastseen));
tile.querySelector('#latest-decision').replaceChildren(renderDate(record.latest_decision));
tile.querySelector('#review-status').replaceChildren(renderUserReviewedStatus(record));
tile.querySelector('#firstname').replaceChildren(renderUserFirstname(record));
tile.querySelector('#lastname').replaceChildren(renderUserLastname(record));
tile.querySelector('#userid').replaceChildren(renderUserId(record.userid));
}
updateIpDetails(data) {
const tile = document.querySelector('#user-ip-tile');
if (!tile) {
return;
}
const record = data.ipDetails;
this.removeLoaderBackground(tile);
tile.querySelector('#datacenter').replaceChildren(renderBoolean(record.withdc));
tile.querySelector('#vpn').replaceChildren(renderBoolean(record.withvpn));
tile.querySelector('#tor').replaceChildren(renderBoolean(record.withtor));
tile.querySelector('#apple-relay').replaceChildren(renderBoolean(record.withar));
tile.querySelector('#ip-shared').replaceChildren(renderBoolean(record.sharedips));
tile.querySelector('#spam-list').replaceChildren(renderBoolean(record.spamlist));
tile.querySelector('#blacklisted').replaceChildren(renderBoolean(record.fraud_detected));
}
updateDayDetails(data) {
const limits = {
median_event_cnt: 20,
login_cnt: 3,
session_cnt: 5,
};
this.updateDateRangeDetails(data.dayDetails, '#day-behaviour-tile', limits);
}
updateWeekDetails(data) {
const limits = {
median_event_cnt: 20,
login_cnt: 10,
session_cnt: 25,
};
this.updateDateRangeDetails(data.weekDetails, '#week-behaviour-tile', limits);
}
updateDateRangeDetails(record, tileId, limits) {
const tile = document.querySelector(tileId);
if (!tile) {
return;
}
const span = document.createElement('span');
span.className = 'nolight';
span.textContent = 'N/A';
this.removeLoaderBackground(tile);
if (record.session_cnt === 0) {
tile.querySelector('#failed-login-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#password-reset-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#auth-error-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#off-hours-login-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#median-event-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#login-count').replaceChildren(span.cloneNode(true));
tile.querySelector('#session-count').replaceChildren(span.cloneNode(true));
} else {
const map = [
['#failed-login-count', 'failed_login_cnt'],
['#password-reset-count', 'password_reset_cnt'],
['#auth-error-count', 'auth_error_cnt'],
['#off-hours-login-count', 'off_hours_login_cnt'],
['#median-event-count', 'median_event_cnt'],
['#login-count', 'login_cnt'],
['#session-count', 'session_cnt'],
];
for (const [id, el] of map) {
tile.querySelector(id).replaceChildren(renderUserCounter(record[el], limits[el] || 1));
}
}
}
removeLoaderBackground(tile) {
const backgrounds = tile.querySelectorAll('.loading-background');
for (let i = 0; i < backgrounds.length; i++) {
backgrounds[i].classList.remove('loading-background');
}
}
get elems() {
return [];
}
get url() {
return URL;
}
}

View File

@@ -0,0 +1,466 @@
import {Loader} from '../Loader.js?v=2';
import {Tooltip} from '../Tooltip.js?v=2';
import {fireEvent} from '../utils/Event.js?v=2';
import {getQueryParams} from '../utils/DataSource.js?v=2';
import {handleAjaxError} from '../utils/ErrorHandler.js?v=2';
import {TotalTile} from '../TotalTile.js?v=2';
import {renderTotalFrame} from '../DataRenderers.js?v=2';
import {
MIDLINE_HELLIP,
} from '../utils/Constants.js?v=2';
export class BaseGrid {
constructor(gridParams) {
this.config = gridParams;
this.loader = new Loader();
this.tooltip = new Tooltip();
this.totalTile = new TotalTile();
this.firstload = true;
this.renderTotalsLoader = this.renderTotalsLoader.bind(this);
this.initLoad();
if (this.config.dateRangeGrid && !this.config.sequential) {
const onDateFilterChanged = this.onDateFilterChanged.bind(this);
window.addEventListener('dateFilterChanged', onDateFilterChanged, false);
}
if (gridParams.choicesFilterEvents) {
const onChoicesFilterChanged = this.onChoicesFilterChanged.bind(this);
for (let i = 0; i < gridParams.choicesFilterEvents.length; i++) {
window.addEventListener(gridParams.choicesFilterEvents[i], onChoicesFilterChanged, false);
}
}
if (!this.config.sequential) {
const onSearchFilterChanged = this.onSearchFilterChanged.bind(this);
window.addEventListener('searchFilterChanged', onSearchFilterChanged, false);
}
}
initLoad() {
const me = this;
const tableId = this.config.tableId;
$(document).ready(() => {
$.extend($.fn.dataTable.ext.classes, {
sStripeEven: '', sStripeOdd: ''
});
$.fn.dataTable.ext.pager.numbers_length = 9;
$.fn.dataTable.ext.errMode = function() {};
$(`#${tableId}`).on('error.dt', me.onError);
const onBeforeLoad = me.onBeforeLoad.bind(me);
$(`#${tableId}`).on('preXhr.dt', onBeforeLoad);
const onBeforePageChange = me.onBeforePageChange.bind(me);
$(`#${tableId}`).on('page.dt', onBeforePageChange);
const config = me.getDataTableConfig();
$(`#${tableId}`).DataTable(config);
const onTableRowClick = me.onTableRowClick.bind(me);
$(`#${tableId} tbody`).on('click', 'tr', onTableRowClick);
const onDraw = me.onDraw.bind(me);
$(`#${tableId}`).on('draw.dt', onDraw);
$(`#${tableId}`).closest('.dt-container').find('nav').empty();
document.getElementById(tableId).classList.add('hide-body');
if (!me.config.sequential) {
me.loadData();
}
});
}
getDataTableConfig() {
const me = this;
const url = this.config.url;
const columns = this.columns;
const columnDefs = this.columnDefs;
const isSortable = this.getConfigParam('isSortable');
const order = this.orderConfig;
const config = {
ajax: function(data, callback, settings) {
$.ajax({
url: url,
method: 'GET',
data: data,
dataType: 'json',
success: function(response, textStatus, jqXHR) {
callback(response);
me.performAdditional(response, me.config);
me.stopAnimation();
},
error: handleAjaxError,
});
},
processing: true,
serverSide: true,
deferRender: true,
deferLoading: 0,
pageLength: 25,
autoWidth: false,
lengthChange: false,
searching: true,
ordering: isSortable,
info: false,
pagingType: 'simple_numbers',
language: {
paginate: {
previous: '&lt;',
next: '&gt;',
},
},
layout: {
topEnd: null,
bottomEnd: {
paging: {
boundaryNumbers: false,
type: 'simple_numbers',
},
},
},
createdRow: function(row, data, dataIndex) {
$(row).attr('data-item-id', data.id);
},
drawCallback: function(settings) {
me.drawCallback(settings);
me.updateTableFooter(this);
},
columnDefs: columnDefs,
columns: columns,
order: order
};
return config;
}
performAdditional(response, config) {
if (!config.calculateTotals) {
fireEvent('dateFilterChangedCompleted');
return;
}
const ids = response.data.map(item => item.id);
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
const dateRange = response.dateRange;
if (dateRange && ids.length) {
const requestData = {
token: token,
ids: ids,
type: config.totals.type,
startDate: dateRange.startDate,
endDate: dateRange.endDate,
};
let preparedBase = {};
response.data.forEach(rec => {
preparedBase[rec.id] = rec;
});
$.ajax({
type: 'GET',
url: '/admin/timeFrameTotal',
data: requestData,
success: (data) => this.onTotalsSuccess(data, config, preparedBase),
error: handleAjaxError,
complete: function() {
fireEvent('dateFilterChangedCompleted');
},
});
} else {
fireEvent('dateFilterChangedCompleted');
}
if (!dateRange && ids.length) {
// actualy making fake response from server
let data = {totals: {}};
let preparedBase = {};
const cols = config.totals.columns;
response.data.forEach(item => {
data.totals[item.id] = {};
preparedBase[item.id] = {};
cols.forEach(col => {
data.totals[item.id][col] = item[col];
preparedBase[item.id][col] = item[col];
});
});
this.onTotalsSuccess(data, config, preparedBase);
}
}
onTotalsSuccess(data, config, base) {
const table = $(`#${config.tableId}`).DataTable();
const columns = config.totals.columns;
let idxs = {};
for (let i = 0; i < columns.length; i++) {
idxs[columns[i]] = -1;
}
table.settings().init().columns.forEach((col, index) => {
columns.forEach(colName => {
if (col.data === colName || col.name === colName) {
idxs[colName] = index;
}
});
});
if (Object.values(idxs).includes(-1)) return;
let rowData;
let id;
table.rows().every(function() {
rowData = this.data();
id = String(rowData.id);
if (id in data.totals) {
for (const col in idxs) {
$(table.cell(this, idxs[col]).node()).html(renderTotalFrame(base[id][col], data.totals[id][col]));
}
}
});
}
drawCallback(settings) {
const me = this;
this.initTooltips();
//this.stopAnimation();
const params = {
tableId: me.config.tableId
};
if (settings && settings.iDraw > 1) {
const total = settings.json.recordsTotal;
const tileId = this.config.tileId;
const tableId = this.config.tableId;
this.totalTile.update(tableId, tileId, total);
this.updateTableTitle(total);
fireEvent('tableLoaded', params);
}
}
updateTableTitle(value) {
const tableId = this.config.tableId;
const wrapper = document.getElementById(tableId).closest('.card');
if (wrapper) {
const span = wrapper.querySelector('header span');
span.textContent = value;
}
}
stopAnimation() {
this.loader.stop();
const el = document.getElementById(`${this.config.tableId}_loader`);
if (el) el.remove();
const table = document.getElementById(this.config.tableId);
table.classList.remove('dim-table');
}
updateTableFooter(dataTable) {
const tableId = this.config.tableId;
const pagerSelector = `#${tableId}_wrapper .dt-paging`;
const api = dataTable.api();
if (api.ajax && typeof api.ajax.json === 'function' && api.ajax.json() === undefined) {
return;
}
$(`#${tableId}`).closest('.dt-container').find('nav').show();
if (api.page.info().pages <= 1) {
$(pagerSelector).hide();
} else {
$(pagerSelector).show();
}
}
initTooltips() {
const tableId = this.config.tableId;
Tooltip.addTooltipsToGridRecords(tableId);
Tooltip.addTooltipToSpans();
Tooltip.addTooltipToParagraphs();
}
onBeforeLoad(e, settings, data) {
if (!this.config.sequential) {
this.startLoader();
}
this.updateTableTitle(MIDLINE_HELLIP);
fireEvent('dateFilterChangedCaught');
//TODO: move to events grid? Or not?
const params = this.config.getParams();
const queryParams = getQueryParams(params);
for (let key in queryParams) {
data[key] = queryParams[key];
}
const token = document.head.querySelector('[name=\'csrf-token\'][content]').content;
data.token = token;
}
onDraw(e, settings) {
if (this.firstload) {
document.getElementById(this.config.tableId).classList.remove('hide-body');
this.firstload = false;
}
}
onBeforePageChange(e, settings) {
const tableId = this.config.tableId;
const pagesPath = `#${tableId}_paginate a`;
[...document.querySelectorAll(pagesPath)].forEach(a => {
a.outerHTML =
a.outerHTML
.replace(/<a/g, '<span')
.replace(/<\/a>/g, '</span>');
});
}
onTableRowClick(event) {
const selection = window.getSelection();
if ('Range' === selection.type) {
return;
}
const row = event.target.closest('tr');
const link = row.querySelector('a');
if (link) {
event.preventDefault();
if (event.ctrlKey || event.metaKey) {
window.open(link.href, '_blank');
} else {
window.location.href = link.href;
}
}
}
//TODO: leave blank and move to the events table
addTableRowsEvents() {
const tableId = this.config.tableId;
const onRowClick = this.onRowClick.bind(this);
if ($(this.table).DataTable().data().any()) {
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
rows.forEach(row => row.addEventListener('click', onRowClick, false));
}
}
startLoader() {
const tableId = this.config.tableId;
const loaderPath = `${tableId}_processing`;
const loaderWrapper = document.getElementById(loaderPath);
const el = document.createElement('p');
el.className = 'text-loader';
loaderWrapper.replaceChildren(el);
this.loader.start(el);
loaderWrapper.style.display = null;
const table = document.getElementById(this.config.tableId);
table.classList.add('dim-table');
}
onRowClick(e) {
const selection = window.getSelection();
if ('Range' === selection.type) {
return;
}
e.preventDefault();
const row = e.target.closest('tr');
const itemId = row.dataset.itemId;
const data = {itemId: itemId};
fireEvent('tableRowClicked', data);
}
onError(e, settings, techNote, message) {
if (settings.jqXHR !== undefined && 403 === settings.jqXHR.status) {
window.location.href = escape('/');
}
//console.warn('An error has been reported by DataTables: ', message);
}
loadData() {
//TODO: create getter for table el: $(me.table).DataTable().ajax.reload()
const me = this;
$(me.table).DataTable().ajax.reload();
}
onDateFilterChanged() {
this.loadData();
}
onSearchFilterChanged() {
this.loadData();
}
onChoicesFilterChanged() {
this.loadData();
}
getConfigParam(key) {
const cfg = this.config;
const value = ('undefined' !== typeof cfg[key]) ? cfg[key] : true;
return value;
}
get orderConfig() {
return this.getConfigParam('isSortable') ? [[1, 'desc']] : [];
}
get table() {
const tableId = this.config.tableId;
const tableEl = document.getElementById(tableId);
return tableEl;
}
renderTotalsLoader(data, type, record, meta) {
const span = document.createElement('span');
const col_name = meta.settings.aoColumns[meta.col].name;
if (this.config.calculateTotals && this.config.totals.columns.includes(col_name)) {
span.className = 'loading-table-total';
span.textContent = MIDLINE_HELLIP;
} else {
span.textContent = data;
}
return span;
}
}

Some files were not shown because too many files have changed in this diff Show More