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.circ/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">
      <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="showResendPopup($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>
    <!-- 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"
        :invite-token-expiry-days="inviteTokenExpiryDays"
        :access-levels="accessLevels"
        :filter-access-levels="filterAccessLevels"
        :initial-site-id="initialSiteId"
        :initial-site-name="initialSiteName"
        :activated-plugins="activatedPlugins"
        @resend-invite="showResendPopup($event.user)"
        @updated="userBeingEdited = $event.user"
      />
    </div>
    <div class="resend-invite-confirm-modal modal" ref="resendInviteConfirmModal">
      <div class="btn-close modal-close"><i class="icon-close"></i></div>
      <div class="modal-content">
        <h2 class="modal-title">{{ translate('UsersManager_ResendInvite') }}</h2>
        <p
          v-if="userBeingEdited"
          v-html="$sanitize(translate(
            'UsersManager_InviteConfirmMessage',
            [`<strong>${userBeingEdited.login}</strong>`,
             `<strong>${userBeingEdited.email}</strong>`]
            ,
          ))"
        ></p>
        <p><strong>
            {{ translate('UsersManager_InviteActionNotes', inviteTokenExpiryDays) }}
        </strong></p>
      </div>
      <div class="modal-footer">
        <span v-if="copied" class="success-copied">
          <i class="icon-success"></i>
          {{ translate('UsersManager_LinkCopied') }}</span>
        <button
          @click="showInviteActionPasswordConfirm('copy')"
          class="btn btn-copy-link modal-action"
          style="margin-right:3.5px"
        >{{ translate('UsersManager_CopyLink') }}</button>
        <button
          class="btn btn-resend modal-action modal-no"
          @click = "showInviteActionPasswordConfirm('send')"
        >{{ translate('UsersManager_ResendInvite') }}</button>
      </div>
    </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>
    <PasswordConfirmation
      v-model="showPasswordConfirmationForInviteAction"
      @confirmed="onInviteAction"
    />
  </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,
} from 'CoreHome';
import { Field, PasswordConfirmation } 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;
  isCurrentUserSuperUser: boolean;
  users: User[];
  userBeingEdited: User | null;
  totalEntries: null | number;
  searchParams: SearchParams;
  isLoadingUsers: boolean;
  addNewUserLoginEmail: string;
  copied: boolean;
  loading: boolean;
  showPasswordConfirmationForInviteAction: boolean;
  inviteAction: string;
}

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,
    },
  },
  components: {
    PasswordConfirmation,
    EnrichedHeadline,
    PagedUsersList,
    UserEditForm,
    Field,
  },
  directives: {
    ContentIntro,
    Tooltips,
  },
  data(): UsersManagerState {
    return {
      isEditing: !!MatomoUrl.urlParsed.value.showadduser,
      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: '',
      copied: false,
      loading: false,
      showPasswordConfirmationForInviteAction: false,
      inviteAction: '',
    };
  },
  created() {
    this.fetchUsers();
  },
  watch: {
    limit() {
      this.fetchUsers();
    },
  },
  methods: {
    showInviteActionPasswordConfirm(action: string) {
      if (this.loading) return;
      this.showPasswordConfirmationForInviteAction = true;
      this.inviteAction = action;
    },
    showResendPopup(user: User) {
      this.userBeingEdited = user;
      $(this.$refs.resendInviteConfirmModal as HTMLElement)
        .modal({
          dismissible: false,
        })
        .modal('open');
      this.copied = false;
    },
    onInviteAction(password: string) {
      if (this.inviteAction === 'send') {
        this.onResendInvite(password);
      } else {
        this.generateInviteLink(password);
      }
    },
    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();
      });
    },
    async generateInviteLink(password: string) {
      if (this.loading) {
        return;
      }
      this.loading = true;
      try {
        const res = await AjaxHelper.post<{ value: string }>(
          {
            method: 'UsersManager.generateInviteLink',
          }, {
            userLogin: this.userBeingEdited!.login,
            passwordConfirmation: password,
          },
        );

        await this.copyToClipboard(res.value);
        // eslint-disable-next-line no-empty
      } catch (e) {

      }
      this.loading = false;
    },
    async copyToClipboard(value: string) {
      try {
        const tempInput = document.createElement('input');
        tempInput.style.top = '-100px';
        tempInput.style.left = '0';
        tempInput.style.position = 'fixed';
        tempInput.value = value;
        document.body.appendChild(tempInput);
        tempInput.select();
        if (window.location.protocol !== 'https:') {
          document.execCommand('copy');
        } else {
          await navigator.clipboard.writeText(tempInput.value);
        }
        document.body.removeChild(tempInput);
        this.copied = true;
        // eslint-disable-next-line no-empty
      } catch (e) {
        const id = NotificationsStore.show({
          message: `<strong>${translate('UsersManager_CopyDenied')}</strong><br>
${translate('UsersManager_CopyDeniedHints', [`<br><span class="invite-link">${value}</span>`])}`,
          id: 'copyError',
          context: 'error',
          type: 'transient',
        });
        NotificationsStore.scrollToNotification(id);
      }
    },
    onResendInvite(password: string) {
      if (password === '') return;
      AjaxHelper.post<AjaxHelper>(
        {
          method: 'UsersManager.resendInvite',
          userLogin: this.userBeingEdited!.login,
        },
        {
          passwordConfirmation: password,
        },
      ).then(() => {
        this.fetchUsers();
        $(this.$refs.resendInviteConfirmModal as HTMLElement).modal('close');
        const id = NotificationsStore.show({
          message: translate('UsersManager_InviteSuccess'),
          id: 'resendInvite',
          context: 'success',
          type: 'transient',
        });
        NotificationsStore.scrollToNotification(id);
      });
    },
    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.isEditing = true;
      this.userBeingEdited = null;
    },
  },
});
</script>