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 typing import List
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import User, Role, PhaseGroup
|
from models import User, Role, PhaseGroup
|
||||||
from schemas import UserCreate, UserUpdate, UserOut
|
from schemas import UserCreate, UserUpdate, UserOut, ChangePasswordRequest
|
||||||
from auth import get_current_user, hash_password, require_permission
|
from auth import get_current_user, hash_password, verify_password, require_permission
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/users", tags=["用户管理"])
|
router = APIRouter(prefix="/api/users", tags=["用户管理"])
|
||||||
|
|
||||||
@ -86,11 +86,28 @@ def update_user(
|
|||||||
user.social_insurance = req.social_insurance
|
user.social_insurance = req.social_insurance
|
||||||
if req.is_active is not None:
|
if req.is_active is not None:
|
||||||
user.is_active = req.is_active
|
user.is_active = req.is_active
|
||||||
|
if req.password:
|
||||||
|
user.password_hash = hash_password(req.password)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
return user_to_out(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)
|
@router.get("/{user_id}", response_model=UserOut)
|
||||||
def get_user(
|
def get_user(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
|||||||
@ -37,6 +37,12 @@ class UserUpdate(BaseModel):
|
|||||||
bonus: Optional[float] = None
|
bonus: Optional[float] = None
|
||||||
social_insurance: Optional[float] = None
|
social_insurance: Optional[float] = None
|
||||||
is_active: Optional[int] = None
|
is_active: Optional[int] = None
|
||||||
|
password: Optional[str] = None # 管理员重置密码
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordRequest(BaseModel):
|
||||||
|
old_password: str
|
||||||
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
class UserOut(BaseModel):
|
class UserOut(BaseModel):
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export const userApi = {
|
|||||||
create: (data) => api.post('/users', data),
|
create: (data) => api.post('/users', data),
|
||||||
update: (id, data) => api.put(`/users/${id}`, data),
|
update: (id, data) => api.put(`/users/${id}`, data),
|
||||||
get: (id) => api.get(`/users/${id}`),
|
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 class="user-role">{{ authStore.user?.role_name }}</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
@ -55,13 +60,34 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</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>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { userApi } from '../api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -106,6 +132,22 @@ function handleLogout() {
|
|||||||
authStore.logout()
|
authStore.logout()
|
||||||
router.push('/login')
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -195,6 +237,9 @@ function handleLogout() {
|
|||||||
.user-meta { display: flex; flex-direction: column; }
|
.user-meta { display: flex; flex-direction: column; }
|
||||||
.user-name { font-size: 13px; font-weight: 600; color: var(--text-primary); line-height: 1.3; }
|
.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-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); }
|
.main-container { background: var(--bg-page); }
|
||||||
|
|||||||
@ -80,6 +80,10 @@
|
|||||||
<el-form-item v-if="editingId" label="状态" style="margin-top:16px">
|
<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-switch v-model="form.is_active" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="停用" />
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showCreate = false">取消</el-button>
|
<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,
|
name: u.name, phase_group: u.phase_group, role_id: u.role_id,
|
||||||
monthly_salary: u.monthly_salary, bonus: u.bonus || 0,
|
monthly_salary: u.monthly_salary, bonus: u.bonus || 0,
|
||||||
social_insurance: u.social_insurance || 0, is_active: u.is_active,
|
social_insurance: u.social_insurance || 0, is_active: u.is_active,
|
||||||
|
password: '',
|
||||||
})
|
})
|
||||||
showCreate.value = true
|
showCreate.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
await userApi.update(editingId.value, {
|
const data = {
|
||||||
name: form.name, phase_group: form.phase_group, role_id: form.role_id,
|
name: form.name, phase_group: form.phase_group, role_id: form.role_id,
|
||||||
monthly_salary: form.monthly_salary, bonus: form.bonus,
|
monthly_salary: form.monthly_salary, bonus: form.bonus,
|
||||||
social_insurance: form.social_insurance, is_active: form.is_active,
|
social_insurance: form.social_insurance, is_active: form.is_active,
|
||||||
})
|
}
|
||||||
|
if (form.password) data.password = form.password
|
||||||
|
await userApi.update(editingId.value, data)
|
||||||
} else {
|
} else {
|
||||||
await userApi.create(form)
|
await userApi.create(form)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user