1 import type { ReactNode } from 'react';
2 import { useEffect, useState } from 'react';
4 import { endOfDay, isAfter, isBefore, startOfDay } from 'date-fns';
5 import { c } from 'ttag';
7 import { useUserSettings } from '@proton/account/userSettings/hooks';
8 import { Pagination, usePaginationAsync } from '@proton/components';
9 import Icon from '@proton/components/components/icon/Icon';
10 import useModalState from '@proton/components/components/modalTwo/useModalState';
11 import TimeIntl from '@proton/components/components/time/TimeIntl';
12 import Toggle from '@proton/components/components/toggle/Toggle';
13 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
14 import useApi from '@proton/components/hooks/useApi';
15 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
16 import useNotifications from '@proton/components/hooks/useNotifications';
17 import { useLoading } from '@proton/hooks';
18 import { getVPNLogDownload, getVPNLogs, getVpnEventTypes } from '@proton/shared/lib/api/b2bevents';
19 import { SORT_DIRECTION } from '@proton/shared/lib/constants';
20 import type { B2BLogsQuery } from '@proton/shared/lib/interfaces/B2BLogs';
21 import noop from '@proton/utils/noop';
23 import { getCountryOptions } from '../../../helpers/countries';
24 import { toCamelCase } from '../../credentialLeak/helpers';
25 import GenericError from '../../error/GenericError';
26 import { FilterAndSortEventsBlock } from '../FilterAndSortEventBlock';
27 import { ALL_EVENTS_DEFAULT, PAGINATION_LIMIT, getLocalTimeStringFromDate, getSearchType } from '../Pass/helpers';
28 import TogglingMonitoringModal from './TogglingMonitoringModal';
29 import VPNEventsTable from './VPNEventsTable';
30 import type { OrganizationSettings } from './api';
31 import { getMonitoringSetting, updateMonitoringSetting } from './api';
32 import type { Event as EventObject } from './helpers';
33 import { downloadVPNEvents, getConnectionEvents } from './helpers';
34 import type { VPNEvent } from './interface';
36 export interface FilterModel {
38 start: Date | undefined;
39 end: Date | undefined;
42 const initialFilter = {
43 eventType: ALL_EVENTS_DEFAULT,
48 const getQueryParams = (filter: FilterModel, searchType: 'ip' | 'email' | 'empty', keyword: string) => {
49 const { eventType, start, end } = filter;
50 const Event = eventType === ALL_EVENTS_DEFAULT ? undefined : eventType;
51 const StartTime = start ? getLocalTimeStringFromDate(start) : undefined;
52 const EndTime = end ? getLocalTimeStringFromDate(endOfDay(end)) : undefined;
53 const Email = searchType === 'email' ? keyword : undefined;
54 const Ip = searchType === 'ip' ? keyword : undefined;
55 return { Email, Ip, Event, StartTime, EndTime };
58 export const VPNEvents = () => {
60 const handleError = useErrorHandler();
61 const [userSettings] = useUserSettings();
63 const countryOptions = getCountryOptions(userSettings);
65 const { page, onNext, onPrevious, onSelect, reset } = usePaginationAsync(1);
66 const { createNotification } = useNotifications();
67 const [loading, withLoading] = useLoading();
68 // const [downloading, withDownloading] = useLoading();
69 const [filter, setFilter] = useState<FilterModel>(initialFilter);
70 const [events, setEvents] = useState<VPNEvent[] | []>([]);
71 const [connectionEvents, setConnectionEvents] = useState([]);
72 const [keyword, setKeyword] = useState<string>('');
73 const [total, setTotal] = useState<number>(0);
74 const [monitoringLoading, setMonitoringLoading] = useState<boolean>(true);
75 const [monitoringEnabling, setMonitoringEnabling] = useState<boolean>(false);
76 const [monitoring, setMonitoring] = useState<boolean>(false);
77 const [monitoringLastChange, setMonitoringLastChange] = useState<number | null>(null);
78 const [togglingMonitoringModalProps, setTogglingMonitoringModalOpen, togglingMonitoringModalRender] =
80 const [businessSettingsAvailable, setBusinessSettingsAvailable] = useState<boolean>(true);
81 const [togglingMonitoringLoading, setTogglingMonitoringLoading] = useState<boolean>(false);
82 const [togglingMonitoringInitializing, withMonitoringInitializing] = useLoading();
83 const [query, setQuery] = useState({});
84 const [error, setError] = useState<string | null>(null);
85 const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.DESC);
87 const fetchVpnConnectionEvents = async () => {
88 const { Items } = await api(getVpnEventTypes());
89 const filteredItems = Items.filter((item: EventObject) => item.EventType !== '' && item.EventTypeName !== '');
90 const sortedItems = filteredItems.sort((a: EventObject, b: EventObject) =>
91 a.EventTypeName.localeCompare(b.EventTypeName)
93 sortedItems.forEach((item: { EventType: string; EventTypeName: string }) => {
94 if (item.EventType === 'session_roaming') {
95 item.EventTypeName = 'Network change';
98 setConnectionEvents(sortedItems);
102 fetchVpnConnectionEvents();
105 const fetchVPNLogs = async (params: B2BLogsQuery) => {
107 const { Items, Total } = await api(
108 getVPNLogs({ ...params, Sort: sortDirection === SORT_DIRECTION.DESC ? 'desc' : 'asc' })
110 const connectionEvents = toCamelCase(Items);
111 connectionEvents.forEach((item: VPNEvent) => {
112 if (item.eventType === 'session_roaming') {
113 item.eventTypeName = 'Network change';
117 setEvents(connectionEvents);
121 setError(c('Error').t`Please try again in a few moments.`);
126 withLoading(fetchVPNLogs({ ...query, Page: page - 1 }).catch(noop));
127 }, [page, query, sortDirection]);
130 withMonitoringInitializing(
131 new Promise(async (resolve) => {
132 const timeout = setTimeout(() => {
133 setMonitoringLoading(false);
134 setMonitoringLastChange(null);
135 setBusinessSettingsAvailable(false);
140 const organizationSettings = await api<OrganizationSettings>(getMonitoringSetting());
142 if (organizationSettings.GatewayMonitoring) {
146 setMonitoringLastChange(organizationSettings.GatewayMonitoringLastUpdate || null);
148 setBusinessSettingsAvailable(false);
149 setMonitoringLastChange(null);
151 setMonitoringLoading(false);
152 clearTimeout(timeout);
159 const handleSearchSubmit = () => {
161 const searchType = getSearchType(keyword);
162 if (searchType === 'invalid') {
163 createNotification({ text: c('Notification').t`Invalid input. Search an email or IP address.` });
166 const queryParams = getQueryParams(filter, searchType, keyword);
167 setQuery({ ...queryParams });
171 const handleDownloadClick = async () => {
172 const response = await api({
173 ...getVPNLogDownload({ ...query, Page: page - 1 }),
177 const responseCode = response.headers.get('x-pm-code') || '';
178 if (response.status === 429) {
180 text: c('Notification').t`Too many recent API requests`,
182 } else if (responseCode !== '1000') {
184 text: c('Notification').t`Number of records exceeds the download limit of 10000`,
188 downloadVPNEvents(response);
191 const handleStartDateChange = (start: Date | undefined) => {
195 if (!filter.end || isBefore(start, filter.end)) {
196 setFilter({ ...filter, start });
198 setFilter({ ...filter, start, end: start });
202 const handleEndDateChange = (end: Date | undefined) => {
206 if (!filter.start || isAfter(end, filter.start)) {
207 setFilter({ ...filter, end });
209 setFilter({ ...filter, start: end, end });
213 const handleSetEventType = (eventType: string) => {
214 setFilter({ ...filter, eventType });
217 const handleClickableEvent = (eventType: string) => {
218 setFilter({ ...filter, eventType });
219 setQuery({ ...query, Event: eventType });
223 const handleClickableTime = (time: string) => {
224 const date = new Date(time);
225 const start = getLocalTimeStringFromDate(startOfDay(date));
226 const end = getLocalTimeStringFromDate(endOfDay(date));
228 setFilter({ ...filter, start: date, end: date });
229 setQuery({ ...query, StartTime: start, EndTime: end });
233 const handleClickableEmailOrIP = (keyword: string) => {
234 const searchType = getSearchType(keyword);
236 if (searchType !== 'email' && searchType !== 'ip') {
240 const updatedQuery = { ...query, [searchType === 'email' ? 'Email' : 'Ip']: keyword };
241 setQuery(updatedQuery);
246 const setGatewayMonitoring = async () => {
248 const enabling = !monitoring;
249 const newSettings = await api<OrganizationSettings>(updateMonitoringSetting(enabling));
251 setMonitoring(enabling);
252 setMonitoringLastChange(newSettings.GatewayMonitoringLastUpdate || Date.now() / 1000);
253 setTogglingMonitoringModalOpen(false);
254 setTogglingMonitoringLoading(false);
256 setTogglingMonitoringModalOpen(false);
260 setTogglingMonitoringLoading(false);
264 const toggleMonitoring = async () => {
265 if (togglingMonitoringLoading || togglingMonitoringInitializing) {
269 const enabling = !monitoring;
270 setMonitoringEnabling(enabling);
271 setTogglingMonitoringModalOpen(true);
274 const getMonitoringInfoText = (): string => {
275 if (monitoringLoading) {
276 return c('Info').t`Loading gateways monitoring current status.`;
279 if (!businessSettingsAvailable) {
280 return c('Info').t`Unable to check gateways monitoring current status.`;
284 ? c('Info').t`Gateways monitoring is enabled.`
285 : c('Info').t`Gateways monitoring is disabled.`;
288 const getMonitoringLastChangeText = (): ReactNode => {
289 if (!monitoringLastChange) {
293 const formattedDateAndTime = (
303 {monitoringLastChange}
308 ? /** translator: formattedDateAndTime be like "25 Sep 2023, 15:37" or just "15:37" if it's on the same day */ c(
310 ).jt`Enabled on ${formattedDateAndTime}`
311 : /** translator: formattedDateAndTime be like "25 Sep 2023, 15:37" or just "15:37" if it's on the same day */ c(
313 ).jt`Disabled on ${formattedDateAndTime}`;
316 const handleToggleSort = (direction: SORT_DIRECTION) => {
317 setSortDirection(direction);
320 const handleResetFilter = () => {
322 setFilter(initialFilter);
329 <SettingsSectionWide customWidth="90em">
330 {togglingMonitoringModalRender && (
331 <TogglingMonitoringModal
332 {...togglingMonitoringModalProps}
333 enabling={monitoringEnabling}
334 onChange={setGatewayMonitoring}
337 <div className="mb-8 flex *:min-size-auto flex-column sm:flex-row gap-2">
338 <div className="flex flex-row flex-nowrap items-center gap-2">
340 loading={togglingMonitoringLoading || togglingMonitoringInitializing}
343 !togglingMonitoringLoading && !togglingMonitoringInitializing && !businessSettingsAvailable
345 onChange={toggleMonitoring}
348 <span className="text-bold">{getMonitoringInfoText()}</span>
349 <span className="block color-weak text-sm">{getMonitoringLastChangeText()}</span>
354 <FilterAndSortEventsBlock
357 setKeyword={setKeyword}
358 handleSetEventType={handleSetEventType}
359 handleStartDateChange={handleStartDateChange}
360 handleEndDateChange={handleEndDateChange}
361 handleDownloadClick={handleDownloadClick}
362 eventTypesList={getConnectionEvents(connectionEvents)}
363 handleSearchSubmit={handleSearchSubmit}
364 hasFilterEvents={true}
365 resetFilter={handleResetFilter}
368 <GenericError className="text-center">{error}</GenericError>
371 <div className="content-center my-3">
372 <Icon name="info-circle" size={4.5} className="mr-1 mb-1" />
373 <span>{c('Title').t`Click a value in the table to use it as filter`}</span>
375 <div className="flex justify-center">
379 onTimeClick={handleClickableTime}
380 onEventClick={handleClickableEvent}
381 onEmailOrIpClick={handleClickableEmailOrIP}
382 onToggleSort={handleToggleSort}
383 countryOptions={countryOptions}
388 limit={PAGINATION_LIMIT}
391 onPrevious={onPrevious}
396 </SettingsSectionWide>