初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import { NCheckbox, NInput, NSelect, NSwitch, useMessage } from 'naive-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { createIdp, getIdentitySetting, type CreateIdpBody } from '@/api/tenant'
|
||||
import FilePicker from '@/components/FilePicker.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormModal from '@/components/FormModal.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean; cfgId: number }>()
|
||||
const emit = defineEmits<{ 'update:show': [boolean]; created: [] }>()
|
||||
|
||||
const message = useMessage()
|
||||
const submitting = ref(false)
|
||||
// 「用户需要提供主电子邮件地址」开启时 JIT 必须映射邮箱
|
||||
const primaryEmailRequired = ref(false)
|
||||
|
||||
const nameIdFormatOptions = [
|
||||
{ label: '无', value: 'saml-none' },
|
||||
{ label: '电子邮件地址', value: 'saml-emailaddress' },
|
||||
{ label: '持久标识符', value: 'saml-persistent' },
|
||||
{ label: '临时标识符', value: 'saml-transient' },
|
||||
{ label: '未指定', value: 'saml-unspecified' },
|
||||
]
|
||||
const idpAttrOptions = [
|
||||
{ label: 'SAML 断言名称 ID(NameID)', value: 'nameid' },
|
||||
{ label: '断言属性', value: 'attribute' },
|
||||
]
|
||||
const userAttrOptions = [
|
||||
{ label: '用户名(userName)', value: 'userName' },
|
||||
{ label: '主电子邮件(emails.primary.value)', value: 'emails.primary.value' },
|
||||
]
|
||||
|
||||
// 默认值与控制台一致:名称 ID 格式「无」、NameID 映射用户名、
|
||||
// JIT 开启建用户不更新、分配 Administrators 组
|
||||
const blank = {
|
||||
name: '',
|
||||
metadata: '',
|
||||
description: '',
|
||||
iconUrl: '',
|
||||
nameIdFormat: 'saml-none',
|
||||
idpUserAttr: 'nameid',
|
||||
assertionAttr: '',
|
||||
userStoreAttribute: 'userName',
|
||||
jitEnabled: true,
|
||||
jitCreate: true,
|
||||
jitUpdate: false,
|
||||
jitAssignAdminGroup: true,
|
||||
jitMapEmail: false,
|
||||
}
|
||||
const form = reactive({ ...blank })
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(v) => {
|
||||
if (!v) return
|
||||
Object.assign(form, blank)
|
||||
void loadIdentitySetting()
|
||||
},
|
||||
)
|
||||
|
||||
async function loadIdentitySetting() {
|
||||
try {
|
||||
const s = await getIdentitySetting(props.cfgId)
|
||||
primaryEmailRequired.value = s.primaryEmailRequired
|
||||
if (s.primaryEmailRequired) form.jitMapEmail = true
|
||||
} catch {
|
||||
primaryEmailRequired.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// IDCS 只接受 http(s) 外链图标,data URI 会被拒(error.saml.identityprovider.invalidIconUrl)
|
||||
const iconUrlValid = computed(() => {
|
||||
const v = form.iconUrl.trim()
|
||||
return !v || /^https?:\/\//.test(v)
|
||||
})
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
!!form.name.trim() &&
|
||||
form.metadata.includes('EntityDescriptor') &&
|
||||
iconUrlValid.value &&
|
||||
(form.idpUserAttr !== 'attribute' || !!form.assertionAttr.trim()),
|
||||
)
|
||||
|
||||
function buildBody(): CreateIdpBody {
|
||||
return {
|
||||
name: form.name.trim(),
|
||||
metadata: form.metadata,
|
||||
description: form.description.trim() || undefined,
|
||||
iconUrl: form.iconUrl.trim() || undefined,
|
||||
nameIdFormat: form.nameIdFormat,
|
||||
assertionAttribute: form.idpUserAttr === 'attribute' ? form.assertionAttr.trim() : undefined,
|
||||
userStoreAttribute: form.userStoreAttribute,
|
||||
jitEnabled: form.jitEnabled,
|
||||
jitCreate: form.jitCreate,
|
||||
jitUpdate: form.jitUpdate,
|
||||
jitAssignAdminGroup: form.jitAssignAdminGroup,
|
||||
jitMapEmail: form.jitEnabled && form.jitMapEmail,
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const created = await createIdp(props.cfgId, buildBody())
|
||||
message.success(`IdP「${created.name}」已创建(禁用态),激活后显示到登录页`)
|
||||
emit('update:show', false)
|
||||
emit('created')
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '创建失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal
|
||||
:show="show"
|
||||
title="创建 SAML IdP"
|
||||
:width="640"
|
||||
:submitting="submitting"
|
||||
:submit-disabled="!canSubmit"
|
||||
submit-text="创建(禁用态)"
|
||||
@update:show="emit('update:show', $event)"
|
||||
@submit="submit"
|
||||
>
|
||||
<FormField label="名称" required>
|
||||
<NInput v-model:value="form.name" placeholder="my-idp" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="IdP SAML metadata XML 文件"
|
||||
required
|
||||
hint="IdP 侧导出的 metadata XML;先在本页「下载 SP 元数据」导入 IdP 侧再创建"
|
||||
:error="
|
||||
form.metadata && !form.metadata.includes('EntityDescriptor')
|
||||
? '文件内容不含 EntityDescriptor,请确认选择的是 SAML metadata XML'
|
||||
: undefined
|
||||
"
|
||||
:feedback="
|
||||
form.metadata.includes('EntityDescriptor')
|
||||
? `已读取 ${(form.metadata.length / 1024).toFixed(1)} KB`
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<FilePicker
|
||||
:key="`md-${show}`"
|
||||
label="拖拽或点击上传 SAML metadata 文件"
|
||||
hint="支持 .xml,文件内容仅本地读取"
|
||||
accept=".xml"
|
||||
@load="(text) => (form.metadata = text)"
|
||||
/>
|
||||
</FormField>
|
||||
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
|
||||
<FormField label="备注">
|
||||
<NInput v-model:value="form.description" placeholder="可选" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Logo(登录页图标)"
|
||||
hint="展示在登录页 IdP 按钮上;身份域只接受 http(s) 外链图片 URL,不支持上传"
|
||||
:error="iconUrlValid ? undefined : '需为 http(s):// 开头的图片外链 URL'"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
v-if="form.iconUrl && iconUrlValid"
|
||||
:src="form.iconUrl"
|
||||
alt=""
|
||||
class="h-7 w-7 flex-none rounded border border-line-soft object-contain"
|
||||
/>
|
||||
<NInput v-model:value="form.iconUrl" placeholder="https://…/logo.png" />
|
||||
</div>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 mb-2 border-t border-line-soft pt-3 text-[12.5px] font-semibold">
|
||||
映射用户身份
|
||||
</div>
|
||||
<FormField label="请求的名称 ID 格式" hint="向 IdP 请求断言时期望的 NameID 格式">
|
||||
<NSelect v-model:value="form.nameIdFormat" :options="nameIdFormatOptions" />
|
||||
</FormField>
|
||||
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
|
||||
<FormField label="IdP 用户属性" hint="从 SAML 断言中取哪个值来识别用户">
|
||||
<NSelect v-model:value="form.idpUserAttr" :options="idpAttrOptions" />
|
||||
</FormField>
|
||||
<FormField label="域用户属性" hint="用取到的值匹配身份域中用户的哪个属性">
|
||||
<NSelect v-model:value="form.userStoreAttribute" :options="userAttrOptions" />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField v-if="form.idpUserAttr === 'attribute'" label="断言属性名" required>
|
||||
<NInput v-model:value="form.assertionAttr" placeholder="如 mail、uid" />
|
||||
</FormField>
|
||||
|
||||
<div class="mt-1 mb-2 flex items-center justify-between border-t border-line-soft pt-3">
|
||||
<span class="text-[12.5px] font-semibold">JIT 预配</span>
|
||||
<NSwitch v-model:value="form.jitEnabled" size="small" />
|
||||
</div>
|
||||
<div v-if="form.jitEnabled" class="mb-2 flex flex-col gap-1.5">
|
||||
<NCheckbox v-model:checked="form.jitCreate">首次联邦登录时自动创建用户</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.jitUpdate">每次登录时按断言更新既有用户</NCheckbox>
|
||||
<NCheckbox v-model:checked="form.jitAssignAdminGroup">
|
||||
将 JIT 用户加入 Administrators 组
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="form.jitMapEmail"
|
||||
:disabled="primaryEmailRequired"
|
||||
>
|
||||
映射邮箱属性(断言 email → 主电子邮件)
|
||||
<span v-if="primaryEmailRequired" class="text-xs text-ink-3">
|
||||
— 域已开启「用户需要提供主电子邮件地址」,必须映射
|
||||
</span>
|
||||
</NCheckbox>
|
||||
</div>
|
||||
<div class="text-xs leading-relaxed text-ink-3">
|
||||
创建后为禁用态,需手动激活;映射与 JIT 均按上方配置写入身份域。
|
||||
</div>
|
||||
</FormModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user