This commit is contained in:
2019-01-03 19:33:16 +03:00
parent a7a36d2199
commit ca773d5b2d
11 changed files with 243 additions and 36 deletions

View File

@@ -1,25 +1,83 @@
import { Alignment, Button, Classes, Navbar } from "@blueprintjs/core"; import {
Alignment,
Button,
Classes,
Menu,
Navbar,
Popover,
} from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { IUserJSON } from "~../../src/entity/User";
import { IAppState } from "~redux/reducers";
import { logoutUser } from "~redux/user/actions";
export function Home() { interface IHomeProps {
return ( user: IUserJSON | null;
<> logout: () => void;
<Navbar>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>Writer</Navbar.Heading>
<Navbar.Divider />
<Button
className={Classes.MINIMAL}
icon="home"
text="Home"
/>
<Button
className={Classes.MINIMAL}
icon="document"
text="Files"
/>
</Navbar.Group>
</Navbar>
</>
);
} }
export class HomeComponent extends React.PureComponent<IHomeProps> {
constructor(props: IHomeProps) {
super(props);
}
public render() {
return (
<>
{this.props.user && (
<>
<Navbar>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>Writer</Navbar.Heading>
<Navbar.Divider />
<Button
className={Classes.MINIMAL}
icon="home"
text="Home"
/>
<Button
className={Classes.MINIMAL}
icon="document"
text="Files"
/>
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<Popover
target={
<Button id="userButton">
{this.props.user.username}
</Button>
}
content={
<Menu>
<Menu.Item
icon="log-out"
text="Logout"
onClick={this.props.logout}
/>
</Menu>
}
/>
</Navbar.Group>
</Navbar>
</>
)}
</>
);
}
}
function mapStateToProps(state: IAppState) {
return { user: state.user.user };
}
function mapDispatchToProps(dispatch: Dispatch) {
return { logout: () => dispatch(logoutUser()) };
}
export const Home = connect(
mapStateToProps,
mapDispatchToProps,
)(HomeComponent);

View File

@@ -0,0 +1,5 @@
import { fetchJSON, fetchJSONAuth } from "../utils";
export async function fetchUser() {
return fetchJSONAuth("/users/user", "GET");
}

View File

@@ -17,7 +17,7 @@ const root = "http://localhost:3000";
export async function fetchJSON( export async function fetchJSON(
path: string, path: string,
method: string, method: string,
body: string | object, body?: string | object,
headers?: Record<string, string>, headers?: Record<string, string>,
) { ) {
if (typeof body === "object") { if (typeof body === "object") {
@@ -38,7 +38,7 @@ export async function fetchJSON(
export async function fetchJSONAuth( export async function fetchJSONAuth(
path: string, path: string,
method: string, method: string,
body: string | object, body?: string | object,
headers?: object, headers?: object,
) { ) {
if (token) { if (token) {

View File

@@ -1,5 +1,7 @@
import { Action } from "redux"; import { Action } from "redux";
import { IUserAuthJSON } from "~../../src/entity/User";
export enum AuthTypes { export enum AuthTypes {
AUTH_START = "AUTH_START", AUTH_START = "AUTH_START",
SIGNUP_START = "SIGNUP_START", SIGNUP_START = "SIGNUP_START",
@@ -27,9 +29,7 @@ export interface ISignupStartActionAction extends Action {
export interface IAuthSuccessActionAction extends Action { export interface IAuthSuccessActionAction extends Action {
type: AuthTypes.AUTH_SUCCESS; type: AuthTypes.AUTH_SUCCESS;
payload: { payload: IUserAuthJSON;
jwt: string;
};
} }
export interface IAuthFailureActionAction extends Action { export interface IAuthFailureActionAction extends Action {
@@ -65,8 +65,8 @@ export function signupStart(
}; };
} }
export function authSuccess(jwt: string): IAuthSuccessActionAction { export function authSuccess(user: IUserAuthJSON): IAuthSuccessActionAction {
return { type: AuthTypes.AUTH_SUCCESS, payload: { jwt } }; return { type: AuthTypes.AUTH_SUCCESS, payload: user };
} }
export function authFail(error: string): IAuthFailureActionAction { export function authFail(error: string): IAuthFailureActionAction {

View File

@@ -1,5 +1,7 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { setToken } from "~redux/api/utils";
import { UserAction, UserTypes } from "~redux/user/actions";
import { AuthAction, AuthTypes } from "./actions"; import { AuthAction, AuthTypes } from "./actions";
export interface IAuthState { export interface IAuthState {
@@ -18,7 +20,7 @@ const defaultAuthState: IAuthState = {
export const authReducer: Reducer<IAuthState, AuthAction> = ( export const authReducer: Reducer<IAuthState, AuthAction> = (
state: IAuthState = defaultAuthState, state: IAuthState = defaultAuthState,
action: AuthAction, action: AuthAction | UserAction,
) => { ) => {
switch (action.type) { switch (action.type) {
case AuthTypes.AUTH_START: case AuthTypes.AUTH_START:
@@ -26,16 +28,25 @@ export const authReducer: Reducer<IAuthState, AuthAction> = (
return { ...defaultAuthState, inProgress: true }; return { ...defaultAuthState, inProgress: true };
break; break;
case AuthTypes.AUTH_SUCCESS: case AuthTypes.AUTH_SUCCESS:
case UserTypes.USER_GET_SUCCESS:
setToken(action.payload.jwt);
return { return {
...defaultAuthState, ...defaultAuthState,
jwt: action.payload.jwt, jwt: action.payload.jwt,
}; };
break; break;
case UserTypes.USER_GET_FAIL:
if (action.payload.logout) {
return defaultAuthState;
}
case AuthTypes.AUTH_FAIL: case AuthTypes.AUTH_FAIL:
return { ...defaultAuthState, formError: action.payload.error }; return { ...defaultAuthState, formError: action.payload.error };
break; break;
case AuthTypes.AUTH_START_FORM_SPINNER: case AuthTypes.AUTH_START_FORM_SPINNER:
return { ...state, formSpinner: true }; return { ...state, formSpinner: true };
case UserTypes.USER_LOGOUT:
return defaultAuthState;
break;
default: default:
return state; return state;
break; break;

View File

@@ -9,7 +9,6 @@ import {
takeLatest, takeLatest,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { login, signup } from "~redux/api/auth"; import { login, signup } from "~redux/api/auth";
import { setToken } from "~redux/api/utils";
import { import {
authFail, authFail,
@@ -43,13 +42,12 @@ function* authStart(action: IAuthStartActionAction) {
} }
if (response.data) { if (response.data) {
const user = response.data; const user = response.data;
yield call(setToken, user.jwt); yield put(authSuccess(user));
yield put(authSuccess(user.jwt));
} else { } else {
yield put(authFail(response.error)); yield put(authFail(response.error));
} }
} catch (e) { } catch (e) {
yield put(authFail(e.toString())); yield put(authFail("Internal error"));
} }
} }
@@ -71,8 +69,7 @@ function* signupStart(action: ISignupStartActionAction) {
} }
if (response.data) { if (response.data) {
const user = response.data; const user = response.data;
yield call(setToken, user.jwt); yield put(authSuccess(user));
yield put(authSuccess(user.jwt));
} else { } else {
yield put(authFail(response.error)); yield put(authFail(response.error));
} }

View File

@@ -2,9 +2,11 @@ import { combineReducers } from "redux";
import { persistReducer } from "redux-persist"; import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage"; import storage from "redux-persist/lib/storage";
import { authReducer, IAuthState } from "~redux/auth/reducer"; import { authReducer, IAuthState } from "~redux/auth/reducer";
import { IUserState, userReducer } from "./user/reducer";
export interface IAppState { export interface IAppState {
auth: IAuthState; auth: IAuthState;
user: IUserState;
} }
const authPersistConfig = { const authPersistConfig = {
@@ -14,4 +16,5 @@ const authPersistConfig = {
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer), auth: persistReducer(authPersistConfig, authReducer),
user: userReducer,
}); });

View File

@@ -5,6 +5,8 @@ import { rootReducer } from "~redux/reducers";
import { setToken } from "./api/utils"; import { setToken } from "./api/utils";
import { authSaga } from "./auth/sagas"; import { authSaga } from "./auth/sagas";
import { getUser } from "./user/actions";
import { userSaga } from "./user/sagas";
const sagaMiddleware = createSagaMiddlware(); const sagaMiddleware = createSagaMiddlware();
@@ -14,7 +16,9 @@ export const persistor = persistStore(store, null, () => {
const state = store.getState(); const state = store.getState();
if (state.auth.jwt) { if (state.auth.jwt) {
setToken(state.auth.jwt); setToken(state.auth.jwt);
store.dispatch(getUser());
} }
}); });
sagaMiddleware.run(authSaga); sagaMiddleware.run(authSaga);
sagaMiddleware.run(userSaga);

View File

@@ -0,0 +1,55 @@
import { Action } from "redux";
import { IUserAuthJSON, IUserJSON } from "~../../src/entity/User";
export enum UserTypes {
USER_GET = "USER_GET",
USER_GET_SUCCESS = "USER_GET_SUCCESS",
USER_GET_FAIL = "USER_GET_FAIL",
USER_LOGOUT = "USER_LOGOUT",
}
export interface IUserGetAction extends Action {
type: UserTypes.USER_GET;
}
export interface IUserLogoutAction extends Action {
type: UserTypes.USER_LOGOUT;
}
export interface IUserGetSuccessAction extends Action {
type: UserTypes.USER_GET_SUCCESS;
payload: IUserAuthJSON;
}
export interface IUserGetFailAction extends Action {
type: UserTypes.USER_GET_FAIL;
payload: {
error: string;
logout: boolean;
};
}
export function getUser(): IUserGetAction {
return { type: UserTypes.USER_GET };
}
export function logoutUser(): IUserLogoutAction {
return { type: UserTypes.USER_LOGOUT };
}
export function getUserSuccess(user: IUserAuthJSON): IUserGetSuccessAction {
return { type: UserTypes.USER_GET_SUCCESS, payload: user };
}
export function getUserFail(
error: string,
logout: boolean,
): IUserGetFailAction {
return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } };
}
export type UserAction =
| IUserGetAction
| IUserGetSuccessAction
| IUserGetFailAction
| IUserLogoutAction;

View File

@@ -0,0 +1,36 @@
import { Reducer } from "react";
import { IUserJSON } from "~../../src/entity/User";
import { AuthAction, AuthTypes } from "~redux/auth/actions";
import { UserAction, UserTypes } from "./actions";
export interface IUserState {
user: IUserJSON | null;
}
const defaultUserState: IUserState = {
user: null,
};
export const userReducer: Reducer<IUserState, AuthAction> = (
state: IUserState = defaultUserState,
action: AuthAction | UserAction,
) => {
switch (action.type) {
case AuthTypes.AUTH_SUCCESS:
case UserTypes.USER_GET_SUCCESS:
return {
...defaultUserState,
user: action.payload,
};
break;
case UserTypes.USER_GET_FAIL:
return defaultUserState;
break;
case UserTypes.USER_LOGOUT:
return defaultUserState;
break;
default:
return state;
break;
}
};

View File

@@ -0,0 +1,38 @@
import { delay } from "redux-saga";
import {
all,
call,
cancel,
fork,
put,
race,
takeLatest,
} from "redux-saga/effects";
import { fetchUser } from "~redux/api/user";
import { getUserFail, getUserSuccess, UserTypes } from "./actions";
function* getUser() {
try {
const { response, timeout } = yield race({
response: call(fetchUser),
timeout: call(delay, 10000),
});
if (timeout) {
yield put(getUserFail("Timeout", false));
return;
}
if (response.data) {
const user = response.data;
yield put(getUserSuccess(user));
} else {
yield put(getUserFail(response.error, true));
}
} catch (e) {
yield put(getUserFail("Internal error", false));
}
}
export function* userSaga() {
yield all([takeLatest(UserTypes.USER_GET, getUser)]);
}