HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/matomo.vdk/plugins/UsersManager/vue/src/UsersManager/UsersManager.vue
<!--
  Matomo - free/libre analytics platform

  @link    https://matomo.org
  @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->

<template>
  <div class="usersManager" v-tooltips>
    <div v-show="!isEditing && !isInviting">
      <div v-content-intro>
        <h2>
          <EnrichedHeadline
            :help-url="externalRawLink('https://matomo.org/docs/manage-users/')"
            feature-name="Users Management"
          >
            {{ translate('UsersManager_ManageUsers') }}
          </EnrichedHeadline>
        </h2>
        <p v-if="currentUserRole === 'superuser'">
          {{ translate('UsersManager_ManageUsersDesc') }}
        </p>
        <p v-if="currentUserRole === 'admin'">
          {{ translate('UsersManager_ManageUsersAdminDesc') }}
        </p>
        <div class="row add-user-container">
          <div class="col s12">
            <div class="input-field" style="margin-right:3.5px">
              <a
                class="btn add-new-user"
                @click="onAddNewUser()"
              >
                {{ translate('UsersManager_InviteNewUser') }}
              </a>
            </div>
            <div
              class="input-field"
              v-if="currentUserRole !== 'superuser'"
            >
              <a
                class="btn add-existing-user"
                @click="showAddExistingUserModal()"
              >
                {{ translate('UsersManager_AddExistingUser') }}
              </a>
            </div>
          </div>
        </div>
        <PagedUsersList
          @edit-user="onEditUser($event.user)"
          @change-user-role="onChangeUserRole($event.users, $event.role, $event.password)"
          @delete-user="onDeleteUser($event.users, $event.password)"
          @search-change="searchParams = $event.params; fetchUsers()"
          @resend-invite="triggerResendInviteForUser = $event.user"
          :initial-site-id="initialSiteId"
          :initial-site-name="initialSiteName"
          :is-loading-users="isLoadingUsers"
          :current-user-role="currentUserRole"
          :access-levels="accessLevels"
          :filter-access-levels="filterAccessLevels"
          :filter-status-levels="filterStatusLevels"
          :search-params="searchParams"
          :users="users"
          :total-entries="totalEntries"
        />
      </div>
    </div>
    <div v-show="isInviting">
      <component
        :is="usedInviteComponent"
        :invite-token-expiry-days="inviteTokenExpiryDays"
        :initial-site-id="initialSiteId"
        :initial-site-name="initialSiteName"
        @aborted="isInviting = false"
        @invited="onInvite($event.user)"
      ></component>
    </div>
    <component
      :is="usedResendInviteComponent"
      :user="triggerResendInviteForUser"
      :invite-token-expiry-days="inviteTokenExpiryDays"
      @close="triggerResendInviteForUser = null"
    ></component>
    <!-- TODO: whether a user is being edited should be part of the URL -->
    <div v-if="isEditing">
      <UserEditForm
        @done="onDoneEditing($event.isUserModified)"
        :user="userBeingEdited"
        :current-user-role="currentUserRole"
        :access-levels="accessLevels"
        :filter-access-levels="filterAccessLevels"
        :activated-plugins="activatedPlugins"
        :password-strength-validation-rules="passwordStrengthValidationRules"
        @resend-invite="triggerResendInviteForUser = $event.user"
        @updated="userBeingEdited = $event.user"
      />
    </div>
    <div class="add-existing-user-modal modal" ref="addExistingUserModal">
      <div class="modal-content">
        <h3>{{ translate('UsersManager_AddExistingUser') }}</h3>
        <p>{{ translate('UsersManager_EnterUsernameOrEmail') }}:</p>
        <div>
          <Field
            v-model="addNewUserLoginEmail"
            name="add-existing-user-email"
            uicontrol="text"
          />
        </div>
      </div>
      <div class="modal-footer">
        <a
          href
          class="modal-action modal-close btn"
          @click.prevent="addExistingUser()"
          style="margin-right:3.5px"
        >{{ translate('General_Add') }}</a>
        <a
          href
          class="modal-action modal-close modal-no"
          @click.prevent="addNewUserLoginEmail = null"
        >{{ translate('General_Cancel') }}</a>
      </div>
    </div>
  </div>
</template>

<!--suppress JSConstantReassignment, TypeScriptValidateTypes -->
<script lang="ts">
/* eslint-disable newline-per-chained-call */

import { defineComponent } from 'vue';
import {
  ContentIntro,
  EnrichedHeadline,
  Tooltips,
  Matomo,
  MatomoUrl,
  AjaxHelper,
  translate,
  NotificationsStore,
  useExternalPluginComponent,
} from 'CoreHome';
import { Field } from 'CorePluginsAdmin';
import PagedUsersList from '../PagedUsersList/PagedUsersList.vue';
import UserEditForm from '../UserEditForm/UserEditForm.vue';
import User from '../User';
import SearchParams from '../PagedUsersList/SearchParams';

interface UsersManagerState {
  isEditing: boolean;
  isInviting: boolean;
  isCurrentUserSuperUser: boolean;
  users: User[];
  userBeingEdited: User | null;
  totalEntries: null | number;
  searchParams: SearchParams;
  isLoadingUsers: boolean;
  addNewUserLoginEmail: string;
  loading: boolean;
  triggerResendInviteForUser: User | null;
}

const NUM_USERS_PER_PAGE = 20;

const { $ } = window;

export default defineComponent({
  props: {
    currentUserRole: {
      type: String,
      required: true,
    },
    initialSiteName: {
      type: String,
      required: true,
    },
    initialSiteId: {
      type: String,
      required: true,
    },
    accessLevels: {
      type: Array,
      required: true,
    },
    filterAccessLevels: {
      type: Array,
      required: true,
    },
    filterStatusLevels: {
      type: Array,
      required: true,
    },
    activatedPlugins: {
      type: Array,
      required: true,
    },
    inviteTokenExpiryDays: {
      type: String,
      required: true,
    },
    passwordStrengthValidationRules: {
      type: Array,
      default: () => [],
    },
    inviteComponent: {
      type: Object,
      required: false,
      default: null,
    },
    resendInviteComponent: {
      type: Object,
      required: false,
      default: null,
    },
  },
  components: {
    EnrichedHeadline,
    PagedUsersList,
    UserEditForm,
    Field,
  },
  directives: {
    ContentIntro,
    Tooltips,
  },
  data(): UsersManagerState {
    return {
      isEditing: !!MatomoUrl.urlParsed.value.showadduser,
      isInviting: false,
      isCurrentUserSuperUser: true,
      users: [],
      totalEntries: null,
      searchParams: {
        offset: 0,
        limit: NUM_USERS_PER_PAGE,
        filter_search: '',
        filter_access: '',
        filter_status: '',
        idSite: this.initialSiteId,
      },
      isLoadingUsers: false,
      userBeingEdited: null,
      addNewUserLoginEmail: '',
      loading: false,
      triggerResendInviteForUser: null,
    };
  },
  created() {
    this.fetchUsers();
  },
  watch: {
    limit() {
      this.fetchUsers();
    },
  },
  methods: {
    onInvite(user: User) {
      this.isInviting = false;
      this.userBeingEdited = user;
      this.isEditing = true;
      this.fetchUsers();
    },
    onEditUser(user: User) {
      Matomo.helper.lazyScrollToContent();
      this.isEditing = true;
      this.userBeingEdited = user;
    },
    onDoneEditing(isUserModified: boolean) {
      this.isEditing = false;
      if (isUserModified) { // if a user was modified, we must reload the users list
        this.fetchUsers();
      }
    },
    showAddExistingUserModal() {
      $(this.$refs.addExistingUserModal as HTMLElement).modal({ dismissible: false }).modal('open');
    },
    onChangeUserRole(users: User[]|string, role: string, password: string) {
      this.isLoadingUsers = true;

      Promise.resolve().then(() => {
        if (users === 'all') {
          return this.getAllUsersInSearch();
        }
        return users as User[];
      }).then((usersResolved) => (
        usersResolved.filter((u) => u.role !== 'superuser').map((u) => u.login)
      )).then((userLogins) => {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const type = this.accessLevels.filter((a: any) => a.key === role).map((a: any) => a.type);

        let requests;
        if (type.length && type[0] === 'capability') {
          requests = userLogins.map((login) => ({
            method: 'UsersManager.addCapabilities',
            userLogin: login,
            capabilities: role,
            idSites: this.searchParams.idSite,
            passwordConfirmation: password,
          }));
        } else {
          requests = userLogins.map((login) => ({
            method: 'UsersManager.setUserAccess',
            userLogin: login,
            access: role,
            idSites: this.searchParams.idSite,
            passwordConfirmation: password,
          }));
        }

        return AjaxHelper.fetch(requests, { createErrorNotification: true });
      }).catch(() => {
        // ignore (errors will still be displayed to the user)
      }).finally(
        () => this.fetchUsers(),
      );
    },
    getAllUsersInSearch() {
      return AjaxHelper.fetch<User[]>({
        method: 'UsersManager.getUsersPlusRole',
        filter_search: this.searchParams.filter_search,
        filter_access: this.searchParams.filter_access,
        filter_status: this.searchParams.filter_status,
        idSite: this.searchParams.idSite,
        filter_limit: '-1',
      });
    },
    onDeleteUser(users: User[]|string, password: string) {
      this.isLoadingUsers = true;

      Promise.resolve().then(() => {
        if (users === 'all') {
          return this.getAllUsersInSearch();
        }
        return users as User[];
      }).then((usersResolved) => usersResolved.map((u) => u.login)).then((userLogins) => {
        const requests = userLogins.map((login) => ({
          method: 'UsersManager.deleteUser',
          userLogin: login,
          passwordConfirmation: password,
        }));
        return AjaxHelper.fetch(requests, { createErrorNotification: true });
      }).then(() => {
        NotificationsStore.scrollToNotification(NotificationsStore.show({
          id: 'removeUserSuccess',
          message: translate('UsersManager_DeleteSuccess'),
          context: 'success',
          type: 'toast',
        }));
        this.fetchUsers();
      }, () => {
        if (users !== 'all' && users.length > 1) {
          // Show a notification that some users might not have been removed if an error occurs
          // and more than one users was tried to remove
          // Note: We do not scroll to this notification, as the error notification from AjaxHandler
          // will be created earlier, which will already be scrolled into view.
          NotificationsStore.show({
            id: 'removeUserSuccess',
            message: translate('UsersManager_DeleteNotSuccessful'),
            context: 'warning',
            type: 'toast',
          });
        }
        this.fetchUsers();
      });
    },
    fetchUsers() {
      this.isLoadingUsers = true;
      return AjaxHelper.fetch<AjaxHelper>(
        {
          ...this.searchParams,
          method: 'UsersManager.getUsersPlusRole',
        },
        { returnResponseObject: true },
      ).then((helper) => {
        const result = helper.getRequestHandle()!;

        this.totalEntries = parseInt(
          result.getResponseHeader('x-matomo-total-results') || '0',
          10,
        );
        this.users = result.responseJSON as User[];

        this.isLoadingUsers = false;
      }).catch(() => {
        this.isLoadingUsers = false;
      });
    },
    addExistingUser() {
      this.isLoadingUsers = true;
      return AjaxHelper.fetch<{ value: boolean }>({
        method: 'UsersManager.userExists',
        userLogin: this.addNewUserLoginEmail,
      }).then((response) => {
        if (response && response.value) {
          return this.addNewUserLoginEmail;
        }

        return AjaxHelper.fetch<{ value: string }>({
          method: 'UsersManager.getUserLoginFromUserEmail',
          userEmail: this.addNewUserLoginEmail,
        }).then((r) => r.value);
      }).then((login) => (
        AjaxHelper.post(
          {
            method: 'UsersManager.setUserAccess',
          },
          {
            userLogin: login,
            access: 'view',
            idSites: this.searchParams.idSite,
          },
        )
      )).then(
        () => this.fetchUsers(),
      ).catch(() => {
        this.isLoadingUsers = false;
      });
    },
    onAddNewUser() {
      const parameters = { isAllowed: true };
      Matomo.postEvent('UsersManager.initAddUser', parameters);
      if (parameters && !parameters.isAllowed) {
        return;
      }

      this.isInviting = true;
      this.userBeingEdited = null;
    },
  },
  computed: {
    usedInviteComponent() {
      if (this.inviteComponent) {
        const [plugin, component] = this.inviteComponent.split('.');
        return useExternalPluginComponent(plugin, component);
      }

      return useExternalPluginComponent('UsersManager', 'UserInvite');
    },
    usedResendInviteComponent() {
      if (this.resendInviteComponent) {
        const [plugin, component] = this.resendInviteComponent.split('.');
        return useExternalPluginComponent(plugin, component);
      }

      return useExternalPluginComponent('UsersManager', 'ResendInviteModal');
    },
  },
});
</script>