@peassoft/mnr-web-topline 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +16 -0
- package/dist/en/modules/password-change-beacon/index.d.ts +14 -0
- package/dist/en/modules/password-change-beacon/index.js +25 -0
- package/dist/en/modules/request/index.js +1 -2
- package/dist/en/modules/topline-service/inner-service.js +3 -3
- package/dist/en/modules/websocket/index.js +9 -3
- package/dist/en/parts/profile/actions/change-password/index.d.ts +16 -0
- package/dist/en/parts/profile/actions/change-password/index.js +102 -0
- package/dist/en/parts/profile/ui/index.js +4 -2
- package/dist/en/parts/profile/ui/password-change-confirmation/index.d.ts +6 -0
- package/dist/en/parts/profile/ui/password-change-confirmation/index.js +22 -0
- package/dist/en/parts/profile/ui/password-change-form/index.d.ts +7 -0
- package/dist/en/parts/profile/ui/password-change-form/index.js +136 -0
- package/dist/en/parts/profile/ui/password-change-form/use-state-with-validation-reset.d.ts +2 -0
- package/dist/en/parts/profile/ui/password-change-form/use-state-with-validation-reset.js +9 -0
- package/dist/en/parts/profile/ui/password-change-marshal/index.d.ts +6 -0
- package/dist/en/parts/profile/ui/password-change-marshal/index.js +32 -0
- package/dist/ru/modules/password-change-beacon/index.d.ts +14 -0
- package/dist/ru/modules/password-change-beacon/index.js +25 -0
- package/dist/ru/modules/request/index.js +1 -2
- package/dist/ru/modules/topline-service/inner-service.js +3 -3
- package/dist/ru/modules/websocket/index.js +9 -3
- package/dist/ru/parts/profile/actions/change-password/index.d.ts +16 -0
- package/dist/ru/parts/profile/actions/change-password/index.js +102 -0
- package/dist/ru/parts/profile/ui/index.js +4 -2
- package/dist/ru/parts/profile/ui/password-change-confirmation/index.d.ts +6 -0
- package/dist/ru/parts/profile/ui/password-change-confirmation/index.js +22 -0
- package/dist/ru/parts/profile/ui/password-change-form/index.d.ts +7 -0
- package/dist/ru/parts/profile/ui/password-change-form/index.js +136 -0
- package/dist/ru/parts/profile/ui/password-change-form/use-state-with-validation-reset.d.ts +2 -0
- package/dist/ru/parts/profile/ui/password-change-form/use-state-with-validation-reset.js +9 -0
- package/dist/ru/parts/profile/ui/password-change-marshal/index.d.ts +6 -0
- package/dist/ru/parts/profile/ui/password-change-marshal/index.js +32 -0
- package/package.json +3 -3
package/dist/css/index.css
CHANGED
|
@@ -165,6 +165,12 @@
|
|
|
165
165
|
margin-top: 1.5rem;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
.topline_profile_subtitle {
|
|
169
|
+
margin: 0;
|
|
170
|
+
font-size: 1.2em;
|
|
171
|
+
text-align: center;
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
.topline_profile_buttonsCont {
|
|
169
175
|
margin-top: 2rem;
|
|
170
176
|
display: flex;
|
|
@@ -172,6 +178,16 @@
|
|
|
172
178
|
gap: 1rem;
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
.topline_profile_confirmationMesage {
|
|
182
|
+
text-align: center;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.topline_profile_confirmationButtonsCont {
|
|
186
|
+
margin-top: 2rem;
|
|
187
|
+
display: flex;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
.topline_c_modal_overlay {
|
|
176
192
|
position: fixed;
|
|
177
193
|
z-index: 1;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create new beacon for a password change operation.
|
|
3
|
+
*
|
|
4
|
+
* Saves the created beacon in the local store.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPasswordChangeBeacon(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check whether given beacon exists in the local store.
|
|
9
|
+
*/
|
|
10
|
+
export declare function passwordChangeBeaconExists(beacon: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Remove given beacon from the local store
|
|
13
|
+
*/
|
|
14
|
+
export declare function removePasswordChangeBeacon(beacon: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
// Local store
|
|
3
|
+
const passwordChangeBeacons = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Create new beacon for a password change operation.
|
|
6
|
+
*
|
|
7
|
+
* Saves the created beacon in the local store.
|
|
8
|
+
*/
|
|
9
|
+
export function createPasswordChangeBeacon() {
|
|
10
|
+
const beacon = v4();
|
|
11
|
+
passwordChangeBeacons.add(beacon);
|
|
12
|
+
return beacon;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check whether given beacon exists in the local store.
|
|
16
|
+
*/
|
|
17
|
+
export function passwordChangeBeaconExists(beacon) {
|
|
18
|
+
return passwordChangeBeacons.has(beacon);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove given beacon from the local store
|
|
22
|
+
*/
|
|
23
|
+
export function removePasswordChangeBeacon(beacon) {
|
|
24
|
+
passwordChangeBeacons.delete(beacon);
|
|
25
|
+
}
|
|
@@ -19,8 +19,7 @@ import shouldRetry from './should-retry.js';
|
|
|
19
19
|
* @param fetchApiOpts Fetch API options
|
|
20
20
|
* @param requestOpts Custom options
|
|
21
21
|
*/
|
|
22
|
-
export default async function request(url, fetchApiOpts) {
|
|
23
|
-
let requestOpts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
22
|
+
export default async function request(url, fetchApiOpts, requestOpts = {}) {
|
|
24
23
|
const {
|
|
25
24
|
expectedStatusCodes,
|
|
26
25
|
timeoutMs,
|
|
@@ -17,11 +17,11 @@ export class InnerService {
|
|
|
17
17
|
#updateRefreshTokenInDb;
|
|
18
18
|
#logError;
|
|
19
19
|
/** Registered outer event listeners */
|
|
20
|
-
#listeners =
|
|
20
|
+
#listeners = new Set();
|
|
21
21
|
/** Registered inner "log-in" event listeners */
|
|
22
|
-
#innerLogInListeners =
|
|
22
|
+
#innerLogInListeners = new Set();
|
|
23
23
|
/** Registered inner "create-account" event listeners */
|
|
24
|
-
#innerCreateAccountListeners =
|
|
24
|
+
#innerCreateAccountListeners = new Set();
|
|
25
25
|
constructor(deps) {
|
|
26
26
|
this.#getRefreshToken = deps.getRefreshToken;
|
|
27
27
|
this.#setGrantToken = deps.setGrantToken;
|
|
@@ -6,6 +6,7 @@ import { innerToplineService, ToplineEventName } from '../topline-service/index.
|
|
|
6
6
|
import { isObject } from '../../types/helpers.js';
|
|
7
7
|
import { isToplineUser } from '../../types/data.js';
|
|
8
8
|
import { userDataChangeBeaconExists, removeUserDataChangeBeacon } from '../user-data-change-beacon/index.js';
|
|
9
|
+
import { passwordChangeBeaconExists, removePasswordChangeBeacon } from '../password-change-beacon/index.js';
|
|
9
10
|
import { updateUser } from '../local-db/index.js';
|
|
10
11
|
let activeSocket = null;
|
|
11
12
|
let currConnectionRetries = 0;
|
|
@@ -29,8 +30,12 @@ function handleSocketMessage(e) {
|
|
|
29
30
|
if (isObject(message) && 'type' in message && typeof message.type === 'string') {
|
|
30
31
|
switch (message.type) {
|
|
31
32
|
case 'logout':
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if ('beacon' in message && typeof message.beacon === 'string' && passwordChangeBeaconExists(message.beacon)) {
|
|
34
|
+
removePasswordChangeBeacon(message.beacon);
|
|
35
|
+
} else {
|
|
36
|
+
innerToplineService.processLogout();
|
|
37
|
+
closeWebsocket();
|
|
38
|
+
}
|
|
34
39
|
break;
|
|
35
40
|
case 'sync':
|
|
36
41
|
if ('syncId' in message && typeof message.syncId === 'string') {
|
|
@@ -39,8 +44,9 @@ function handleSocketMessage(e) {
|
|
|
39
44
|
break;
|
|
40
45
|
case 'user_data_change':
|
|
41
46
|
if ('beacon' in message && typeof message.beacon === 'string' && 'user' in message && isToplineUser(message.user)) {
|
|
42
|
-
if (
|
|
47
|
+
if (userDataChangeBeaconExists(message.beacon)) {
|
|
43
48
|
removeUserDataChangeBeacon(message.beacon);
|
|
49
|
+
} else {
|
|
44
50
|
void updateUser(message.user);
|
|
45
51
|
innerToplineService.emit(ToplineEventName.UserChange, message.user);
|
|
46
52
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare enum PasswordChangeResult {
|
|
2
|
+
Success = "success",
|
|
3
|
+
NetworkFailure = "network_failure",
|
|
4
|
+
/**
|
|
5
|
+
* Old password is incorrect. This case might occure when:
|
|
6
|
+
* (1) user makes an error while typing the old password, or
|
|
7
|
+
* (2) user doesn't remember their current password and tries a wrong one, or
|
|
8
|
+
* (3) user has changed the password in another instance of the app, the `logout`
|
|
9
|
+
* message has not been received on a websocket (for any reason), and
|
|
10
|
+
* the current `grant token` has not expired yet.
|
|
11
|
+
*/
|
|
12
|
+
IncorrectOldPassword = "incorrect_old_password",
|
|
13
|
+
/** HTTP request is not authenticated with `refresh token`, or user no longer exists. */
|
|
14
|
+
Unauthorized = "unauthorized"
|
|
15
|
+
}
|
|
16
|
+
export default function changePassword(beacon: string, oldPassword: string, newPassword: string): Promise<PasswordChangeResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import WebError from '@memnrev/web-error';
|
|
2
|
+
import { getGrantToken, setGrantToken, setRefreshToken } from '../../../../modules/tokens/index.js';
|
|
3
|
+
import { logInvariant, logError } from '../../../../modules/logger/index.js';
|
|
4
|
+
import { getApiBaseUrl, getRequestCredentialsMode } from '../../../../modules/env/index.js';
|
|
5
|
+
import request from '../../../../modules/request/index.js';
|
|
6
|
+
import { innerToplineService } from '../../../../modules/topline-service/index.js';
|
|
7
|
+
import { updateGrantToken, updateRefreshToken } from '../../../../modules/local-db/index.js';
|
|
8
|
+
export var PasswordChangeResult;
|
|
9
|
+
(function (PasswordChangeResult) {
|
|
10
|
+
PasswordChangeResult["Success"] = "success";
|
|
11
|
+
PasswordChangeResult["NetworkFailure"] = "network_failure";
|
|
12
|
+
/**
|
|
13
|
+
* Old password is incorrect. This case might occure when:
|
|
14
|
+
* (1) user makes an error while typing the old password, or
|
|
15
|
+
* (2) user doesn't remember their current password and tries a wrong one, or
|
|
16
|
+
* (3) user has changed the password in another instance of the app, the `logout`
|
|
17
|
+
* message has not been received on a websocket (for any reason), and
|
|
18
|
+
* the current `grant token` has not expired yet.
|
|
19
|
+
*/
|
|
20
|
+
PasswordChangeResult["IncorrectOldPassword"] = "incorrect_old_password";
|
|
21
|
+
/** HTTP request is not authenticated with `refresh token`, or user no longer exists. */
|
|
22
|
+
PasswordChangeResult["Unauthorized"] = "unauthorized";
|
|
23
|
+
})(PasswordChangeResult || (PasswordChangeResult = {}));
|
|
24
|
+
export default async function changePassword(beacon, oldPassword, newPassword) {
|
|
25
|
+
const grantToken = getGrantToken();
|
|
26
|
+
if (!grantToken) {
|
|
27
|
+
// This situation is impossible: if a logout event is received on a websocket,
|
|
28
|
+
// this modal window is closed automatically; thus the user has no chance to start
|
|
29
|
+
// a saving operation.
|
|
30
|
+
logInvariant('Invariant: grant token must exist while changing password');
|
|
31
|
+
return PasswordChangeResult.Unauthorized;
|
|
32
|
+
}
|
|
33
|
+
const url = `${getApiBaseUrl()}/private/user/change-password`;
|
|
34
|
+
const options = {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${grantToken}`,
|
|
38
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
beacon,
|
|
42
|
+
currPassword: oldPassword,
|
|
43
|
+
newPassword
|
|
44
|
+
}),
|
|
45
|
+
credentials: getRequestCredentialsMode(),
|
|
46
|
+
cache: 'no-store',
|
|
47
|
+
redirect: 'error'
|
|
48
|
+
};
|
|
49
|
+
const response = await request(url, options, {
|
|
50
|
+
// Explicitly avoid retries as this operation is not idempotent
|
|
51
|
+
maxRetries: 0
|
|
52
|
+
});
|
|
53
|
+
if (!response) {
|
|
54
|
+
return PasswordChangeResult.NetworkFailure;
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 401) {
|
|
57
|
+
const upgradedGrantToken = await innerToplineService.upgradeGrantToken();
|
|
58
|
+
if (!upgradedGrantToken) {
|
|
59
|
+
innerToplineService.processLogout();
|
|
60
|
+
return PasswordChangeResult.Unauthorized;
|
|
61
|
+
}
|
|
62
|
+
return await changePassword(beacon, oldPassword, newPassword);
|
|
63
|
+
}
|
|
64
|
+
let payload;
|
|
65
|
+
try {
|
|
66
|
+
payload = await response.json();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logError(new WebError({
|
|
69
|
+
name: 'UnexpectedError',
|
|
70
|
+
...(e instanceof Error ? {
|
|
71
|
+
cause: e
|
|
72
|
+
} : {})
|
|
73
|
+
}, `parsing 200 response of request to ${url} failed`));
|
|
74
|
+
return PasswordChangeResult.NetworkFailure;
|
|
75
|
+
}
|
|
76
|
+
const {
|
|
77
|
+
result
|
|
78
|
+
} = payload;
|
|
79
|
+
switch (result) {
|
|
80
|
+
case 'OK':
|
|
81
|
+
{
|
|
82
|
+
const {
|
|
83
|
+
grantToken,
|
|
84
|
+
refreshToken
|
|
85
|
+
} = payload.tokens;
|
|
86
|
+
setGrantToken(grantToken);
|
|
87
|
+
setRefreshToken(refreshToken);
|
|
88
|
+
await Promise.all([updateGrantToken(grantToken), updateRefreshToken(refreshToken)]);
|
|
89
|
+
return PasswordChangeResult.Success;
|
|
90
|
+
}
|
|
91
|
+
case 'NO_USER':
|
|
92
|
+
innerToplineService.processLogout();
|
|
93
|
+
return PasswordChangeResult.Unauthorized;
|
|
94
|
+
case 'INVALID_PASSWORD':
|
|
95
|
+
return PasswordChangeResult.IncorrectOldPassword;
|
|
96
|
+
default:
|
|
97
|
+
{
|
|
98
|
+
const _exhaustiveCheck = result;
|
|
99
|
+
return _exhaustiveCheck;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useContext, useState, useCallback } from 'react';
|
|
3
3
|
import Modal from '../../../shared/components/modal/index.js';
|
|
4
4
|
import Root from './root/index.js';
|
|
5
|
+
import PasswordChangeMarshal from './password-change-marshal/index.js';
|
|
5
6
|
import { ToplineContext } from '../../shell/index.js';
|
|
6
7
|
var LocalStep;
|
|
7
8
|
(function (LocalStep) {
|
|
@@ -17,6 +18,7 @@ export default function Profile(props) {
|
|
|
17
18
|
} = useContext(ToplineContext);
|
|
18
19
|
const [localStep, setLocalStep] = useState(LocalStep.Root);
|
|
19
20
|
const handleSwitchToPasswordChange = useCallback(() => setLocalStep(LocalStep.PasswordChange), []);
|
|
21
|
+
const handleReturningFromPasswordChange = useCallback(() => setLocalStep(LocalStep.Root), []);
|
|
20
22
|
const getContents = user => {
|
|
21
23
|
switch (localStep) {
|
|
22
24
|
case LocalStep.Root:
|
|
@@ -26,8 +28,8 @@ export default function Profile(props) {
|
|
|
26
28
|
onCancel: onClose
|
|
27
29
|
});
|
|
28
30
|
case LocalStep.PasswordChange:
|
|
29
|
-
return _jsx(
|
|
30
|
-
|
|
31
|
+
return _jsx(PasswordChangeMarshal, {
|
|
32
|
+
onCancel: handleReturningFromPasswordChange
|
|
31
33
|
});
|
|
32
34
|
default:
|
|
33
35
|
{
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
3
|
+
export default function PasswordChangeConfirmation(props) {
|
|
4
|
+
const {
|
|
5
|
+
onOk
|
|
6
|
+
} = props;
|
|
7
|
+
return _jsxs(_Fragment, {
|
|
8
|
+
children: [_jsx("h2", {
|
|
9
|
+
className: 'topline_profile_subtitle',
|
|
10
|
+
children: "Password Change"
|
|
11
|
+
}), _jsx("p", {
|
|
12
|
+
className: 'topline_profile_confirmationMesage',
|
|
13
|
+
children: "You password has successfully been changed."
|
|
14
|
+
}), _jsx("div", {
|
|
15
|
+
className: 'topline_profile_confirmationButtonsCont',
|
|
16
|
+
children: _jsx(Button, {
|
|
17
|
+
label: 'OK',
|
|
18
|
+
onClick: onOk
|
|
19
|
+
})
|
|
20
|
+
})]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { InputField } from '@peassoft/mnr-web-ui-kit/input-field/index.js';
|
|
4
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
5
|
+
import { ErrorMessage } from '@peassoft/mnr-web-ui-kit/error-message/index.js';
|
|
6
|
+
import useStateWithValidationReset from './use-state-with-validation-reset.js';
|
|
7
|
+
import changePassword, { PasswordChangeResult } from '../../actions/change-password/index.js';
|
|
8
|
+
import { createPasswordChangeBeacon } from '../../../../modules/password-change-beacon/index.js';
|
|
9
|
+
var NewPasswordConfirmationError;
|
|
10
|
+
(function (NewPasswordConfirmationError) {
|
|
11
|
+
NewPasswordConfirmationError["None"] = "none";
|
|
12
|
+
NewPasswordConfirmationError["Empty"] = "empty";
|
|
13
|
+
NewPasswordConfirmationError["NotMatching"] = "not_matching";
|
|
14
|
+
})(NewPasswordConfirmationError || (NewPasswordConfirmationError = {}));
|
|
15
|
+
export default function PasswordChangeForm(props) {
|
|
16
|
+
const {
|
|
17
|
+
onSuccess,
|
|
18
|
+
onCancel
|
|
19
|
+
} = props;
|
|
20
|
+
const [isInProgress, setIsInProgress] = useState(false);
|
|
21
|
+
const [hasOldPasswordValidationError, setHasOldPasswordValidationError] = useState(false);
|
|
22
|
+
const [hasNewPasswordValidationError, setHasNewPasswordValidationError] = useState(false);
|
|
23
|
+
const [newPasswordConfirmationError, setNewPasswordConfirmationError] = useState(NewPasswordConfirmationError.None);
|
|
24
|
+
const [oldPassword, setOldPassword] = useStateWithValidationReset('', setHasOldPasswordValidationError, false);
|
|
25
|
+
const [newPassword, setNewPassword] = useStateWithValidationReset('', setHasNewPasswordValidationError, false);
|
|
26
|
+
const [newPasswordConfirmation, setNewPasswordConfirmation] = useStateWithValidationReset('', setNewPasswordConfirmationError, NewPasswordConfirmationError.None);
|
|
27
|
+
const [savingError, setSavingError] = useState('');
|
|
28
|
+
const newPasswordConfirmationErrorMessage = useMemo(() => {
|
|
29
|
+
switch (newPasswordConfirmationError) {
|
|
30
|
+
case NewPasswordConfirmationError.None:
|
|
31
|
+
return '';
|
|
32
|
+
case NewPasswordConfirmationError.Empty:
|
|
33
|
+
return "Re-entering new password is required.";
|
|
34
|
+
case NewPasswordConfirmationError.NotMatching:
|
|
35
|
+
return "Passwords do not match.";
|
|
36
|
+
default:
|
|
37
|
+
{
|
|
38
|
+
const _exhaustiveCheck = newPasswordConfirmationError;
|
|
39
|
+
return _exhaustiveCheck;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [newPasswordConfirmationError]);
|
|
43
|
+
const validateInput = useCallback(() => {
|
|
44
|
+
let isCorrect = true;
|
|
45
|
+
if (!oldPassword) {
|
|
46
|
+
isCorrect = false;
|
|
47
|
+
setHasOldPasswordValidationError(true);
|
|
48
|
+
}
|
|
49
|
+
if (!newPassword) {
|
|
50
|
+
isCorrect = false;
|
|
51
|
+
setHasNewPasswordValidationError(true);
|
|
52
|
+
}
|
|
53
|
+
if (!newPasswordConfirmation) {
|
|
54
|
+
isCorrect = false;
|
|
55
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.Empty);
|
|
56
|
+
} else if (newPasswordConfirmation !== newPassword) {
|
|
57
|
+
isCorrect = false;
|
|
58
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.NotMatching);
|
|
59
|
+
}
|
|
60
|
+
return isCorrect;
|
|
61
|
+
}, [oldPassword, newPassword, newPasswordConfirmation]);
|
|
62
|
+
const resetErrors = useCallback(() => {
|
|
63
|
+
setHasOldPasswordValidationError(false);
|
|
64
|
+
setHasNewPasswordValidationError(false);
|
|
65
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.None);
|
|
66
|
+
setSavingError('');
|
|
67
|
+
}, []);
|
|
68
|
+
const handleSave = useCallback(async () => {
|
|
69
|
+
if (!validateInput()) return;
|
|
70
|
+
resetErrors();
|
|
71
|
+
setIsInProgress(true);
|
|
72
|
+
const beacon = createPasswordChangeBeacon();
|
|
73
|
+
const result = await changePassword(beacon, oldPassword, newPassword);
|
|
74
|
+
switch (result) {
|
|
75
|
+
case PasswordChangeResult.Success:
|
|
76
|
+
onSuccess();
|
|
77
|
+
break;
|
|
78
|
+
case PasswordChangeResult.NetworkFailure:
|
|
79
|
+
setSavingError("An error occurred while saving the new password. Check your internet connection and/or try again.");
|
|
80
|
+
setIsInProgress(false);
|
|
81
|
+
break;
|
|
82
|
+
case PasswordChangeResult.IncorrectOldPassword:
|
|
83
|
+
setSavingError("The current password is incorrect.");
|
|
84
|
+
setIsInProgress(false);
|
|
85
|
+
break;
|
|
86
|
+
case PasswordChangeResult.Unauthorized:
|
|
87
|
+
// Do nothing here. Everything needed has already been done by the action.
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
{
|
|
91
|
+
const _exhaustiveCheck = result;
|
|
92
|
+
return _exhaustiveCheck;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [validateInput, resetErrors, onSuccess, oldPassword, newPassword]);
|
|
96
|
+
return _jsxs("form", {
|
|
97
|
+
children: [_jsx("h2", {
|
|
98
|
+
className: 'topline_profile_subtitle',
|
|
99
|
+
children: "Password Change"
|
|
100
|
+
}), _jsx(InputField, {
|
|
101
|
+
type: 'password',
|
|
102
|
+
autocompleteAttribute: 'current-password',
|
|
103
|
+
label: "Your current password. Required.",
|
|
104
|
+
value: oldPassword,
|
|
105
|
+
errorMessage: hasOldPasswordValidationError ? "Enter your current password." : undefined,
|
|
106
|
+
onChange: setOldPassword
|
|
107
|
+
}), _jsx(InputField, {
|
|
108
|
+
type: 'password',
|
|
109
|
+
autocompleteAttribute: 'new-password',
|
|
110
|
+
label: "New password. Required.",
|
|
111
|
+
value: newPassword,
|
|
112
|
+
errorMessage: hasNewPasswordValidationError ? "Enter a new password." : undefined,
|
|
113
|
+
onChange: setNewPassword
|
|
114
|
+
}), _jsx(InputField, {
|
|
115
|
+
type: 'password',
|
|
116
|
+
label: "Enter the new password once again. Required.",
|
|
117
|
+
value: newPasswordConfirmation,
|
|
118
|
+
errorMessage: newPasswordConfirmationErrorMessage || undefined,
|
|
119
|
+
onChange: setNewPasswordConfirmation
|
|
120
|
+
}), !!savingError && _jsx(ErrorMessage, {
|
|
121
|
+
variant: 'standalone',
|
|
122
|
+
children: savingError
|
|
123
|
+
}), _jsxs("div", {
|
|
124
|
+
className: 'topline_profile_buttonsCont',
|
|
125
|
+
children: [_jsx(Button, {
|
|
126
|
+
label: "Save new password",
|
|
127
|
+
isInProgress: isInProgress,
|
|
128
|
+
onClick: handleSave
|
|
129
|
+
}), !isInProgress && _jsx(Button, {
|
|
130
|
+
label: "Cancel",
|
|
131
|
+
variant: 'secondary',
|
|
132
|
+
onClick: onCancel
|
|
133
|
+
})]
|
|
134
|
+
})]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
export default function useStateWithValidationReset(initialState, validationSetFn, validationResetValue) {
|
|
3
|
+
const [value, setValue] = useState(initialState);
|
|
4
|
+
const handleChange = useCallback(newVal => {
|
|
5
|
+
validationSetFn(validationResetValue);
|
|
6
|
+
setValue(newVal);
|
|
7
|
+
}, [validationSetFn, validationResetValue]);
|
|
8
|
+
return [value, handleChange];
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import PasswordChangeForm from '../password-change-form/index.js';
|
|
4
|
+
import PasswordChangeConfirmation from '../password-change-confirmation/index.js';
|
|
5
|
+
var LocalStep;
|
|
6
|
+
(function (LocalStep) {
|
|
7
|
+
LocalStep["Form"] = "form";
|
|
8
|
+
LocalStep["SuccessConfirmation"] = "success_confirmation";
|
|
9
|
+
})(LocalStep || (LocalStep = {}));
|
|
10
|
+
export default function PasswordChangeMarshal(props) {
|
|
11
|
+
const {
|
|
12
|
+
onCancel
|
|
13
|
+
} = props;
|
|
14
|
+
const [localStep, setLocalStep] = useState(LocalStep.Form);
|
|
15
|
+
const handleSuccess = useCallback(() => setLocalStep(LocalStep.SuccessConfirmation), []);
|
|
16
|
+
switch (localStep) {
|
|
17
|
+
case LocalStep.Form:
|
|
18
|
+
return _jsx(PasswordChangeForm, {
|
|
19
|
+
onSuccess: handleSuccess,
|
|
20
|
+
onCancel: onCancel
|
|
21
|
+
});
|
|
22
|
+
case LocalStep.SuccessConfirmation:
|
|
23
|
+
return _jsx(PasswordChangeConfirmation, {
|
|
24
|
+
onOk: onCancel
|
|
25
|
+
});
|
|
26
|
+
default:
|
|
27
|
+
{
|
|
28
|
+
const _exhaustiveCheck = localStep;
|
|
29
|
+
return _exhaustiveCheck;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create new beacon for a password change operation.
|
|
3
|
+
*
|
|
4
|
+
* Saves the created beacon in the local store.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPasswordChangeBeacon(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check whether given beacon exists in the local store.
|
|
9
|
+
*/
|
|
10
|
+
export declare function passwordChangeBeaconExists(beacon: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Remove given beacon from the local store
|
|
13
|
+
*/
|
|
14
|
+
export declare function removePasswordChangeBeacon(beacon: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
// Local store
|
|
3
|
+
const passwordChangeBeacons = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Create new beacon for a password change operation.
|
|
6
|
+
*
|
|
7
|
+
* Saves the created beacon in the local store.
|
|
8
|
+
*/
|
|
9
|
+
export function createPasswordChangeBeacon() {
|
|
10
|
+
const beacon = v4();
|
|
11
|
+
passwordChangeBeacons.add(beacon);
|
|
12
|
+
return beacon;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check whether given beacon exists in the local store.
|
|
16
|
+
*/
|
|
17
|
+
export function passwordChangeBeaconExists(beacon) {
|
|
18
|
+
return passwordChangeBeacons.has(beacon);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove given beacon from the local store
|
|
22
|
+
*/
|
|
23
|
+
export function removePasswordChangeBeacon(beacon) {
|
|
24
|
+
passwordChangeBeacons.delete(beacon);
|
|
25
|
+
}
|
|
@@ -19,8 +19,7 @@ import shouldRetry from './should-retry.js';
|
|
|
19
19
|
* @param fetchApiOpts Fetch API options
|
|
20
20
|
* @param requestOpts Custom options
|
|
21
21
|
*/
|
|
22
|
-
export default async function request(url, fetchApiOpts) {
|
|
23
|
-
let requestOpts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
22
|
+
export default async function request(url, fetchApiOpts, requestOpts = {}) {
|
|
24
23
|
const {
|
|
25
24
|
expectedStatusCodes,
|
|
26
25
|
timeoutMs,
|
|
@@ -17,11 +17,11 @@ export class InnerService {
|
|
|
17
17
|
#updateRefreshTokenInDb;
|
|
18
18
|
#logError;
|
|
19
19
|
/** Registered outer event listeners */
|
|
20
|
-
#listeners =
|
|
20
|
+
#listeners = new Set();
|
|
21
21
|
/** Registered inner "log-in" event listeners */
|
|
22
|
-
#innerLogInListeners =
|
|
22
|
+
#innerLogInListeners = new Set();
|
|
23
23
|
/** Registered inner "create-account" event listeners */
|
|
24
|
-
#innerCreateAccountListeners =
|
|
24
|
+
#innerCreateAccountListeners = new Set();
|
|
25
25
|
constructor(deps) {
|
|
26
26
|
this.#getRefreshToken = deps.getRefreshToken;
|
|
27
27
|
this.#setGrantToken = deps.setGrantToken;
|
|
@@ -6,6 +6,7 @@ import { innerToplineService, ToplineEventName } from '../topline-service/index.
|
|
|
6
6
|
import { isObject } from '../../types/helpers.js';
|
|
7
7
|
import { isToplineUser } from '../../types/data.js';
|
|
8
8
|
import { userDataChangeBeaconExists, removeUserDataChangeBeacon } from '../user-data-change-beacon/index.js';
|
|
9
|
+
import { passwordChangeBeaconExists, removePasswordChangeBeacon } from '../password-change-beacon/index.js';
|
|
9
10
|
import { updateUser } from '../local-db/index.js';
|
|
10
11
|
let activeSocket = null;
|
|
11
12
|
let currConnectionRetries = 0;
|
|
@@ -29,8 +30,12 @@ function handleSocketMessage(e) {
|
|
|
29
30
|
if (isObject(message) && 'type' in message && typeof message.type === 'string') {
|
|
30
31
|
switch (message.type) {
|
|
31
32
|
case 'logout':
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if ('beacon' in message && typeof message.beacon === 'string' && passwordChangeBeaconExists(message.beacon)) {
|
|
34
|
+
removePasswordChangeBeacon(message.beacon);
|
|
35
|
+
} else {
|
|
36
|
+
innerToplineService.processLogout();
|
|
37
|
+
closeWebsocket();
|
|
38
|
+
}
|
|
34
39
|
break;
|
|
35
40
|
case 'sync':
|
|
36
41
|
if ('syncId' in message && typeof message.syncId === 'string') {
|
|
@@ -39,8 +44,9 @@ function handleSocketMessage(e) {
|
|
|
39
44
|
break;
|
|
40
45
|
case 'user_data_change':
|
|
41
46
|
if ('beacon' in message && typeof message.beacon === 'string' && 'user' in message && isToplineUser(message.user)) {
|
|
42
|
-
if (
|
|
47
|
+
if (userDataChangeBeaconExists(message.beacon)) {
|
|
43
48
|
removeUserDataChangeBeacon(message.beacon);
|
|
49
|
+
} else {
|
|
44
50
|
void updateUser(message.user);
|
|
45
51
|
innerToplineService.emit(ToplineEventName.UserChange, message.user);
|
|
46
52
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare enum PasswordChangeResult {
|
|
2
|
+
Success = "success",
|
|
3
|
+
NetworkFailure = "network_failure",
|
|
4
|
+
/**
|
|
5
|
+
* Old password is incorrect. This case might occure when:
|
|
6
|
+
* (1) user makes an error while typing the old password, or
|
|
7
|
+
* (2) user doesn't remember their current password and tries a wrong one, or
|
|
8
|
+
* (3) user has changed the password in another instance of the app, the `logout`
|
|
9
|
+
* message has not been received on a websocket (for any reason), and
|
|
10
|
+
* the current `grant token` has not expired yet.
|
|
11
|
+
*/
|
|
12
|
+
IncorrectOldPassword = "incorrect_old_password",
|
|
13
|
+
/** HTTP request is not authenticated with `refresh token`, or user no longer exists. */
|
|
14
|
+
Unauthorized = "unauthorized"
|
|
15
|
+
}
|
|
16
|
+
export default function changePassword(beacon: string, oldPassword: string, newPassword: string): Promise<PasswordChangeResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import WebError from '@memnrev/web-error';
|
|
2
|
+
import { getGrantToken, setGrantToken, setRefreshToken } from '../../../../modules/tokens/index.js';
|
|
3
|
+
import { logInvariant, logError } from '../../../../modules/logger/index.js';
|
|
4
|
+
import { getApiBaseUrl, getRequestCredentialsMode } from '../../../../modules/env/index.js';
|
|
5
|
+
import request from '../../../../modules/request/index.js';
|
|
6
|
+
import { innerToplineService } from '../../../../modules/topline-service/index.js';
|
|
7
|
+
import { updateGrantToken, updateRefreshToken } from '../../../../modules/local-db/index.js';
|
|
8
|
+
export var PasswordChangeResult;
|
|
9
|
+
(function (PasswordChangeResult) {
|
|
10
|
+
PasswordChangeResult["Success"] = "success";
|
|
11
|
+
PasswordChangeResult["NetworkFailure"] = "network_failure";
|
|
12
|
+
/**
|
|
13
|
+
* Old password is incorrect. This case might occure when:
|
|
14
|
+
* (1) user makes an error while typing the old password, or
|
|
15
|
+
* (2) user doesn't remember their current password and tries a wrong one, or
|
|
16
|
+
* (3) user has changed the password in another instance of the app, the `logout`
|
|
17
|
+
* message has not been received on a websocket (for any reason), and
|
|
18
|
+
* the current `grant token` has not expired yet.
|
|
19
|
+
*/
|
|
20
|
+
PasswordChangeResult["IncorrectOldPassword"] = "incorrect_old_password";
|
|
21
|
+
/** HTTP request is not authenticated with `refresh token`, or user no longer exists. */
|
|
22
|
+
PasswordChangeResult["Unauthorized"] = "unauthorized";
|
|
23
|
+
})(PasswordChangeResult || (PasswordChangeResult = {}));
|
|
24
|
+
export default async function changePassword(beacon, oldPassword, newPassword) {
|
|
25
|
+
const grantToken = getGrantToken();
|
|
26
|
+
if (!grantToken) {
|
|
27
|
+
// This situation is impossible: if a logout event is received on a websocket,
|
|
28
|
+
// this modal window is closed automatically; thus the user has no chance to start
|
|
29
|
+
// a saving operation.
|
|
30
|
+
logInvariant('Invariant: grant token must exist while changing password');
|
|
31
|
+
return PasswordChangeResult.Unauthorized;
|
|
32
|
+
}
|
|
33
|
+
const url = `${getApiBaseUrl()}/private/user/change-password`;
|
|
34
|
+
const options = {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${grantToken}`,
|
|
38
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
beacon,
|
|
42
|
+
currPassword: oldPassword,
|
|
43
|
+
newPassword
|
|
44
|
+
}),
|
|
45
|
+
credentials: getRequestCredentialsMode(),
|
|
46
|
+
cache: 'no-store',
|
|
47
|
+
redirect: 'error'
|
|
48
|
+
};
|
|
49
|
+
const response = await request(url, options, {
|
|
50
|
+
// Explicitly avoid retries as this operation is not idempotent
|
|
51
|
+
maxRetries: 0
|
|
52
|
+
});
|
|
53
|
+
if (!response) {
|
|
54
|
+
return PasswordChangeResult.NetworkFailure;
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 401) {
|
|
57
|
+
const upgradedGrantToken = await innerToplineService.upgradeGrantToken();
|
|
58
|
+
if (!upgradedGrantToken) {
|
|
59
|
+
innerToplineService.processLogout();
|
|
60
|
+
return PasswordChangeResult.Unauthorized;
|
|
61
|
+
}
|
|
62
|
+
return await changePassword(beacon, oldPassword, newPassword);
|
|
63
|
+
}
|
|
64
|
+
let payload;
|
|
65
|
+
try {
|
|
66
|
+
payload = await response.json();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logError(new WebError({
|
|
69
|
+
name: 'UnexpectedError',
|
|
70
|
+
...(e instanceof Error ? {
|
|
71
|
+
cause: e
|
|
72
|
+
} : {})
|
|
73
|
+
}, `parsing 200 response of request to ${url} failed`));
|
|
74
|
+
return PasswordChangeResult.NetworkFailure;
|
|
75
|
+
}
|
|
76
|
+
const {
|
|
77
|
+
result
|
|
78
|
+
} = payload;
|
|
79
|
+
switch (result) {
|
|
80
|
+
case 'OK':
|
|
81
|
+
{
|
|
82
|
+
const {
|
|
83
|
+
grantToken,
|
|
84
|
+
refreshToken
|
|
85
|
+
} = payload.tokens;
|
|
86
|
+
setGrantToken(grantToken);
|
|
87
|
+
setRefreshToken(refreshToken);
|
|
88
|
+
await Promise.all([updateGrantToken(grantToken), updateRefreshToken(refreshToken)]);
|
|
89
|
+
return PasswordChangeResult.Success;
|
|
90
|
+
}
|
|
91
|
+
case 'NO_USER':
|
|
92
|
+
innerToplineService.processLogout();
|
|
93
|
+
return PasswordChangeResult.Unauthorized;
|
|
94
|
+
case 'INVALID_PASSWORD':
|
|
95
|
+
return PasswordChangeResult.IncorrectOldPassword;
|
|
96
|
+
default:
|
|
97
|
+
{
|
|
98
|
+
const _exhaustiveCheck = result;
|
|
99
|
+
return _exhaustiveCheck;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useContext, useState, useCallback } from 'react';
|
|
3
3
|
import Modal from '../../../shared/components/modal/index.js';
|
|
4
4
|
import Root from './root/index.js';
|
|
5
|
+
import PasswordChangeMarshal from './password-change-marshal/index.js';
|
|
5
6
|
import { ToplineContext } from '../../shell/index.js';
|
|
6
7
|
var LocalStep;
|
|
7
8
|
(function (LocalStep) {
|
|
@@ -17,6 +18,7 @@ export default function Profile(props) {
|
|
|
17
18
|
} = useContext(ToplineContext);
|
|
18
19
|
const [localStep, setLocalStep] = useState(LocalStep.Root);
|
|
19
20
|
const handleSwitchToPasswordChange = useCallback(() => setLocalStep(LocalStep.PasswordChange), []);
|
|
21
|
+
const handleReturningFromPasswordChange = useCallback(() => setLocalStep(LocalStep.Root), []);
|
|
20
22
|
const getContents = user => {
|
|
21
23
|
switch (localStep) {
|
|
22
24
|
case LocalStep.Root:
|
|
@@ -26,8 +28,8 @@ export default function Profile(props) {
|
|
|
26
28
|
onCancel: onClose
|
|
27
29
|
});
|
|
28
30
|
case LocalStep.PasswordChange:
|
|
29
|
-
return _jsx(
|
|
30
|
-
|
|
31
|
+
return _jsx(PasswordChangeMarshal, {
|
|
32
|
+
onCancel: handleReturningFromPasswordChange
|
|
31
33
|
});
|
|
32
34
|
default:
|
|
33
35
|
{
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
3
|
+
export default function PasswordChangeConfirmation(props) {
|
|
4
|
+
const {
|
|
5
|
+
onOk
|
|
6
|
+
} = props;
|
|
7
|
+
return _jsxs(_Fragment, {
|
|
8
|
+
children: [_jsx("h2", {
|
|
9
|
+
className: 'topline_profile_subtitle',
|
|
10
|
+
children: "Смена пароля"
|
|
11
|
+
}), _jsx("p", {
|
|
12
|
+
className: 'topline_profile_confirmationMesage',
|
|
13
|
+
children: "Ваш пароль успешно изменён."
|
|
14
|
+
}), _jsx("div", {
|
|
15
|
+
className: 'topline_profile_confirmationButtonsCont',
|
|
16
|
+
children: _jsx(Button, {
|
|
17
|
+
label: 'OK',
|
|
18
|
+
onClick: onOk
|
|
19
|
+
})
|
|
20
|
+
})]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { InputField } from '@peassoft/mnr-web-ui-kit/input-field/index.js';
|
|
4
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
5
|
+
import { ErrorMessage } from '@peassoft/mnr-web-ui-kit/error-message/index.js';
|
|
6
|
+
import useStateWithValidationReset from './use-state-with-validation-reset.js';
|
|
7
|
+
import changePassword, { PasswordChangeResult } from '../../actions/change-password/index.js';
|
|
8
|
+
import { createPasswordChangeBeacon } from '../../../../modules/password-change-beacon/index.js';
|
|
9
|
+
var NewPasswordConfirmationError;
|
|
10
|
+
(function (NewPasswordConfirmationError) {
|
|
11
|
+
NewPasswordConfirmationError["None"] = "none";
|
|
12
|
+
NewPasswordConfirmationError["Empty"] = "empty";
|
|
13
|
+
NewPasswordConfirmationError["NotMatching"] = "not_matching";
|
|
14
|
+
})(NewPasswordConfirmationError || (NewPasswordConfirmationError = {}));
|
|
15
|
+
export default function PasswordChangeForm(props) {
|
|
16
|
+
const {
|
|
17
|
+
onSuccess,
|
|
18
|
+
onCancel
|
|
19
|
+
} = props;
|
|
20
|
+
const [isInProgress, setIsInProgress] = useState(false);
|
|
21
|
+
const [hasOldPasswordValidationError, setHasOldPasswordValidationError] = useState(false);
|
|
22
|
+
const [hasNewPasswordValidationError, setHasNewPasswordValidationError] = useState(false);
|
|
23
|
+
const [newPasswordConfirmationError, setNewPasswordConfirmationError] = useState(NewPasswordConfirmationError.None);
|
|
24
|
+
const [oldPassword, setOldPassword] = useStateWithValidationReset('', setHasOldPasswordValidationError, false);
|
|
25
|
+
const [newPassword, setNewPassword] = useStateWithValidationReset('', setHasNewPasswordValidationError, false);
|
|
26
|
+
const [newPasswordConfirmation, setNewPasswordConfirmation] = useStateWithValidationReset('', setNewPasswordConfirmationError, NewPasswordConfirmationError.None);
|
|
27
|
+
const [savingError, setSavingError] = useState('');
|
|
28
|
+
const newPasswordConfirmationErrorMessage = useMemo(() => {
|
|
29
|
+
switch (newPasswordConfirmationError) {
|
|
30
|
+
case NewPasswordConfirmationError.None:
|
|
31
|
+
return '';
|
|
32
|
+
case NewPasswordConfirmationError.Empty:
|
|
33
|
+
return "Необходимо повторить ввод нового пароля.";
|
|
34
|
+
case NewPasswordConfirmationError.NotMatching:
|
|
35
|
+
return "Подтверждение нового пароля не совпадает.";
|
|
36
|
+
default:
|
|
37
|
+
{
|
|
38
|
+
const _exhaustiveCheck = newPasswordConfirmationError;
|
|
39
|
+
return _exhaustiveCheck;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [newPasswordConfirmationError]);
|
|
43
|
+
const validateInput = useCallback(() => {
|
|
44
|
+
let isCorrect = true;
|
|
45
|
+
if (!oldPassword) {
|
|
46
|
+
isCorrect = false;
|
|
47
|
+
setHasOldPasswordValidationError(true);
|
|
48
|
+
}
|
|
49
|
+
if (!newPassword) {
|
|
50
|
+
isCorrect = false;
|
|
51
|
+
setHasNewPasswordValidationError(true);
|
|
52
|
+
}
|
|
53
|
+
if (!newPasswordConfirmation) {
|
|
54
|
+
isCorrect = false;
|
|
55
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.Empty);
|
|
56
|
+
} else if (newPasswordConfirmation !== newPassword) {
|
|
57
|
+
isCorrect = false;
|
|
58
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.NotMatching);
|
|
59
|
+
}
|
|
60
|
+
return isCorrect;
|
|
61
|
+
}, [oldPassword, newPassword, newPasswordConfirmation]);
|
|
62
|
+
const resetErrors = useCallback(() => {
|
|
63
|
+
setHasOldPasswordValidationError(false);
|
|
64
|
+
setHasNewPasswordValidationError(false);
|
|
65
|
+
setNewPasswordConfirmationError(NewPasswordConfirmationError.None);
|
|
66
|
+
setSavingError('');
|
|
67
|
+
}, []);
|
|
68
|
+
const handleSave = useCallback(async () => {
|
|
69
|
+
if (!validateInput()) return;
|
|
70
|
+
resetErrors();
|
|
71
|
+
setIsInProgress(true);
|
|
72
|
+
const beacon = createPasswordChangeBeacon();
|
|
73
|
+
const result = await changePassword(beacon, oldPassword, newPassword);
|
|
74
|
+
switch (result) {
|
|
75
|
+
case PasswordChangeResult.Success:
|
|
76
|
+
onSuccess();
|
|
77
|
+
break;
|
|
78
|
+
case PasswordChangeResult.NetworkFailure:
|
|
79
|
+
setSavingError("При сохранении нового пароля произошла ошибка. Проверьте ваше подключение к интернету и/или попробуйте ещё раз.");
|
|
80
|
+
setIsInProgress(false);
|
|
81
|
+
break;
|
|
82
|
+
case PasswordChangeResult.IncorrectOldPassword:
|
|
83
|
+
setSavingError("Текущий пароль неверный.");
|
|
84
|
+
setIsInProgress(false);
|
|
85
|
+
break;
|
|
86
|
+
case PasswordChangeResult.Unauthorized:
|
|
87
|
+
// Do nothing here. Everything needed has already been done by the action.
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
{
|
|
91
|
+
const _exhaustiveCheck = result;
|
|
92
|
+
return _exhaustiveCheck;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [validateInput, resetErrors, onSuccess, oldPassword, newPassword]);
|
|
96
|
+
return _jsxs("form", {
|
|
97
|
+
children: [_jsx("h2", {
|
|
98
|
+
className: 'topline_profile_subtitle',
|
|
99
|
+
children: "Смена пароля"
|
|
100
|
+
}), _jsx(InputField, {
|
|
101
|
+
type: 'password',
|
|
102
|
+
autocompleteAttribute: 'current-password',
|
|
103
|
+
label: "Ваш текущий пароль. Обязательное поле.",
|
|
104
|
+
value: oldPassword,
|
|
105
|
+
errorMessage: hasOldPasswordValidationError ? "Введите ваш текущий пароль." : undefined,
|
|
106
|
+
onChange: setOldPassword
|
|
107
|
+
}), _jsx(InputField, {
|
|
108
|
+
type: 'password',
|
|
109
|
+
autocompleteAttribute: 'new-password',
|
|
110
|
+
label: "Новый пароль. Обязательное поле.",
|
|
111
|
+
value: newPassword,
|
|
112
|
+
errorMessage: hasNewPasswordValidationError ? "Введите новый пароль." : undefined,
|
|
113
|
+
onChange: setNewPassword
|
|
114
|
+
}), _jsx(InputField, {
|
|
115
|
+
type: 'password',
|
|
116
|
+
label: "Введите новый пароль ещё раз. Обязательное поле.",
|
|
117
|
+
value: newPasswordConfirmation,
|
|
118
|
+
errorMessage: newPasswordConfirmationErrorMessage || undefined,
|
|
119
|
+
onChange: setNewPasswordConfirmation
|
|
120
|
+
}), !!savingError && _jsx(ErrorMessage, {
|
|
121
|
+
variant: 'standalone',
|
|
122
|
+
children: savingError
|
|
123
|
+
}), _jsxs("div", {
|
|
124
|
+
className: 'topline_profile_buttonsCont',
|
|
125
|
+
children: [_jsx(Button, {
|
|
126
|
+
label: "Сохранить новый пароль",
|
|
127
|
+
isInProgress: isInProgress,
|
|
128
|
+
onClick: handleSave
|
|
129
|
+
}), !isInProgress && _jsx(Button, {
|
|
130
|
+
label: "Отменить",
|
|
131
|
+
variant: 'secondary',
|
|
132
|
+
onClick: onCancel
|
|
133
|
+
})]
|
|
134
|
+
})]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
export default function useStateWithValidationReset(initialState, validationSetFn, validationResetValue) {
|
|
3
|
+
const [value, setValue] = useState(initialState);
|
|
4
|
+
const handleChange = useCallback(newVal => {
|
|
5
|
+
validationSetFn(validationResetValue);
|
|
6
|
+
setValue(newVal);
|
|
7
|
+
}, [validationSetFn, validationResetValue]);
|
|
8
|
+
return [value, handleChange];
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import PasswordChangeForm from '../password-change-form/index.js';
|
|
4
|
+
import PasswordChangeConfirmation from '../password-change-confirmation/index.js';
|
|
5
|
+
var LocalStep;
|
|
6
|
+
(function (LocalStep) {
|
|
7
|
+
LocalStep["Form"] = "form";
|
|
8
|
+
LocalStep["SuccessConfirmation"] = "success_confirmation";
|
|
9
|
+
})(LocalStep || (LocalStep = {}));
|
|
10
|
+
export default function PasswordChangeMarshal(props) {
|
|
11
|
+
const {
|
|
12
|
+
onCancel
|
|
13
|
+
} = props;
|
|
14
|
+
const [localStep, setLocalStep] = useState(LocalStep.Form);
|
|
15
|
+
const handleSuccess = useCallback(() => setLocalStep(LocalStep.SuccessConfirmation), []);
|
|
16
|
+
switch (localStep) {
|
|
17
|
+
case LocalStep.Form:
|
|
18
|
+
return _jsx(PasswordChangeForm, {
|
|
19
|
+
onSuccess: handleSuccess,
|
|
20
|
+
onCancel: onCancel
|
|
21
|
+
});
|
|
22
|
+
case LocalStep.SuccessConfirmation:
|
|
23
|
+
return _jsx(PasswordChangeConfirmation, {
|
|
24
|
+
onOk: onCancel
|
|
25
|
+
});
|
|
26
|
+
default:
|
|
27
|
+
{
|
|
28
|
+
const _exhaustiveCheck = localStep;
|
|
29
|
+
return _exhaustiveCheck;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peassoft/mnr-web-topline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Peassoft Topline widget for mem'n'rev web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/stampit": "^4.3.4",
|
|
39
39
|
"autoprefixer": "^10.3.4",
|
|
40
40
|
"clean-webpack-plugin": "^4.0.0",
|
|
41
|
-
"copy-webpack-plugin": "^
|
|
41
|
+
"copy-webpack-plugin": "^13.0.0",
|
|
42
42
|
"cpy-cli": "^5.0.0",
|
|
43
43
|
"css-loader": "^7.1.2",
|
|
44
44
|
"eslint": "^9.8.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@memnrev/web-error": "^0.3.0",
|
|
66
66
|
"email-validator": "^2.0.4",
|
|
67
67
|
"md5": "^2.3.0",
|
|
68
|
-
"react-error-boundary": "^
|
|
68
|
+
"react-error-boundary": "^6.0.0",
|
|
69
69
|
"stampit": "^4.3.2",
|
|
70
70
|
"uuid": "^11.1.0"
|
|
71
71
|
},
|