Skip to content

Commit

Permalink
Remove user sessions when password is updated (#4043)
Browse files Browse the repository at this point in the history
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
Co-authored-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Co-authored-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
  • Loading branch information
tegioz and cynthia-sg authored Sep 17, 2024
1 parent 78e386e commit 219e8b4
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 4 deletions.
10 changes: 9 additions & 1 deletion database/migrations/functions/users/update_user_password.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
22 changes: 20 additions & 2 deletions database/tests/functions/users/update_user_password.sql
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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');
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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');
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,25 +23,37 @@ interface FormValidation {
}

const UpdatePassword = () => {
const navigate = useNavigate();
const form = useRef<HTMLFormElement>(null);
const oldPasswordInput = useRef<RefInputField>(null);
const passwordInput = useRef<RefInputField>(null);
const repeatPasswordInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [password, setPassword] = useState<Password>({ value: '', isValid: false });
const [isValidated, setIsValidated] = useState(false);
const { dispatch } = useContext(AppCtx);

const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
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);
await API.updatePassword(oldPassword, newPassword);
cleanForm();
setIsSending(false);
setIsValidated(false);
onSuccess();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
setIsSending(false);
Expand Down

0 comments on commit 219e8b4

Please sign in to comment.