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:
parent
b25641cfc6
commit
8b49d49048
33
API方案.md
Normal file
33
API方案.md
Normal 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 变更你得跟着维护
|
||||
一句话结论
|
||||
队友是自己人 → 方案一,省事;对外卖服务/不信任对方 → 方案二,可控。
|
||||
@ -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),
|
||||
|
||||
@ -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 登录密码"""
|
||||
|
||||
@ -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})
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user