basic password change

This commit is contained in:
2020-06-09 12:55:06 +03:00
parent 317b113398
commit 51a6803fe2
12 changed files with 4524 additions and 1976 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,38 +6,38 @@
"test": "jest"
},
"devDependencies": {
"@types/autoprefixer": "^9.7.1",
"@types/autoprefixer": "^9.7.2",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/highlight.js": "^9.12.3",
"@types/jest": "^25.1.4",
"@types/highlight.js": "^9.12.4",
"@types/jest": "^25.2.3",
"@types/parcel-bundler": "^1.12.1",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/react-redux": "^7.1.7",
"@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-router": "^5.1.7",
"@types/react-router-dom": "^5.1.5",
"@types/sass": "^1.16.0",
"autoprefixer": "^9.7.4",
"autoprefixer": "^9.8.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"jest": "^25.1.0",
"jest": "^26.0.1",
"parcel-bundler": "^1.12.4",
"postcss-modules": "^1.5.0",
"postcss-modules": "^2.0.0",
"redux-devtools-extension": "^2.13.8",
"sass": "^1.26.3",
"ts-jest": "^25.2.1"
"sass": "^1.26.8",
"ts-jest": "^26.1.0"
},
"dependencies": {
"@blueprintjs/core": "^3.24.0",
"@blueprintjs/icons": "^3.14.0",
"highlight.js": "^9.18.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"@blueprintjs/core": "^3.28.1",
"@blueprintjs/icons": "^3.18.0",
"highlight.js": "^10.0.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.0",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-spring": "^8.0.27",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",

View File

@@ -1,7 +1,76 @@
import * as React from "react";
export function AccountComponent() {
return <div>Hello</div>;
}
export { AccountComponent as Account };
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { IAppState } from "~redux/reducers";
import { userPassChange } from "~redux/user/actions";
export interface IAccountComponentProps {
username: string;
changePass: (password: string) => void;
}
export function AccountComponent(props: IAccountComponentProps) {
const [pass, setPass] = React.useState("");
return (
<Card className="AuthForm" elevation={2}>
<form
onSubmit={(e: React.FormEvent<any>) => {
e.preventDefault();
if (pass.trim()) {
props.changePass(pass);
}
}}
>
<div className="header">
<H2>Account</H2>
</div>
<FormGroup label="Username">
<InputGroup
name="username"
leftIcon="person"
disabled={true}
value={props.username}
/>
</FormGroup>
<FormGroup label="Password">
<InputGroup
name="password"
type="password"
leftIcon="key"
value={pass}
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setPass(e.currentTarget.value)
}
/>
</FormGroup>
<div className="footer">
<Button
className="submit"
intent="primary"
icon="floppy-disk"
type="submit"
>
Save
</Button>
</div>
</form>
</Card>
);
}
function mapStateToProps(state: IAppState) {
return { username: state.user.user.username };
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
changePass: (password: string) => dispatch(userPassChange(password)),
};
}
export const Account = connect(
mapStateToProps,
mapDispatchToProps,
)(AccountComponent);

View File

@@ -24,3 +24,19 @@ export function showSharedToast() {
timeout: 2000,
});
}
export function showPasswordSavedToast() {
AppToaster.show({
message: "Password saved!",
intent: "success",
timeout: 2000,
});
}
export function showPasswordNotSavedToast(error: string) {
AppToaster.show({
message: "Password not saved! " + error,
intent: "danger",
timeout: 2000,
});
}

View File

@@ -3,3 +3,7 @@ import { fetchJSON, fetchJSONAuth } from "../utils";
export async function fetchUser() {
return fetchJSONAuth("/users/user", "GET");
}
export async function changeUserPassword(newPassword: string) {
return fetchJSONAuth("/users/edit", "POST", { password: newPassword });
}

View File

@@ -1,11 +1,15 @@
import { Action } from "redux";
import { IUserAuthJSON, IUserJSON } from "~../../src/entity/User";
import { showPasswordNotSavedToast, showPasswordSavedToast } from "~AppToaster";
export enum UserTypes {
USER_GET = "USER_GET",
USER_GET_SUCCESS = "USER_GET_SUCCESS",
USER_GET_FAIL = "USER_GET_FAIL",
USER_LOGOUT = "USER_LOGOUT",
USER_PASS_CHANGE = "USER_PASS_CHANGE",
USER_PASS_CHANGE_SUCCESS = "USER_PASS_CHANGE_SUCCESS",
USER_PASS_CHANGE_FAIL = "USER_PASS_CHANGE_FAIL",
}
export interface IUserGetAction extends Action {
@@ -29,6 +33,24 @@ export interface IUserGetFailAction extends Action {
};
}
export interface IUserPassChangeAction extends Action {
type: UserTypes.USER_PASS_CHANGE;
password: string;
}
export interface IUserPassChangeSuccessAction extends Action {
type: UserTypes.USER_PASS_CHANGE_SUCCESS;
payload: IUserAuthJSON;
}
export interface IUserPassChangeFailAction extends Action {
type: UserTypes.USER_PASS_CHANGE_FAIL;
payload: {
error: string;
logout: boolean;
};
}
export function getUser(): IUserGetAction {
return { type: UserTypes.USER_GET };
}
@@ -48,8 +70,33 @@ export function getUserFail(
return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } };
}
export function userPassChange(password: string): IUserPassChangeAction {
return { type: UserTypes.USER_PASS_CHANGE, password };
}
export function userPassChangeSuccess(
user: IUserAuthJSON,
): IUserPassChangeSuccessAction {
showPasswordSavedToast();
return { type: UserTypes.USER_PASS_CHANGE_SUCCESS, payload: user };
}
export function userPassChangeFail(
error: string,
logout: boolean,
): IUserPassChangeFailAction {
showPasswordNotSavedToast(error);
return {
type: UserTypes.USER_PASS_CHANGE_FAIL,
payload: { error, logout },
};
}
export type UserAction =
| IUserGetAction
| IUserGetSuccessAction
| IUserGetFailAction
| IUserLogoutAction;
| IUserLogoutAction
| IUserPassChangeAction
| IUserPassChangeFailAction
| IUserPassChangeSuccessAction;

View File

@@ -18,12 +18,14 @@ export const userReducer: Reducer<IUserState, AuthAction> = (
switch (action.type) {
case AuthTypes.AUTH_SUCCESS:
case UserTypes.USER_GET_SUCCESS:
case UserTypes.USER_PASS_CHANGE_SUCCESS:
return {
...defaultUserState,
user: action.payload,
};
break;
case UserTypes.USER_GET_FAIL:
case UserTypes.USER_PASS_CHANGE_FAIL:
return defaultUserState;
break;
case UserTypes.USER_LOGOUT:

View File

@@ -1,7 +1,14 @@
import { all, call, delay, put, race, takeLatest } from "redux-saga/effects";
import { fetchUser } from "~redux/api/user";
import { changeUserPassword, fetchUser } from "~redux/api/user";
import { getUserFail, getUserSuccess, UserTypes } from "./actions";
import {
getUserFail,
getUserSuccess,
IUserPassChangeAction,
userPassChangeFail,
userPassChangeSuccess,
UserTypes,
} from "./actions";
function* getUser() {
try {
@@ -25,6 +32,31 @@ function* getUser() {
}
}
export function* userSaga() {
yield all([takeLatest(UserTypes.USER_GET, getUser)]);
function* userPassChange(action: IUserPassChangeAction) {
try {
const { response, timeout } = yield race({
response: call(changeUserPassword, action.password),
timeout: delay(10000),
});
if (timeout) {
yield put(userPassChangeFail("Timeout", false));
return;
}
if (response.data) {
const user = response.data;
yield put(userPassChangeSuccess(user));
} else {
yield put(userPassChangeFail(response.error, true));
}
} catch (e) {
yield put(userPassChangeFail("Internal error", false));
}
}
export function* userSaga() {
yield all([
takeLatest(UserTypes.USER_GET, getUser),
takeLatest(UserTypes.USER_PASS_CHANGE, userPassChange),
]);
}