diff --git a/database/migrations/functions/users/update_user_password.sql b/database/migrations/functions/users/update_user_password.sql index 894efe5fc..98ff1f3ed 100644 --- a/database/migrations/functions/users/update_user_password.sql +++ b/database/migrations/functions/users/update_user_password.sql @@ -2,7 +2,15 @@ -- the database. create or replace function update_user_password(p_requesting_user_id uuid, p_old text, p_new text) returns void as $$ +begin + -- Update user password update "user" set password = p_new where user_id = p_requesting_user_id and password = p_old; -$$ language sql; + + -- Invalidate current user sessions + if found then + delete from session where user_id = p_requesting_user_id; + end if; +end +$$ language plpgsql; diff --git a/database/tests/functions/users/update_user_password.sql b/database/tests/functions/users/update_user_password.sql index 13259ec97..389fec8c3 100644 --- a/database/tests/functions/users/update_user_password.sql +++ b/database/tests/functions/users/update_user_password.sql @@ -1,12 +1,13 @@ -- Start transaction and plan tests begin; -select plan(2); +select plan(4); -- Declare some variables \set user1ID '00000000-0000-0000-0000-000000000001' --- Seed user +-- Seed some data insert into "user" (user_id, alias, email, password) values (:'user1ID', 'user1', 'user1@email.com', 'old'); +insert into session (session_id, user_id) values (gen_random_bytes(32), :'user1ID'); -- Update user password providing correct old password select update_user_password(:'user1ID', 'old', 'new'); @@ -17,6 +18,16 @@ select results_eq( $$ values ('new') $$, 'User password should have been updated' ); +select is_empty( + $$ + select * from session + where user_id = '00000000-0000-0000-0000-000000000001' + $$, + 'User1 sessions should have been deleted after updating the password successfully' +); + +-- Seed some data +insert into session (session_id, user_id) values (gen_random_bytes(32), :'user1ID'); -- Try updating user password providing incorrect old password select update_user_password(:'user1ID', 'incorrect', 'new2'); @@ -27,6 +38,13 @@ select results_eq( $$ values ('new') $$, 'User password should not have been updated' ); +select isnt_empty( + $$ + select * from session + where user_id = '00000000-0000-0000-0000-000000000001' + $$, + 'User1 sessions should not have been deleted as the password was not updated' +); -- Finish tests and rollback transaction select * from finish(); diff --git a/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.test.tsx b/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.test.tsx index 46c870ba5..d777ce713 100644 --- a/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.test.tsx +++ b/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.test.tsx @@ -9,6 +9,13 @@ import UpdatePassword from './UpdatePassword'; jest.mock('../../../../../api'); jest.mock('../../../../../utils/alertDispatcher'); +const mockUseNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router-dom') as object), + useNavigate: () => mockUseNavigate, +})); + describe('Update password - user settings', () => { afterEach(() => { jest.resetAllMocks(); @@ -48,6 +55,8 @@ describe('Update password - user settings', () => { await waitFor(() => { expect(API.updatePassword).toBeCalledTimes(1); expect(API.updatePassword).toHaveBeenCalledWith('oldpass', 'newpass'); + expect(mockUseNavigate).toHaveBeenCalledTimes(1); + expect(mockUseNavigate).toHaveBeenCalledWith('/?modal=login&redirect=/control-panel/settings'); }); }); diff --git a/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.tsx b/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.tsx index 28ce339d7..c8225b87a 100644 --- a/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.tsx +++ b/web/src/layout/controlPanel/settings/userSettings/profile/UpdatePassword.tsx @@ -1,9 +1,11 @@ import classnames from 'classnames'; import every from 'lodash/every'; -import { ChangeEvent, useRef, useState } from 'react'; +import { ChangeEvent, useContext, useRef, useState } from 'react'; import { FaPencilAlt } from 'react-icons/fa'; +import { useNavigate } from 'react-router-dom'; import API from '../../../../../api'; +import { AppCtx, signOut } from '../../../../../context/AppCtx'; import { ErrorKind, RefInputField } from '../../../../../types'; import alertDispatcher from '../../../../../utils/alertDispatcher'; import compoundErrorMessage from '../../../../../utils/compoundErrorMessage'; @@ -21,6 +23,7 @@ interface FormValidation { } const UpdatePassword = () => { + const navigate = useNavigate(); const form = useRef(null); const oldPasswordInput = useRef(null); const passwordInput = useRef(null); @@ -28,11 +31,21 @@ const UpdatePassword = () => { const [isSending, setIsSending] = useState(false); const [password, setPassword] = useState({ value: '', isValid: false }); const [isValidated, setIsValidated] = useState(false); + const { dispatch } = useContext(AppCtx); const onPasswordChange = (e: ChangeEvent) => { setPassword({ value: e.target.value, isValid: e.currentTarget.checkValidity() }); }; + const onSuccess = (): void => { + alertDispatcher.postAlert({ + type: 'success', + message: 'Your password has been successfully updated. Please, sign in again.', + }); + dispatch(signOut()); + navigate('/?modal=login&redirect=/control-panel/settings'); + }; + async function updatePassword(oldPassword: string, newPassword: string) { try { setIsSending(true); @@ -40,6 +53,7 @@ const UpdatePassword = () => { cleanForm(); setIsSending(false); setIsValidated(false); + onSuccess(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { setIsSending(false);