feat: add edit sub-account profile + verify all password features

- Add edit profile (display name, phone, email) with Volcengine sync
- Add IAMService.update_user for Volcengine UpdateUser API
- Add edit-profile API endpoint and URL
- Add Edit Profile dialog in IAMUserList frontend
- Verify admin change password, sub-account change password, set login password all working

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-28 15:57:02 +08:00
parent b25641cfc6
commit 8b49d49048
6 changed files with 152 additions and 3 deletions

33
API方案.md Normal file
View File

@ -0,0 +1,33 @@
方案一:火山 IAM 子账号(推荐)
思路:给队友创建火山子用户,让他直连火山 API。
给他的东西
用途 给什么
Assets API 子账号的 AK/SK
Seedance 调用 子账号的 Ark API Key + 专用接入点(只绑 Seedance 2.0
对账 控制台登录密码
项目标识 ProjectName: "int_dev_Airlabs"
权限范围
✅ Ark 模型调用、Assets 素材管理、费用中心(只读)
❌ IAM 管理、其他云服务、充值/支付
优缺点
✅ 零开发量,控制台几分钟搞定
✅ 他能自己对账
✅ 权限可控,随时可禁用/删除
✅ 互不影响,你的服务挂了不影响他
❌ 他能直接接触火山资源(但权限受限)
方案二:后端转发
思路你的后端包一层队友只调你的接口AK/SK 不出服务器。
给他的东西
用途 给什么
所有调用 后端地址 + 账号密码JWT 认证)
优缺点
✅ 队友什么密钥都不需要,最安全
✅ 你能完全掌控调用行为
❌ 要写新接口把火山 API 都包一遍
❌ 对账要你自己做用量统计页面
❌ 多一跳,依赖你的服务稳定性
❌ 火山 API 变更你得跟着维护
一句话结论
队友是自己人 → 方案一,省事;对外卖服务/不信任对方 → 方案二,可控。

View File

@ -17,6 +17,7 @@ urlpatterns = [
path('iam-users/import/', views.iam_user_import_view),
path('iam-users/<int:pk>/', views.iam_user_detail_view),
path('iam-users/<int:pk>/update/', views.iam_user_update_view),
path('iam-users/<int:pk>/edit-profile/', views.iam_user_edit_profile_view),
path('iam-users/<int:pk>/set-login/', views.iam_user_set_login_view),
path('iam-users/<int:pk>/disable/', views.iam_user_disable_view),
path('iam-users/<int:pk>/enable/', views.iam_user_enable_view),

View File

@ -369,6 +369,50 @@ def iam_user_update_view(request, pk):
return Response(IAMUserSerializer(user).data)
@api_view(['POST'])
def iam_user_edit_profile_view(request, pk):
"""编辑子账号信息(显示名、手机号、邮箱),同步到火山"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
display_name = request.data.get('display_name')
email = request.data.get('email')
phone = request.data.get('phone')
# Update on Volcengine
account = user.volc_account
ak = decrypt(account.access_key_enc)
sk = decrypt(account.secret_key_enc)
iam = IAMService(ak, sk)
try:
iam.update_user(user.username,
display_name=display_name,
email=email,
phone=phone)
except VolcengineAPIError as e:
return Response({'message': f'火山 API 更新失败: {e}'},
status=status.HTTP_400_BAD_REQUEST)
# Update locally
if display_name is not None:
user.display_name = display_name
if email is not None:
user.email = email
if phone is not None:
user.phone = phone
user.save()
AlertRecord.objects.create(
iam_user=user, alert_type='manual',
title=f'编辑子账号信息 {user.username}',
content=f'操作人: {request.user.username}',
)
return Response({'message': '已更新', 'user': IAMUserSerializer(user).data})
@api_view(['POST'])
def iam_user_set_login_view(request, pk):
"""设置子账号的 AirGate 登录密码"""

View File

@ -76,6 +76,17 @@ class IAMService:
"PolicyType": policy_type,
})
def update_user(self, username: str, display_name: str = None,
email: str = None, phone: str = None) -> dict:
params = {"UserName": username}
if display_name is not None:
params["NewDisplayName"] = display_name
if email is not None:
params["NewEmail"] = email
if phone is not None:
params["NewMobilePhone"] = phone
return self.client.call("UpdateUser", params)
def list_attached_user_policies(self, username: str) -> dict:
return self.client.call("ListAttachedUserPolicies", {"UserName": username})

View File

@ -1,7 +1,7 @@
version: '3.8'
services:
airgate-backend:
backend:
build: ./backend
ports:
- "8101:8100"
@ -15,12 +15,12 @@ services:
- backend-data:/app/data
restart: unless-stopped
airgate-web:
frontend:
build: ./frontend
ports:
- "5174:80"
depends_on:
- airgate-backend
- backend
restart: unless-stopped
volumes:

View File

@ -78,6 +78,7 @@
<el-dropdown-menu>
<el-dropdown-item @click="openProjectsDialog(row)">项目管理</el-dropdown-item>
<el-dropdown-item @click="openConfig(row)">监控配置</el-dropdown-item>
<el-dropdown-item @click="openEditProfile(row)">编辑信息</el-dropdown-item>
<el-dropdown-item @click="openPolicies(row)">权限策略</el-dropdown-item>
<el-dropdown-item @click="openQuotaHistory(row)">划拨记录</el-dropdown-item>
<el-dropdown-item @click="openSetLogin(row)">登录密码</el-dropdown-item>
@ -343,6 +344,32 @@
</template>
</el-dialog>
<!-- Edit Profile Dialog -->
<el-dialog v-model="editProfileVisible" :title="`编辑 ${editProfileUser?.username} 信息`"
width="90%" style="max-width: 500px;">
<el-form :model="editProfileForm" label-width="80px">
<el-form-item label="用户名">
<el-input :model-value="editProfileUser?.username" disabled />
</el-form-item>
<el-form-item label="显示名">
<el-input v-model="editProfileForm.display_name" placeholder="如:视频部门" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="editProfileForm.phone" placeholder="如 13800138000" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="editProfileForm.email" placeholder="选填" />
</el-form-item>
</el-form>
<div style="font-size:12px; color:#999; margin-top:8px;">
修改会同步到火山引擎 IAM
</div>
<template #footer>
<el-button @click="editProfileVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditProfile" :loading="editProfileSaving">保存</el-button>
</template>
</el-dialog>
<!-- Secret Key Display Dialog -->
<el-dialog v-model="showSecretKey" title="API 密钥已生成" width="90%" style="max-width: 580px;" :close-on-click-modal="false">
<el-alert type="error" :closable="false" style="margin-bottom:16px;"
@ -447,6 +474,39 @@ async function handleEnable(row) {
}
}
// Edit Profile
const editProfileVisible = ref(false)
const editProfileUser = ref(null)
const editProfileForm = ref({ display_name: '', phone: '', email: '' })
const editProfileSaving = ref(false)
function openEditProfile(row) {
editProfileUser.value = row
editProfileForm.value = {
display_name: row.display_name || '',
phone: row.phone || '',
email: row.email || '',
}
editProfileVisible.value = true
}
async function handleEditProfile() {
editProfileSaving.value = true
try {
const { data } = await api.post(
`/api/v1/iam-users/${editProfileUser.value.id}/edit-profile/`,
editProfileForm.value
)
ElMessage.success(data.message)
editProfileVisible.value = false
await loadUsers()
} catch (e) {
ElMessage.error(e.response?.data?.message || '更新失败')
} finally {
editProfileSaving.value = false
}
}
// Policies
const policiesVisible = ref(false)
const policiesUser = ref(null)