feat: 支持展示并复制重置后的密码
- 账号重置接口返回本次使用的默认强密码 - 管理端新增紧凑结果弹窗与复制反馈 - 补充服务测试和中英文文案
This commit is contained in:
@@ -203,11 +203,17 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
return value == null ? null : String.valueOf(value);
|
return value == null ? null : String.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定账号密码重置为系统默认强密码。
|
||||||
|
*
|
||||||
|
* @param id 账号 ID
|
||||||
|
* @return 本次重置后使用的明文密码
|
||||||
|
*/
|
||||||
@PostMapping("/resetPassword")
|
@PostMapping("/resetPassword")
|
||||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
public Result<Void> resetPassword(@JsonBody(value = "id", required = true) BigInteger id) {
|
public Result<String> resetPassword(@JsonBody(value = "id", required = true) BigInteger id) {
|
||||||
service.resetPassword(id, SaTokenUtil.getLoginAccount().getId());
|
String password = service.resetPassword(id, SaTokenUtil.getLoginAccount().getId());
|
||||||
return Result.ok();
|
return Result.ok(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/removeBatchWithResult")
|
@PostMapping("/removeBatchWithResult")
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ public interface SysAccountService extends IService<SysAccount> {
|
|||||||
|
|
||||||
SysAccount getByUsername(String userKey);
|
SysAccount getByUsername(String userKey);
|
||||||
|
|
||||||
void resetPassword(BigInteger accountId, BigInteger operatorId);
|
/**
|
||||||
|
* 将账号密码重置为系统默认强密码。
|
||||||
|
*
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @param operatorId 操作人账号 ID
|
||||||
|
* @return 本次重置后使用的明文密码
|
||||||
|
*/
|
||||||
|
String resetPassword(BigInteger accountId, BigInteger operatorId);
|
||||||
|
|
||||||
SysAccountBatchActionResultVo removeBatchWithResult(Collection<BigInteger> ids);
|
SysAccountBatchActionResultVo removeBatchWithResult(Collection<BigInteger> ids);
|
||||||
|
|
||||||
|
|||||||
@@ -231,21 +231,30 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
return getOne(w);
|
return getOne(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将账号密码重置为系统默认强密码。
|
||||||
|
*
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @param operatorId 操作人账号 ID
|
||||||
|
* @return 本次重置后使用的明文密码
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void resetPassword(BigInteger accountId, BigInteger operatorId) {
|
public String resetPassword(BigInteger accountId, BigInteger operatorId) {
|
||||||
SysAccount record = getById(accountId);
|
SysAccount record = getById(accountId);
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
throw new BusinessException("用户不存在");
|
throw new BusinessException("用户不存在");
|
||||||
}
|
}
|
||||||
validateResetPasswordAllowed(record);
|
validateResetPasswordAllowed(record);
|
||||||
|
String plainPassword = accountSecurityProperties.getDefaultResetPassword();
|
||||||
SysAccount update = new SysAccount();
|
SysAccount update = new SysAccount();
|
||||||
update.setId(accountId);
|
update.setId(accountId);
|
||||||
update.setPassword(BCrypt.hashpw(accountSecurityProperties.getDefaultResetPassword()));
|
update.setPassword(BCrypt.hashpw(plainPassword));
|
||||||
update.setPasswordResetRequired(true);
|
update.setPasswordResetRequired(true);
|
||||||
update.setModified(new Date());
|
update.setModified(new Date());
|
||||||
update.setModifiedBy(operatorId);
|
update.setModifiedBy(operatorId);
|
||||||
updateById(update);
|
updateById(update);
|
||||||
StpUtil.kickout(accountId);
|
StpUtil.kickout(accountId);
|
||||||
|
return plainPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
package tech.easyflow.system.service.impl;
|
package tech.easyflow.system.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import tech.easyflow.common.constant.enums.EnumAccountType;
|
||||||
|
import tech.easyflow.system.config.AccountSecurityProperties;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -51,4 +60,54 @@ public class SysAccountServiceImplTest {
|
|||||||
assertEquals("3", displayNameMap.get(BigInteger.valueOf(3)));
|
assertEquals("3", displayNameMap.get(BigInteger.valueOf(3)));
|
||||||
verify(service, times(1)).list(any(QueryWrapper.class));
|
verify(service, times(1)).list(any(QueryWrapper.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证重置密码返回实际明文,同时持久化对应密文并踢出旧登录态。
|
||||||
|
*
|
||||||
|
* @throws Exception 注入测试配置失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldReturnPlainPasswordAfterReset() throws Exception {
|
||||||
|
BigInteger accountId = BigInteger.valueOf(10);
|
||||||
|
BigInteger operatorId = BigInteger.valueOf(20);
|
||||||
|
String expectedPassword = "!Reset123";
|
||||||
|
SysAccountServiceImpl service = spy(new SysAccountServiceImpl());
|
||||||
|
AccountSecurityProperties properties = new AccountSecurityProperties();
|
||||||
|
properties.setDefaultResetPassword(expectedPassword);
|
||||||
|
properties.afterPropertiesSet();
|
||||||
|
setField(service, "accountSecurityProperties", properties);
|
||||||
|
|
||||||
|
SysAccount account = new SysAccount();
|
||||||
|
account.setId(accountId);
|
||||||
|
account.setAccountType(EnumAccountType.NORMAL.getCode());
|
||||||
|
doReturn(account).when(service).getById(accountId);
|
||||||
|
doReturn(true).when(service).updateById(any(SysAccount.class));
|
||||||
|
|
||||||
|
try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
|
||||||
|
String actualPassword = service.resetPassword(accountId, operatorId);
|
||||||
|
|
||||||
|
assertEquals(expectedPassword, actualPassword);
|
||||||
|
ArgumentCaptor<SysAccount> updateCaptor = ArgumentCaptor.forClass(SysAccount.class);
|
||||||
|
verify(service).updateById(updateCaptor.capture());
|
||||||
|
SysAccount update = updateCaptor.getValue();
|
||||||
|
assertTrue(BCrypt.checkpw(expectedPassword, update.getPassword()));
|
||||||
|
assertEquals(Boolean.TRUE, update.getPasswordResetRequired());
|
||||||
|
assertEquals(operatorId, update.getModifiedBy());
|
||||||
|
stp.verify(() -> StpUtil.kickout(accountId), times(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置测试对象的私有字段。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
* @throws Exception 字段不存在或不可访问
|
||||||
|
*/
|
||||||
|
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Field field = SysAccountServiceImpl.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
"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.",
|
||||||
"resetPasswordSuccess": "Password has been reset to the system default strong password and must be changed on next login",
|
"resetPasswordSuccess": "Password has been reset to the system default strong password and must be changed on next login",
|
||||||
|
"resetPasswordResultTitle": "Password Reset Successful",
|
||||||
|
"resetPasswordResultDescription": "Copy the password and share it securely. The user must change it at the next sign-in.",
|
||||||
|
"resetPasswordResultLabel": "New Password",
|
||||||
|
"resetPasswordResultMissing": "The password was reset, but the new password was not returned. Contact an administrator.",
|
||||||
|
"copyPassword": "Copy password",
|
||||||
"batchSelectedCount": "{count} selected",
|
"batchSelectedCount": "{count} selected",
|
||||||
"batchToolbarHint": "Batch actions are available for selected accounts",
|
"batchToolbarHint": "Batch actions are available for selected accounts",
|
||||||
"batchActionSelectRequired": "Please select at least one account",
|
"batchActionSelectRequired": "Please select at least one account",
|
||||||
|
|||||||
@@ -30,6 +30,11 @@
|
|||||||
"resetPassword": "重置密码",
|
"resetPassword": "重置密码",
|
||||||
"resetPasswordConfirm": "确认将该用户密码重置为系统默认强密码吗?重置后用户下次登录必须先修改密码。",
|
"resetPasswordConfirm": "确认将该用户密码重置为系统默认强密码吗?重置后用户下次登录必须先修改密码。",
|
||||||
"resetPasswordSuccess": "密码已重置为系统默认强密码,用户下次登录需修改密码",
|
"resetPasswordSuccess": "密码已重置为系统默认强密码,用户下次登录需修改密码",
|
||||||
|
"resetPasswordResultTitle": "密码重置成功",
|
||||||
|
"resetPasswordResultDescription": "请复制密码并安全地告知用户,用户下次登录时需修改密码。",
|
||||||
|
"resetPasswordResultLabel": "新密码",
|
||||||
|
"resetPasswordResultMissing": "密码已重置,但接口未返回新密码,请联系管理员",
|
||||||
|
"copyPassword": "复制密码",
|
||||||
"batchSelectedCount": "已选择 {count} 项",
|
"batchSelectedCount": "已选择 {count} 项",
|
||||||
"batchToolbarHint": "可对选中账号执行批量操作",
|
"batchToolbarHint": "可对选中账号执行批量操作",
|
||||||
"batchActionSelectRequired": "请先选择要操作的账号",
|
"batchActionSelectRequired": "请先选择要操作的账号",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useDictStore } from '#/store';
|
|||||||
|
|
||||||
import SysAccountImportModal from './SysAccountImportModal.vue';
|
import SysAccountImportModal from './SysAccountImportModal.vue';
|
||||||
import SysAccountModal from './SysAccountModal.vue';
|
import SysAccountModal from './SysAccountModal.vue';
|
||||||
|
import SysAccountPasswordResultModal from './SysAccountPasswordResultModal.vue';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initDict();
|
initDict();
|
||||||
@@ -40,6 +41,7 @@ onMounted(() => {
|
|||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
const importDialog = ref();
|
const importDialog = ref();
|
||||||
|
const passwordResultDialog = ref();
|
||||||
const saveDialog = ref();
|
const saveDialog = ref();
|
||||||
const selectedRows = ref<any[]>([]);
|
const selectedRows = ref<any[]>([]);
|
||||||
const batchActionLoading = ref(false);
|
const batchActionLoading = ref(false);
|
||||||
@@ -124,6 +126,7 @@ function remove(row: any) {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
function resetPassword(row: any) {
|
function resetPassword(row: any) {
|
||||||
|
let plainPassword = '';
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
$t('sysAccount.resetPasswordConfirm'),
|
$t('sysAccount.resetPasswordConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
@@ -139,7 +142,7 @@ function resetPassword(row: any) {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('sysAccount.resetPasswordSuccess'));
|
plainPassword = typeof res.data === 'string' ? res.data : '';
|
||||||
clearSelection();
|
clearSelection();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -152,7 +155,15 @@ function resetPassword(row: any) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).catch(() => {});
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (!plainPassword) {
|
||||||
|
ElMessage.error($t('sysAccount.resetPasswordResultMissing'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordResultDialog.value?.openDialog(plainPassword);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
function handleSelectionChange(rows: any[]) {
|
function handleSelectionChange(rows: any[]) {
|
||||||
selectedRows.value = rows;
|
selectedRows.value = rows;
|
||||||
@@ -260,6 +271,7 @@ function isAdmin(data: any) {
|
|||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysAccountImportModal ref="importDialog" @reload="reset" />
|
<SysAccountImportModal ref="importDialog" @reload="reset" />
|
||||||
<SysAccountModal ref="saveDialog" @reload="reset" />
|
<SysAccountModal ref="saveDialog" @reload="reset" />
|
||||||
|
<SysAccountPasswordResultModal ref="passwordResultDialog" />
|
||||||
<ListPageShell>
|
<ListPageShell>
|
||||||
<template #filters>
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
|
import { CopyDocument } from '@element-plus/icons-vue';
|
||||||
|
import { ElButton, ElInput } from 'element-plus';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const password = ref('');
|
||||||
|
|
||||||
|
watch(dialogVisible, (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
password.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDialog(value: string) {
|
||||||
|
password.value = value;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPassword(event: FocusEvent) {
|
||||||
|
if (event.target instanceof HTMLInputElement) {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPassword() {
|
||||||
|
await copyTextWithFeedback(
|
||||||
|
password.value,
|
||||||
|
$t('message.copySuccess'),
|
||||||
|
$t('message.copyFail'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openDialog,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EasyFlowPanelModal
|
||||||
|
v-model:open="dialogVisible"
|
||||||
|
centered
|
||||||
|
width="482px"
|
||||||
|
:confirm-text="$t('message.ok')"
|
||||||
|
:description="$t('sysAccount.resetPasswordResultDescription')"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:title="$t('sysAccount.resetPasswordResultTitle')"
|
||||||
|
@confirm="closeDialog"
|
||||||
|
>
|
||||||
|
<div class="password-result">
|
||||||
|
<label class="password-result__label" for="reset-password-result">
|
||||||
|
{{ $t('sysAccount.resetPasswordResultLabel') }}
|
||||||
|
</label>
|
||||||
|
<ElInput
|
||||||
|
id="reset-password-result"
|
||||||
|
class="password-result__input"
|
||||||
|
:model-value="password"
|
||||||
|
readonly
|
||||||
|
size="large"
|
||||||
|
@focus="selectPassword"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<ElButton
|
||||||
|
class="password-result__copy"
|
||||||
|
:aria-label="$t('sysAccount.copyPassword')"
|
||||||
|
:icon="CopyDocument"
|
||||||
|
link
|
||||||
|
:title="$t('sysAccount.copyPassword')"
|
||||||
|
@click="copyPassword"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
</div>
|
||||||
|
</EasyFlowPanelModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.password-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-result__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__input .el-input__wrapper) {
|
||||||
|
padding-inline: var(--space-4) var(--space-2);
|
||||||
|
background: hsl(var(--surface-subtle));
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
box-shadow: 0 0 0 1px hsl(var(--line-subtle)) inset;
|
||||||
|
transition:
|
||||||
|
background-color var(--motion-duration-fast) var(--motion-ease-standard),
|
||||||
|
box-shadow var(--motion-duration-fast) var(--motion-ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__input .el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px hsl(var(--border)) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__input .el-input__wrapper.is-focus) {
|
||||||
|
background: hsl(var(--surface-panel));
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--primary) / 18%) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__input .el-input__inner) {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__copy) {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.password-result__copy:not(.is-disabled):hover),
|
||||||
|
:deep(.password-result__copy:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--primary) / 8%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user