feat: password management for admin and sub-accounts
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
33c8963d46
commit
fac5e1b541
@ -16,4 +16,5 @@ urlpatterns = [
|
|||||||
path('iam/me/', views.iam_me_view),
|
path('iam/me/', views.iam_me_view),
|
||||||
path('iam/my-keys/', views.iam_my_keys_view),
|
path('iam/my-keys/', views.iam_my_keys_view),
|
||||||
path('iam/my-keys/<int:pk>/reveal/', views.iam_my_key_reveal_view),
|
path('iam/my-keys/<int:pk>/reveal/', views.iam_my_key_reveal_view),
|
||||||
|
path('iam/change-password/', views.iam_change_password_view),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -350,3 +350,59 @@ def iam_my_key_reveal_view(request, pk):
|
|||||||
'key_name': key.key_name,
|
'key_name': key.key_name,
|
||||||
'project_name': key.project_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': '密码修改成功,请重新登录'})
|
||||||
|
|||||||
@ -43,6 +43,10 @@
|
|||||||
<el-icon><Key /></el-icon>
|
<el-icon><Key /></el-icon>
|
||||||
<span>我的 API Key</span>
|
<span>我的 API Key</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/my-password">
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
<span>修改密码</span>
|
||||||
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const routes = [
|
|||||||
{ path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') },
|
{ path: 'admin', name: 'Admin', component: () => import('../views/admin/AdminView.vue') },
|
||||||
// IAM user (sub-account) routes
|
// IAM user (sub-account) routes
|
||||||
{ path: 'my-keys', name: 'MyKeys', component: () => import('../views/portal/MyKeysView.vue') },
|
{ path: 'my-keys', name: 'MyKeys', component: () => import('../views/portal/MyKeysView.vue') },
|
||||||
|
{ path: 'my-password', name: 'MyPassword', component: () => import('../views/portal/MyPasswordView.vue') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -80,6 +80,7 @@
|
|||||||
<el-dropdown-item @click="openConfig(row)">监控配置</el-dropdown-item>
|
<el-dropdown-item @click="openConfig(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 v-if="row.status === 'active'" divided
|
<el-dropdown-item v-if="row.status === 'active'" divided
|
||||||
@click="handleDisable(row)" style="color:#f56c6c;">停用账号</el-dropdown-item>
|
@click="handleDisable(row)" style="color:#f56c6c;">停用账号</el-dropdown-item>
|
||||||
<el-dropdown-item v-if="row.status === 'disabled'" divided
|
<el-dropdown-item v-if="row.status === 'disabled'" divided
|
||||||
@ -237,6 +238,25 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Set Login Password Dialog -->
|
||||||
|
<el-dialog v-model="loginPwdVisible" :title="`设置 ${loginPwdUser?.username} 的 AirGate 登录密码`"
|
||||||
|
width="90%" style="max-width: 450px;">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="登录状态">
|
||||||
|
<el-switch v-model="loginPwdEnabled"
|
||||||
|
active-text="允许登录" inactive-text="禁止登录" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码">
|
||||||
|
<el-input v-model="loginPwdValue" type="password" show-password
|
||||||
|
placeholder="至少6位(留空则不修改密码)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="loginPwdVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSetLogin" :loading="loginPwdSaving">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Quota History Dialog -->
|
<!-- Quota History Dialog -->
|
||||||
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
|
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
|
||||||
<el-table :data="quotaHistory" stripe v-loading="historyLoading" empty-text="暂无划拨记录">
|
<el-table :data="quotaHistory" stripe v-loading="historyLoading" empty-text="暂无划拨记录">
|
||||||
@ -651,6 +671,42 @@ async function saveConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Quota History ---
|
// --- 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) {
|
async function openQuotaHistory(row) {
|
||||||
historyUser.value = row
|
historyUser.value = row
|
||||||
historyVisible.value = true
|
historyVisible.value = true
|
||||||
|
|||||||
66
frontend/src/views/portal/MyPasswordView.vue
Normal file
66
frontend/src/views/portal/MyPasswordView.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div style="max-width: 500px; margin: 0 auto;">
|
||||||
|
<h2 style="margin-bottom: 16px;">修改密码</h2>
|
||||||
|
<el-card>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="原密码">
|
||||||
|
<el-input v-model="form.old_password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码">
|
||||||
|
<el-input v-model="form.new_password" type="password" show-password
|
||||||
|
placeholder="至少6位" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码">
|
||||||
|
<el-input v-model="form.confirm_password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleChange" :loading="loading">修改密码</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
import api from '../../api'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const form = ref({ old_password: '', new_password: '', confirm_password: '' })
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function handleChange() {
|
||||||
|
if (!form.value.old_password || !form.value.new_password) {
|
||||||
|
ElMessage.warning('请填写完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.value.new_password !== form.value.confirm_password) {
|
||||||
|
ElMessage.warning('两次密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.value.new_password.length < 6) {
|
||||||
|
ElMessage.warning('密码至少6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.post('/api/v1/auth/iam/change-password/', {
|
||||||
|
old_password: form.value.old_password,
|
||||||
|
new_password: form.value.new_password,
|
||||||
|
})
|
||||||
|
ElMessage.success(data.message)
|
||||||
|
setTimeout(() => {
|
||||||
|
auth.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}, 1500)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || '修改失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user