Skip to content
Snippets Groups Projects
PermissionModal.vue 15.91 KiB
<script setup lang="ts">
import { onMounted, reactive, watch, ref, computed } from "vue";
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import DeleteModal from "@/components/modals/DeleteModal.vue";
import SearchUserModal from "@/components/modals/SearchUserModal.vue";
import { Modal } from "bootstrap";
import dayjs from "dayjs";
import type {
  BucketPermissionOut,
  BucketPermissionIn,
  BucketPermissionParameters,
} from "@/client/s3proxy";
import type { User } from "@/client/auth";
import type { FolderTree } from "@/types/PseudoFolder";
import { Permission } from "@/client/s3proxy";
import { Toast } from "bootstrap";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import { useBucketStore } from "@/stores/buckets";
import BootstrapToast from "@/components/BootstrapToast.vue";

// Props
// -----------------------------------------------------------------------------
const props = defineProps<{
  modalID: string;
  bucketName: string;
  subFolders: FolderTree;
  editUserPermission?: BucketPermissionOut;
  readonly: boolean;
  editable: boolean;
  deletable: boolean;
  backModalId?: string;
}>();

const bucketRepository = useBucketStore();

const emit = defineEmits<{ (e: "permission-deleted"): void }>();
// Variables
// -----------------------------------------------------------------------------
const randomIDSuffix = Math.random().toString(16).substring(2, 8);
let permissionModal: Modal | null = null;
let successToast: Toast | null = null;

// Reactive State
// -----------------------------------------------------------------------------
// eslint-disable-next-line vue/no-setup-props-destructure
const formState = reactive<{
  loading: boolean;
  grantee_name: string;
  error: boolean;
  readonly: boolean;
}>({
  loading: false,
  grantee_name: "",
  error: false,
  readonly: props.readonly,
});

// eslint-disable-next-line vue/no-setup-props-destructure
const permission = reactive<BucketPermissionIn>({
  from_timestamp: undefined,
  to_timestamp: undefined,
  file_prefix: undefined,
  permission: undefined,
  uid: "",
  bucket_name: props.bucketName,
});

const permissionDeleted = ref<boolean>(false);
const permissionForm = ref<HTMLFormElement | undefined>(undefined);

// Computes Properties
// -----------------------------------------------------------------------------
const editPermission = computed<boolean>(
  () => props.editUserPermission != undefined,
);

const possibleSubFolders = computed<string[]>(() =>
  findSubFolders(props.subFolders, []),
);

const permissionUserReadonly = computed<boolean>(() => {
  return formState.readonly || editPermission.value;
});

// Watchers
// -----------------------------------------------------------------------------
watch(
  () => props.bucketName,
  (newBucketName) => {
    updateLocalPermission();
    permission.bucket_name = newBucketName;
  },
);

watch(
  () => props.editUserPermission,
  () => updateLocalPermission(),
);

// Functions
// -----------------------------------------------------------------------------
/**
 * Reset the form. Triggered when the modal is closed.
 */
function modalClosed() {
  formState.readonly = props.readonly;
  formState.error = false;
  if (editPermission.value) {
    updateLocalPermission();
  }
}

/**
 * Callback when the toast is hidden again.
 */
function toastHidden() {
  permissionDeleted.value = false;
}

/**
 * Check if an input should be visible based on its state
 * @param input Input which visibility should be determined.
 */
function inputVisible(input?: number | string | null): boolean {
  return !formState.readonly || input != undefined;
}

/**
 * Update the form content
 */
function updateLocalPermission() {
  if (props.editUserPermission != undefined) {
    permission.bucket_name = props.editUserPermission.bucket_name;
    permission.file_prefix = props.editUserPermission.file_prefix;
    permission.uid = props.editUserPermission.uid;
    formState.grantee_name = props.editUserPermission.grantee_display_name;
    permission.from_timestamp = props.editUserPermission.from_timestamp;
    permission.to_timestamp = props.editUserPermission.to_timestamp;
    permission.permission = props.editUserPermission.permission;
  } else {
    permission.file_prefix = undefined;
    permission.uid = "";
    permission.from_timestamp = undefined;
    permission.to_timestamp = undefined;
    permission.permission = undefined;
    formState.grantee_name = "";
  }
}

/**
 * Find recursively all sub folders based on the folder structure
 * @param currentFolder Current Folder
 * @param parentFolders All parent folders
 */
function findSubFolders(
  currentFolder: FolderTree,
  parentFolders: string[],
): string[] {
  const arr: string[] = [];
  for (const subFolder of Object.keys(currentFolder.subFolders)) {
    const subFolderString =
      (parentFolders.length > 0 ? parentFolders.join("/") + "/" : "") +
      subFolder +
      (subFolder.endsWith("/") ? "" : "/");
    arr.push(
      subFolderString,
      ...findSubFolders(
        currentFolder.subFolders[subFolder],
        subFolderString.slice(0, subFolderString.length - 1).split("/"),
      ),
    );
  }
  return arr;
}

/**
 * Submit the form
 */
function formSubmit() {
  formState.error = false;
  if (permissionForm.value?.checkValidity()) {
    formState.loading = true;
    const serverAnswerPromise = editPermission.value
      ? bucketRepository.updateBucketPermission(
          permission.bucket_name,
          permission.uid,
          {
            to_timestamp: permission.to_timestamp,
            from_timestamp: permission.from_timestamp,
            permission: permission.permission,
            file_prefix: permission.file_prefix,
          } as BucketPermissionParameters,
        )
      : bucketRepository.createBucketPermission(permission);
    serverAnswerPromise
      .then(() => {
        permissionModal?.hide();
        successToast?.show();
        updateLocalPermission();
      })
      .catch(() => {
        formState.error = true;
      })
      .finally(() => {
        formState.loading = false;
      });
  }
}

/**
 * Delete a permission for a bucket user combination
 * @param bucketName Bucket to delete
 * @param uid ID of grantee of the permission
 */
function confirmedDeletePermission(bucketName: string, uid: string) {
  if (!formState.loading) {
    formState.loading = true;
    bucketRepository
      .deleteBucketPermission(bucketName, uid)
      .then(() => {
        permissionDeleted.value = true;
        emit("permission-deleted");
        permissionModal?.hide();
        successToast?.show();
      })
      .catch(() => {
        formState.error = true;
      })
      .finally(() => {
        formState.loading = false;
      });
  }
}

function updateUser(user: User) {
  permission.uid = user.uid;
  formState.grantee_name = user.display_name;
}

// Lifecycle Hooks
// -----------------------------------------------------------------------------
onMounted(() => {
  permissionModal = new Modal("#" + props.modalID);
  successToast = new Toast("#" + "toast-" + randomIDSuffix);
  updateLocalPermission();
});

function fromTimestampChanged(target?: HTMLInputElement | null) {
  permission.from_timestamp = target?.value
    ? dayjs(target?.value).unix()
    : undefined;
}

function toTimestampChanged(target?: HTMLInputElement | null) {
  permission.to_timestamp = target?.value
    ? dayjs(target?.value).unix()
    : undefined;
}
</script>

<template>
  <DeleteModal
    :modalID="'delete-permission-modal' + randomIDSuffix"
    object-name-delete="permission"
    :back-modal-id="modalID"
    @confirm-delete="
      confirmedDeletePermission(permission.bucket_name, permission.uid)
    "
  />
  <SearchUserModal
    :modalID="'search-user-modal' + randomIDSuffix"
    :back-modal-id="modalID"
    @user-found="updateUser"
  />
  <bootstrap-toast
    :toast-id="'toast-' + randomIDSuffix"
    v-on="{ 'hidden.bs.toast': toastHidden }"
  >
    Successfully
    <template v-if="permissionDeleted">deleted</template>
    <template v-else-if="editPermission">edited</template>
    <template v-else>created</template>
    Permission
  </bootstrap-toast>
  <bootstrap-modal
    :modalID="modalID"
    :static-backdrop="true"
    modal-label="Permission Modal"
    v-on="{ 'hidden.bs.modal': modalClosed }"
  >
    <template v-slot:header v-if="formState.readonly"
      >View Permission
    </template>
    <template v-slot:header v-else-if="props.editUserPermission !== undefined"
      >Edit Permission
    </template>
    <template v-slot:header v-else> Create new Bucket Permission</template>
    <template v-slot:extra-button>
      <font-awesome-icon
        v-if="props.deletable"
        icon="fa-solid fa-trash"
        class="me-2 cursor-pointer"
        :class="{ 'delete-icon': !formState.loading }"
        data-bs-toggle="modal"
        :data-bs-target="'#delete-permission-modal' + randomIDSuffix"
      />
      <font-awesome-icon
        v-if="formState.readonly && props.editable"
        icon="fa-solid fa-pen"
        class="pseudo-link cursor-pointer"
        @click="formState.readonly = false"
      />
    </template>
    <template v-slot:body>
      <form
        @submit.prevent="formSubmit"
        :id="'permissionCreateEditForm' + randomIDSuffix"
        ref="permissionForm"
      >
        <div class="mb-3 row">
          <label for="bucketNameInput" class="col-2 col-form-label"
            >Bucket<span v-if="!formState.readonly">*</span></label
          >
          <div class="col-10">
            <input
              type="text"
              readonly
              class="form-control-plaintext"
              id="bucketNameInput"
              required
              :value="permission.bucket_name"
            />
          </div>
        </div>
        <div class="mb-3 row align-items-center d-flex">
          <label for="permissionGranteeInput" class="col-2 col-form-label">
            User<span v-if="!formState.readonly">*</span>
          </label>
          <div
            :class="{
              'col-10': permissionUserReadonly,
              'col-9': !permissionUserReadonly,
            }"
          >
            <input
              type="text"
              class="form-control"
              id="permissionGranteeInput"
              required
              placeholder="Search for a user"
              v-model.trim="formState.grantee_name"
              readonly
            />
          </div>
          <div v-if="!permissionUserReadonly" class="col-1">
            <button
              type="button"
              class="btn btn-secondary btn-sm float-end"
              data-bs-toggle="modal"
              :data-bs-target="'#search-user-modal' + randomIDSuffix"
            >
              <font-awesome-icon icon="fa-solid fa-magnifying-glass" />
            </button>
          </div>
        </div>
        <div class="mb-3 row">
          <label for="permissionTypeInput" class="col-3 col-form-label">
            Permission Type<span v-if="!formState.readonly">*</span>
          </label>
          <div class="col-9">
            <select
              class="form-select text-lowercase"
              id="permissionTypeInput"
              required
              :disabled="formState.readonly"
              v-model="permission.permission"
            >
              <option disabled selected>Select one...</option>
              <option v-for="perm in Permission" :key="perm" :value="perm">
                {{ perm.toLowerCase() }}
              </option>
            </select>
          </div>
        </div>
        <div class="mb-3 row">
          <label
            for="permissionDateFromInput"
            class="col-2 col-form-label"
            v-if="inputVisible(permission.from_timestamp)"
          >
            From
          </label>
          <div class="col-4" v-if="inputVisible(permission.from_timestamp)">
            <input
              type="date"
              class="form-control"
              id="permissionDateFromInput"
              :readonly="formState.readonly"
              :min="dayjs().format('YYYY-MM-DD')"
              :value="
                permission.from_timestamp
                  ? dayjs.unix(permission.from_timestamp).format('YYYY-MM-DD')
                  : undefined
              "
              @input="
                (event) =>
                  fromTimestampChanged(event.target as HTMLInputElement)
              "
            />
          </div>
          <label
            for="permissionDateToInput"
            class="col-2 col-form-label"
            v-if="inputVisible(permission.to_timestamp)"
          >
            To
          </label>
          <div class="col-4" v-if="inputVisible(permission.to_timestamp)">
            <input
              type="date"
              class="form-control"
              id="permissionDateToInput"
              :readonly="formState.readonly"
              :min="
                permission.from_timestamp != null
                  ? dayjs
                      .unix(permission.from_timestamp)
                      .add(1, 'day')
                      .format('YYYY-MM-DD')
                  : dayjs().add(1, 'day').format('YYYY-MM-DD')
              "
              :value="
                permission.to_timestamp
                  ? dayjs.unix(permission.to_timestamp).format('YYYY-MM-DD')
                  : undefined
              "
              @input="
                (event) => toTimestampChanged(event.target as HTMLInputElement)
              "
            />
          </div>
        </div>
        <div
          class="mb-3 row align-items-center d-flex"
          v-if="
            inputVisible(permission.file_prefix) &&
            possibleSubFolders.length > 0
          "
        >
          <label for="permissionSubFolderInput" class="col-2 col-form-label">
            Subfolder
          </label>
          <div
            :class="{
              'col-10': formState.readonly,
              'col-9': !formState.readonly,
            }"
          >
            <select
              class="form-select"
              id="permissionSubFolderInput"
              :disabled="formState.readonly"
              v-model="permission.file_prefix"
            >
              <option disabled selected>Select one folder...</option>
              <option
                v-for="folder in possibleSubFolders"
                :key="folder"
                :value="folder"
              >
                {{ folder }}
              </option>
            </select>
          </div>
          <div class="col-1" v-if="!formState.readonly">
            <button
              type="button"
              class="btn btn-outline-danger btn-sm float-end"
              @click="permission.file_prefix = undefined"
              :hidden="permission.file_prefix == undefined"
            >
              <font-awesome-icon icon="fa-solid fa-x" />
            </button>
          </div>
        </div>
      </form>
      <span class="text-danger" v-if="formState.error"
        >There was some kind of error<br />Try again later</span
      >
    </template>
    <template v-slot:footer>
      <button
        v-if="backModalId !== undefined"
        type="button"
        class="btn btn-secondary"
        :data-bs-target="'#' + props.backModalId"
        data-bs-toggle="modal"
      >
        Back
      </button>
      <button
        v-else
        type="button"
        class="btn btn-secondary"
        data-bs-dismiss="modal"
      >
        Close
      </button>
      <button
        type="submit"
        :form="'permissionCreateEditForm' + randomIDSuffix"
        class="btn btn-primary"
        :disabled="formState.loading"
        v-if="!formState.readonly"
      >
        <span
          v-if="formState.loading"
          class="spinner-border spinner-border-sm"
          role="status"
          aria-hidden="true"
        ></span>
        Save
      </button>
    </template>
  </bootstrap-modal>
</template>

<style scoped>
.pseudo-link {
  color: var(--bs-secondary);
}

.pseudo-link:hover {
  color: var(--bs-link-hover-color);
}

.delete-icon {
  color: var(--bs-secondary) !important;
}

.delete-icon:hover {
  color: var(--bs-danger) !important;
}
</style>