mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-28 16:07:49 +01:00
basic password change
This commit is contained in:
4668
frontend/package-lock.json
generated
4668
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -1,7 +1,76 @@
|
||||
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 function AccountComponent() {
|
||||
return <div>Hello</div>;
|
||||
export interface IAccountComponentProps {
|
||||
username: string;
|
||||
changePass: (password: string) => void;
|
||||
}
|
||||
|
||||
export { AccountComponent as Account };
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
1457
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -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",
|
||||
|
||||
@@ -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() };
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user