Skip to content

Commit 6fb4ec4

Browse files
authored
feat(users): clean tokens on change password (#166)
* feat(user): add clear user tokens on change password * feat(users): update permissions * feat(users): default workflow for clear tokens * feat(users): update change password tests * feat(users): update change password tests * feat(users): update change password clear tokens tests * feat(users): update tests * feat(users): update clear user tokens * feat(users): update deps * feat(users): update clear user token payload error messages * feat(users): update jsdocs * feat(users): update change user password validation for admin (#167) * feat(users): update jsdocs * feat(users): update tests * feat(users): add clear user tokens tests * feat(users): clean up * feat(users): update http requests
1 parent 984fffb commit 6fb4ec4

File tree

14 files changed

+439
-53
lines changed

14 files changed

+439
-53
lines changed

http-requests/users/requests.http

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,39 @@ Authorization: Bearer admintokenooooooooooooooooooooon
2121
}
2222
}
2323
}
24+
25+
### Change user password as an application admin
26+
POST http://127.0.0.1:3000
27+
Accept: application/json
28+
Content-Type: application/json
29+
## Admin token
30+
Authorization: Bearer admintokenooooooooooooooooooooon
31+
32+
{
33+
"id": "1",
34+
"method": "users.user.change-password",
35+
"params": {
36+
"userId": "68827b31-33e9-45b5-bf9f-8823b993d0ef",
37+
"newPassword": "123456789!A",
38+
"allowedByAdmin": true
39+
}
40+
}
41+
42+
### Change user password as an user
43+
POST http://127.0.0.1:3000
44+
Accept: application/json
45+
Content-Type: application/json
46+
## Admin token
47+
Authorization: Bearer usertokenooooooooooooooooooooooo
48+
49+
{
50+
"id": "1",
51+
"method": "users.user.change-password",
52+
"params": {
53+
"userId": "d7275b35-5f67-4f7c-bb58-731cddbcb93f",
54+
"newPassword": "123456789!A",
55+
"oldPassword": "Dasha.123",
56+
"confirmCode": "405799",
57+
"confirmBy": "email"
58+
}
59+
}

microservices/authorization/migrations/permissions/list/models/users.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,14 @@
719719
"admin": "allow"
720720
}
721721
},
722+
"clearTokensType": {
723+
"in": {
724+
"admin": "allow"
725+
},
726+
"out": {
727+
"admin": "allow"
728+
}
729+
},
722730
"oldPassword": {
723731
"in": {
724732
"user": "allow",

microservices/users/__tests__/config/remote.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('config/remote', () => {
1212

1313
it('should correctly return cookies config: with remote', async () => {
1414
expect(await remoteConfig()).to.deep.equal({
15+
changePasswordClearTokensType: CONST.MS_USER_CHANGE_PASSWORD_CLEAR_TOKENS_TYPE,
1516
passwordSaltRounds: CONST.MS_USER_PASSWORD_SALT_ROUNDS,
1617
removedAccountRestoreTime: CONST.MS_USER_REMOVE_ACCOUNT_RESTORE_TIME,
1718
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Api } from '@lomray/microservice-helpers';
2+
import { TypeormMock } from '@lomray/microservice-helpers/mocks';
3+
import { waitResult } from '@lomray/microservice-helpers/test-helpers';
4+
import { expect } from 'chai';
5+
import sinon from 'sinon';
6+
import UserRepository from '@repositories/user';
7+
8+
describe('repositories/user', () => {
9+
const sandbox = sinon.createSandbox();
10+
const repository = TypeormMock.entityManager.getCustomRepository(UserRepository);
11+
const apiErrorMock = { status: 500, message: 'Mock error.', service: 'authentication', code: 1 };
12+
13+
beforeEach(() => {
14+
TypeormMock.sandbox.reset();
15+
});
16+
17+
afterEach(() => {
18+
sandbox.restore();
19+
});
20+
21+
describe('clearUserTokens', () => {
22+
it('should stop clear all user tokens: tokens not found', async () => {
23+
sandbox.stub(Api.get().authentication.token, 'count').resolves({ result: { count: 0 } });
24+
const clearTokensStub = sandbox.stub(Api.get().authentication.token, 'remove');
25+
26+
await repository.clearUserTokens('user-id');
27+
28+
expect(clearTokensStub).to.not.called;
29+
});
30+
31+
it('should stop clear rest user tokens: tokens not found', async () => {
32+
sandbox.stub(Api.get().authentication.token, 'count').resolves({ result: { count: 0 } });
33+
const clearTokensStub = sandbox.stub(Api.get().authentication.token, 'remove');
34+
35+
await repository.clearUserTokens('user-id', 'token-id');
36+
37+
expect(clearTokensStub).to.not.called;
38+
});
39+
40+
it('should clear all user tokens', async () => {
41+
sandbox.stub(Api.get().authentication.token, 'count').resolves({ result: { count: 2 } });
42+
const clearTokensStub = sandbox.stub(Api.get().authentication.token, 'remove').resolves({});
43+
44+
await repository.clearUserTokens('user-id');
45+
46+
expect(clearTokensStub).to.calledOnce;
47+
});
48+
49+
it('should clear rest user tokens', async () => {
50+
sandbox.stub(Api.get().authentication.token, 'count').resolves({ result: { count: 2 } });
51+
const clearTokensStub = sandbox.stub(Api.get().authentication.token, 'remove').resolves({});
52+
53+
await repository.clearUserTokens('user-id', 'token-id');
54+
55+
expect(clearTokensStub).to.calledOnce;
56+
});
57+
58+
it('should stop clear all user tokens: token count error', async () => {
59+
sandbox.stub(Api.get().authentication.token, 'count').resolves({
60+
error: apiErrorMock,
61+
});
62+
63+
expect(await waitResult(repository.clearUserTokens('user-id'))).to.throw(
64+
'Failed to clear rest user tokens.',
65+
);
66+
});
67+
68+
it('should stop clear rest user tokens: token count error', async () => {
69+
sandbox.stub(Api.get().authentication.token, 'count').resolves({
70+
error: apiErrorMock,
71+
});
72+
73+
expect(await waitResult(repository.clearUserTokens('user-id', 'token-id'))).to.throw(
74+
'Failed to clear rest user tokens.',
75+
);
76+
});
77+
});
78+
});
Lines changed: 153 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { TypeormMock } from '@lomray/microservice-helpers/mocks';
22
import { waitResult } from '@lomray/microservice-helpers/test-helpers';
33
import { expect } from 'chai';
4+
import sinon from 'sinon';
5+
import * as remoteConfig from '@config/remote';
46
import User from '@entities/user';
7+
import TClearUserTokens from '@interfaces/clear-user-tokens';
58
import UserRepository from '@repositories/user';
69
import ChangePassword from '@services/change-password';
710

811
describe('services/change-password', () => {
12+
const sandbox = sinon.createSandbox();
913
const repository = TypeormMock.entityManager.getCustomRepository(UserRepository);
14+
let clearUserTokensStub: sinon.SinonStub;
1015
const userId = 'user-id';
1116
const newPassword = 'new-password';
1217
const oldPassword = 'old-password';
@@ -21,68 +26,177 @@ describe('services/change-password', () => {
2126
});
2227

2328
beforeEach(() => {
29+
clearUserTokensStub = sandbox.stub(repository, 'clearUserTokens');
2430
TypeormMock.sandbox.reset();
2531
});
2632

27-
it('should throw error: user not found', async () => {
28-
const service = ChangePassword.init({
29-
userId,
30-
repository,
33+
afterEach(() => {
34+
sandbox.restore();
35+
});
36+
37+
describe('init', () => {
38+
it('should correctly build service', () => {
39+
expect(
40+
ChangePassword.init({
41+
userId,
42+
repository,
43+
}),
44+
).to.instanceof(ChangePassword);
3145
});
46+
});
3247

33-
TypeormMock.entityManager.findOne.resolves(undefined);
48+
describe('change', () => {
49+
it('should throw error: user not found', async () => {
50+
const service = ChangePassword.init({
51+
userId,
52+
repository,
53+
});
3454

35-
expect(await waitResult(service.change(newPassword, oldPassword))).to.throw('User not found');
36-
});
55+
TypeormMock.entityManager.findOne.resolves(undefined);
3756

38-
it('should throw error: oldPassword or confirmation not provided', async () => {
39-
const service = ChangePassword.init({
40-
userId,
41-
repository,
57+
expect(await waitResult(service.change(newPassword, oldPassword))).to.throw('User not found');
4258
});
4359

44-
expect(await waitResult(service.change(newPassword))).to.throw(
45-
'Either of confirm methods should be provided',
46-
);
47-
});
60+
it('should throw error: oldPassword or confirmation not provided', async () => {
61+
const service = ChangePassword.init({
62+
userId,
63+
repository,
64+
});
4865

49-
it('should throw error: invalid old password', async () => {
50-
const service = ChangePassword.init({
51-
userId,
52-
repository,
66+
expect(await waitResult(service.change(newPassword))).to.throw(
67+
'Either of confirm methods should be provided',
68+
);
5369
});
5470

55-
TypeormMock.entityManager.findOne.resolves(mockUser);
71+
it('should throw error: invalid old password', async () => {
72+
const service = ChangePassword.init({
73+
userId,
74+
repository,
75+
});
5676

57-
expect(await waitResult(service.change(newPassword, 'invalid-password'))).to.throw(
58-
'Invalid old password',
59-
);
60-
});
77+
TypeormMock.entityManager.findOne.resolves(mockUser);
78+
79+
expect(await waitResult(service.change(newPassword, 'invalid-password'))).to.throw(
80+
'Invalid old password',
81+
);
82+
});
83+
84+
it('should throw error: invalid confirmation', async () => {
85+
const service = ChangePassword.init({
86+
userId,
87+
repository,
88+
isConfirmed: () => false,
89+
});
6190

62-
it('should throw error: invalid confirmation', async () => {
63-
const service = ChangePassword.init({
64-
userId,
65-
repository,
66-
isConfirmed: () => false,
91+
TypeormMock.entityManager.findOne.resolves(mockUser);
92+
93+
expect(await waitResult(service.change(newPassword))).to.throw('Invalid confirmation code');
6794
});
6895

69-
TypeormMock.entityManager.findOne.resolves(mockUser);
96+
it('should successful change password', async () => {
97+
const service = ChangePassword.init({
98+
userId,
99+
repository,
100+
});
101+
102+
// Private method
103+
// @ts-ignore
104+
const handleClearUserTokensStub = sandbox.stub(service, 'handleClearUserTokens');
105+
106+
TypeormMock.entityManager.findOne.resolves(mockUser);
107+
108+
await service.change(newPassword, oldPassword);
109+
110+
const [, user] = TypeormMock.entityManager.save.firstCall.args;
111+
const [argUserId] = handleClearUserTokensStub.firstCall.args;
70112

71-
expect(await waitResult(service.change(newPassword))).to.throw('Invalid confirmation code');
113+
expect(repository.isValidPassword(user as User, newPassword)).to.true;
114+
expect(handleClearUserTokensStub).to.calledOnce;
115+
expect(argUserId).to.equal(userId);
116+
});
72117
});
73118

74-
it('should successful change password', async () => {
75-
const service = ChangePassword.init({
76-
userId,
77-
repository,
119+
describe('handleClearUserTokens', () => {
120+
it('should stop validation: type is undefined or none', async () => {
121+
for (const type of [undefined, 'none']) {
122+
const service = ChangePassword.init({
123+
userId,
124+
repository,
125+
clearTokensType: type as TClearUserTokens,
126+
});
127+
128+
await service['handleClearUserTokens'](userId);
129+
130+
expect(clearUserTokensStub).to.not.called;
131+
}
78132
});
79133

80-
TypeormMock.entityManager.findOne.resolves(mockUser);
134+
it('should stop validation: type is undefined or none', async () => {
135+
for (const type of [undefined, 'none']) {
136+
const service = ChangePassword.init({
137+
userId,
138+
repository,
139+
});
140+
141+
const remoteConfigStub = sinon.stub().resolves({
142+
changePasswordClearTokensType: type,
143+
});
81144

82-
await service.change(newPassword, oldPassword);
145+
// @ts-ignore
146+
sinon.replace(remoteConfig, 'default', remoteConfigStub);
83147

84-
const [, user] = TypeormMock.entityManager.save.firstCall.args;
148+
await service['handleClearUserTokens'](userId);
85149

86-
expect(repository.isValidPassword(user as User, newPassword)).to.true;
150+
expect(clearUserTokensStub).to.not.called;
151+
152+
sinon.restore();
153+
}
154+
});
155+
156+
it('should call clear all user tokens: with user id', async () => {
157+
const service = ChangePassword.init({
158+
userId,
159+
repository,
160+
clearTokensType: 'all',
161+
});
162+
163+
await service['handleClearUserTokens'](userId);
164+
165+
const [argUserId] = clearUserTokensStub.firstCall.args;
166+
167+
expect(clearUserTokensStub).to.calledOnce;
168+
expect(argUserId).to.equal(userId);
169+
});
170+
171+
it('should call clear rest user tokens: with user id', async () => {
172+
const token = 'token-id';
173+
174+
const service = ChangePassword.init({
175+
userId,
176+
repository,
177+
clearTokensType: 'rest',
178+
currentToken: token,
179+
});
180+
181+
await service['handleClearUserTokens'](userId);
182+
183+
const [argUserId, argToken] = clearUserTokensStub.firstCall.args;
184+
185+
expect(clearUserTokensStub).to.calledOnce;
186+
expect(argUserId).to.equal(userId);
187+
expect(argToken).to.equal(token);
188+
});
189+
190+
it('should skip rest tokens clean up: current token not passed', async () => {
191+
const service = ChangePassword.init({
192+
userId,
193+
repository,
194+
clearTokensType: 'rest',
195+
});
196+
197+
await service['handleClearUserTokens'](userId);
198+
199+
expect(clearUserTokensStub).to.not.called;
200+
});
87201
});
88202
});

0 commit comments

Comments
 (0)