<template>
  <section class="identity-page">
    <div class="identity-page-head">
      <div>
        <div class="identity-kicker">TOTP CHANNEL</div>
        <h1 class="identity-title">绑定双因素认证</h1>
        <p class="identity-subtitle">
          使用认证器 App 扫描二维码,再输入 6 位动态验证码。
        </p>
      </div>
      <div class="identity-head-icon identity-head-icon--green">
        <q-icon name="phonelink_lock" size="30px" />
      </div>
    </div>

    <q-card flat bordered class="identity-card">
      <q-card-section class="two-factor-grid">
        <div class="two-factor-qr">
          <q-inner-loading :showing="loading">
            <q-spinner color="primary" size="34px" />
          </q-inner-loading>
          <img
            v-if="setup?.qrCodeSetupImageUrl"
            :src="setup.qrCodeSetupImageUrl"
            alt="双因素认证二维码"
          />
          <q-icon v-else name="qr_code_2" size="92px" />
        </div>

        <div class="two-factor-copy">
          <div class="two-factor-step">
            <span>01</span>
            打开 Microsoft Authenticator、Google Authenticator 或兼容 TOTP
            的应用。
          </div>
          <div class="two-factor-step">
            <span>02</span>
            扫描二维码;如果无法扫描,可以手动输入密钥。
          </div>
          <q-input
            :model-value="setup?.manualEntryKey ?? ''"
            outlined
            readonly
            label="手动密钥"
            class="identity-input"
          >
            <template #prepend>
              <q-icon name="key" />
            </template>
            <template #append>
              <q-btn flat round icon="content_copy" @click="copyKey">
                <q-tooltip>复制密钥</q-tooltip>
              </q-btn>
            </template>
          </q-input>
          <q-input
            v-model="pinCode"
            outlined
            label="6 位验证码"
            autocomplete="one-time-code"
            inputmode="numeric"
            maxlength="6"
            :disable="binding || loading"
            class="identity-input"
            @update:model-value="updatePinCode"
          >
            <template #prepend>
              <q-icon name="pin" />
            </template>
            <template #append>
              <div class="identity-pin-slots" aria-hidden="true">
                <span
                  v-for="index in 6"
                  :key="index"
                  :class="{ 'is-filled': pinCode.length >= index }"
                />
              </div>
            </template>
          </q-input>
          <q-btn
            class="identity-primary-action"
            color="primary"
            icon="verified"
            label="完成绑定"
            :loading="binding"
            :disable="loading || pinCode.length !== 6"
            unelevated
            @click="bind"
          />
          <q-btn
            flat
            icon="refresh"
            label="重新获取二维码"
            :disable="binding || loading"
            @click="loadSetup"
          />
        </div>
      </q-card-section>
    </q-card>
  </section>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useQuasar } from "quasar";
import type {
  UserSecurityBindingStatus,
  UserTwoFactorSetup,
} from "../../models/AuthModels";
import { UserSecurityService } from "../../services/UserSecurityService";

const emit = defineEmits<{
  bound: [status: UserSecurityBindingStatus];
}>();

const $q = useQuasar();
const security = new UserSecurityService();
const loading = ref(false);
const binding = ref(false);
const setup = ref<UserTwoFactorSetup | null>(null);
const pinCode = ref("");

onMounted(loadSetup);

async function loadSetup() {
  loading.value = true;
  try {
    const result = await security.twoFactorSetup();
    if (!result.success || !result.data) {
      $q.notify({ type: "warning", message: result.message ?? "获取密钥失败" });
      return;
    }
    setup.value = result.data;
  } finally {
    loading.value = false;
  }
}

async function bind() {
  if (!setup.value?.setupToken) {
    $q.notify({ type: "warning", message: "二维码已过期,请重新获取" });
    return;
  }
  binding.value = true;
  try {
    const result = await security.bindTwoFactor(
      pinCode.value,
      setup.value.setupToken,
    );
    $q.notify({
      type: result.success ? "positive" : "negative",
      message: result.message ?? "操作完成",
    });
    if (result.success && result.data) {
      emit("bound", result.data);
    }
  } finally {
    binding.value = false;
  }
}

async function copyKey() {
  if (!setup.value?.manualEntryKey) {
    return;
  }
  await navigator.clipboard.writeText(setup.value.manualEntryKey);
  $q.notify({ type: "positive", message: "密钥已复制" });
}

function updatePinCode(value: string | number | null) {
  pinCode.value = String(value ?? "")
    .replace(/\D/g, "")
    .slice(0, 6);
}
</script>
评论加载中...