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/import/', views.iam_user_import_view),
|
||||||
path('iam-users/<int:pk>/', views.iam_user_detail_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>/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>/set-login/', views.iam_user_set_login_view),
|
||||||
path('iam-users/<int:pk>/disable/', views.iam_user_disable_view),
|
path('iam-users/<int:pk>/disable/', views.iam_user_disable_view),
|
||||||
path('iam-users/<int:pk>/enable/', views.iam_user_enable_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)
|
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'])
|
@api_view(['POST'])
|
||||||
def iam_user_set_login_view(request, pk):
|
def iam_user_set_login_view(request, pk):
|
||||||
"""设置子账号的 AirGate 登录密码"""
|
"""设置子账号的 AirGate 登录密码"""
|
||||||
|
|||||||
@ -76,6 +76,17 @@ class IAMService:
|
|||||||
"PolicyType": policy_type,
|
"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:
|
def list_attached_user_policies(self, username: str) -> dict:
|
||||||
return self.client.call("ListAttachedUserPolicies", {"UserName": username})
|
return self.client.call("ListAttachedUserPolicies", {"UserName": username})
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
airgate-backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
ports:
|
ports:
|
||||||
- "8101:8100"
|
- "8101:8100"
|
||||||
@ -15,12 +15,12 @@ services:
|
|||||||
- backend-data:/app/data
|
- backend-data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
airgate-web:
|
frontend:
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
ports:
|
ports:
|
||||||
- "5174:80"
|
- "5174:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- airgate-backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@ -78,6 +78,7 @@
|
|||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item @click="openProjectsDialog(row)">项目管理</el-dropdown-item>
|
<el-dropdown-item @click="openProjectsDialog(row)">项目管理</el-dropdown-item>
|
||||||
<el-dropdown-item @click="openConfig(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="openPolicies(row)">权限策略</el-dropdown-item>
|
||||||
<el-dropdown-item @click="openQuotaHistory(row)">划拨记录</el-dropdown-item>
|
<el-dropdown-item @click="openQuotaHistory(row)">划拨记录</el-dropdown-item>
|
||||||
<el-dropdown-item @click="openSetLogin(row)">登录密码</el-dropdown-item>
|
<el-dropdown-item @click="openSetLogin(row)">登录密码</el-dropdown-item>
|
||||||
@ -343,6 +344,32 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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 -->
|
<!-- Secret Key Display Dialog -->
|
||||||
<el-dialog v-model="showSecretKey" title="API 密钥已生成" width="90%" style="max-width: 580px;" :close-on-click-modal="false">
|
<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;"
|
<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
|
// Policies
|
||||||
const policiesVisible = ref(false)
|
const policiesVisible = ref(false)
|
||||||
const policiesUser = ref(null)
|
const policiesUser = ref(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user