From fac5e1b54170273b1f2dea1a87658f751a898871 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Sat, 21 Mar 2026 15:54:35 +0800 Subject: [PATCH] feat: password management for admin and sub-accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin: set sub-account AirGate login password via dropdown menu - Admin: toggle sub-account login enabled/disabled - Sub-account: change own password (sidebar "修改密码") - Sub-account: auto-redirect to login page after password change Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/apps/accounts/urls.py | 1 + backend/apps/accounts/views.py | 56 +++++++++++++++++ frontend/src/layouts/MainLayout.vue | 4 ++ frontend/src/router/index.js | 1 + frontend/src/views/iam/IAMUserList.vue | 56 +++++++++++++++++ frontend/src/views/portal/MyPasswordView.vue | 66 ++++++++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 frontend/src/views/portal/MyPasswordView.vue diff --git a/backend/apps/accounts/urls.py b/backend/apps/accounts/urls.py index e8472ba..3ee1e49 100644 --- a/backend/apps/accounts/urls.py +++ b/backend/apps/accounts/urls.py @@ -16,4 +16,5 @@ urlpatterns = [ path('iam/me/', views.iam_me_view), path('iam/my-keys/', views.iam_my_keys_view), path('iam/my-keys//reveal/', views.iam_my_key_reveal_view), + path('iam/change-password/', views.iam_change_password_view), ] diff --git a/backend/apps/accounts/views.py b/backend/apps/accounts/views.py index 83377af..ce1685f 100644 --- a/backend/apps/accounts/views.py +++ b/backend/apps/accounts/views.py @@ -350,3 +350,59 @@ def iam_my_key_reveal_view(request, pk): 'key_name': key.key_name, 'project_name': key.project_name, }) + + +@api_view(['POST']) +@authentication_classes([]) +@permission_classes([AllowAny]) +def iam_change_password_view(request): + """子账号修改自己的 AirGate 登录密码""" + import jwt + from django.conf import settings + + auth_header = request.headers.get('Authorization', '') + if not auth_header.startswith('Bearer '): + return Response({'error': 'unauthorized'}, status=status.HTTP_401_UNAUTHORIZED) + + token = auth_header.split(' ', 1)[1] + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) + except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): + return Response({'error': 'invalid_token'}, status=status.HTTP_401_UNAUTHORIZED) + + if payload.get('role') != 'iam_user': + return Response({'error': 'not_iam_user'}, status=status.HTTP_403_FORBIDDEN) + + from apps.monitor.models import IAMUser + try: + iam_user = IAMUser.objects.get(pk=payload['iam_user_id']) + except IAMUser.DoesNotExist: + return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND) + + old_password = request.data.get('old_password', '') + new_password = request.data.get('new_password', '') + + if not old_password or not new_password: + return Response({'error': 'missing', 'message': '请输入原密码和新密码'}, + status=status.HTTP_400_BAD_REQUEST) + + if not iam_user.check_login_password(old_password): + return Response({'error': 'wrong_password', 'message': '原密码错误'}, + status=status.HTTP_400_BAD_REQUEST) + + if len(new_password) < 6: + return Response({'error': 'weak_password', 'message': '密码至少6位'}, + status=status.HTTP_400_BAD_REQUEST) + + iam_user.set_login_password(new_password) + iam_user.save(update_fields=['login_password_hash']) + + from apps.monitor.models import AlertRecord + AlertRecord.objects.create( + iam_user=iam_user, + alert_type=AlertRecord.AlertType.MANUAL, + title=f"子账号 {iam_user.username} 修改 AirGate 密码", + content=f"操作人: {iam_user.username}(自行修改)", + ) + + return Response({'message': '密码修改成功,请重新登录'}) diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 137d154..21a988f 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -43,6 +43,10 @@ 我的 API Key + + + 修改密码 + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 5f7cb7e..89b96b2 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -28,6 +28,7 @@ const routes = [ { path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') }, // IAM user (sub-account) routes { path: 'my-keys', name: 'MyKeys', component: () => import('../views/portal/MyKeysView.vue') }, + { path: 'my-password', name: 'MyPassword', component: () => import('../views/portal/MyPasswordView.vue') }, ], }, ] diff --git a/frontend/src/views/iam/IAMUserList.vue b/frontend/src/views/iam/IAMUserList.vue index 9edaf90..faff8d0 100644 --- a/frontend/src/views/iam/IAMUserList.vue +++ b/frontend/src/views/iam/IAMUserList.vue @@ -80,6 +80,7 @@ 监控配置 权限策略 划拨记录 + 登录密码 停用账号 + + + + + + + + + + + + + @@ -651,6 +671,42 @@ async function saveConfig() { } // --- Quota History --- +// === Set Login Password === +const loginPwdVisible = ref(false) +const loginPwdUser = ref(null) +const loginPwdValue = ref('') +const loginPwdEnabled = ref(false) +const loginPwdSaving = ref(false) + +function openSetLogin(row) { + loginPwdUser.value = row + loginPwdValue.value = '' + loginPwdEnabled.value = row.login_enabled || false + loginPwdVisible.value = true +} + +async function handleSetLogin() { + const payload = { login_enabled: loginPwdEnabled.value } + if (loginPwdValue.value) { + if (loginPwdValue.value.length < 6) { + ElMessage.warning('密码至少6位') + return + } + payload.password = loginPwdValue.value + } + loginPwdSaving.value = true + try { + const { data } = await api.post(`/api/v1/iam-users/${loginPwdUser.value.id}/set-login/`, payload) + ElMessage.success(data.message) + loginPwdVisible.value = false + await loadUsers() + } catch (e) { + ElMessage.error(e.response?.data?.message || '操作失败') + } finally { + loginPwdSaving.value = false + } +} + async function openQuotaHistory(row) { historyUser.value = row historyVisible.value = true diff --git a/frontend/src/views/portal/MyPasswordView.vue b/frontend/src/views/portal/MyPasswordView.vue new file mode 100644 index 0000000..b317e27 --- /dev/null +++ b/frontend/src/views/portal/MyPasswordView.vue @@ -0,0 +1,66 @@ + + +