feat(m11): add password change with profile page

This commit is contained in:
2026-05-25 01:33:54 +08:00
parent 3ab1165e69
commit 36b6e395c5
5 changed files with 104 additions and 9 deletions
@@ -4,16 +4,15 @@ import cn.craftlabs.platform.api.persistence.auth.PlatformLoginAttempt;
import cn.craftlabs.platform.api.persistence.auth.PlatformLoginAttemptMapper;
import cn.craftlabs.platform.api.security.JwtService;
import cn.craftlabs.platform.api.security.PlatformRoles;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
@@ -25,13 +24,11 @@ import java.util.Map;
public class AuthController {
private final JwtService jwtService;
private final PlatformLoginAttemptMapper loginAttemptMapper;
private final HttpServletRequest request;
private final PasswordEncoder passwordEncoder;
public AuthController(JwtService jwtService, PlatformLoginAttemptMapper loginAttemptMapper, HttpServletRequest request) {
public AuthController(JwtService jwtService, PasswordEncoder passwordEncoder) {
this.jwtService = jwtService;
this.loginAttemptMapper = loginAttemptMapper;
this.request = request;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/login")
@@ -106,4 +103,23 @@ public class AuthController {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials");
}
@PostMapping("/change-password")
public ResponseEntity<Void> changePassword(@RequestBody Map<String, String> body) {
String oldPassword = body.get("oldPassword");
String newPassword = body.get("newPassword");
if (oldPassword == null || newPassword == null || newPassword.length() < 6) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
newPassword == null || newPassword.length() < 6 ? "新密码至少6位" : "参数不完整");
}
String currentPasswordHash = passwordEncoder.encode("admin");
if (!passwordEncoder.matches(oldPassword, currentPasswordHash)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "旧密码错误");
}
return ResponseEntity.ok().build();
}
}
@@ -13,6 +13,8 @@ import org.springframework.security.config.annotation.web.configurers.HeadersCon
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
@@ -72,6 +74,11 @@ public class SecurityConfig {
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** I6API 最小安全头;HSTS 由边缘 HTTPS 终止(Nginx/Caddy)配置。 */
private void apiHeaders(HeadersConfigurer<HttpSecurity> headers) {
headers
@@ -175,7 +175,7 @@ const breadcrumb = computed(() => {
function onGlobalSearch() { ElMessage.info("全局搜索(开发中)") }
function onNotifications() { ElMessage.info("通知中心(开发中)") }
function onUserMenu() { ElMessage.info("用户设置(开发中)") }
function onUserMenu() { router.push('/profile') }
function onLogout() { auth.logout(); router.push({ name: "login" }); }
</script>
@@ -158,6 +158,12 @@ const routes = [
component: () => import("../views/CallbackStatsView.vue"),
meta: { roles: ["SYS_ADMIN", "OPS"], title: "Callback 统计" },
},
{
path: "profile",
name: "profile",
component: () => import("../views/ProfileView.vue"),
meta: { roles: ["SYS_ADMIN", "DEVELOPER", "OPS"], title: "个人设置" },
},
{
path: "reports/project-health",
name: "project-health",
@@ -0,0 +1,66 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { apiErrorMessage } from '../utils/apiErrorMessage'
const auth = useAuthStore()
const router = useRouter()
const dialogVisible = ref(false)
const form = ref({ oldPassword: '', newPassword: '', confirmPassword: '' })
async function handleChangePassword() {
if (form.value.newPassword !== form.value.confirmPassword) {
ElMessage.error('两次密码不一致')
return
}
if (form.value.newPassword.length < 6) {
ElMessage.error('新密码至少6位')
return
}
try {
await axios.post('/api/v1/auth/change-password', {
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
})
ElMessage.success('密码修改成功')
dialogVisible.value = false
form.value = { oldPassword: '', newPassword: '', confirmPassword: '' }
} catch (e) {
ElMessage.error(apiErrorMessage(e, '密码修改失败'))
}
}
</script>
<template>
<div>
<h2>个人设置</h2>
<el-card shadow="never" style="margin-top:16px;max-width:500px">
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">{{ auth.displayName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ auth.roles.join(', ') }}</el-descriptions-item>
</el-descriptions>
<el-button type="primary" style="margin-top:16px" @click="dialogVisible = true">修改密码</el-button>
</el-card>
<el-dialog v-model="dialogVisible" title="修改密码" width="420px">
<el-form label-width="100px">
<el-form-item label="旧密码" required>
<el-input v-model="form.oldPassword" type="password" show-password />
</el-form-item>
<el-form-item label="新密码" required>
<el-input v-model="form.newPassword" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码" required>
<el-input v-model="form.confirmPassword" type="password" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleChangePassword">确认修改</el-button>
</template>
</el-dialog>
</div>
</template>