feat: 密码管理 — 用户自改密码 + 管理员重置密码
- 侧边栏新增「修改密码」入口,任何用户可改自己的密码(需验证原密码) - 用户管理编辑弹窗新增「重置密码」区域,管理员可直接重设任意用户密码 - 后端新增 POST /api/users/change-password 接口 - UserUpdate schema 增加 password 可选字段 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
11b1d9b105
commit
74106ac21b
@ -4,8 +4,8 @@ from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from database import get_db
|
||||
from models import User, Role, PhaseGroup
|
||||
from schemas import UserCreate, UserUpdate, UserOut
|
||||
from auth import get_current_user, hash_password, require_permission
|
||||
from schemas import UserCreate, UserUpdate, UserOut, ChangePasswordRequest
|
||||
from auth import get_current_user, hash_password, verify_password, require_permission
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["用户管理"])
|
||||
|
||||
@ -86,11 +86,28 @@ def update_user(
|
||||
user.social_insurance = req.social_insurance
|
||||
if req.is_active is not None:
|
||||
user.is_active = req.is_active
|
||||
if req.password:
|
||||
user.password_hash = hash_password(req.password)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user_to_out(user)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
def change_password(
|
||||
req: ChangePasswordRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
if not verify_password(req.old_password, current_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
if len(req.new_password) < 4:
|
||||
raise HTTPException(status_code=400, detail="新密码至少4位")
|
||||
current_user.password_hash = hash_password(req.new_password)
|
||||
db.commit()
|
||||
return {"message": "密码修改成功"}
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserOut)
|
||||
def get_user(
|
||||
user_id: int,
|
||||
|
||||
@ -37,6 +37,12 @@ class UserUpdate(BaseModel):
|
||||
bonus: Optional[float] = None
|
||||
social_insurance: Optional[float] = None
|
||||
is_active: Optional[int] = None
|
||||
password: Optional[str] = None # 管理员重置密码
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
|
||||
@ -51,6 +51,7 @@ export const userApi = {
|
||||
create: (data) => api.post('/users', data),
|
||||
update: (id, data) => api.put(`/users/${id}`, data),
|
||||
get: (id) => api.get(`/users/${id}`),
|
||||
changePassword: (data) => api.post('/users/change-password', data),
|
||||
}
|
||||
|
||||
// ── 项目 ──
|
||||
|
||||
@ -35,6 +35,11 @@
|
||||
<div class="user-role">{{ authStore.user?.role_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<el-button text size="small" class="change-pwd-btn" @click="showChangePwd = true">
|
||||
<el-icon :size="14"><Lock /></el-icon> 修改密码
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
|
||||
@ -55,13 +60,34 @@
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<el-dialog v-model="showChangePwd" title="修改密码" width="400px" destroy-on-close>
|
||||
<el-form :model="pwdForm" label-width="80px">
|
||||
<el-form-item label="原密码">
|
||||
<el-input v-model="pwdForm.old_password" type="password" show-password placeholder="请输入当前密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="pwdForm.new_password" type="password" show-password placeholder="至少4位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="pwdForm.confirm" type="password" show-password placeholder="再次输入新密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showChangePwd = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleChangePwd">确认修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { userApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -106,6 +132,22 @@ function handleLogout() {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const showChangePwd = ref(false)
|
||||
const pwdForm = reactive({ old_password: '', new_password: '', confirm: '' })
|
||||
|
||||
async function handleChangePwd() {
|
||||
if (!pwdForm.old_password) { ElMessage.warning('请输入原密码'); return }
|
||||
if (pwdForm.new_password.length < 4) { ElMessage.warning('新密码至少4位'); return }
|
||||
if (pwdForm.new_password !== pwdForm.confirm) { ElMessage.warning('两次输入的新密码不一致'); return }
|
||||
try {
|
||||
await userApi.changePassword({ old_password: pwdForm.old_password, new_password: pwdForm.new_password })
|
||||
ElMessage.success('密码修改成功')
|
||||
showChangePwd.value = false
|
||||
pwdForm.old_password = ''; pwdForm.new_password = ''; pwdForm.confirm = ''
|
||||
} catch {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -195,6 +237,9 @@ function handleLogout() {
|
||||
.user-meta { display: flex; flex-direction: column; }
|
||||
.user-name { font-size: 13px; font-weight: 600; color: var(--text-primary); line-height: 1.3; }
|
||||
.user-role { font-size: 11px; color: var(--text-secondary); }
|
||||
.user-actions { margin-top: 8px; }
|
||||
.change-pwd-btn { color: var(--text-secondary) !important; font-size: 12px !important; padding: 0 !important; gap: 4px; }
|
||||
.change-pwd-btn:hover { color: var(--primary) !important; }
|
||||
|
||||
/* ── 顶栏 ── */
|
||||
.main-container { background: var(--bg-page); }
|
||||
|
||||
@ -80,6 +80,10 @@
|
||||
<el-form-item v-if="editingId" label="状态" style="margin-top:16px">
|
||||
<el-switch v-model="form.is_active" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
<el-divider v-if="editingId" content-position="left">重置密码</el-divider>
|
||||
<el-form-item v-if="editingId" label="新密码">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="留空则不修改密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
@ -127,17 +131,20 @@ function editUser(u) {
|
||||
name: u.name, phase_group: u.phase_group, role_id: u.role_id,
|
||||
monthly_salary: u.monthly_salary, bonus: u.bonus || 0,
|
||||
social_insurance: u.social_insurance || 0, is_active: u.is_active,
|
||||
password: '',
|
||||
})
|
||||
showCreate.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (editingId.value) {
|
||||
await userApi.update(editingId.value, {
|
||||
const data = {
|
||||
name: form.name, phase_group: form.phase_group, role_id: form.role_id,
|
||||
monthly_salary: form.monthly_salary, bonus: form.bonus,
|
||||
social_insurance: form.social_insurance, is_active: form.is_active,
|
||||
})
|
||||
}
|
||||
if (form.password) data.password = form.password
|
||||
await userApi.update(editingId.value, data)
|
||||
} else {
|
||||
await userApi.create(form)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user