mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 13:54:35 +03:00
Wired password reset for non-owner users
refs https://github.com/TryGhost/Team/issues/3351 - adds password validation and reset for non-owner users - password validation uses basic checks for now and will include more checks from current admin in future
This commit is contained in:
parent
8098f8c53e
commit
94882fd6c8
@ -7,12 +7,13 @@ import Menu from '../../../../admin-x-ds/global/Menu';
|
||||
import Modal from '../../../../admin-x-ds/global/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Radio from '../../../../admin-x-ds/global/Radio';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useContext, useEffect, useRef, useState} from 'react';
|
||||
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/Toggle';
|
||||
import useRoles from '../../../../hooks/useRoles';
|
||||
import {ServicesContext} from '../../../providers/ServiceProvider';
|
||||
import {User} from '../../../../types/api';
|
||||
import {generateAvatarColor, getInitials, isOwnerUser} from '../../../../utils/helpers';
|
||||
|
||||
@ -249,8 +250,44 @@ const EmailNotifications: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Password: React.FC = () => {
|
||||
function passwordValidation({password, confirmPassword}: {password: string; confirmPassword: string}) {
|
||||
const errors: {
|
||||
newPassword?: string;
|
||||
confirmNewPassword?: string;
|
||||
} = {};
|
||||
if (password !== confirmPassword) {
|
||||
errors.newPassword = 'Your new passwords do not match';
|
||||
errors.confirmNewPassword = 'Your new passwords do not match';
|
||||
}
|
||||
if (password.length < 10) {
|
||||
errors.newPassword = 'Password must be at least 10 characters';
|
||||
}
|
||||
|
||||
//ToDo: add more validations
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const Password: React.FC<UserDetailProps> = ({user}) => {
|
||||
const [editPassword, setEditPassword] = useState(false);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [saveState, setSaveState] = useState<'saving'|'saved'|'error'|''>('');
|
||||
const [errors, setErrors] = useState<{
|
||||
newPassword?: string;
|
||||
confirmNewPassword?: string;
|
||||
}>({});
|
||||
const newPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const confirmNewPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const {api} = useContext(ServicesContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState === 'saved') {
|
||||
setTimeout(() => {
|
||||
setSaveState('');
|
||||
}, 2000);
|
||||
}
|
||||
}, [saveState]);
|
||||
|
||||
const showPasswordInputs = () => {
|
||||
setEditPassword(true);
|
||||
@ -263,18 +300,70 @@ const Password: React.FC = () => {
|
||||
onClick={showPasswordInputs}
|
||||
/>
|
||||
);
|
||||
|
||||
let buttonLabel = 'Change password';
|
||||
if (saveState === 'saving') {
|
||||
buttonLabel = 'Updating...';
|
||||
} else if (saveState === 'saved') {
|
||||
buttonLabel = 'Updated';
|
||||
} else if (saveState === 'error') {
|
||||
buttonLabel = 'Retry';
|
||||
}
|
||||
const form = (
|
||||
<>
|
||||
<TextField
|
||||
error={!!errors.newPassword}
|
||||
hint={errors.newPassword}
|
||||
inputRef={newPasswordRef}
|
||||
title="New password"
|
||||
type="password"
|
||||
value=''
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
error={!!errors.confirmNewPassword}
|
||||
hint={errors.confirmNewPassword}
|
||||
inputRef={confirmNewPasswordRef}
|
||||
title="Verify password"
|
||||
type="password"
|
||||
value=''
|
||||
onChange={(e) => {
|
||||
setConfirmNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color='red'
|
||||
label={buttonLabel}
|
||||
onClick={async () => {
|
||||
setSaveState('saving');
|
||||
const validationErrros = passwordValidation({password: newPassword, confirmPassword: confirmNewPassword});
|
||||
setErrors(validationErrros);
|
||||
if (Object.keys(validationErrros).length > 0) {
|
||||
// show errors
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
if (newPasswordRef.current) {
|
||||
newPasswordRef.current.value = '';
|
||||
}
|
||||
if (confirmNewPasswordRef.current) {
|
||||
confirmNewPasswordRef.current.value = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.users.updatePassword({
|
||||
newPassword,
|
||||
confirmNewPassword,
|
||||
oldPassword: '',
|
||||
userId: user?.id
|
||||
});
|
||||
setSaveState('saved');
|
||||
} catch (e) {
|
||||
setSaveState('error');
|
||||
// show errors
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -284,6 +373,7 @@ const Password: React.FC = () => {
|
||||
border={false}
|
||||
customHeader={<CustomHeader>Password</CustomHeader>}
|
||||
title='Password'
|
||||
|
||||
>
|
||||
{editPassword ? form : view}
|
||||
</SettingGroup>
|
||||
@ -406,7 +496,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
||||
<Basic setUserData={setUserData} user={userData} />
|
||||
<Details setUserData={setUserData} user={userData} />
|
||||
<EmailNotifications setUserData={setUserData} user={userData} />
|
||||
<Password />
|
||||
<Password user={userData} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -38,6 +38,12 @@ export interface ImagesResponseType {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PasswordUpdateResponseType {
|
||||
password: [{
|
||||
message: string;
|
||||
}];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method?: string;
|
||||
body?: string | FormData;
|
||||
@ -46,6 +52,13 @@ interface RequestOptions {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePasswordOptions {
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
userId: string;
|
||||
oldPassword?: string;
|
||||
}
|
||||
|
||||
interface API {
|
||||
settings: {
|
||||
browse: () => Promise<SettingsResponseType>;
|
||||
@ -55,6 +68,7 @@ interface API {
|
||||
browse: () => Promise<UsersResponseType>;
|
||||
currentUser: () => Promise<User>;
|
||||
edit: (editedUser: User) => Promise<UsersResponseType>;
|
||||
updatePassword: (options: UpdatePasswordOptions) => Promise<PasswordUpdateResponseType>;
|
||||
};
|
||||
roles: {
|
||||
browse: () => Promise<RolesResponseType>;
|
||||
@ -143,6 +157,22 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
||||
|
||||
const data: UsersResponseType = await response.json();
|
||||
return data;
|
||||
},
|
||||
updatePassword: async ({newPassword, confirmNewPassword, userId, oldPassword}) => {
|
||||
const payload = JSON.stringify({
|
||||
password: [{
|
||||
user_id: userId,
|
||||
oldPassword: oldPassword || '',
|
||||
newPassword: newPassword,
|
||||
ne2Password: confirmNewPassword
|
||||
}]
|
||||
});
|
||||
const response = await fetcher(`/users/password/`, {
|
||||
method: 'PUT',
|
||||
body: payload
|
||||
});
|
||||
const data: PasswordUpdateResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
},
|
||||
roles: {
|
||||
|
Loading…
Reference in New Issue
Block a user