0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05: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:
Rishabh 2023-06-02 17:57:09 +05:30
parent 8098f8c53e
commit 94882fd6c8
2 changed files with 124 additions and 4 deletions

View file

@ -7,12 +7,13 @@ import Menu from '../../../../admin-x-ds/global/Menu';
import Modal from '../../../../admin-x-ds/global/Modal'; import Modal from '../../../../admin-x-ds/global/Modal';
import NiceModal from '@ebay/nice-modal-react'; import NiceModal from '@ebay/nice-modal-react';
import Radio from '../../../../admin-x-ds/global/Radio'; 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 SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent'; import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/TextField'; import TextField from '../../../../admin-x-ds/global/TextField';
import Toggle from '../../../../admin-x-ds/global/Toggle'; import Toggle from '../../../../admin-x-ds/global/Toggle';
import useRoles from '../../../../hooks/useRoles'; import useRoles from '../../../../hooks/useRoles';
import {ServicesContext} from '../../../providers/ServiceProvider';
import {User} from '../../../../types/api'; import {User} from '../../../../types/api';
import {generateAvatarColor, getInitials, isOwnerUser} from '../../../../utils/helpers'; 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 [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 = () => { const showPasswordInputs = () => {
setEditPassword(true); setEditPassword(true);
@ -263,18 +300,70 @@ const Password: React.FC = () => {
onClick={showPasswordInputs} 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 = ( const form = (
<> <>
<TextField <TextField
error={!!errors.newPassword}
hint={errors.newPassword}
inputRef={newPasswordRef}
title="New password" title="New password"
type="password" type="password"
value='' value=''
onChange={(e) => {
setNewPassword(e.target.value);
}}
/> />
<TextField <TextField
error={!!errors.confirmNewPassword}
hint={errors.confirmNewPassword}
inputRef={confirmNewPasswordRef}
title="Verify password" title="Verify password"
type="password" type="password"
value='' 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} border={false}
customHeader={<CustomHeader>Password</CustomHeader>} customHeader={<CustomHeader>Password</CustomHeader>}
title='Password' title='Password'
> >
{editPassword ? form : view} {editPassword ? form : view}
</SettingGroup> </SettingGroup>
@ -406,7 +496,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
<Basic setUserData={setUserData} user={userData} /> <Basic setUserData={setUserData} user={userData} />
<Details setUserData={setUserData} user={userData} /> <Details setUserData={setUserData} user={userData} />
<EmailNotifications setUserData={setUserData} user={userData} /> <EmailNotifications setUserData={setUserData} user={userData} />
<Password /> <Password user={userData} />
</div> </div>
</div> </div>
</Modal> </Modal>

View file

@ -38,6 +38,12 @@ export interface ImagesResponseType {
}[]; }[];
} }
export interface PasswordUpdateResponseType {
password: [{
message: string;
}];
}
interface RequestOptions { interface RequestOptions {
method?: string; method?: string;
body?: string | FormData; body?: string | FormData;
@ -46,6 +52,13 @@ interface RequestOptions {
}; };
} }
interface UpdatePasswordOptions {
newPassword: string;
confirmNewPassword: string;
userId: string;
oldPassword?: string;
}
interface API { interface API {
settings: { settings: {
browse: () => Promise<SettingsResponseType>; browse: () => Promise<SettingsResponseType>;
@ -55,6 +68,7 @@ interface API {
browse: () => Promise<UsersResponseType>; browse: () => Promise<UsersResponseType>;
currentUser: () => Promise<User>; currentUser: () => Promise<User>;
edit: (editedUser: User) => Promise<UsersResponseType>; edit: (editedUser: User) => Promise<UsersResponseType>;
updatePassword: (options: UpdatePasswordOptions) => Promise<PasswordUpdateResponseType>;
}; };
roles: { roles: {
browse: () => Promise<RolesResponseType>; browse: () => Promise<RolesResponseType>;
@ -143,6 +157,22 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
const data: UsersResponseType = await response.json(); const data: UsersResponseType = await response.json();
return data; 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: { roles: {