179 lines
5.7 KiB
Vue
179 lines
5.7 KiB
Vue
<script setup lang="ts">
|
|
import { NButton, NDataTable, NPopconfirm, useDialog, type DataTableColumns } from 'naive-ui'
|
|
import { h, ref } from 'vue'
|
|
|
|
import { clearUserMfa, deleteUser, deleteUserApiKeys, listUsers, resetUserPassword } from '@/api/tenant'
|
|
import FootNote from '@/components/FootNote.vue'
|
|
import StatusBadge from '@/components/StatusBadge.vue'
|
|
import UserFormModal from '@/components/tenant/UserFormModal.vue'
|
|
import { fmtTime } from '@/composables/useFormat'
|
|
import { useAsync } from '@/composables/useAsync'
|
|
import type { IamUser } from '@/types/api'
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
const props = defineProps<{ cfgId: number; domainId?: string }>()
|
|
|
|
const message = useToast()
|
|
const dialog = useDialog()
|
|
const users = useAsync(() => listUsers(props.cfgId, props.domainId))
|
|
const showForm = ref(false)
|
|
const editingUser = ref<IamUser | null>(null)
|
|
|
|
function openCreate() {
|
|
editingUser.value = null
|
|
showForm.value = true
|
|
}
|
|
|
|
function openEdit(row: IamUser) {
|
|
editingUser.value = row
|
|
showForm.value = true
|
|
}
|
|
|
|
async function act(fn: () => Promise<unknown>, ok: string) {
|
|
try {
|
|
await fn()
|
|
message.success(ok)
|
|
void users.run()
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '操作失败')
|
|
}
|
|
}
|
|
|
|
function confirmDelete(row: IamUser) {
|
|
dialog.warning({
|
|
title: '删除用户',
|
|
content: () =>
|
|
h('div', { class: 'text-[13px]' }, [
|
|
h('p', `确定删除用户「${row.name}」?此操作不可恢复。`),
|
|
h('p', { class: 'mt-1.5 text-ink-3' }, '删除后该用户的全部 API Key 将一并吊销'),
|
|
]),
|
|
positiveText: '确认删除',
|
|
negativeText: '取消',
|
|
onPositiveClick: () => act(() => deleteUser(props.cfgId, row.id, props.domainId), `已删除用户 ${row.name}`),
|
|
})
|
|
}
|
|
|
|
async function resetPwd(row: IamUser) {
|
|
try {
|
|
const { password } = await resetUserPassword(props.cfgId, row.id, props.domainId)
|
|
dialog.success({
|
|
title: '一次性密码',
|
|
content: () =>
|
|
h('div', [
|
|
h('p', { class: 'mb-2 text-[13px]' }, `${row.name} 的新密码(首次登录须修改):`),
|
|
h('code', { class: 'mono rounded bg-line-soft px-2 py-1 select-all' }, password),
|
|
]),
|
|
positiveText: '已保存',
|
|
})
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '重置失败')
|
|
}
|
|
}
|
|
|
|
const columns: DataTableColumns<IamUser> = [
|
|
{
|
|
title: '用户',
|
|
key: 'name',
|
|
minWidth: 220,
|
|
render: (row) =>
|
|
h('div', [
|
|
h('div', { class: 'flex items-center gap-2' }, [
|
|
h('span', { class: 'font-medium' }, row.name),
|
|
row.isCurrentUser
|
|
? h(StatusBadge, { kind: 'snatch', label: '当前签名用户', dot: false })
|
|
: null,
|
|
]),
|
|
h('div', { class: 'text-xs text-ink-3 mt-px' }, row.email || row.description || '—'),
|
|
]),
|
|
},
|
|
{
|
|
title: '状态',
|
|
key: 'lifecycleState',
|
|
width: 90,
|
|
render: (row) =>
|
|
h(StatusBadge, {
|
|
kind: row.lifecycleState === 'ACTIVE' ? 'run' : 'stop',
|
|
label: row.lifecycleState === 'ACTIVE' ? '活跃' : row.lifecycleState,
|
|
}),
|
|
},
|
|
{
|
|
title: 'MFA',
|
|
key: 'mfaActivated',
|
|
width: 70,
|
|
render: (row) => h('span', { class: 'text-[13px] tabular-nums' }, row.mfaActivated ? '已启用' : '未启用'),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
key: 'timeCreated',
|
|
width: 150,
|
|
render: (row) => h('span', { class: 'text-[13px] text-ink-2' }, fmtTime(row.timeCreated)),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 290,
|
|
render: (row) =>
|
|
h('div', { class: 'flex items-center gap-1' }, [
|
|
h(NButton, { size: 'tiny', quaternary: true, onClick: () => openEdit(row) }, { default: () => '修改' }),
|
|
h(NButton, { size: 'tiny', quaternary: true, onClick: () => resetPwd(row) }, { default: () => '重置密码' }),
|
|
h(
|
|
NPopconfirm,
|
|
{
|
|
onPositiveClick: () =>
|
|
act(() => clearUserMfa(props.cfgId, row.id, props.domainId), `已清除 ${row.name} 的全部 MFA`),
|
|
},
|
|
{
|
|
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '清 MFA' }),
|
|
default: () => '清除该用户全部 MFA 因子?',
|
|
},
|
|
),
|
|
h(
|
|
NPopconfirm,
|
|
{
|
|
onPositiveClick: () =>
|
|
act(() => deleteUserApiKeys(props.cfgId, row.id), `已删除 ${row.name} 的 API Key`),
|
|
},
|
|
{
|
|
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '删 Key' }),
|
|
default: () => '删除该用户全部 API Key(跳过当前配置使用中的指纹)?',
|
|
},
|
|
),
|
|
row.isCurrentUser
|
|
? null
|
|
: h(
|
|
NButton,
|
|
{ size: 'tiny', quaternary: true, type: 'error', onClick: () => confirmDelete(row) },
|
|
{ default: () => '删除' },
|
|
),
|
|
]),
|
|
},
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<div class="panel">
|
|
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
|
<div class="text-sm font-semibold">IAM 用户</div>
|
|
<NButton size="small" type="primary" @click="openCreate">添加用户</NButton>
|
|
</div>
|
|
<NDataTable
|
|
:columns="columns"
|
|
:data="users.data.value ?? []"
|
|
:loading="users.loading.value"
|
|
:scroll-x="800"
|
|
:row-key="(r: IamUser) => r.id"
|
|
/>
|
|
<FootNote>
|
|
重置密码生成一次性密码,用户首次登录须修改;当前配置签名用户不可删除,避免面板失联
|
|
</FootNote>
|
|
|
|
<UserFormModal
|
|
v-model:show="showForm"
|
|
:cfg-id="cfgId"
|
|
:domain-id="domainId"
|
|
:user="editingUser"
|
|
@created="users.run()"
|
|
/>
|
|
</div>
|
|
</template>
|