diff --git a/src/api/tenant.ts b/src/api/tenant.ts index 1fb0653..887c89e 100644 --- a/src/api/tenant.ts +++ b/src/api/tenant.ts @@ -13,6 +13,7 @@ import { import { mockOn, mocked, rawFetch, request } from './request' import type { AuditEventsResult, + CreatedApiKey, IamUser, IdentityDomain, IamUserDetail, @@ -26,6 +27,7 @@ import type { SignOnRule, SubscriptionDetail, SubscriptionSummary, + UserApiKey, } from '@/types/api' // ---- 配额 ---- @@ -169,15 +171,55 @@ export function clearUserMfa(id: number, userId: string, domainId?: string): Pro return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE', query: { domainId } }) } -export function deleteUserApiKeys( - id: number, - userId: string, - includeCurrent = false, -): Promise<{ deletedApiKeys: number }> { - if (mockOn) return mocked({ deletedApiKeys: 1 }) - return request(`/oci-configs/${id}/users/${userId}/api-keys`, { +// ---- 用户 API 签名 Key ---- + +const mockCreatedKey: CreatedApiKey = { + fingerprint: '11:22:33:44:55:66:77:88:99:00:aa:bb:cc:dd:ee:ff', + privateKey: '-----BEGIN RSA PRIVATE KEY-----\nMOCKMOCKMOCK\n-----END RSA PRIVATE KEY-----\n', + configIni: + '[DEFAULT]\nuser=ocid1.user.oc1..mock\nfingerprint=11:22:33:44:55:66:77:88:99:00:aa:bb:cc:dd:ee:ff\ntenancy=ocid1.tenancy.oc1..mock\nregion=ap-tokyo-1\nkey_file=~/.oci/oci_api_key.pem\n', +} + +export function listUserApiKeys(id: number, userId: string): Promise<{ items: UserApiKey[] }> { + if (mockOn) + return mocked({ + items: [ + { + fingerprint: 'aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99', + timeCreated: '2026-07-01T08:00:00Z', + isCurrent: true, + configIni: mockCreatedKey.configIni, + }, + { + fingerprint: '99:88:77:66:55:44:33:22:11:00:ff:ee:dd:cc:bb:aa', + timeCreated: '2026-07-10T08:00:00Z', + isCurrent: false, + configIni: mockCreatedKey.configIni, + }, + ], + }) + return request(`/oci-configs/${id}/users/${userId}/api-keys`) +} + +/** 后端生成密钥对并上传公钥;私钥仅本次返回 */ +export function addUserApiKey(id: number, userId: string): Promise { + if (mockOn) return mocked({ ...mockCreatedKey }) + return request(`/oci-configs/${id}/users/${userId}/api-keys`, { method: 'POST' }) +} + +export function deleteUserApiKey(id: number, userId: string, fingerprint: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}/users/${userId}/api-keys/${encodeURIComponent(fingerprint)}`, { method: 'DELETE', - query: { includeCurrent }, + }) +} + +/** 把刚创建的 key 设为本配置签名凭据(验证可用后落库,不删旧 key);私钥为创建时下发的那份回传 */ +export function activateApiKey(id: number, fingerprint: string, privateKey: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}/activate-api-key`, { + method: 'POST', + body: { fingerprint, privateKey }, }) } diff --git a/src/components/tenant/ApiKeysModal.vue b/src/components/tenant/ApiKeysModal.vue new file mode 100644 index 0000000..3ccfc60 --- /dev/null +++ b/src/components/tenant/ApiKeysModal.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/components/tenant/UsersTab.vue b/src/components/tenant/UsersTab.vue index cc5b1ba..eabdbef 100644 --- a/src/components/tenant/UsersTab.vue +++ b/src/components/tenant/UsersTab.vue @@ -2,9 +2,10 @@ import { NButton, NDataTable, useDialog, type DataTableColumns } from 'naive-ui' import { h, ref } from 'vue' -import { clearUserMfa, deleteUser, deleteUserApiKeys, listUsers, resetUserPassword } from '@/api/tenant' +import { clearUserMfa, deleteUser, listUsers, resetUserPassword } from '@/api/tenant' import FootNote from '@/components/FootNote.vue' import StatusBadge from '@/components/StatusBadge.vue' +import ApiKeysModal from '@/components/tenant/ApiKeysModal.vue' import UserFormModal from '@/components/tenant/UserFormModal.vue' import { fmtTime } from '@/composables/useFormat' import { useAsync } from '@/composables/useAsync' @@ -19,6 +20,8 @@ const dialog = useDialog() const users = useAsync(() => listUsers(props.cfgId, props.domainId)) const showForm = ref(false) const editingUser = ref(null) +const showKeys = ref(false) +const keysUser = ref(null) function openCreate() { editingUser.value = null @@ -30,6 +33,11 @@ function openEdit(row: IamUser) { showForm.value = true } +function openKeys(row: IamUser) { + keysUser.value = row + showKeys.value = true +} + async function act(fn: () => Promise, ok: string) { try { await fn() @@ -112,11 +120,23 @@ const columns: DataTableColumns = [ { title: '操作', key: 'actions', - width: 290, + width: 300, 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( + ConfirmPop, + { + title: '重置密码?', + content: `将为 ${row.name} 生成一次性新密码。`, + kind: 'warn', + confirmText: '重置', + onConfirm: () => resetPwd(row), + }, + { + trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '重置密码' }), + }, + ), h( ConfirmPop, { @@ -131,19 +151,7 @@ const columns: DataTableColumns = [ trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '清 MFA' }), }, ), - h( - ConfirmPop, - { - title: '删除全部 API Key?', - content: '跳过当前配置使用中的指纹。', - confirmText: '删除', - onConfirm: () => - act(() => deleteUserApiKeys(props.cfgId, row.id), `已删除 ${row.name} 的 API Key`), - }, - { - trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '删 Key' }), - }, - ), + h(NButton, { size: 'tiny', quaternary: true, onClick: () => openKeys(row) }, { default: () => 'API Keys' }), row.isCurrentUser ? null : h( @@ -180,5 +188,7 @@ const columns: DataTableColumns = [ :user="editingUser" @created="users.run()" /> + + diff --git a/src/types/api.ts b/src/types/api.ts index ecc0668..7f379b7 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -639,6 +639,21 @@ export interface IamUser { lastLoginTime: string | null } +// 用户 API 签名 key;isCurrent 表示当前配置正用它签名,configIni 为该 key 的 CLI 配置模板 +export interface UserApiKey { + fingerprint: string + timeCreated: string | null + isCurrent: boolean + configIni: string +} + +// 新建/轮换 key 的一次性返回;私钥仅此一次,关闭即不可再取 +export interface CreatedApiKey { + fingerprint: string + privateKey: string + configIni: string +} + // 编辑表单用的域档案与管理员状态;inDomain 为 false 表示经典 IAM 用户 export interface IamUserDetail { inDomain: boolean