index.vue 9.43 KB
<script lang="ts" setup>
import { useKeycloakStore } from "@core/stores/keycloakStore";

const keycloakStore = useKeycloakStore();

const isCurrentPasswordVisible = ref(false);
const isNewPasswordVisible = ref(false);
const isConfirmPasswordVisible = ref(false);
const currentPassword = ref("");
const newPassword = ref("");
const newPasswordError = ref("");
const confirmPassword = ref("");
const isSubmitting = ref(false);

watch(newPassword, (newValue) => {
  if (currentPassword.value && newValue === currentPassword.value) {
    newPasswordError.value =
      "Kata sandi baru tidak boleh sama dengan kata sandi lama.";
  } else {
    newPasswordError.value = "";
  }
});

const passwordRequirements = [
  "Panjang minimal 8 karakter, maksimal 20 karakter",
  "Minimal satu karakter huruf besar",
  "Minimal satu angka",
  "Minimal satu simbol, atau karakter spasi",
];

// Aturan Validasi
const oldPasswordRules = [
  (v: string) => !!v || "Konfirmasi kata sandi diperlukan",
];

const passwordRules = [
  (v: string) => !!v || "Kata sandi diperlukan",
  (v: string) => v.length >= 8 || "Kata sandi minimal terdiri dari 8 karakter",
  (v: string) =>
    /[a-z]/.test(v) || "Kata sandi setidaknya mengandung satu huruf kecil",
  (v: string) =>
    /[A-Z]/.test(v) || "Kata sandi setidaknya mengandung satu huruf besar",
  (v: string) => /\d/.test(v) || "Kata sandi setidaknya berisi satu angka",
  (v: string) =>
    /[ !"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]/.test(v) ||
    "Kata sandi setidaknya mengandung satu simbol atau spasi",
];

const confirmPasswordRules = [
  (v: string) => !!v || "Konfirmasi kata sandi diperlukan",
  (v: string) => v === newPassword.value || "Kata sandi tidak cocok",
];

// Generate Password
function generatePassword(length: number = 10): string {
  const lowercase = "abcdefghijklmnopqrstuvwxyz";
  const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const numbers = "0123456789";
  const symbols = ` !"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`;

  // At least one character from each required category
  const mandatoryCharacters = [
    lowercase[Math.floor(Math.random() * lowercase.length)],
    uppercase[Math.floor(Math.random() * uppercase.length)],
    numbers[Math.floor(Math.random() * numbers.length)],
    symbols[Math.floor(Math.random() * symbols.length)],
  ];

  // Combine all character sets into one charset
  const allCharacters = lowercase + uppercase + numbers + symbols;
  const remainingLength = length - mandatoryCharacters.length;
  let password = mandatoryCharacters;

  // Fill the rest of the password with random characters from the combined charset
  for (let i = 0; i < remainingLength; i++) {
    const randomChar =
      allCharacters[Math.floor(Math.random() * allCharacters.length)];
    password.push(randomChar);
  }

  // Shuffle the password to ensure randomness
  password = password.sort(() => Math.random() - 0.5);

  // Return the password as a string
  return password.join("");
}

// fungsi untuk encoding Base64
function toBase64(str: string) {
  return btoa(unescape(encodeURIComponent(str)));
}

// Set Password
async function setPassword() {
  isSubmitting.value = true;

  if (isEmpty(currentPassword.value)) {
    alert("Kata Sandi Lama tidak boleh kosong");
    return;
  }

  // Buat objek data dengan password yang sudah di-encoding
  const requestData = {
    oldPassword: toBase64(currentPassword.value),
    newPassword: toBase64(newPassword.value),
  };

  if (
    newPassword.value.length < 8 ||
    !/[a-z]/.test(newPassword.value) ||
    !/\d/.test(newPassword.value) ||
    !/[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(newPassword.value)
  ) {
    alert(
      "Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus."
    );
    return;
  }

  if (newPassword.value === currentPassword.value) {
    alert("Kata sandi baru tidak boleh sama dengan kata sandi lama.");
    return;
  }

  if (newPassword.value !== confirmPassword.value) {
    alert("Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok.");
    return;
  }

  try {
    // Panggil API untuk mengganti password
    const apiEndpoint = `https://api.ui.ac.id/my/pw`;
    const response = await fetch(apiEndpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${keycloakStore.accessToken}`,
      },
      body: JSON.stringify(requestData),
    });

    // Check for HTTP status code 401 which indicates old password is incorrect
    if (response.status === 401) {
      throw new Error("Kata sandi lama tidak sesuai");
    }

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || "Gagal mengubah password");
    }

    // Jika berhasil, tampilkan pesan dan reset form
    alert(
      "Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru."
    );
    currentPassword.value = "";
    newPassword.value = "";
    confirmPassword.value = "";
  } catch (error) {
    // Handle error
    if (error instanceof Error) {
      alert(error.message);
    } else {
      alert("Terjadi kesalahan saat mengubah kata sandi");
    }
  } finally {
    isSubmitting.value = false;
  }

  // Reset input
  currentPassword.value = "";
  newPassword.value = "";
  confirmPassword.value = "";
}
</script>

<template>
  <VRow>
    <!-- SECTION: Change Password -->
    <VCol cols="12">
      <VCard>
        <VCardItem class="pb-6">
          <VCardTitle>Ganti Kata Sandi</VCardTitle>
        </VCardItem>
        <VForm>
          <VCardText class="pt-0">
            <!-- 👉 Current Password -->
            <VRow>
              <VCol cols="12" md="6">
                <!-- 👉 current password -->
                <VTextField
                  v-model="currentPassword"
                  :type="isCurrentPasswordVisible ? 'text' : 'password'"
                  :maxlength="20"
                  :append-inner-icon="
                    isCurrentPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
                  "
                  autocomplete="on"
                  label="Kata Sandi Lama"
                  @click:append-inner="
                    isCurrentPasswordVisible = !isCurrentPasswordVisible
                  "
                  :rules="oldPasswordRules"
                  clearable
                />
              </VCol>
            </VRow>

            <!-- 👉 New Password -->
            <VRow>
              <VCol cols="12" md="6">
                <!-- 👉 new password -->
                <VTextField
                  v-model="newPassword"
                  :type="isNewPasswordVisible ? 'text' : 'password'"
                  :maxlength="20"
                  :append-inner-icon="
                    isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
                  "
                  label="Kata Sandi Baru"
                  autocomplete="on"
                  @click:append-inner="
                    isNewPasswordVisible = !isNewPasswordVisible
                  "
                  :rules="passwordRules"
                  :error-messages="newPasswordError"
                  clearable
                />
              </VCol>

              <VCol cols="12" md="6">
                <!-- 👉 confirm password -->
                <VTextField
                  v-model="confirmPassword"
                  :type="isConfirmPasswordVisible ? 'text' : 'password'"
                  :maxlength="20"
                  :append-inner-icon="
                    isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
                  "
                  autocomplete="on"
                  label="Konfirmasi Kata Sandi"
                  @click:append-inner="
                    isConfirmPasswordVisible = !isConfirmPasswordVisible
                  "
                  :rules="confirmPasswordRules"
                  clearable
                />
              </VCol>
            </VRow>
          </VCardText>

          <!-- 👉 Password Requirements -->
          <VCardText>
            <h6 class="text-h6 text-medium-emphasis mt-1">
              Persyaratan Kata Sandi:
            </h6>

            <VList>
              <VListItem
                v-for="(item, index) in passwordRequirements"
                :key="index"
                class="px-0 mt-n4 mb-n2"
              >
                <template #prepend>
                  <VIcon
                    size="8"
                    icon="ri-circle-fill"
                    color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
                  />
                </template>
                <VListItemTitle class="text-medium-emphasis text-wrap">
                  {{ item }}
                </VListItemTitle>
              </VListItem>
            </VList>

            <!-- 👉 Action Buttons -->
            <div class="d-flex flex-wrap gap-4">
              <VBtn @click="setPassword" :disabled="isSubmitting">{{
                isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
              }}</VBtn>

              <VBtn type="reset" color="secondary" variant="outlined">
                Reset
              </VBtn>
              <VBtn
                color="secondary"
                variant="outlined"
                @click="
                  newPassword = generatePassword();
                  confirmPassword = newPassword;
                "
              >
                Generate Kata Sandi</VBtn
              >
            </div>
          </VCardText>
        </VForm>
      </VCard>
    </VCol>
    <!-- !SECTION -->
  </VRow>
</template>