AirGate/frontend/src/views/settings/SettingsView.vue
seaislee1209 5edf247a7f feat: auto-authorize policies when adding projects to sub-accounts
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>
2026-03-19 23:24:45 +08:00

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>