Project-level authorization: - Adding a project to a sub-account now auto-calls AttachPolicyInProject to grant default policies (ArkFullAccess, TOSFullAccess) in that project scope - Removing a project auto-calls DetachPolicyInProject to revoke those policies - Each project records which policies were attached (attached_policies field) so removal knows exactly what to revoke Configuration: - GlobalConfig.default_project_policies: configurable list of policies to auto-attach (editable in Settings page, defaults to ArkFullAccess + TOSFullAccess) IAM Service: - Added attach_policy_in_project() and detach_policy_in_project() methods using standard AttachUserPolicy/DetachUserPolicy with ProjectName parameter Frontend: - Projects dialog now shows "已授权策略" column with policy tags - Settings page has "项目默认授权策略" config field Alert logging: - Project add/remove operations are logged with attached/detached policy details Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
6.7 KiB
Vue
201 lines
6.7 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>系统设置</h2>
|
|
</div>
|
|
|
|
<!-- Global Config -->
|
|
<el-card style="margin-bottom: 20px;">
|
|
<template #header><span>全局默认配置</span></template>
|
|
<el-form :model="config" label-width="180px" v-loading="loadingConfig">
|
|
<el-form-item label="默认告警阶梯(%)">
|
|
<el-input v-model="alertThresholdsStr" placeholder="50,80,90" />
|
|
<div style="font-size:12px;color:#999;margin-top:4px;">
|
|
逗号分隔的百分比,如 50,80,90 表示消费达到已划拨额度的 50%/80%/90% 时告警
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="项目默认授权策略">
|
|
<el-input v-model="projectPoliciesStr" placeholder="ArkFullAccess,TOSFullAccess" />
|
|
<div style="font-size:12px;color:#999;margin-top:4px;">
|
|
逗号分隔。添加项目时自动在项目范围内授权这些策略,移除项目时自动回收
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="监控间隔(秒)">
|
|
<el-input-number v-model="config.monitor_interval_seconds" :min="60" :step="60" />
|
|
</el-form-item>
|
|
<el-form-item label="飞书 Webhook URL">
|
|
<el-input v-model="config.feishu_webhook_url" placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..." />
|
|
</el-form-item>
|
|
<el-form-item label="飞书通知手机号">
|
|
<el-input v-model="config.feishu_alert_mobiles" placeholder="手机号1,手机号2" />
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="saveConfig" :loading="savingConfig">保存配置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-card>
|
|
|
|
<!-- Volcengine Account -->
|
|
<el-card>
|
|
<template #header>
|
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
<span>火山引擎主账号</span>
|
|
<el-button size="small" type="primary" @click="showAddAccount = true">添加主账号</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="accounts" stripe style="width:100%;" v-loading="loadingAccounts">
|
|
<el-table-column prop="name" label="名称" width="200" />
|
|
<el-table-column prop="access_key_hint" label="AccessKey" width="200" />
|
|
<el-table-column prop="is_active" label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
|
|
{{ row.is_active ? '启用' : '停用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="200">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click="testAccount(row.id)">测试</el-button>
|
|
<el-button size="small" type="danger" @click="deleteAccount(row.id)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<!-- Add Account Dialog -->
|
|
<el-dialog v-model="showAddAccount" title="添加火山主账号" width="500px">
|
|
<el-form :model="accountForm" label-width="120px">
|
|
<el-form-item label="账号名称">
|
|
<el-input v-model="accountForm.name" placeholder="如:主账号" />
|
|
</el-form-item>
|
|
<el-form-item label="AccessKey">
|
|
<el-input v-model="accountForm.access_key" placeholder="AKLT..." />
|
|
</el-form-item>
|
|
<el-form-item label="SecretKey">
|
|
<el-input v-model="accountForm.secret_key" type="password" show-password
|
|
placeholder="输入后加密存储,不可回显" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showAddAccount = false">取消</el-button>
|
|
<el-button type="primary" @click="addAccount" :loading="addingAccount">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import api from '../../api'
|
|
|
|
// Global config
|
|
const config = ref({})
|
|
const loadingConfig = ref(false)
|
|
const savingConfig = ref(false)
|
|
|
|
const projectPoliciesStr = computed({
|
|
get: () => (config.value.default_project_policies || []).join(','),
|
|
set: (val) => {
|
|
config.value.default_project_policies = val
|
|
.split(',')
|
|
.map(s => s.trim())
|
|
.filter(Boolean)
|
|
},
|
|
})
|
|
|
|
const alertThresholdsStr = computed({
|
|
get: () => (config.value.default_alert_thresholds || []).join(','),
|
|
set: (val) => {
|
|
config.value.default_alert_thresholds = val
|
|
.split(',')
|
|
.map(s => parseInt(s.trim()))
|
|
.filter(n => n >= 1 && n <= 99)
|
|
.sort((a, b) => a - b)
|
|
},
|
|
})
|
|
|
|
// Volc accounts
|
|
const accounts = ref([])
|
|
const loadingAccounts = ref(false)
|
|
const showAddAccount = ref(false)
|
|
const accountForm = ref({ name: '默认主账号', access_key: '', secret_key: '' })
|
|
const addingAccount = ref(false)
|
|
|
|
async function loadConfig() {
|
|
loadingConfig.value = true
|
|
try {
|
|
const { data } = await api.get('/api/v1/config/')
|
|
config.value = data
|
|
} catch (e) {
|
|
ElMessage.error('加载配置失败')
|
|
} finally {
|
|
loadingConfig.value = false
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
savingConfig.value = true
|
|
try {
|
|
await api.put('/api/v1/config/', config.value)
|
|
ElMessage.success('配置已保存')
|
|
} catch (e) {
|
|
ElMessage.error('保存失败')
|
|
} finally {
|
|
savingConfig.value = false
|
|
}
|
|
}
|
|
|
|
async function loadAccounts() {
|
|
loadingAccounts.value = true
|
|
try {
|
|
const { data } = await api.get('/api/v1/volc-accounts/')
|
|
accounts.value = data
|
|
} catch (e) {
|
|
ElMessage.error('加载主账号失败')
|
|
} finally {
|
|
loadingAccounts.value = false
|
|
}
|
|
}
|
|
|
|
async function addAccount() {
|
|
addingAccount.value = true
|
|
try {
|
|
await api.post('/api/v1/volc-accounts/', accountForm.value)
|
|
ElMessage.success('主账号已添加')
|
|
showAddAccount.value = false
|
|
accountForm.value = { name: '默认主账号', access_key: '', secret_key: '' }
|
|
await loadAccounts()
|
|
} catch (e) {
|
|
ElMessage.error('添加失败')
|
|
} finally {
|
|
addingAccount.value = false
|
|
}
|
|
}
|
|
|
|
async function testAccount(id) {
|
|
try {
|
|
const { data } = await api.post(`/api/v1/volc-accounts/${id}/test/`)
|
|
ElMessage.success(data.message)
|
|
} catch (e) {
|
|
ElMessage.error(e.response?.data?.message || '测试失败')
|
|
}
|
|
}
|
|
|
|
async function deleteAccount(id) {
|
|
await ElMessageBox.confirm('确定删除此主账号配置?', '确认删除', { type: 'warning' })
|
|
try {
|
|
await api.delete(`/api/v1/volc-accounts/${id}/`)
|
|
ElMessage.success('已删除')
|
|
await loadAccounts()
|
|
} catch (e) {
|
|
ElMessage.error('删除失败')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadConfig()
|
|
loadAccounts()
|
|
})
|
|
</script>
|