初始提交:OCI 面板前端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:29 +08:00
commit db45a669e3
135 changed files with 25220 additions and 0 deletions
+218
View File
@@ -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 断言名称 IDNameID', 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>