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),
]);
}

1457
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,66 @@
{
"name": "writer-backend",
"devDependencies": {
"@blueprintjs/tslint-config": "^2.0.0",
"@blueprintjs/tslint-config": "^3.0.0",
"@types/bcrypt": "^3.0.0",
"@types/chai": "^4.2.11",
"@types/concurrently": "^5.1.0",
"@types/eslint": "^6.1.8",
"@types/eslint-plugin-prettier": "^2.2.0",
"@types/jsonwebtoken": "^8.3.8",
"@types/koa": "^2.11.2",
"@types/concurrently": "^5.2.1",
"@types/eslint": "^6.8.1",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.11.3",
"@types/koa-logger": "^3.1.1",
"@types/koa-router": "^7.4.0",
"@types/koa-router": "^7.4.1",
"@types/koa-send": "^4.1.2",
"@types/koa-sslify": "^4.0.1",
"@types/koa-static": "^4.0.1",
"@types/koa__cors": "^3.0.1",
"@types/lodash": "^4.14.149",
"@types/lodash": "^4.14.155",
"@types/mocha": "^7.0.2",
"@types/mysql": "^2.15.9",
"@types/node": "^13.9.1",
"@types/prettier": "^1.19.0",
"@types/supertest": "^2.0.8",
"@types/mysql": "^2.15.13",
"@types/node": "^14.0.12",
"@types/prettier": "^2.0.1",
"@types/supertest": "^2.0.9",
"chai": "^4.2.0",
"eslint": "^6.8.0",
"eslint": "^7.2.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.21.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^2.5.0",
"husky": "^4.2.3",
"mocha": "^7.1.0",
"prettier": "^1.19.1",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.4",
"husky": "^4.2.5",
"mocha": "^7.2.0",
"prettier": "^2.0.5",
"supertest": "^4.0.2",
"ts-node-dev": "^1.0.0-pre.44",
"tslint": "^6.1.0",
"tslint": "^6.1.2",
"tslint-config-prettier": "^1.18.0",
"tslint-no-unused-expression-chai": "^0.1.4",
"tslint-plugin-prettier": "^2.2.0"
"tslint-plugin-prettier": "^2.3.0"
},
"dependencies": {
"@koa/cors": "^3.0.0",
"bcrypt": "^4.0.1",
"concurrently": "^5.1.0",
"@koa/cors": "^3.1.0",
"bcrypt": "^5.0.0",
"concurrently": "^5.2.0",
"cross-env": "^7.0.2",
"jsonwebtoken": "^8.5.1",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-jwt": "^3.6.0",
"koa": "^2.12.0",
"koa-body": "^4.1.3",
"koa-jwt": "^4.0.0",
"koa-logger": "^3.2.1",
"koa-router": "^8.0.8",
"koa-router": "^9.0.1",
"koa-send": "^5.0.0",
"koa-sslify": "^4.0.3",
"koa-static": "^5.0.0",
"lodash": "^4.17.15",
"mysql": "^2.18.1",
"reflect-metadata": "^0.1.13",
"ts-node": "8.6.2",
"ts-node": "8.10.2",
"tsconfig-paths": "^3.9.0",
"typeorm": "0.2.24",
"typescript": "3.8.3"
"typeorm": "0.2.25",
"typescript": "3.9.5"
},
"cacheDirectories": [
"frontend/node_modules",

View File

@@ -3,7 +3,7 @@ import { IUserJWT, User } from "~entity/User";
export const userRouter = new Router();
userRouter.get("/users/user", async ctx => {
userRouter.get("/users/user", async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -15,7 +15,7 @@ userRouter.get("/users/user", async ctx => {
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/login", async ctx => {
userRouter.post("/users/login", async (ctx) => {
const request = ctx.request as any;
if (!request.body) {
@@ -38,7 +38,7 @@ userRouter.post("/users/login", async ctx => {
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/signup", async ctx => {
userRouter.post("/users/signup", async (ctx) => {
const request = ctx.request as any;
if (!request.body) {
@@ -68,3 +68,35 @@ userRouter.post("/users/signup", async ctx => {
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/edit", async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const jwt = ctx.state.user as IUserJWT;
const user = await User.findOne(jwt.id);
const request = ctx.request as any;
if (!request.body) {
ctx.throw(400);
}
const { password } = request.body as {
password: string | undefined;
};
if (!password) {
ctx.throw(400);
}
await user.setPassword(password);
try {
await user.save();
} catch (e) {
ctx.throw(400);
}
ctx.body = { error: false, data: user.toAuthJSON() };
});

View File

@@ -102,4 +102,41 @@ describe("users", () => {
expect(response.body.error).to.be.equal("User already exists");
expect(response.body.data).to.be.false;
});
it("should change user's password", async () => {
const response = await request(callback)
.post("/users/edit")
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
"Content-Type": "application/json",
})
.send({
password: "User1NewPass",
})
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.error).to.be.false;
const loginResponse = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1NewPass" })
.expect("Content-Type", /json/)
.expect(200);
expect(loginResponse.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
expect(user).to.deep.equal(seed.user1.toJSON());
const badLoginResponse = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" })
.expect(404);
expect(badLoginResponse.body.error).to.be.equal("User not found");
expect(badLoginResponse.body.data).to.be.false;
});
});