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:
parent
8098f8c53e
commit
94882fd6c8
2 changed files with 124 additions and 4 deletions
|
@ -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>
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue