Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Admin 与 Chat 分别维护请求客户端和业务接口。
`import WorkspaceApi from '@/api/admin/system/workspace'`;System 登录设置使用
`import AuthSettingApi from '@/api/admin/system/settings/auth-setting'`。

系统登录设置的 `system_options` 仅用于登录方式选项,默认登录方式从已选 `login_methods` 中选择。
保存仅提交 `login_methods`、`default_value`、`max_attempts`、`failed_attempts`、`lock_time`、
`role_id`、`workspace_id` 和 `permission`,不透传查询响应中的选项或 `group_id`。

## 枚举与类型组织

`enums/state.ts` 的 `STATE_TYPES` 维护跨业务复用的任务状态,联合类型 `State` 定义在
Expand Down Expand Up @@ -155,8 +159,9 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
`admin/system/chat-management/portal-setting.ts` 通过 Admin `/portal` 读取和局部保存门户配置,
公共类型维护在 `types/portal.ts`。JSON 用于访问开关和认证、跨域配置,门户名称与 Logo 使用
FormData;保存返回完整配置,页面以该返回值作为唯一数据来源。
认证配置保留未编辑字段,仅提交后端读取的 `login_value`、`max_attempts`、`failed_attempts`、
`lock_time`;跨域地址提交 `cross_domain_list` 数组,不另建 `cors_config`。
认证设置的登录方式选项来自 `auth_config.system_options`,默认登录方式从草稿已选的 `login_value`
中选择。认证配置仅提交 `login_value`、`type`、`max_attempts`、`failed_attempts`、`lock_time`,
不透传查询响应中的选项;跨域地址提交 `cross_domain_list` 数组,不另建 `cors_config`。

### 当前账号密码

Expand Down
3 changes: 2 additions & 1 deletion ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排
查询失败展示重新加载入口,不展示默认开关;保存期间使用 loading 与 inert,失败保留已保存配置。
知识库或资源范围变化时重新查询;示例与地址来自响应,只读展示/复制,不在前端拼接。
- 门户设置已通过 Portal API 持久化,JSON 保存开关、认证及 `cross_domain_list`,FormData 保存名称/Logo。
成功以完整响应回显,保留现有编辑权限;认证选项来自 LoginSetting,跨域地址按行清理,取消不回写。
成功以完整响应回显,保留现有编辑权限;认证选项来自门户配置的 `auth_config.system_options`,
默认登录方式仅展示草稿中已选的 `login_value`;跨域地址按行清理,取消不回写。
- 用户导入、改密、批量角色/配额流程留在各自业务目录;工作空间成员按用户联动角色行选择,
删除按 `user_relation_id` 去重,不按用户 ID 丢弃角色记录。
- 登录页使用 `formEl.validate` 回调和清晰的 LDAP/本地分支,不额外抽象或改写提交控制流程。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,36 @@
import { computed, reactive, ref, useTemplateRef } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { LOGIN_METHOD } from '@/api/enums'
import type { LoginMethod, OptionItem, PortalSetting, PortalSettingPayload } from '@/api/types'
import { LOGIN_METHOD_LABELS } from '@/constants/auth'
import type { PortalAuthConfig, PortalSetting, PortalSettingPayload } from '@/api/types'

const props = defineProps<{ disabled: boolean; setting: PortalSetting; saving: boolean; save: (payload: PortalSettingPayload) => Promise<void> }>()

/* 可用登录方式与默认登录方式均取自后端返回的门户 auth_config */
const authConfig = computed(() => props.setting.auth_config ?? {})

const loginMethodOptions = computed<string[]>(() => {
const values = (authConfig.value.system_options ?? []).map((option) => option.value)
return values.length ? values : [LOGIN_METHOD.LOCAL]
})

const defaultLoginMethodOptions = computed<OptionItem<string>[]>(() => {
const authTypes = authConfig.value.auth_types ?? []
return authTypes.length ? authTypes : (authConfig.value.system_options ?? [])
})
/* 登录方式选项来自 system_options,默认登录方式仅允许选择草稿中已选的 login_value */
const loginMethodOptions = ref<NonNullable<PortalAuthConfig['system_options']>>([])
const defaultLoginMethodOptions = computed(() =>
loginMethodOptions.value.filter(({ value }) => authForm.login_value.some((loginMethod) => loginMethod === value)),
)

const authVisible = ref(false)
const enableAuthAfterSave = ref(false)
const authFormRef = useTemplateRef<FormInstance>('authFormRef')
const authForm = reactive({
loginMethods: [LOGIN_METHOD.LOCAL] as string[],
default_value: LOGIN_METHOD.LOCAL as string,
maxAttempts: 1,
failedAttempts: 5,
lockTime: 10,
login_value: [LOGIN_METHOD.LOCAL] as string[],
type: LOGIN_METHOD.LOCAL as string,
max_attempts: 1,
failed_attempts: 5,
lock_time: 10,
})

const authRules: FormRules<typeof authForm> = {
loginMethods: [{ type: 'array', required: true, min: 1, message: '请选择至少一种登录方式', trigger: 'change' }],
default_value: [{ required: true, message: '请选择默认登录方式', trigger: 'change' }],
login_value: [{ type: 'array', required: true, min: 1, message: '请选择至少一种登录方式', trigger: 'change' }],
type: [{ required: true, message: '请选择默认登录方式', trigger: 'change' }],
}

/* 登录方式变化时,确保默认登录方式仍为已启用的方式 */
function handleLoginMethodsChange(loginMethods: string[]) {
if (!loginMethods.includes(authForm.default_value)) {
authForm.default_value = loginMethods[0] ?? LOGIN_METHOD.LOCAL
if (!loginMethods.includes(authForm.type)) {
authForm.type = loginMethods[0] ?? ''
}
}

Expand All @@ -47,16 +40,17 @@ function open(enableAfterSave = false) {
if (props.disabled || props.saving) return

enableAuthAfterSave.value = enableAfterSave
const config = authConfig.value
const config = props.setting.auth_config ?? {}
loginMethodOptions.value = config.system_options ?? []
const loginMethods = [...(config.login_value?.length ? config.login_value : [LOGIN_METHOD.LOCAL])]
const defaultType = config.type ?? LOGIN_METHOD.LOCAL
const default_value = loginMethods.includes(defaultType) ? defaultType : loginMethods[0] ?? LOGIN_METHOD.LOCAL
const type = loginMethods.includes(defaultType) ? defaultType : (loginMethods[0] ?? LOGIN_METHOD.LOCAL)
Object.assign(authForm, {
loginMethods,
default_value,
maxAttempts: config.max_attempts ?? 1,
failedAttempts: config.failed_attempts ?? 5,
lockTime: config.lock_time ?? 10,
login_value: loginMethods,
type,
max_attempts: config.max_attempts ?? 1,
failed_attempts: config.failed_attempts ?? 5,
lock_time: config.lock_time ?? 10,
})
authVisible.value = true
}
Expand All @@ -68,16 +62,16 @@ function handleOpenAuthSetting() {
function handleClosed() {
enableAuthAfterSave.value = false
Object.assign(authForm, {
loginMethods: [LOGIN_METHOD.LOCAL],
default_value: LOGIN_METHOD.LOCAL,
maxAttempts: 1,
failedAttempts: 5,
lockTime: 10,
login_value: [LOGIN_METHOD.LOCAL],
type: LOGIN_METHOD.LOCAL,
max_attempts: 1,
failed_attempts: 5,
lock_time: 10,
})
authFormRef.value?.clearValidate()
}

/* 保存认证配置,仅提交后端读取的字段并保留其余配置 */
/* 保存认证配置,仅提交表单字段 */
function handleSaveAuthSetting() {
if (props.disabled || props.saving) return

Expand All @@ -87,12 +81,11 @@ function handleSaveAuthSetting() {
.save({
...(enableAuthAfterSave.value ? { enable_auth: true } : {}),
auth_config: {
...props.setting.auth_config,
login_value: [...authForm.loginMethods],
type: authForm.default_value,
max_attempts: authForm.maxAttempts,
failed_attempts: authForm.failedAttempts,
lock_time: authForm.lockTime,
login_value: [...authForm.login_value],
type: authForm.type,
max_attempts: authForm.max_attempts,
failed_attempts: authForm.failed_attempts,
lock_time: authForm.lock_time,
},
})
.then(() => {
Expand All @@ -119,15 +112,15 @@ defineExpose({ open })
require-asterisk-position="right"
@submit.prevent
>
<el-form-item label="登录方式" prop="loginMethods">
<el-checkbox-group v-model="authForm.loginMethods" @change="handleLoginMethodsChange" class="flex-wrap">
<template v-for="loginMethod in loginMethodOptions" :key="loginMethod">
<el-checkbox :value="loginMethod" class="w-32">{{ LOGIN_METHOD_LABELS[loginMethod as LoginMethod] || loginMethod }}</el-checkbox>
</template>
<el-form-item label="登录方式" prop="login_value">
<el-checkbox-group v-model="authForm.login_value" @change="handleLoginMethodsChange" class="flex-wrap">
<el-checkbox v-for="loginMethodOption in loginMethodOptions" :key="loginMethodOption.value" :value="loginMethodOption.value" class="w-32">
{{ loginMethodOption.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="默认登录方式" prop="default_value">
<el-select v-model="authForm.default_value">
<el-form-item label="默认登录方式" prop="type">
<el-select v-model="authForm.type">
<el-option
v-for="loginMethodOption in defaultLoginMethodOptions"
:key="loginMethodOption.value"
Expand All @@ -140,14 +133,14 @@ defineExpose({ open })
<div class="mk-gray-card-lg w-full space-y-4">
<div class="flex-align-center gap-2">
<span class="shrink-0">登录失败</span>
<el-input-number v-model="authForm.maxAttempts" class="w-32!" :min="-1" :max="100" :precision="0" :value-on-clear="1" />
<el-input-number v-model="authForm.max_attempts" class="w-32!" :min="-1" :max="100" :precision="0" :value-on-clear="1" />
<span class="shrink-0">次显示验证码</span>
</div>
<div class="flex-align-center gap-2">
<span class="shrink-0">登录失败</span>
<el-input-number v-model="authForm.failedAttempts" class="w-32!" :min="1" :max="100" :precision="0" :value-on-clear="5" />
<el-input-number v-model="authForm.failed_attempts" class="w-32!" :min="1" :max="100" :precision="0" :value-on-clear="5" />
<span class="shrink-0">次,锁定账号</span>
<el-input-number v-model="authForm.lockTime" class="w-32!" :min="1" :max="1440" :precision="0" :value-on-clear="10" />
<el-input-number v-model="authForm.lock_time" class="w-32!" :min="1" :max="1440" :precision="0" :value-on-clear="10" />
<span class="shrink-0">分钟</span>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function handleAccessChange(field: AccessField, value: string | number | boolean
return savePortalSetting({ [field]: Boolean(value) })
}

/* 认证配置保存,payload 已保留未编辑字段 */
/* 认证配置保存 */
function savePortalAuthSetting(payload: PortalSettingPayload) {
return savePortalSetting(payload)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, useTemplateRef } from 'vue'
import { pick } from 'lodash'
import CurrentUserApi from '@/api/admin/auth/current-user'
import AuthSettingApi from '@/api/admin/system/settings/auth-setting'
import UserGroupsApi from '@/api/admin/system/user-groups'
import type { ListItem, LoginAuthSettingPayload, SystemUserGroup, LoginMethod } from '@/api/types'
import type { ListItem, LoginAuthSettingPayload, SystemUserGroup } from '@/api/types'
import type { FormInstance, FormRules } from 'element-plus'
import { LOGIN_METHOD, ROLE_TYPE } from '@/api/enums'
import { LOGIN_METHOD_LABELS } from '@/constants'
import { MsgSuccess } from '@/utils/message'
import { useStore } from '@/stores'
import perm from '@/permission'
Expand All @@ -15,7 +15,17 @@ defineOptions({ name: 'AuthenticationSetting' })
const { auth } = useStore()
const authFormRef = useTemplateRef<FormInstance>('authFormRef')
const loading = ref(false)
const form = reactive<LoginAuthSettingPayload>({
const loginSettingFields = [
'login_methods',
'default_value',
'max_attempts',
'failed_attempts',
'lock_time',
'role_id',
'workspace_id',
'permission',
] as const
const form = reactive<Omit<LoginAuthSettingPayload, 'system_options'>>({
login_methods: [LOGIN_METHOD.LOCAL],
default_value: LOGIN_METHOD.LOCAL,
max_attempts: 1,
Expand All @@ -33,13 +43,16 @@ const rules: FormRules<LoginAuthSettingPayload> = {
}

/* 登录方式加载与提交 */
const loginMethodOptions = computed(() => form.login_methods)
const defaultLoginMethodOptions = computed(() => form.auth_types)
const loginMethodOptions = ref<NonNullable<LoginAuthSettingPayload['system_options']>>([])
const defaultLoginMethodOptions = computed(() =>
loginMethodOptions.value.filter(({ value }) => form.login_methods.some((loginMethod) => loginMethod === value)),
)
function loadSetting() {
loading.value = true
return AuthSettingApi.getLoginSetting()
.then((setting) => {
Object.assign(form, setting)
loginMethodOptions.value = setting.system_options ?? []
Object.assign(form, pick(setting, [...loginSettingFields, 'group_id']))
})
.finally(() => {
loading.value = false
Expand Down Expand Up @@ -128,7 +141,7 @@ function submit() {
if (!valid) return

loading.value = true
AuthSettingApi.putLoginSetting(form)
AuthSettingApi.putLoginSetting(pick(form, loginSettingFields))
.then(() => MsgSuccess('保存成功'))
.finally(() => {
loading.value = false
Expand All @@ -155,8 +168,8 @@ onMounted(() => {
>
<el-form-item label="登录方式" prop="login_methods">
<el-checkbox-group v-model="form.login_methods" @change="handleLoginMethodsChange" class="flex-wrap">
<el-checkbox v-for="loginMethodOption in loginMethodOptions" :key="loginMethodOption" :value="loginMethodOption" class="w-40">
{{ LOGIN_METHOD_LABELS[loginMethodOption as LoginMethod] }}
<el-checkbox v-for="loginMethodOption in loginMethodOptions" :key="loginMethodOption.value" :value="loginMethodOption.value" class="w-40">
{{ loginMethodOption.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
Expand Down
Loading