• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / gui / 1301920191

23 May 2024 07:13AM UTC coverage: 83.42% (-16.5%) from 99.964%
1301920191

Pull #4421

gitlab-ci

mzedel
fix: fixed an issue that sometimes prevented reopening paginated auditlog links

Ticket: None
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4421: MEN-7034 - device information in auditlog entries

4456 of 6367 branches covered (69.99%)

34 of 35 new or added lines in 7 files covered. (97.14%)

1668 existing lines in 162 files now uncovered.

8473 of 10157 relevant lines covered (83.42%)

140.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.15
/src/js/actions/organizationActions.js
1
// Copyright 2020 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import { jwtDecode } from 'jwt-decode';
15
import hashString from 'md5';
16
import Cookies from 'universal-cookie';
17

18
import Api, { apiUrl, headerNames } from '../api/general-api';
19
import { SET_ANNOUNCEMENT, SORTING_OPTIONS, TIMEOUTS, locations } from '../constants/appConstants';
20
import { DEVICE_LIST_DEFAULTS } from '../constants/deviceConstants';
21
import {
22
  RECEIVE_AUDIT_LOGS,
23
  RECEIVE_CURRENT_CARD,
24
  RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS,
25
  RECEIVE_SETUP_INTENT,
26
  RECEIVE_SSO_CONFIGS,
27
  RECEIVE_WEBHOOK_EVENTS,
28
  SET_AUDITLOG_STATE,
29
  SET_ORGANIZATION
30
} from '../constants/organizationConstants';
31
import { deepCompare, getSsoByContentType } from '../helpers';
32
import { getCurrentSession, getTenantCapabilities } from '../selectors';
33
import { commonErrorFallback, commonErrorHandler, setFirstLoginAfterSignup, setSnackbar } from './appActions';
34
import { deviceAuthV2, iotManagerBaseURL } from './deviceActions';
35

36
const cookies = new Cookies();
184✔
37
export const auditLogsApiUrl = `${apiUrl.v1}/auditlogs`;
184✔
38
export const tenantadmApiUrlv1 = `${apiUrl.v1}/tenantadm`;
184✔
39
export const tenantadmApiUrlv2 = `${apiUrl.v2}/tenantadm`;
184✔
40
export const ssoIdpApiUrlv1 = `${apiUrl.v1}/useradm/sso/idp/metadata`;
184✔
41

42
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
184✔
43

44
export const cancelRequest = (tenantId, reason) => dispatch =>
184✔
45
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/cancel`, { reason: reason }).then(() =>
1✔
46
    Promise.resolve(dispatch(setSnackbar('Deactivation request was sent successfully', TIMEOUTS.fiveSeconds, '')))
1✔
47
  );
48

49
export const getTargetLocation = key => {
184✔
50
  if (devLocations.includes(window.location.hostname)) {
14✔
51
    return '';
6✔
52
  }
53
  let subdomainSections = window.location.hostname.substring(0, window.location.hostname.indexOf(locations.us.location)).split('.');
8✔
54
  subdomainSections = subdomainSections.splice(0, subdomainSections.length - 1);
8✔
55
  if (!subdomainSections.find(section => section === key)) {
8✔
56
    subdomainSections = key === locations.us.key ? subdomainSections.filter(section => !locations[section]) : [...subdomainSections, key];
7✔
57
    return `https://${[...subdomainSections, ...locations.us.location.split('.')].join('.')}`;
7✔
58
  }
59
  return `https://${window.location.hostname}`;
1✔
60
};
61

62
const devLocations = ['localhost', 'docker.mender.io'];
184✔
63
export const createOrganizationTrial = data => dispatch => {
184✔
64
  const { key } = locations[data.location];
2✔
65
  const targetLocation = getTargetLocation(key);
2✔
66
  const target = `${targetLocation}${tenantadmApiUrlv2}/tenants/trial`;
2✔
67
  return Api.postUnauthorized(target, data)
2✔
68
    .catch(err => {
UNCOV
69
      if (err.response.status >= 400 && err.response.status < 500) {
×
UNCOV
70
        dispatch(setSnackbar(err.response.data.error, TIMEOUTS.fiveSeconds, ''));
×
UNCOV
71
        return Promise.reject(err);
×
72
      }
73
    })
74
    .then(({ headers }) => {
75
      cookies.remove('oauth');
2✔
76
      cookies.remove('externalID');
2✔
77
      cookies.remove('email');
2✔
78
      dispatch(setFirstLoginAfterSignup(true));
2✔
79
      return new Promise(resolve =>
2✔
80
        setTimeout(() => {
2✔
81
          window.location.assign(`${targetLocation}${headers.location || ''}`);
1✔
82
          return resolve();
1✔
83
        }, TIMEOUTS.fiveSeconds)
84
      );
85
    });
86
};
87

88
export const startCardUpdate = () => dispatch =>
184✔
89
  Api.post(`${tenantadmApiUrlv2}/billing/card`)
1✔
90
    .then(res => {
91
      dispatch({
1✔
92
        type: RECEIVE_SETUP_INTENT,
93
        intentId: res.data.intent_id
94
      });
95
      return Promise.resolve(res.data.secret);
1✔
96
    })
UNCOV
97
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch));
×
98

99
export const confirmCardUpdate = () => (dispatch, getState) =>
184✔
100
  Api.post(`${tenantadmApiUrlv2}/billing/card/${getState().organization.intentId}/confirm`)
1✔
101
    .then(() =>
102
      Promise.all([
1✔
103
        dispatch(setSnackbar('Payment card was updated successfully')),
104
        dispatch({
105
          type: RECEIVE_SETUP_INTENT,
106
          intentId: null
107
        })
108
      ])
109
    )
UNCOV
110
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch));
×
111

112
export const getCurrentCard = () => dispatch =>
184✔
113
  Api.get(`${tenantadmApiUrlv2}/billing`).then(res => {
2✔
114
    const { last4, exp_month, exp_year, brand } = res.data.card || {};
2!
115
    return Promise.resolve(
2✔
116
      dispatch({
117
        type: RECEIVE_CURRENT_CARD,
118
        card: {
119
          brand,
120
          last4,
121
          expiration: { month: exp_month, year: exp_year }
122
        }
123
      })
124
    );
125
  });
126

127
export const startUpgrade = tenantId => dispatch =>
184✔
128
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/start`)
1✔
129
    .then(({ data }) => Promise.resolve(data.secret))
1✔
UNCOV
130
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch));
×
131

132
export const cancelUpgrade = tenantId => () => Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/cancel`);
184✔
133

134
export const completeUpgrade = (tenantId, plan) => dispatch =>
184✔
135
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/complete`, { plan })
1✔
UNCOV
136
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch))
×
137
    .then(() => Promise.resolve(dispatch(getUserOrganization())));
1✔
138

139
const prepareAuditlogQuery = ({ startDate, endDate, user: userFilter, type, detail: detailFilter, sort = {} }) => {
184✔
140
  const userId = userFilter?.id || userFilter;
8✔
141
  const detail = detailFilter?.id || detailFilter;
8✔
142
  const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
8✔
143
  const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
8✔
144
  const typeSearch = type ? `&object_type=${type.value}`.toLowerCase() : '';
8!
145
  const userSearch = userId ? `&actor_id=${userId}` : '';
8!
146
  const objectSearch = type && detail ? `&${type.queryParameter}=${encodeURIComponent(detail)}` : '';
8!
147
  const { direction = SORTING_OPTIONS.desc } = sort;
8✔
148
  return `${createdAfter}${createdBefore}${userSearch}${typeSearch}${objectSearch}&sort=${direction}`;
8✔
149
};
150

151
export const getAuditLogs = selectionState => (dispatch, getState) => {
184✔
152
  const { page, perPage } = selectionState;
7✔
153
  const { hasAuditlogs } = getTenantCapabilities(getState());
7✔
154
  if (!hasAuditlogs) {
7✔
155
    return Promise.resolve();
1✔
156
  }
157
  return Api.get(`${auditLogsApiUrl}/logs?page=${page}&per_page=${perPage}${prepareAuditlogQuery(selectionState)}`)
6✔
158
    .then(res => {
159
      let total = res.headers[headerNames.total];
6✔
160
      total = Number(total || res.data.length);
6!
161
      return Promise.resolve(dispatch({ type: RECEIVE_AUDIT_LOGS, events: res.data, total }));
6✔
162
    })
UNCOV
163
    .catch(err => commonErrorHandler(err, `There was an error retrieving audit logs:`, dispatch));
×
164
};
165

166
export const getAuditLogsCsvLink = () => (dispatch, getState) =>
184✔
167
  Promise.resolve(`${auditLogsApiUrl}/logs/export?limit=20000${prepareAuditlogQuery(getState().organization.auditlog.selectionState)}`);
2✔
168

169
export const setAuditlogsState = selectionState => (dispatch, getState) => {
184✔
170
  const currentState = getState().organization.auditlog.selectionState;
8✔
171
  let nextState = {
8✔
172
    ...currentState,
173
    ...selectionState,
174
    sort: { ...currentState.sort, ...selectionState.sort }
175
  };
176
  let tasks = [];
8✔
177
  // eslint-disable-next-line no-unused-vars
178
  const { isLoading: currentLoading, selectedIssue: currentIssue, ...currentRequestState } = currentState;
8✔
179
  // eslint-disable-next-line no-unused-vars
180
  const { isLoading: selectionLoading, selectedIssue: selectionIssue, ...selectionRequestState } = nextState;
8✔
181
  if (!deepCompare(currentRequestState, selectionRequestState)) {
8✔
182
    nextState.isLoading = true;
4✔
183
    tasks.push(dispatch(getAuditLogs(nextState)).finally(() => dispatch(setAuditlogsState({ isLoading: false }))));
4✔
184
  }
185
  tasks.push(dispatch({ type: SET_AUDITLOG_STATE, state: nextState }));
8✔
186
  return Promise.all(tasks);
8✔
187
};
188

189
/*
190
  Tenant management + Hosted Mender
191
*/
192
export const tenantDataDivergedMessage = 'The system detected there is a change in your plan or purchased add-ons. Please log out and log in again';
184✔
193
export const getUserOrganization = () => (dispatch, getState) =>
184✔
194
  Api.get(`${tenantadmApiUrlv1}/user/tenant`).then(res => {
8✔
195
    let tasks = [dispatch({ type: SET_ORGANIZATION, organization: res.data })];
7✔
196
    const { addons, plan, trial } = res.data;
7✔
197
    const { token } = getCurrentSession(getState());
7✔
198
    const jwt = jwtDecode(token);
7✔
199
    const jwtData = { addons: jwt['mender.addons'], plan: jwt['mender.plan'], trial: jwt['mender.trial'] };
7✔
200
    if (!deepCompare({ addons, plan, trial }, jwtData)) {
7!
201
      const hash = hashString(tenantDataDivergedMessage);
7✔
202
      cookies.remove(`${jwt.sub}${hash}`);
7✔
203
      tasks.push(dispatch({ type: SET_ANNOUNCEMENT, announcement: tenantDataDivergedMessage }));
7✔
204
    }
205
    return Promise.all(tasks);
7✔
206
  });
207

208
export const sendSupportMessage = content => dispatch =>
184✔
209
  Api.post(`${tenantadmApiUrlv2}/contact/support`, content)
1✔
UNCOV
210
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
211
    .then(() => Promise.resolve(dispatch(setSnackbar('Your request was sent successfully', TIMEOUTS.fiveSeconds, ''))));
1✔
212

213
export const requestPlanChange = (tenantId, content) => dispatch =>
184✔
214
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/plan`, content)
1✔
UNCOV
215
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
216
    .then(() => Promise.resolve(dispatch(setSnackbar('Your request was sent successfully', TIMEOUTS.fiveSeconds, ''))));
1✔
217

218
export const downloadLicenseReport = () => dispatch =>
184✔
219
  Api.get(`${deviceAuthV2}/reports/devices`)
1✔
UNCOV
220
    .catch(err => commonErrorHandler(err, 'There was an error downloading the report', dispatch, commonErrorFallback))
×
221
    .then(res => res.data);
1✔
222

223
export const createIntegration = integration => dispatch => {
184✔
224
  // eslint-disable-next-line no-unused-vars
225
  const { credentials, id, provider, ...remainder } = integration;
1✔
226
  return Api.post(`${iotManagerBaseURL}/integrations`, { provider, credentials, ...remainder })
1✔
UNCOV
227
    .catch(err => commonErrorHandler(err, 'There was an error creating the integration', dispatch, commonErrorFallback))
×
228
    .then(() => Promise.all([dispatch(setSnackbar('The integration was set up successfully')), dispatch(getIntegrations())]));
1✔
229
};
230

231
export const changeIntegration = integration => dispatch =>
184✔
232
  Api.put(`${iotManagerBaseURL}/integrations/${integration.id}/credentials`, integration.credentials)
1✔
UNCOV
233
    .catch(err => commonErrorHandler(err, 'There was an error updating the integration', dispatch, commonErrorFallback))
×
234
    .then(() => Promise.all([dispatch(setSnackbar('The integration was updated successfully')), dispatch(getIntegrations())]));
1✔
235

236
export const deleteIntegration = integration => (dispatch, getState) =>
184✔
237
  Api.delete(`${iotManagerBaseURL}/integrations/${integration.id}`, {})
1✔
UNCOV
238
    .catch(err => commonErrorHandler(err, 'There was an error removing the integration', dispatch, commonErrorFallback))
×
239
    .then(() => {
240
      const integrations = getState().organization.externalDeviceIntegrations.filter(item => integration.provider !== item.provider);
1✔
241
      return Promise.all([
1✔
242
        dispatch(setSnackbar('The integration was removed successfully')),
243
        dispatch({ type: RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS, value: integrations })
244
      ]);
245
    });
246

247
export const getIntegrations = () => (dispatch, getState) =>
184✔
248
  Api.get(`${iotManagerBaseURL}/integrations`)
10✔
UNCOV
249
    .catch(err => commonErrorHandler(err, 'There was an error retrieving the integration', dispatch, commonErrorFallback))
×
250
    .then(({ data }) => {
251
      const existingIntegrations = getState().organization.externalDeviceIntegrations;
10✔
252
      const integrations = data.reduce((accu, item) => {
10✔
253
        const existingIntegration = existingIntegrations.find(integration => item.id === integration.id) ?? {};
20✔
254
        const integration = { ...existingIntegration, ...item };
20✔
255
        accu.push(integration);
20✔
256
        return accu;
20✔
257
      }, []);
258
      return Promise.resolve(dispatch({ type: RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS, value: integrations }));
10✔
259
    });
260

261
export const getWebhookEvents =
262
  (config = {}) =>
184✔
263
  (dispatch, getState) => {
3✔
264
    const { isFollowUp, page = defaultPage, perPage = defaultPerPage } = config;
3✔
265
    return Api.get(`${iotManagerBaseURL}/events?page=${page}&per_page=${perPage}`)
3✔
UNCOV
266
      .catch(err => commonErrorHandler(err, 'There was an error retrieving activity for this integration', dispatch, commonErrorFallback))
×
267
      .then(({ data }) => {
268
        let tasks = [
3✔
269
          dispatch({
270
            type: RECEIVE_WEBHOOK_EVENTS,
271
            value: isFollowUp ? getState().organization.webhooks.events : data,
3✔
272
            total: (page - 1) * perPage + data.length
273
          })
274
        ];
275
        if (data.length >= perPage && !isFollowUp) {
3✔
276
          tasks.push(dispatch(getWebhookEvents({ isFollowUp: true, page: page + 1, perPage: 1 })));
1✔
277
        }
278
        return Promise.all(tasks);
3✔
279
      });
280
  };
281

282
const ssoConfigActions = {
184✔
283
  create: { success: 'stored', error: 'storing' },
284
  edit: { success: 'updated', error: 'updating' },
285
  read: { success: '', error: 'retrieving' },
286
  remove: { success: 'removed', error: 'removing' }
287
};
288

289
const ssoConfigActionErrorHandler = (err, type) => dispatch =>
184✔
UNCOV
290
  commonErrorHandler(err, `There was an error ${ssoConfigActions[type].error} the SSO configuration.`, dispatch, commonErrorFallback);
×
291

292
const ssoConfigActionSuccessHandler = type => dispatch => dispatch(setSnackbar(`The SSO configuration was ${ssoConfigActions[type].success} successfully`));
184✔
293

294
export const storeSsoConfig =
295
  ({ config, contentType }) =>
184✔
296
  dispatch =>
1✔
297
    Api.post(ssoIdpApiUrlv1, config, { headers: { 'Content-Type': contentType, Accept: 'application/json' } })
1✔
UNCOV
298
      .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'create')))
×
299
      .then(() => Promise.all([dispatch(ssoConfigActionSuccessHandler('create')), dispatch(getSsoConfigs())]));
1✔
300

301
export const changeSsoConfig =
302
  ({ id, config, contentType }) =>
184✔
303
  dispatch =>
1✔
304
    Api.put(`${ssoIdpApiUrlv1}/${id}`, config, { headers: { 'Content-Type': contentType, Accept: 'application/json' } })
1✔
UNCOV
305
      .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'edit')))
×
306
      .then(() => Promise.all([dispatch(ssoConfigActionSuccessHandler('edit')), dispatch(getSsoConfigs())]));
1✔
307

308
export const deleteSsoConfig =
309
  ({ id }) =>
184✔
310
  (dispatch, getState) =>
3✔
311
    Api.delete(`${ssoIdpApiUrlv1}/${id}`)
3✔
UNCOV
312
      .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'remove')))
×
313
      .then(() => {
314
        const configs = getState().organization.ssoConfigs.filter(item => id !== item.id);
5✔
315
        return Promise.all([dispatch(ssoConfigActionSuccessHandler('remove')), dispatch({ type: RECEIVE_SSO_CONFIGS, value: configs })]);
3✔
316
      });
317

318
const getSsoConfigById = config => dispatch =>
184✔
319
  Api.get(`${ssoIdpApiUrlv1}/${config.id}`)
10✔
UNCOV
320
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'read')))
×
321
    .then(({ data, headers }) => {
322
      const sso = getSsoByContentType(headers['content-type']);
10✔
323
      return sso ? Promise.resolve({ ...config, config: data, type: sso.id }) : Promise.reject('Not supported SSO config content type.');
10!
324
    });
325

326
export const getSsoConfigs = () => dispatch =>
184✔
327
  Api.get(ssoIdpApiUrlv1)
5✔
UNCOV
328
    .catch(err => commonErrorHandler(err, 'There was an error retrieving SSO configurations', dispatch, commonErrorFallback))
×
329
    .then(({ data }) =>
330
      Promise.all(data.map(config => Promise.resolve(dispatch(getSsoConfigById(config)))))
10✔
331
        .then(configs => {
332
          return dispatch({ type: RECEIVE_SSO_CONFIGS, value: configs });
5✔
333
        })
UNCOV
334
        .catch(err => commonErrorHandler(err, err, dispatch, ''))
×
335
    );
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc