[Improvement-14387][UI] Support to reset user's password. (#14498)

This commit is contained in:
calvin 2023-07-11 12:01:49 +08:00 committed by GitHub
parent 0246327083
commit c3c2dda861
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 12 deletions

View File

@ -144,6 +144,7 @@ export default {
delete_confirm: 'Are you sure to delete?',
delete_confirm_tip:
'Deleting user is a dangerous operationplease be careful',
reset_password: 'Reset Password',
project: 'Project',
resource: 'Resource',
file_resource: 'File Resource',
@ -165,6 +166,7 @@ export default {
user_password: 'Password',
user_password_tips:
'Please enter a password containing letters and numbers with a length between 6 and 20',
confirm_password_tips: 'The both of password and confirm password are not same.',
user_type: 'User Type',
ordinary_user: 'Ordinary users',
administrator: 'Administrator',

View File

@ -142,6 +142,7 @@ export default {
edit_user: '编辑用户',
delete_user: '删除用户',
delete_confirm: '确定删除吗?',
reset_password: '重新设置密码',
project: '项目',
resource: '资源',
file_resource: '文件资源',
@ -162,6 +163,7 @@ export default {
username_tips: '请输入用户名',
user_password: '密码',
user_password_tips: '请输入包含字母和数字长度在620之间的密码',
confirm_password_tips: '两次密码输入不一致',
user_type: '用户类型',
ordinary_user: '普通用户',
administrator: '管理员',

View File

@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
defineComponent,
getCurrentInstance,
PropType,
toRefs,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import { NInput, NForm, NFormItem } from 'naive-ui'
import { usePassword } from './use-password'
import Modal from '@/components/modal'
import type { IRecord } from '../types'
const props = {
show: {
type: Boolean as PropType<boolean>,
default: false
},
currentRecord: {
type: Object as PropType<IRecord | null>,
default: {}
}
}
export const PasswordModal = defineComponent({
name: 'password-modal',
props,
emits: ['cancel', 'update'],
setup(props, ctx) {
const { t } = useI18n()
const { state, IS_ADMIN, formRules, onReset, onSave, onSetValues } =
usePassword()
const onCancel = () => {
onReset()
ctx.emit('cancel')
}
const onConfirm = async () => {
if (props.currentRecord?.id) {
const result = await onSave(props.currentRecord)
if (!result) return
}
onCancel()
ctx.emit('update')
}
const trim = getCurrentInstance()?.appContext.config.globalProperties.trim
watch(
() => props.show,
() => {
if (props.show && props.currentRecord?.id) {
onSetValues(props.currentRecord)
}
}
)
return {
t,
...toRefs(state),
IS_ADMIN,
formRules,
onCancel,
onConfirm,
trim
}
},
render() {
const { t } = this
return (
<Modal
show={this.show}
title={t('security.user.reset_password')}
onCancel={this.onCancel}
confirmLoading={this.loading}
onConfirm={this.onConfirm}
confirmClassName='btn-submit'
cancelClassName='btn-cancel'
>
<NForm
ref='formRef'
model={this.formData}
rules={this.formRules}
labelPlacement='left'
labelAlign='left'
labelWidth={150}
>
<NFormItem label={t('security.user.username')} path='userName'>
<NInput
allowInput={this.trim}
class='input-username'
v-model:value={this.formData.userName}
minlength={3}
maxlength={39}
disabled={true}
placeholder={t('security.user.username_tips')}
/>
</NFormItem>
<NFormItem
label={t('security.user.user_password')}
path='userPassword'
>
<NInput
allowInput={this.trim}
class='input-password'
type='password'
v-model:value={this.formData.userPassword}
placeholder={t('security.user.user_password_tips')}
/>
</NFormItem>
<NFormItem
label={t('password.confirm_password')}
path='confirmPassword'
>
<NInput
allowInput={this.trim}
type='password'
v-model:value={this.formData.confirmPassword}
placeholder={t('password.confirm_password_tips')}
/>
</NFormItem>
</NForm>
</Modal>
)
}
})
export default PasswordModal

View File

@ -0,0 +1,134 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { pick } from 'lodash'
import { useUserStore } from '@/store/user/user'
import { updateUser } from '@/service/modules/users'
import type { IRecord, UserInfoRes } from '../types'
import { FormItemRule } from 'naive-ui'
import { UserReq } from '../types'
import { IdReq } from '@/service/modules/users/types'
export function usePassword() {
const { t } = useI18n()
const userStore = useUserStore()
const userInfo = userStore.getUserInfo as UserInfoRes
const IS_ADMIN = userInfo.userType === 'ADMIN_USER'
const initialValues = {
userName: '',
userPassword: '',
confirmPassword: ''
}
const state = reactive({
formRef: ref(),
formData: { ...initialValues },
saving: false,
loading: false
})
function validatePasswordStartWith(
rule: FormItemRule,
value: string
): boolean {
return (
!!state.formRef.model.userPassword &&
state.formRef.model.userPassword.startsWith(value) &&
state.formRef.model.userPassword.length >= value.length
)
}
function validatePasswordSame(rule: FormItemRule, value: string): boolean {
return value === state.formRef.model.userPassword
}
const formRules = {
userPassword: {
trigger: ['input', 'blur'],
required: true,
validator(validator: any, value: string) {
if (
!value ||
!/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?![`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]+$)[`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~@#¥%……&*()——\-+={}|《》“”【】、。、0-9A-Za-z]{6,22}$/.test(
value
)
) {
return new Error(t('security.user.user_password_tips'))
}
}
},
confirmPassword: [
{
trigger: ['input', 'blur'],
required: true
},
{
validator: validatePasswordStartWith,
message: t('security.user.confirm_password_tips'),
trigger: ['input']
},
{
validator: validatePasswordSame,
message: t('security.user.confirm_password_tips'),
trigger: ['blur', 'password-input']
}
]
}
const onReset = () => {
state.formData = { ...initialValues }
}
const onSave = async (record: IRecord): Promise<boolean> => {
try {
await state.formRef.validate()
if (state.saving) return false
state.saving = true
const resetPasswordReq = {
...pick(record, [
'id',
'userName',
'tenantId',
'email',
'queue',
'phone',
'state'
]),
userPassword: state.formData.userPassword
} as IdReq & UserReq
await updateUser(resetPasswordReq)
state.saving = false
return true
} catch (err) {
state.saving = false
return false
}
}
const onSetValues = (record: IRecord) => {
state.formData = {
...pick(record, ['userName']),
userPassword: '',
confirmPassword: ''
}
}
return { state, formRules, IS_ADMIN, onReset, onSave, onSetValues }
}

View File

@ -110,7 +110,7 @@ export const UserModal = defineComponent({
rules={this.formRules}
labelPlacement='left'
labelAlign='left'
labelWidth={80}
labelWidth={120}
>
<NFormItem label={t('security.user.username')} path='userName'>
<NInput

View File

@ -23,6 +23,7 @@ import { useColumns } from './use-columns'
import { useTable } from './use-table'
import UserDetailModal from './components/user-detail-modal'
import AuthorizeModal from './components/authorize-modal'
import PasswordModal from './components/password-modal'
import Card from '@/components/card'
import Search from '@/components/input-search'
@ -44,6 +45,10 @@ const UsersManage = defineComponent({
const onAuthorizeModalCancel = () => {
state.authorizeModalShow = false
}
const onPasswordModalCancel = () => {
state.passwordModalShow = false
}
const trim = getCurrentInstance()?.appContext.config.globalProperties.trim
return {
@ -56,6 +61,7 @@ const UsersManage = defineComponent({
onUpdatedList: updateList,
onDetailModalCancel,
onAuthorizeModalCancel,
onPasswordModalCancel,
trim
}
},
@ -120,6 +126,11 @@ const UsersManage = defineComponent({
userId={this.currentRecord?.id}
onCancel={this.onAuthorizeModalCancel}
/>
<PasswordModal
show={this.passwordModalShow}
currentRecord={this.currentRecord}
onCancel={this.onPasswordModalCancel}
/>
</NSpace>
)
}

View File

@ -41,6 +41,8 @@ interface IRecord {
state: 0 | 1
createTime: string
updateTime: string
userPassword?: string
confirmPassword?: string
}
interface IResourceOption {

View File

@ -26,17 +26,28 @@ import {
NDropdown,
NPopconfirm
} from 'naive-ui'
import { EditOutlined, DeleteOutlined, UserOutlined } from '@vicons/antd'
import {
EditOutlined,
DeleteOutlined,
UserOutlined,
KeyOutlined
} from '@vicons/antd'
import {
COLUMN_WIDTH_CONFIG,
calculateTableWidth,
DefaultTableWidth
} from '@/common/column-width-config'
import type { TableColumns, InternalRowData } from './types'
import { useUserStore } from '@/store/user/user'
import { UserInfoRes } from './types'
export function useColumns(onCallback: Function) {
const { t } = useI18n()
const userStore = useUserStore()
const userInfo = userStore.getUserInfo as UserInfoRes
const IS_ADMIN = userInfo.userType === 'ADMIN_USER'
const columnsRef = ref({
columns: [] as TableColumns,
tableWidth: DefaultTableWidth
@ -116,8 +127,8 @@ export function useColumns(onCallback: Function) {
{
title: t('security.user.operation'),
key: 'operation',
...COLUMN_WIDTH_CONFIG['operation'](3),
render: (rowData: any, unused: number) => {
...COLUMN_WIDTH_CONFIG['operation'](4),
render: (rowData: InternalRowData, unused: number) => {
return h(NSpace, null, {
default: () => [
h(
@ -158,7 +169,7 @@ export function useColumns(onCallback: Function) {
NButton,
{
circle: true,
type: 'warning',
type: 'info',
size: 'small',
class: 'authorize'
},
@ -189,6 +200,27 @@ export function useColumns(onCallback: Function) {
default: () => t('security.user.edit')
}
),
IS_ADMIN &&
h(
NTooltip,
{ trigger: 'hover' },
{
trigger: () =>
h(
NButton,
{
circle: true,
type: 'error',
size: 'small',
class: 'edit',
onClick: () =>
void onCallback({ rowData }, 'resetPassword')
},
() => h(NIcon, null, () => h(KeyOutlined))
),
default: () => t('security.user.reset_password')
}
),
h(
NPopconfirm,
{

View File

@ -22,6 +22,7 @@ import { parseTime } from '@/common/common'
import type { IRecord, TAuthType } from './types'
export function useTable() {
const state = reactive({
page: 1,
pageSize: 10,
@ -32,7 +33,8 @@ export function useTable() {
currentRecord: {} as IRecord | null,
authorizeType: 'authorize_project' as TAuthType,
detailModalShow: false,
authorizeModalShow: false
authorizeModalShow: false,
passwordModalShow: false
})
const getList = async () => {
@ -74,7 +76,7 @@ export function useTable() {
const onOperationClick = (
data: { rowData: IRecord; key?: TAuthType },
type: 'authorize' | 'edit' | 'delete'
type: 'authorize' | 'edit' | 'delete' | 'resetPassword'
) => {
state.currentRecord = data.rowData
if (type === 'edit') {
@ -87,13 +89,11 @@ export function useTable() {
if (type === 'delete') {
deleteUser(data.rowData.id)
}
if (type === 'resetPassword') {
state.passwordModalShow = true
}
}
// const deleteRecord = async (id: number) => {
// const ignored = await deleteAlertPluginInstance(id)
// updateList()
// }
const changePage = (page: number) => {
state.page = page
getList()