Files
oci-portal-dash/src/api/tenant.ts
T
2026-07-30 12:23:16 +08:00

441 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
mockAuditEvents,
mockIdentitySetting,
mockIdps,
mockLimits,
mockPolicies,
mockRecipients,
mockSignOnRules,
mockSubscriptions,
mockUserDetails,
mockUsers,
} from './mock'
import { mockOn, mocked, rawFetch, request } from './request'
import type {
AuditEventsResult,
CreatedApiKey,
IamUser,
IdentityDomain,
IamUserDetail,
IdentityProvider,
IdentitySetting,
IdpIconUpload,
LimitItem,
LimitService,
NotificationRecipients,
PasswordPolicy,
SignOnRule,
SubscriptionDetail,
SubscriptionSummary,
UserApiKey,
} from '@/types/api'
// ---- 配额 ----
export interface LimitsQuery {
service?: string
region?: string
name?: string
scopeType?: 'GLOBAL' | 'REGION' | 'AD'
availabilityDomain?: string
/** 服务端过滤上限为 0 的配额项,查用量时不占并发名额 */
nonZero?: boolean
withAvailability?: boolean
}
export function listLimits(id: number, query: LimitsQuery = {}): Promise<LimitItem[]> {
if (mockOn) {
if (query.service === 'regions')
return mocked([{ name: 'subscribed-region-count', scopeType: 'GLOBAL', value: 3 }])
const needle = (query.name ?? '').toLowerCase()
return mocked(
mockLimits
.filter((l) => !needle || l.name.toLowerCase().includes(needle))
.filter((l) => !query.scopeType || l.scopeType === query.scopeType)
.filter((l) => !query.nonZero || l.value > 0),
)
}
return request(`/oci-configs/${id}/limits`, { query: { ...query } })
}
export function listLimitServices(id: number): Promise<LimitService[]> {
if (mockOn)
return mocked([
{ name: 'compute', description: 'Compute' },
{ name: 'vcn', description: 'Virtual Cloud Network' },
{ name: 'block-storage', description: 'Block Volume' },
])
return request(`/oci-configs/${id}/limits/services`)
}
// ---- 订阅 ----
export function listSubscriptions(id: number): Promise<SubscriptionSummary[]> {
if (mockOn)
return mocked(
mockSubscriptions.map((d) => ({
id: d.id,
serviceName: d.serviceName,
timeCreated: d.timeCreated,
timeUpdated: d.timeUpdated,
})),
)
return request(`/oci-configs/${id}/subscriptions`)
}
export function getSubscription(id: number, subscriptionId: string): Promise<SubscriptionDetail> {
if (mockOn)
return mocked(mockSubscriptions.find((d) => d.id === subscriptionId) ?? mockSubscriptions[0])
return request(`/oci-configs/${id}/subscriptions/${subscriptionId}`)
}
// ---- 身份域 ----
/** 租户 ACTIVE 身份域列表;空数组表示无域老租户(隐藏选择器走经典路径) */
export function listIdentityDomains(id: number): Promise<IdentityDomain[]> {
if (mockOn)
return mocked([
{ id: 'ocid1.domain.default', displayName: 'Default', homeRegion: 'us-phoenix-1', type: 'DEFAULT', licenseType: 'free' },
{ id: 'ocid1.domain.idcs', displayName: 'OracleIdentityCloudService', homeRegion: 'us-phoenix-1', type: 'SECONDARY', licenseType: 'idcs-foundation' },
])
return request(`/oci-configs/${id}/domains`)
}
// ---- 用户 ----
export function listUsers(id: number, domainId?: string): Promise<IamUser[]> {
if (mockOn) return mocked(mockUsers)
return request(`/oci-configs/${id}/users`, { query: { domainId } })
}
// 编辑表单预填充用;不在身份域的经典用户返回 inDomain: false
export function getUserDetail(id: number, userId: string, domainId?: string): Promise<IamUserDetail> {
if (mockOn)
return mocked(
mockUserDetails[userId] ?? {
inDomain: false,
givenName: '',
familyName: '',
isDomainAdmin: false,
inAdminGroup: false,
},
)
return request(`/oci-configs/${id}/users/${userId}`, { query: { domainId } })
}
export interface CreateUserBody {
name: string
givenName: string
familyName: string
description?: string
email?: string
/** 同时勾选时后端先授身份域管理员,再加入管理员组 */
grantDomainAdmin?: boolean
addToAdminGroup?: boolean
}
export function createUser(id: number, body: CreateUserBody, domainId?: string): Promise<IamUser> {
if (mockOn)
return mocked({ ...mockUsers[2], id: 'ocid1.user..new', name: body.name, email: body.email ?? '' })
return request(`/oci-configs/${id}/users`, { method: 'POST', body, query: { domainId } })
}
export interface UpdateUserBody {
description?: string
email?: string
givenName?: string
familyName?: string
/** 三态:true 授予/加入,false 撤销/移出,缺省不动 */
grantDomainAdmin?: boolean
addToAdminGroup?: boolean
}
export function updateUser(id: number, userId: string, body: UpdateUserBody, domainId?: string): Promise<IamUser> {
if (mockOn) {
const u = mockUsers.find((x) => x.id === userId) ?? mockUsers[2]
if (body.description !== undefined) u.description = body.description
if (body.email !== undefined) u.email = body.email
return mocked({ ...u })
}
return request(`/oci-configs/${id}/users/${userId}`, { method: 'PUT', body, query: { domainId } })
}
export function deleteUser(id: number, userId: string, domainId?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/users/${userId}`, { method: 'DELETE', query: { domainId } })
}
export function resetUserPassword(id: number, userId: string, domainId?: string): Promise<{ password: string }> {
if (mockOn) return mocked({ password: 'Xy9#kQ2m$Lp8' })
return request(`/oci-configs/${id}/users/${userId}/reset-password`, { method: 'POST', query: { domainId } })
}
export function clearUserMfa(id: number, userId: string, domainId?: string): Promise<{ deletedTotpDevices: number }> {
if (mockOn) return mocked({ deletedTotpDevices: 1 })
return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE', query: { domainId } })
}
// ---- 用户 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<CreatedApiKey> {
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<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/users/${userId}/api-keys/${encodeURIComponent(fingerprint)}`, {
method: 'DELETE',
})
}
/** 把刚创建的 key 设为本配置签名凭据(验证可用后落库,不删旧 key);私钥为创建时下发的那份回传;
* userId 给定且异于当前签名用户时,一并把面板签名用户切换为该用户 */
export function activateApiKey(
id: number,
fingerprint: string,
privateKey: string,
userId?: string,
): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/activate-api-key`, {
method: 'POST',
body: { userId, fingerprint, privateKey },
})
}
// ---- 审计日志 ----
/** 批式懒加载查询审计事件:cursor 空自当前时刻首查,
* 非空从上次响应游标向更早续取一批(~200 条,服务端分窗回溯);
* q 为服务端全文检索关键字,仅首查生效(续查沿用游标内嵌关键字) */
export function listAuditEvents(
id: number,
opts: { region?: string; cursor?: string; limit?: number; q?: string } = {},
): Promise<AuditEventsResult> {
// mock:首批带游标,续查一批后到尽头,演示懒加载链路;q 演示不区分大小写的包含匹配
if (mockOn) {
const kw = (opts.q ?? '').replace(/\*/g, '').toLowerCase()
const items = kw
? mockAuditEvents.filter((e) =>
[e.eventName, e.resourceName, e.principalName, e.ipAddress, e.source].some((v) =>
(v ?? '').toLowerCase().includes(kw),
),
)
: mockAuditEvents
return mocked({ items, cursor: opts.cursor ? undefined : 'mock-cursor', exhausted: !!opts.cursor }, 300)
}
return request(`/oci-configs/${id}/audit-events`, { query: { ...opts } })
}
/** 取回单条审计事件的原始 JSON;404 表示缓存过期且重查未找回(前端降级展示精简字段) */
export function getAuditEventDetail(
id: number,
q: { eventId: string; eventTime: string; region?: string },
): Promise<{ raw: unknown }> {
if (mockOn) return mocked({ raw: { eventId: q.eventId, mock: true, note: '演示原始事件' } }, 200)
return request(`/oci-configs/${id}/audit-events/detail`, { query: q })
}
// ---- 域设置 ----
export function getNotificationRecipients(id: number, domainId?: string): Promise<NotificationRecipients> {
if (mockOn) return mocked(mockRecipients)
return request(`/oci-configs/${id}/notification-recipients`, { query: { domainId } })
}
export function updateNotificationRecipients(
id: number,
recipients: string[],
domainId?: string,
): Promise<NotificationRecipients> {
if (mockOn) return mocked({ recipients, testModeEnabled: recipients.length > 0 })
return request(`/oci-configs/${id}/notification-recipients`, {
method: 'PUT',
body: { recipients },
query: { domainId },
})
}
export function listPasswordPolicies(id: number, domainId?: string): Promise<PasswordPolicy[]> {
if (mockOn) return mocked(mockPolicies)
return request(`/oci-configs/${id}/password-policies`, { query: { domainId } })
}
export function updatePasswordPolicy(
id: number,
policyId: string,
body: { passwordExpiresAfter?: number; minLength?: number; numPasswordsInHistory?: number },
domainId?: string,
): Promise<PasswordPolicy> {
if (mockOn) return mocked({ ...mockPolicies[0], ...body })
return request(`/oci-configs/${id}/password-policies/${policyId}`, { method: 'PUT', body, query: { domainId } })
}
// ---- 身份设置 ----
export function getIdentitySetting(id: number, domainId?: string): Promise<IdentitySetting> {
if (mockOn) return mocked({ ...mockIdentitySetting })
return request(`/oci-configs/${id}/identity-settings`, { query: { domainId } })
}
export function updateIdentitySetting(
id: number,
primaryEmailRequired: boolean,
domainId?: string,
): Promise<IdentitySetting> {
if (mockOn) {
mockIdentitySetting.primaryEmailRequired = primaryEmailRequired
return mocked({ ...mockIdentitySetting })
}
return request(`/oci-configs/${id}/identity-settings`, {
method: 'PUT',
body: { primaryEmailRequired },
query: { domainId },
})
}
export async function downloadSamlMetadata(id: number, domainId?: string): Promise<void> {
if (mockOn) {
const blob = new Blob(['<mock-saml-metadata/>'], { type: 'application/xml' })
triggerDownload(blob, 'oci-domain-saml-metadata.xml')
return
}
const { useAuthStore } = await import('@/stores/auth')
const auth = useAuthStore()
const qs = domainId ? `?domainId=${encodeURIComponent(domainId)}` : ''
const resp = await fetch(`/api/v1/oci-configs/${id}/saml-metadata${qs}`, {
headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : {},
})
if (!resp.ok) throw new Error(`下载失败(${resp.status}`)
triggerDownload(await resp.blob(), 'oci-domain-saml-metadata.xml')
}
function triggerDownload(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
// ---- Federation ----
export interface CreateIdpBody {
name: string
metadata: string
description?: string
/** logohttp(s) 外链 URL(可先经 IdP 图标上传接口取得) */
iconUrl?: string
nameIdFormat?: string
/** 非空表示按断言属性映射;空则按 SAML NameID 映射 */
assertionAttribute?: string
userStoreAttribute?: string
jitEnabled?: boolean
jitCreate?: boolean
jitUpdate?: boolean
jitAssignAdminGroup?: boolean
jitMapEmail?: boolean
}
export function createIdp(id: number, body: CreateIdpBody, domainId?: string): Promise<IdentityProvider> {
if (mockOn) {
const created = {
...mockIdps[1],
id: `idp-${mockIdps.length + 1}`,
name: body.name,
enabled: false,
}
mockIdps.push(created)
return mocked(created, 900)
}
return request(`/oci-configs/${id}/identity-providers`, { method: 'POST', body, query: { domainId } })
}
/** 上传 IdP 图标到身份域公共图片存储,返回可填入 iconUrl 的公网地址(multipart 走 rawFetch) */
export async function uploadIdpIcon(
id: number,
file: File,
domainId?: string,
): Promise<IdpIconUpload> {
if (mockOn)
return mocked({ url: 'https://mock.local/images/idp-icon.png', fileName: 'images/idp-icon.png' }, 600)
const fd = new FormData()
fd.append('file', file, file.name)
const resp = await rawFetch(`/oci-configs/${id}/idp-icons`, {
method: 'POST',
query: { domainId },
body: fd,
})
return (await resp.json()) as IdpIconUpload
}
/** 删除尚未被 IdP 采用的身份域公共图片 */
export function deleteIdpIcon(id: number, fileName: string, domainId?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/idp-icons`, {
method: 'DELETE',
query: { domainId, fileName },
})
}
export function listIdps(id: number, domainId?: string): Promise<IdentityProvider[]> {
if (mockOn) return mocked(mockIdps)
return request(`/oci-configs/${id}/identity-providers`, { query: { domainId } })
}
export function activateIdp(id: number, idpId: string, enabled: boolean, domainId?: string): Promise<IdentityProvider> {
if (mockOn) return mocked({ ...mockIdps[0], enabled })
return request(`/oci-configs/${id}/identity-providers/${idpId}/activate`, {
method: 'POST',
body: { enabled },
query: { domainId },
})
}
export function deleteIdp(id: number, idpId: string, domainId?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/identity-providers/${idpId}`, { method: 'DELETE', query: { domainId } })
}
export function listSignOnRules(id: number, domainId?: string): Promise<SignOnRule[]> {
if (mockOn) return mocked(mockSignOnRules)
return request(`/oci-configs/${id}/sign-on-rules`, { query: { domainId } })
}
export function createSignOnExemption(id: number, identityProviderId: string, domainId?: string): Promise<SignOnRule> {
if (mockOn) return mocked(mockSignOnRules[0])
return request(`/oci-configs/${id}/sign-on-exemptions`, {
method: 'POST',
body: { identityProviderId },
query: { domainId },
})
}
export function deleteSignOnExemption(id: number, ruleId: string, domainId?: string): Promise<void> {
if (mockOn) return mocked(undefined)
return request(`/oci-configs/${id}/sign-on-exemptions/${ruleId}`, { method: 'DELETE', query: { domainId } })
}