fix: 修复强制重置密码流程与表单状态
- 修正强制改密跳转及免旧密码的服务端校验 - 统一双端表单错误布局与更新按钮加载状态 - 补充认证服务和强制改密路由测试
This commit is contained in:
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
||||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||||
|
import tech.easyflow.auth.service.AuthService;
|
||||||
import tech.easyflow.common.constant.enums.EnumAccountType;
|
import tech.easyflow.common.constant.enums.EnumAccountType;
|
||||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
@@ -30,6 +31,7 @@ import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
|||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@@ -48,6 +50,8 @@ import java.util.Map;
|
|||||||
@RequestMapping("/api/v1/sysAccount")
|
@RequestMapping("/api/v1/sysAccount")
|
||||||
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
||||||
private final AuthCredentialKeyService credentialKeyService;
|
private final AuthCredentialKeyService credentialKeyService;
|
||||||
|
@Resource
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
public SysAccountController(SysAccountService service, AuthCredentialKeyService credentialKeyService) {
|
public SysAccountController(SysAccountService service, AuthCredentialKeyService credentialKeyService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -150,7 +154,8 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
/**
|
/**
|
||||||
* 修改密码,用于修改用户自己的密码
|
* 修改密码,用于修改用户自己的密码
|
||||||
*
|
*
|
||||||
* @param encryptedCredential 加密后的旧密码、新密码与确认密码
|
* @param encryptedCredential 加密后的当前密码、新密码与确认密码
|
||||||
|
* @return 密码修改结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/updatePassword")
|
@PostMapping("/updatePassword")
|
||||||
public Result<Void> updatePassword(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
public Result<Void> updatePassword(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
||||||
@@ -159,25 +164,13 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
String newPassword = payload.getString("newPassword");
|
String newPassword = payload.getString("newPassword");
|
||||||
String confirmPassword = payload.getString("confirmPassword");
|
String confirmPassword = payload.getString("confirmPassword");
|
||||||
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
|
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
|
||||||
SysAccount record = service.getById(loginAccountId);
|
authService.updateOwnPassword(
|
||||||
if (record == null) {
|
loginAccountId,
|
||||||
return Result.fail("修改失败");
|
password,
|
||||||
}
|
newPassword,
|
||||||
String pwdDb = record.getPassword();
|
confirmPassword,
|
||||||
if (!BCrypt.checkpw(password, pwdDb)) {
|
StpUtil.getLoginDevice()
|
||||||
return Result.fail(1, "密码不正确");
|
);
|
||||||
}
|
|
||||||
if (!newPassword.equals(confirmPassword)) {
|
|
||||||
return Result.fail(2, "两次密码不一致");
|
|
||||||
}
|
|
||||||
SysPasswordPolicy.validateStrongPassword(newPassword);
|
|
||||||
SysAccount update = new SysAccount();
|
|
||||||
update.setId(loginAccountId);
|
|
||||||
update.setPassword(BCrypt.hashpw(newPassword));
|
|
||||||
update.setPasswordResetRequired(false);
|
|
||||||
update.setModified(new Date());
|
|
||||||
update.setModifiedBy(loginAccountId);
|
|
||||||
service.updateById(update);
|
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package tech.easyflow.usercenter.controller.system;
|
package tech.easyflow.usercenter.controller.system;
|
||||||
|
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -8,13 +8,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
import tech.easyflow.auth.entity.EncryptedCredentialDTO;
|
||||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||||
|
import tech.easyflow.auth.service.AuthService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -34,6 +34,8 @@ public class UcSysAccountController {
|
|||||||
private SysAccountService service;
|
private SysAccountService service;
|
||||||
@Resource
|
@Resource
|
||||||
private AuthCredentialKeyService credentialKeyService;
|
private AuthCredentialKeyService credentialKeyService;
|
||||||
|
@Resource
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户的信息
|
* 获取用户的信息
|
||||||
@@ -66,7 +68,8 @@ public class UcSysAccountController {
|
|||||||
/**
|
/**
|
||||||
* 修改密码
|
* 修改密码
|
||||||
*
|
*
|
||||||
* @param encryptedCredential 加密后的旧密码、新密码与确认密码
|
* @param encryptedCredential 加密后的当前密码、新密码与确认密码
|
||||||
|
* @return 密码修改结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/updatePassword")
|
@PostMapping("/updatePassword")
|
||||||
public Result<Void> updatePassword(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
public Result<Void> updatePassword(@JsonBody EncryptedCredentialDTO encryptedCredential) {
|
||||||
@@ -75,25 +78,13 @@ public class UcSysAccountController {
|
|||||||
String newPassword = payload.getString("newPassword");
|
String newPassword = payload.getString("newPassword");
|
||||||
String confirmPassword = payload.getString("confirmPassword");
|
String confirmPassword = payload.getString("confirmPassword");
|
||||||
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
|
BigInteger loginAccountId = SaTokenUtil.getLoginAccount().getId();
|
||||||
SysAccount record = service.getById(loginAccountId);
|
authService.updateOwnPassword(
|
||||||
if (record == null) {
|
loginAccountId,
|
||||||
return Result.fail("修改失败");
|
password,
|
||||||
}
|
newPassword,
|
||||||
String pwdDb = record.getPassword();
|
confirmPassword,
|
||||||
if (!BCrypt.checkpw(password, pwdDb)) {
|
StpUtil.getLoginDevice()
|
||||||
return Result.fail(1, "密码不正确");
|
);
|
||||||
}
|
|
||||||
if (!newPassword.equals(confirmPassword)) {
|
|
||||||
return Result.fail(2, "两次密码不一致");
|
|
||||||
}
|
|
||||||
SysPasswordPolicy.validateStrongPassword(newPassword);
|
|
||||||
SysAccount update = new SysAccount();
|
|
||||||
update.setId(loginAccountId);
|
|
||||||
update.setPassword(BCrypt.hashpw(newPassword));
|
|
||||||
update.setPasswordResetRequired(false);
|
|
||||||
update.setModified(new Date());
|
|
||||||
update.setModifiedBy(loginAccountId);
|
|
||||||
service.updateById(update);
|
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,5 +38,11 @@
|
|||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.12.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import tech.easyflow.auth.entity.LoginVO;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号认证与登录服务。
|
||||||
|
*/
|
||||||
public interface AuthService {
|
public interface AuthService {
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
@@ -25,4 +28,21 @@ public interface AuthService {
|
|||||||
* 通过账号ID登录
|
* 通过账号ID登录
|
||||||
*/
|
*/
|
||||||
LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds);
|
LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改当前账号密码。
|
||||||
|
*
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @param currentPassword 当前密码;强制重置密码时可为空
|
||||||
|
* @param newPassword 新密码
|
||||||
|
* @param confirmPassword 确认密码
|
||||||
|
* @param loginDevice 当前登录会话设备类型
|
||||||
|
*/
|
||||||
|
void updateOwnPassword(
|
||||||
|
BigInteger accountId,
|
||||||
|
String currentPassword,
|
||||||
|
String newPassword,
|
||||||
|
String confirmPassword,
|
||||||
|
String loginDevice
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.SaLoginModel;
|
|||||||
import cn.dev33.satoken.stp.StpInterface;
|
import cn.dev33.satoken.stp.StpInterface;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import com.mybatisflex.core.tenant.TenantManager;
|
import com.mybatisflex.core.tenant.TenantManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -23,15 +24,19 @@ import tech.easyflow.system.service.SysApiKeyService;
|
|||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.service.SysMenuService;
|
import tech.easyflow.system.service.SysMenuService;
|
||||||
import tech.easyflow.system.service.SysRoleService;
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号认证、会话创建与凭证更新服务。
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AuthServiceImpl implements AuthService, StpInterface {
|
public class AuthServiceImpl implements AuthService, StpInterface {
|
||||||
|
|
||||||
@@ -100,6 +105,56 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
|||||||
return createApiKeyLoginVO(record, timeoutSeconds);
|
return createApiKeyLoginVO(record, timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改当前账号密码,并按数据库中的强制重置状态决定是否校验当前密码。
|
||||||
|
*
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @param currentPassword 当前密码;强制重置密码时可为空
|
||||||
|
* @param newPassword 新密码
|
||||||
|
* @param confirmPassword 确认密码
|
||||||
|
* @param loginDevice 当前登录会话设备类型
|
||||||
|
* @throws BusinessException 账号不存在、凭证不正确、会话来源不可信或密码更新失败时抛出
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateOwnPassword(
|
||||||
|
BigInteger accountId,
|
||||||
|
String currentPassword,
|
||||||
|
String newPassword,
|
||||||
|
String confirmPassword,
|
||||||
|
String loginDevice
|
||||||
|
) {
|
||||||
|
SysAccount record = sysAccountService.getById(accountId);
|
||||||
|
if (record == null) {
|
||||||
|
throw new BusinessException(404, 1, "账号不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean passwordResetRequired = Boolean.TRUE.equals(record.getPasswordResetRequired());
|
||||||
|
if (passwordResetRequired) {
|
||||||
|
if (!AuthLoginSessionPolicy.WEB_DEVICE.equals(loginDevice)) {
|
||||||
|
throw new BusinessException(403, 3, "请使用账号密码登录后重置密码");
|
||||||
|
}
|
||||||
|
} else if (currentPassword == null
|
||||||
|
|| record.getPassword() == null
|
||||||
|
|| !BCrypt.checkpw(currentPassword, record.getPassword())) {
|
||||||
|
throw new BusinessException(400, 1, "密码不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.equals(newPassword, confirmPassword)) {
|
||||||
|
throw new BusinessException(400, 2, "两次密码不一致");
|
||||||
|
}
|
||||||
|
SysPasswordPolicy.validateStrongPassword(newPassword);
|
||||||
|
|
||||||
|
SysAccount update = new SysAccount();
|
||||||
|
update.setId(accountId);
|
||||||
|
update.setPassword(BCrypt.hashpw(newPassword));
|
||||||
|
update.setPasswordResetRequired(false);
|
||||||
|
update.setModified(new Date());
|
||||||
|
update.setModifiedBy(accountId);
|
||||||
|
if (!sysAccountService.updateById(update)) {
|
||||||
|
throw new BusinessException(500, 4, "密码更新失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package tech.easyflow.auth.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AuthServiceImpl} 密码修改规则测试。
|
||||||
|
*/
|
||||||
|
public class AuthServiceImplPasswordTest {
|
||||||
|
|
||||||
|
private static final BigInteger ACCOUNT_ID = BigInteger.valueOf(10);
|
||||||
|
private static final String CURRENT_PASSWORD = "Current!123";
|
||||||
|
private static final String NEW_PASSWORD = "Changed!456";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证强制重置状态下,Web 会话无需提交当前密码即可更新。
|
||||||
|
*
|
||||||
|
* @throws Exception 注入测试依赖失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldAllowForcedResetWithoutCurrentPasswordForWebSession() throws Exception {
|
||||||
|
SysAccountService accountService = mock(SysAccountService.class);
|
||||||
|
AuthServiceImpl authService = createService(accountService);
|
||||||
|
SysAccount account = buildAccount(true);
|
||||||
|
when(accountService.getById(ACCOUNT_ID)).thenReturn(account);
|
||||||
|
when(accountService.updateById(any(SysAccount.class))).thenReturn(true);
|
||||||
|
|
||||||
|
authService.updateOwnPassword(
|
||||||
|
ACCOUNT_ID,
|
||||||
|
null,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
AuthLoginSessionPolicy.WEB_DEVICE
|
||||||
|
);
|
||||||
|
|
||||||
|
ArgumentCaptor<SysAccount> updateCaptor = ArgumentCaptor.forClass(SysAccount.class);
|
||||||
|
verify(accountService).updateById(updateCaptor.capture());
|
||||||
|
SysAccount update = updateCaptor.getValue();
|
||||||
|
assertTrue(BCrypt.checkpw(NEW_PASSWORD, update.getPassword()));
|
||||||
|
assertFalse(update.getPasswordResetRequired());
|
||||||
|
assertEquals(ACCOUNT_ID, update.getModifiedBy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证普通改密仍需校验当前密码。
|
||||||
|
*
|
||||||
|
* @throws Exception 注入测试依赖失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRequireCurrentPasswordForRegularChange() throws Exception {
|
||||||
|
SysAccountService accountService = mock(SysAccountService.class);
|
||||||
|
AuthServiceImpl authService = createService(accountService);
|
||||||
|
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(false));
|
||||||
|
|
||||||
|
BusinessException exception = expectBusinessException(() -> authService.updateOwnPassword(
|
||||||
|
ACCOUNT_ID,
|
||||||
|
"Wrong!123",
|
||||||
|
NEW_PASSWORD,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
AuthLoginSessionPolicy.WEB_DEVICE
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(400, exception.getHttpStatus());
|
||||||
|
assertEquals("密码不正确", exception.getMessage());
|
||||||
|
verify(accountService, never()).updateById(any(SysAccount.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 API Key 会话不能使用免当前密码的强制重置流程。
|
||||||
|
*
|
||||||
|
* @throws Exception 注入测试依赖失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRejectApiKeyForcedReset() throws Exception {
|
||||||
|
SysAccountService accountService = mock(SysAccountService.class);
|
||||||
|
AuthServiceImpl authService = createService(accountService);
|
||||||
|
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(true));
|
||||||
|
|
||||||
|
BusinessException exception = expectBusinessException(() -> authService.updateOwnPassword(
|
||||||
|
ACCOUNT_ID,
|
||||||
|
null,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
AuthLoginSessionPolicy.API_KEY_DEVICE
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(403, exception.getHttpStatus());
|
||||||
|
assertEquals("请使用账号密码登录后重置密码", exception.getMessage());
|
||||||
|
verify(accountService, never()).updateById(any(SysAccount.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证普通改密在当前密码正确时仍可成功更新。
|
||||||
|
*
|
||||||
|
* @throws Exception 注入测试依赖失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateRegularPasswordWithCorrectCurrentPassword() throws Exception {
|
||||||
|
SysAccountService accountService = mock(SysAccountService.class);
|
||||||
|
AuthServiceImpl authService = createService(accountService);
|
||||||
|
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(false));
|
||||||
|
when(accountService.updateById(any(SysAccount.class))).thenReturn(true);
|
||||||
|
|
||||||
|
authService.updateOwnPassword(
|
||||||
|
ACCOUNT_ID,
|
||||||
|
CURRENT_PASSWORD,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
NEW_PASSWORD,
|
||||||
|
AuthLoginSessionPolicy.WEB_DEVICE
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(accountService).updateById(any(SysAccount.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造测试账号。
|
||||||
|
*
|
||||||
|
* @param passwordResetRequired 是否要求重置密码
|
||||||
|
* @return 测试账号
|
||||||
|
*/
|
||||||
|
private SysAccount buildAccount(boolean passwordResetRequired) {
|
||||||
|
SysAccount account = new SysAccount();
|
||||||
|
account.setId(ACCOUNT_ID);
|
||||||
|
account.setPassword(BCrypt.hashpw(CURRENT_PASSWORD));
|
||||||
|
account.setPasswordResetRequired(passwordResetRequired);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并注入账号服务。
|
||||||
|
*
|
||||||
|
* @param accountService 账号服务
|
||||||
|
* @return 认证服务
|
||||||
|
* @throws Exception 注入测试依赖失败
|
||||||
|
*/
|
||||||
|
private AuthServiceImpl createService(SysAccountService accountService) throws Exception {
|
||||||
|
AuthServiceImpl authService = new AuthServiceImpl();
|
||||||
|
Field field = AuthServiceImpl.class.getDeclaredField("sysAccountService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(authService, accountService);
|
||||||
|
return authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行操作并返回业务异常。
|
||||||
|
*
|
||||||
|
* @param action 待执行操作
|
||||||
|
* @return 捕获的业务异常
|
||||||
|
*/
|
||||||
|
private BusinessException expectBusinessException(Runnable action) {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
fail("预期抛出 BusinessException");
|
||||||
|
return null;
|
||||||
|
} catch (BusinessException exception) {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"repeatPwd": "Please confirm your password again",
|
"repeatPwd": "Please confirm your password again",
|
||||||
"notSamePwd": "The two passwords are inconsistent",
|
"notSamePwd": "The two passwords are inconsistent",
|
||||||
"passwordStrongTip": "Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters",
|
"passwordStrongTip": "Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters",
|
||||||
|
"passwordStrengthInvalid": "Password strength requirements are not met",
|
||||||
"forceChangePasswordNavigateTip": "For account security, please change your password before visiting other pages.",
|
"forceChangePasswordNavigateTip": "For account security, please change your password before visiting other pages.",
|
||||||
"resetPassword": "Reset Password",
|
"resetPassword": "Reset Password",
|
||||||
"resetPasswordConfirm": "Reset this account password to the system default strong password? The user will be required to change it on next login.",
|
"resetPasswordConfirm": "Reset this account password to the system default strong password? The user will be required to change it on next login.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"repeatPwd": "请再次输入密码",
|
"repeatPwd": "请再次输入密码",
|
||||||
"notSamePwd": "两次输入的密码不一致",
|
"notSamePwd": "两次输入的密码不一致",
|
||||||
"passwordStrongTip": "密码必须至少 8 位,且包含大写字母、小写字母、数字和特殊字符",
|
"passwordStrongTip": "密码必须至少 8 位,且包含大写字母、小写字母、数字和特殊字符",
|
||||||
|
"passwordStrengthInvalid": "密码强度不足",
|
||||||
"forceChangePasswordNavigateTip": "为了账户安全,请先修改密码后再继续访问其他页面",
|
"forceChangePasswordNavigateTip": "为了账户安全,请先修改密码后再继续访问其他页面",
|
||||||
"resetPassword": "重置密码",
|
"resetPassword": "重置密码",
|
||||||
"resetPasswordConfirm": "确认将该用户密码重置为系统默认强密码吗?重置后用户下次登录必须先修改密码。",
|
"resetPasswordConfirm": "确认将该用户密码重置为系统默认强密码吗?重置后用户下次登录必须先修改密码。",
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildForcePasswordRoute,
|
||||||
|
isForcePasswordRoute,
|
||||||
|
isPasswordResetRequired,
|
||||||
|
resolveForcePasswordPath,
|
||||||
|
} from '#/utils/password-reset';
|
||||||
|
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('#/locales', () => ({
|
||||||
|
$t: (key: string) => key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createTestRouter() {
|
||||||
|
return createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'FallbackNotFound',
|
||||||
|
path: '/:path(.*)*',
|
||||||
|
component: { template: '<div />' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('password reset route', () => {
|
||||||
|
it('can resolve the forced password path before Profile is registered', () => {
|
||||||
|
const router = createTestRouter();
|
||||||
|
const route = router.resolve(buildForcePasswordRoute());
|
||||||
|
|
||||||
|
expect(route.fullPath).toBe('/profile?force=1&tab=password');
|
||||||
|
expect(isForcePasswordRoute(route)).toBe(true);
|
||||||
|
expect(resolveForcePasswordPath(router)).toBe(
|
||||||
|
'/profile?force=1&tab=password',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches Profile after the dynamic route is registered', () => {
|
||||||
|
const router = createTestRouter();
|
||||||
|
const targetPath = router.resolve(buildForcePasswordRoute()).fullPath;
|
||||||
|
|
||||||
|
router.addRoute({
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: { template: '<div />' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(router.resolve(targetPath).name).toBe('Profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the account state as the authoritative password reset signal', () => {
|
||||||
|
expect(isPasswordResetRequired({ passwordResetRequired: true })).toBe(true);
|
||||||
|
expect(isPasswordResetRequired({ passwordResetRequired: false })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isPasswordResetRequired()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,12 +5,13 @@ import { ElMessage } from 'element-plus';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const FORCE_PASSWORD_NOTICE_INTERVAL = 1500;
|
const FORCE_PASSWORD_NOTICE_INTERVAL = 1500;
|
||||||
|
const PROFILE_PATH = '/profile';
|
||||||
|
|
||||||
let lastForcePasswordNoticeAt = 0;
|
let lastForcePasswordNoticeAt = 0;
|
||||||
|
|
||||||
export function buildForcePasswordRoute() {
|
export function buildForcePasswordRoute() {
|
||||||
return {
|
return {
|
||||||
name: 'Profile',
|
path: PROFILE_PATH,
|
||||||
query: {
|
query: {
|
||||||
force: '1',
|
force: '1',
|
||||||
tab: 'password',
|
tab: 'password',
|
||||||
@@ -19,19 +20,20 @@ export function buildForcePasswordRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isForcePasswordRoute(
|
export function isForcePasswordRoute(
|
||||||
route: Pick<
|
route: Pick<{ path?: string; query?: Record<string, any> }, 'path' | 'query'>,
|
||||||
{ name?: null | string | symbol; query?: Record<string, any> },
|
|
||||||
'name' | 'query'
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
return route.name === 'Profile' && route.query?.tab === 'password';
|
return route.path === PROFILE_PATH && route.query?.tab === 'password';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldForcePasswordChange(
|
export function shouldForcePasswordChange(
|
||||||
userInfo?: null | Record<string, any>,
|
userInfo?: null | Record<string, any>,
|
||||||
forceChangePassword?: boolean,
|
forceChangePassword?: boolean,
|
||||||
) {
|
) {
|
||||||
return !!forceChangePassword || !!userInfo?.passwordResetRequired;
|
return !!forceChangePassword || isPasswordResetRequired(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPasswordResetRequired(userInfo?: null | Record<string, any>) {
|
||||||
|
return !!userInfo?.passwordResetRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveForcePasswordPath(router: Router) {
|
export function resolveForcePasswordPath(router: Router) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { EasyFlowFormSchema } from '#/adapter/form';
|
import type { EasyFlowFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import {useRoute, useRouter} from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { ProfilePasswordSetting, z } from '@easyflow/common-ui';
|
import { ProfilePasswordSetting, z } from '@easyflow/common-ui';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
@@ -16,29 +16,19 @@ import {$t} from '#/locales';
|
|||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
import { encryptCredentialPayload } from '#/utils/credential-encryption';
|
import { encryptCredentialPayload } from '#/utils/credential-encryption';
|
||||||
import { isStrongPassword } from '#/utils/password-policy';
|
import { isStrongPassword } from '#/utils/password-policy';
|
||||||
|
import { isPasswordResetRequired } from '#/utils/password-reset';
|
||||||
|
|
||||||
const profilePasswordSettingRef = ref();
|
const profilePasswordSettingRef = ref();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const isForcedPasswordChange = computed(() => {
|
const isForcedPasswordChange = computed(() => {
|
||||||
return (
|
return isPasswordResetRequired(userStore.userInfo);
|
||||||
!!userStore.userInfo?.passwordResetRequired || route.query.force === '1'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||||
return [
|
const schema: EasyFlowFormSchema[] = [
|
||||||
{
|
|
||||||
fieldName: 'password',
|
|
||||||
label: $t('sysAccount.oldPwd'),
|
|
||||||
component: 'EasyFlowInputPassword',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldName: 'newPassword',
|
fieldName: 'newPassword',
|
||||||
label: $t('sysAccount.newPwd'),
|
label: $t('sysAccount.newPwd'),
|
||||||
@@ -58,7 +48,7 @@ const formSchema = computed((): EasyFlowFormSchema[] => {
|
|||||||
})
|
})
|
||||||
.min(1, { message: $t('sysAccount.newPwd') + $t('common.isRequired') })
|
.min(1, { message: $t('sysAccount.newPwd') + $t('common.isRequired') })
|
||||||
.refine((value) => isStrongPassword(value), {
|
.refine((value) => isStrongPassword(value), {
|
||||||
message: $t('sysAccount.passwordStrongTip'),
|
message: $t('sysAccount.passwordStrengthInvalid'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,10 +73,29 @@ const formSchema = computed((): EasyFlowFormSchema[] => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isForcedPasswordChange.value) {
|
||||||
|
schema.unshift({
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('sysAccount.oldPwd'),
|
||||||
|
component: 'EasyFlowInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
||||||
|
},
|
||||||
|
rules: z
|
||||||
|
.string({
|
||||||
|
required_error: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
||||||
|
})
|
||||||
|
.min(1, { message: $t('sysAccount.oldPwd') + $t('common.isRequired') }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLoading = ref(false);
|
const updateLoading = ref(false);
|
||||||
async function handleSubmit(values: any) {
|
async function handleSubmit(values: any) {
|
||||||
|
const wasForcedPasswordChange = isForcedPasswordChange.value;
|
||||||
updateLoading.value = true;
|
updateLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const encryptedPayload = await encryptCredentialPayload(
|
const encryptedPayload = await encryptCredentialPayload(
|
||||||
@@ -100,7 +109,7 @@ async function handleSubmit(values: any) {
|
|||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('message.success'));
|
ElMessage.success($t('message.success'));
|
||||||
const userInfo = await authStore.fetchUserInfo();
|
const userInfo = await authStore.fetchUserInfo();
|
||||||
if (isForcedPasswordChange.value) {
|
if (wasForcedPasswordChange) {
|
||||||
await router.replace(
|
await router.replace(
|
||||||
userInfo?.homePath || preferences.app.defaultHomePath || '/',
|
userInfo?.homePath || preferences.app.defaultHomePath || '/',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition name="slide-up" v-if="!compact">
|
<Transition name="slide-up" v-if="!compact">
|
||||||
<FormMessage class="absolute" />
|
<FormMessage />
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ defineExpose({
|
|||||||
<EasyFlowButton
|
<EasyFlowButton
|
||||||
:loading="buttonLoading"
|
:loading="buttonLoading"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="mx-auto mt-4 block h-8 w-[106px] p-0"
|
class="mx-auto mt-4 flex h-8 w-28 p-0"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"repeatPwd": "Please confirm your password again",
|
"repeatPwd": "Please confirm your password again",
|
||||||
"notSamePwd": "The two passwords are inconsistent",
|
"notSamePwd": "The two passwords are inconsistent",
|
||||||
"passwordStrongTip": "Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters",
|
"passwordStrongTip": "Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters",
|
||||||
|
"passwordStrengthInvalid": "Password strength requirements are not met",
|
||||||
"forceChangePasswordNavigateTip": "For account security, please change your password before visiting other pages."
|
"forceChangePasswordNavigateTip": "For account security, please change your password before visiting other pages."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"repeatPwd": "请再次输入密码",
|
"repeatPwd": "请再次输入密码",
|
||||||
"notSamePwd": "两次输入的密码不一致",
|
"notSamePwd": "两次输入的密码不一致",
|
||||||
"passwordStrongTip": "密码必须至少 8 位,且包含大写字母、小写字母、数字和特殊字符",
|
"passwordStrongTip": "密码必须至少 8 位,且包含大写字母、小写字母、数字和特殊字符",
|
||||||
|
"passwordStrengthInvalid": "密码强度不足",
|
||||||
"forceChangePasswordNavigateTip": "为了账户安全,请先修改密码后再继续访问其他页面"
|
"forceChangePasswordNavigateTip": "为了账户安全,请先修改密码后再继续访问其他页面"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildForcePasswordRoute,
|
||||||
|
isForcePasswordRoute,
|
||||||
|
isPasswordResetRequired,
|
||||||
|
resolveForcePasswordPath,
|
||||||
|
} from '#/utils/password-reset';
|
||||||
|
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElMessage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('#/locales', () => ({
|
||||||
|
$t: (key: string) => key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createTestRouter() {
|
||||||
|
return createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'FallbackNotFound',
|
||||||
|
path: '/:path(.*)*',
|
||||||
|
component: { template: '<div />' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('password reset route', () => {
|
||||||
|
it('can resolve the forced password path before Profile is registered', () => {
|
||||||
|
const router = createTestRouter();
|
||||||
|
const route = router.resolve(buildForcePasswordRoute());
|
||||||
|
|
||||||
|
expect(route.fullPath).toBe('/profile?force=1&tab=password');
|
||||||
|
expect(isForcePasswordRoute(route)).toBe(true);
|
||||||
|
expect(resolveForcePasswordPath(router)).toBe(
|
||||||
|
'/profile?force=1&tab=password',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches Profile after the dynamic route is registered', () => {
|
||||||
|
const router = createTestRouter();
|
||||||
|
const targetPath = router.resolve(buildForcePasswordRoute()).fullPath;
|
||||||
|
|
||||||
|
router.addRoute({
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: { template: '<div />' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(router.resolve(targetPath).name).toBe('Profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the account state as the authoritative password reset signal', () => {
|
||||||
|
expect(isPasswordResetRequired({ passwordResetRequired: true })).toBe(true);
|
||||||
|
expect(isPasswordResetRequired({ passwordResetRequired: false })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isPasswordResetRequired()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,12 +5,13 @@ import { ElMessage } from 'element-plus';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const FORCE_PASSWORD_NOTICE_INTERVAL = 1500;
|
const FORCE_PASSWORD_NOTICE_INTERVAL = 1500;
|
||||||
|
const PROFILE_PATH = '/profile';
|
||||||
|
|
||||||
let lastForcePasswordNoticeAt = 0;
|
let lastForcePasswordNoticeAt = 0;
|
||||||
|
|
||||||
export function buildForcePasswordRoute() {
|
export function buildForcePasswordRoute() {
|
||||||
return {
|
return {
|
||||||
name: 'Profile',
|
path: PROFILE_PATH,
|
||||||
query: {
|
query: {
|
||||||
force: '1',
|
force: '1',
|
||||||
tab: 'password',
|
tab: 'password',
|
||||||
@@ -19,16 +20,20 @@ export function buildForcePasswordRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isForcePasswordRoute(
|
export function isForcePasswordRoute(
|
||||||
route: Pick<{ name?: string | symbol | null; query?: Record<string, any> }, 'name' | 'query'>,
|
route: Pick<{ path?: string; query?: Record<string, any> }, 'path' | 'query'>,
|
||||||
) {
|
) {
|
||||||
return route.name === 'Profile' && route.query?.tab === 'password';
|
return route.path === PROFILE_PATH && route.query?.tab === 'password';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldForcePasswordChange(
|
export function shouldForcePasswordChange(
|
||||||
userInfo?: null | Record<string, any>,
|
userInfo?: null | Record<string, any>,
|
||||||
forceChangePassword?: boolean,
|
forceChangePassword?: boolean,
|
||||||
) {
|
) {
|
||||||
return !!forceChangePassword || !!userInfo?.passwordResetRequired;
|
return !!forceChangePassword || isPasswordResetRequired(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPasswordResetRequired(userInfo?: null | Record<string, any>) {
|
||||||
|
return !!userInfo?.passwordResetRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveForcePasswordPath(router: Router) {
|
export function resolveForcePasswordPath(router: Router) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { EasyFlowFormSchema } from '#/adapter/form';
|
import type { EasyFlowFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import {useRoute, useRouter} from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { ProfilePasswordSetting, z } from '@easyflow/common-ui';
|
import { ProfilePasswordSetting, z } from '@easyflow/common-ui';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
@@ -16,27 +16,19 @@ import {$t} from '#/locales';
|
|||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
import { encryptCredentialPayload } from '#/utils/credential-encryption';
|
import { encryptCredentialPayload } from '#/utils/credential-encryption';
|
||||||
import { isStrongPassword } from '#/utils/password-policy';
|
import { isStrongPassword } from '#/utils/password-policy';
|
||||||
|
import { isPasswordResetRequired } from '#/utils/password-reset';
|
||||||
|
|
||||||
const profilePasswordSettingRef = ref();
|
const profilePasswordSettingRef = ref();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const isForcedPasswordChange = computed(() => {
|
const isForcedPasswordChange = computed(() => {
|
||||||
return !!userStore.userInfo?.passwordResetRequired || route.query.force === '1';
|
return isPasswordResetRequired(userStore.userInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formSchema = computed((): EasyFlowFormSchema[] => {
|
const formSchema = computed((): EasyFlowFormSchema[] => {
|
||||||
return [
|
const schema: EasyFlowFormSchema[] = [
|
||||||
{
|
|
||||||
fieldName: 'password',
|
|
||||||
label: $t('sysAccount.oldPwd'),
|
|
||||||
component: 'EasyFlowInputPassword',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldName: 'newPassword',
|
fieldName: 'newPassword',
|
||||||
label: $t('sysAccount.newPwd'),
|
label: $t('sysAccount.newPwd'),
|
||||||
@@ -51,10 +43,12 @@ const formSchema = computed((): EasyFlowFormSchema[] => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
rules: z
|
rules: z
|
||||||
.string({ required_error: $t('sysAccount.newPwd') + $t('common.isRequired') })
|
.string({
|
||||||
|
required_error: $t('sysAccount.newPwd') + $t('common.isRequired'),
|
||||||
|
})
|
||||||
.min(1, { message: $t('sysAccount.newPwd') + $t('common.isRequired') })
|
.min(1, { message: $t('sysAccount.newPwd') + $t('common.isRequired') })
|
||||||
.refine((value) => isStrongPassword(value), {
|
.refine((value) => isStrongPassword(value), {
|
||||||
message: $t('sysAccount.passwordStrongTip'),
|
message: $t('sysAccount.passwordStrengthInvalid'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,10 +73,29 @@ const formSchema = computed((): EasyFlowFormSchema[] => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!isForcedPasswordChange.value) {
|
||||||
|
schema.unshift({
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('sysAccount.oldPwd'),
|
||||||
|
component: 'EasyFlowInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
||||||
|
},
|
||||||
|
rules: z
|
||||||
|
.string({
|
||||||
|
required_error: $t('sysAccount.oldPwd') + $t('common.isRequired'),
|
||||||
|
})
|
||||||
|
.min(1, { message: $t('sysAccount.oldPwd') + $t('common.isRequired') }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLoading = ref(false);
|
const updateLoading = ref(false);
|
||||||
async function handleSubmit(values: any) {
|
async function handleSubmit(values: any) {
|
||||||
|
const wasForcedPasswordChange = isForcedPasswordChange.value;
|
||||||
updateLoading.value = true;
|
updateLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const encryptedPayload = await encryptCredentialPayload(
|
const encryptedPayload = await encryptCredentialPayload(
|
||||||
@@ -96,7 +109,7 @@ async function handleSubmit(values: any) {
|
|||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('message.success'));
|
ElMessage.success($t('message.success'));
|
||||||
const userInfo = await authStore.fetchUserInfo();
|
const userInfo = await authStore.fetchUserInfo();
|
||||||
if (isForcedPasswordChange.value) {
|
if (wasForcedPasswordChange) {
|
||||||
await router.replace(
|
await router.replace(
|
||||||
userInfo?.homePath || preferences.app.defaultHomePath || '/',
|
userInfo?.homePath || preferences.app.defaultHomePath || '/',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
|
|||||||
|
|
||||||
import { CircleAlert } from '@easyflow-core/icons';
|
import { CircleAlert } from '@easyflow-core/icons';
|
||||||
import {
|
import {
|
||||||
|
EasyFlowRenderContent,
|
||||||
|
EasyFlowTooltip,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
EasyFlowRenderContent,
|
|
||||||
EasyFlowTooltip,
|
|
||||||
} from '@easyflow-core/shadcn-ui';
|
} from '@easyflow-core/shadcn-ui';
|
||||||
import { cn, isFunction, isObject, isString } from '@easyflow-core/shared/utils';
|
import {
|
||||||
|
cn,
|
||||||
|
isFunction,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
} from '@easyflow-core/shared/utils';
|
||||||
|
|
||||||
import { toTypedSchema } from '@vee-validate/zod';
|
import { toTypedSchema } from '@vee-validate/zod';
|
||||||
import { useFieldError, useFormValues } from 'vee-validate';
|
import { useFieldError, useFormValues } from 'vee-validate';
|
||||||
@@ -387,7 +392,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition name="slide-up" v-if="!compact">
|
<Transition name="slide-up" v-if="!compact">
|
||||||
<FormMessage class="absolute" />
|
<FormMessage />
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
formSchema?: EasyFlowFormSchema[];
|
formSchema?: EasyFlowFormSchema[];
|
||||||
|
buttonText?: string;
|
||||||
|
buttonLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
formSchema: () => [],
|
formSchema: () => [],
|
||||||
|
buttonText: '更新密码',
|
||||||
|
buttonLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -49,8 +53,13 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Form />
|
<Form />
|
||||||
<EasyFlowButton type="submit" class="mt-4" @click="handleSubmit">
|
<EasyFlowButton
|
||||||
更新密码
|
:loading="buttonLoading"
|
||||||
|
type="submit"
|
||||||
|
class="mt-4 flex h-8 w-28 p-0"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ buttonText }}
|
||||||
</EasyFlowButton>
|
</EasyFlowButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user