<template>
  <div class="admin-page-stack">
    <AdminPageHeader
      :title="isEdit ? '编辑客户端' : '新建客户端'"
      :subtitle="
        isEdit
          ? '调整客户端展示信息、回调地址与授权范围。'
          : '创建 OpenID Connect 客户端并配置安全授权能力。'
      "
      icon="edit_note"
      accent="cyan"
      :metrics="formMetrics"
    >
      <template #actions>
        <q-btn
          flat
          icon="arrow_back"
          label="返回"
          @click="navigate('#/applications')"
        />
        <q-btn
          color="primary"
          icon="save"
          label="保存"
          unelevated
          :loading="saving"
          :disable="loading"
          @click="saveAndReturn"
        />
      </template>
    </AdminPageHeader>

    <q-banner v-if="errorMessage" rounded class="admin-error-banner">
      <template #avatar><q-icon name="error_outline" /></template>
      {{ errorMessage }}
      <template #action>
        <q-btn v-if="isEdit" flat color="negative" label="重试" @click="load" />
      </template>
    </q-banner>

    <q-inner-loading :showing="loading" label="加载应用配置..." />

    <div class="admin-form-grid">
      <q-card flat class="admin-panel admin-form-section">
        <q-card-section>
          <div class="admin-section-title">
            <q-icon name="badge" />
            <div>
              <strong>基础信息</strong>
              <span>客户端身份与展示信息</span>
            </div>
          </div>

          <div class="row q-col-gutter-md">
            <q-input
              v-model="model.clientId"
              class="col-12 col-md-6"
              outlined
              dense
              label="ClientId"
              :readonly="isEdit"
              :disable="saving"
            />
            <q-input
              v-model="model.displayName"
              class="col-12 col-md-6"
              outlined
              dense
              label="显示名称"
              :disable="saving"
            />
            <q-input
              v-model="model.logo"
              class="col-12 admin-long-field"
              outlined
              dense
              label="Logo URL"
              :disable="saving"
            />
            <q-select
              :key="`application-type-${selectModeKey}`"
              v-model="model.applicationType"
              class="col-12 col-md-6"
              outlined
              dense
              emit-value
              map-options
              behavior="menu"
              label="应用类型"
              :options="applicationTypeOptions"
              options-dense
              popup-content-class="admin-select-menu"
              :disable="saving"
            />
            <q-select
              :key="`client-type-${selectModeKey}`"
              v-model="model.clientType"
              class="col-12 col-md-6"
              outlined
              dense
              emit-value
              map-options
              behavior="menu"
              label="客户端类型"
              :options="clientTypeOptions"
              options-dense
              popup-content-class="admin-select-menu"
              :disable="saving"
            />
          </div>
        </q-card-section>
      </q-card>

      <q-card flat class="admin-panel admin-form-section">
        <q-card-section>
          <div class="admin-section-title">
            <q-icon name="key" />
            <div>
              <strong>安全凭据</strong>
              <span>机密客户端创建时必须设置 Secret</span>
            </div>
          </div>

          <q-input
            v-model="model.clientSecret"
            outlined
            dense
            :type="showSecret ? 'text' : 'password'"
            :label="isEdit ? 'ClientSecret(更新时不修改)' : 'ClientSecret'"
            :disable="saving || isEdit"
          >
            <template #append>
              <q-btn
                flat
                round
                dense
                :icon="showSecret ? 'visibility_off' : 'visibility'"
                @click="showSecret = !showSecret"
              />
            </template>
          </q-input>

          <q-banner rounded class="admin-security-hint q-mt-md">
            <template #avatar><q-icon name="verified_user" /></template>
            保存时将根据账号安全绑定状态进行 Passkey 或 2FA 确认。
          </q-banner>
        </q-card-section>
      </q-card>

      <q-card
        flat
        class="admin-panel admin-form-section admin-form-section--wide"
      >
        <q-card-section>
          <div class="admin-section-title">
            <q-icon name="route" />
            <div>
              <strong>回调地址</strong>
              <span>每行一个 Redirect URI,也支持分号分隔</span>
            </div>
          </div>

          <q-input
            v-model="model.redirectUrisText"
            outlined
            dense
            autogrow
            label="Redirect URIs"
            :disable="saving"
          />
          <q-input
            v-model="model.postLogoutRedirectUri"
            class="q-mt-md"
            outlined
            dense
            label="Post Logout Redirect URI"
            :disable="saving"
          />
        </q-card-section>
      </q-card>

      <q-card
        flat
        class="admin-panel admin-form-section admin-form-section--wide"
      >
        <q-card-section>
          <div class="admin-section-title">
            <q-icon name="policy" />
            <div>
              <strong>权限范围</strong>
              <span>选择 OpenIddict 权限,或追加自定义权限字符串</span>
            </div>
          </div>

          <div class="admin-permission-groups">
            <section
              v-for="group in permissionGroups"
              :key="group.name"
              class="admin-permission-group"
            >
              <div class="admin-permission-group__title">{{ group.label }}</div>
              <q-option-group
                v-model="model.permissions"
                type="checkbox"
                color="primary"
                :options="group.options"
              />
            </section>
          </div>

          <div class="admin-custom-permission">
            <q-input
              v-model="model.customPermission"
              outlined
              dense
              label="自定义权限"
              :disable="saving"
              @keyup.enter="addCustomPermission"
            />
            <q-btn
              icon="add"
              label="添加"
              unelevated
              color="primary"
              :disable="!model.customPermission"
              @click="addCustomPermission"
            />
          </div>

          <div class="admin-permission-chips">
            <q-chip
              v-for="permission in model.permissions"
              :key="permission"
              removable
              square
              color="primary"
              text-color="white"
              @remove="removePermission(permission)"
            >
              {{ permission }}
            </q-chip>
          </div>
        </q-card-section>
      </q-card>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useQuasar } from "quasar";
import AdminPageHeader from "../components/AdminPageHeader.vue";
import { useApplicationForm } from "../composables/useApplicationForm";
import type { AdminMetric } from "../types";

const props = defineProps<{
  id?: string;
  navigate: (hash: string) => void;
}>();

const $q = useQuasar();
const showSecret = ref(false);
const {
  model,
  loading,
  saving,
  errorMessage,
  isEdit,
  load,
  save,
  addCustomPermission,
} = useApplicationForm(props.id);

const applicationTypeOptions = [
  { label: "Web", value: "web" },
  { label: "Native", value: "native" },
  { label: "Service", value: "service" },
];

const clientTypeOptions = [
  { label: "Confidential", value: "confidential" },
  { label: "Public", value: "public" },
];

const selectModeKey = computed(() => ($q.screen.lt.md ? "mobile" : "desktop"));

const permissionGroups = [
  {
    name: "endpoints",
    label: "Endpoints",
    options: [
      "ept:authorization",
      "ept:device_authorization",
      "ept:end_session",
      "ept:introspection",
      "ept:pushed_authorization",
      "ept:revocation",
      "ept:token",
    ].map(toOption),
  },
  {
    name: "grantTypes",
    label: "Grant Types",
    options: [
      "gt:authorization_code",
      "gt:client_credentials",
      "gt:device_code",
      "gt:implicit",
      "gt:password",
      "gt:refresh_token",
      "gt:urn:ietf:params:oauth:grant-type:token-exchange",
    ].map(toOption),
  },
  {
    name: "responseTypes",
    label: "Response Types",
    options: [
      "rst:code",
      "rst:code id_token",
      "rst:code id_token token",
      "rst:code token",
      "rst:id_token",
      "rst:id_token token",
      "rst:none",
      "rst:token",
    ].map(toOption),
  },
  {
    name: "scopes",
    label: "Scopes",
    options: [
      "scp:openid",
      "scp:address",
      "scp:email",
      "scp:profile",
      "scp:phone",
      "scp:roles",
    ].map(toOption),
  },
];

const formMetrics = computed<AdminMetric[]>(() => [
  {
    label: "模式",
    value: isEdit.value ? "编辑" : "新建",
    icon: isEdit.value ? "edit" : "add_circle",
  },
  {
    label: "回调地址",
    value: model.redirectUrisText
      .split(/[\n;]/)
      .map((item) => item.trim())
      .filter(Boolean).length,
    icon: "route",
  },
  {
    label: "权限数",
    value: model.permissions.length,
    icon: "policy",
  },
  {
    label: "客户端类型",
    value: model.clientType || "-",
    icon: "security",
    tone: "muted",
  },
]);

onMounted(() => {
  void load();
});

async function saveAndReturn() {
  const success = await save();
  if (success) {
    props.navigate("#/applications");
  }
}

function removePermission(permission: string) {
  model.permissions = model.permissions.filter((item) => item !== permission);
}

function toOption(value: string) {
  return { label: value, value };
}
</script>
评论加载中...