<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>
AI 正在分析代码…
评论加载中...