login form

This commit is contained in:
2019-01-02 00:24:06 +03:00
parent 28fc6d957e
commit 1529e3a410
15 changed files with 330 additions and 79 deletions

View File

@@ -2,26 +2,96 @@ import "./Auth.scss";
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { authStart } from "~redux/auth/actions";
import { IAppState } from "~redux/reducers";
export function Login() {
return (
<>
<Card className="AuthForm" elevation={2}>
<form>
<H2>Login</H2>
<FormGroup label="Username">
<InputGroup leftIcon="person" />
</FormGroup>
<FormGroup label="Password">
<InputGroup leftIcon="key" />
</FormGroup>
<div className="buttons">
<Button className="submit" intent="primary">
Login
</Button>
</div>
</form>
</Card>
</>
);
interface ILoginComponentProps {
inProgress: boolean;
error: string;
login: (username: string, password: string) => void;
}
interface ILoginComponentState {
username: string;
password: string;
}
export class LoginComponent extends React.PureComponent<
ILoginComponentProps,
ILoginComponentState
> {
constructor(props: ILoginComponentProps) {
super(props);
this.submit = this.submit.bind(this);
this.updateFields = this.updateFields.bind(this);
this.state = { username: "", password: "" };
}
public submit() {
const { username, password } = this.state;
this.props.login(username, password);
}
public updateFields(e: React.FormEvent<HTMLInputElement>) {
const { value, name } = e.currentTarget;
this.setState({ ...this.state, [name]: value });
}
public render() {
return (
<>
<Card className="AuthForm" elevation={2}>
<form>
<H2>Login</H2>
<FormGroup label="Username">
<InputGroup
name="username"
value={this.state.username}
onChange={this.updateFields}
leftIcon="person"
/>
</FormGroup>
<FormGroup label="Password">
<InputGroup
name="password"
value={this.state.password}
onChange={this.updateFields}
type="password"
leftIcon="key"
/>
</FormGroup>
<div className="buttons">
<div id="errors">{this.props.error}</div>
<Button
className="submit"
intent="primary"
onClick={this.submit}
active={!this.props.inProgress}
>
Login
</Button>
</div>
</form>
</Card>
</>
);
}
}
function mapStateToProps(state: IAppState) {
return { inProgress: state.auth.inProgress, error: state.auth.error };
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
login: (username: string, password: string) =>
dispatch(authStart(username, password)),
};
}
export const Login = connect(
mapStateToProps,
mapDispatchToProps,
)(LoginComponent);

View File

@@ -0,0 +1,8 @@
import { fetchJSON } from "../utils";
export async function login(username: string, password: string) {
return fetchJSON("/users/login", "POST", {
username,
password,
});
}

View File

@@ -0,0 +1,50 @@
let token: string | null;
export function setToken(_token: string) {
token = _token;
}
export function getToken() {
return token;
}
export function deleteToken(_token: string) {
token = null;
}
const root = "http://localhost:3000";
export async function fetchJSON(
path: string,
method: string,
body: string | object,
headers?: Record<string, string>,
) {
if (typeof body === "object") {
body = JSON.stringify(body);
}
const response = await fetch(root + path, {
method,
body,
headers: {
...headers,
"Content-Type": "application/json",
},
});
const json = await response.json();
return json;
}
export async function fetchJSONAuth(
path: string,
method: string,
body: string | object,
headers?: object,
) {
if (token) {
return fetchJSON(path, method, body, {
...headers,
Authorization: `Bearer ${token}`,
});
}
}

View File

@@ -1,10 +1,49 @@
import { Action } from "redux";
export const AUTH_SUCCESS = "AUTH_SUCCESS";
class AuthSuccessAction implements Action {
public readonly type = AUTH_SUCCESS;
constructor(public jwt: string) {}
export enum AuthTypes {
AUTH_START = "AUTH_START",
AUTH_SUCCESS = "AUTH_SUCCESS",
AUTH_FAIL = "AUTH_FAIL",
}
export type AuthAction = AuthSuccessAction;
export interface IAuthStartActionAction extends Action {
type: AuthTypes.AUTH_START;
payload: {
username: string;
password: string;
};
}
export interface IAuthSuccessActionAction extends Action {
type: AuthTypes.AUTH_SUCCESS;
payload: {
jwt: string;
};
}
export interface IAuthFailureActionAction extends Action {
type: AuthTypes.AUTH_FAIL;
payload: {
error: string;
};
}
export function authStart(
username: string,
password: string,
): IAuthStartActionAction {
return { type: AuthTypes.AUTH_START, payload: { username, password } };
}
export function authSuccess(jwt: string): IAuthSuccessActionAction {
return { type: AuthTypes.AUTH_SUCCESS, payload: { jwt } };
}
export function authFail(error: string): IAuthFailureActionAction {
return { type: AuthTypes.AUTH_FAIL, payload: { error } };
}
export type AuthAction =
| IAuthStartActionAction
| IAuthSuccessActionAction
| IAuthFailureActionAction;

View File

@@ -1,15 +1,17 @@
import { Reducer } from "react";
import { AUTH_SUCCESS, AuthAction } from "./actions";
import { AuthAction, AuthTypes } from "./actions";
export interface IAuthState {
jwt: string | null;
inProgress: boolean;
error: string | null;
}
const defaultAuthState: IAuthState = {
jwt: null,
inProgress: false,
error: null,
};
export const auth: Reducer<IAuthState, AuthAction> = (
@@ -17,8 +19,14 @@ export const auth: Reducer<IAuthState, AuthAction> = (
action: AuthAction,
) => {
switch (action.type) {
case AUTH_SUCCESS:
return { ...state, jwt: action.jwt, inProgress: false };
case AuthTypes.AUTH_START:
return { ...state, inProgress: true };
break;
case AuthTypes.AUTH_SUCCESS:
return { ...state, jwt: action.payload.jwt, inProgress: false };
break;
case AuthTypes.AUTH_FAIL:
return { ...defaultAuthState, error: action.payload.error };
break;
default:
return state;

View File

@@ -0,0 +1,38 @@
import { delay } from "redux-saga";
import { call, put, race, takeLatest } from "redux-saga/effects";
import { login } from "~redux/api/auth";
import { setToken } from "~redux/api/utils";
import {
authFail,
authSuccess,
AuthTypes,
IAuthStartActionAction,
} from "./actions";
function* authStart(action: IAuthStartActionAction) {
const { username, password } = action.payload;
try {
const { response, timeout } = yield race({
response: call(login, username, password),
timeout: call(delay, 1000),
});
if (timeout) {
return put(authFail("Timeout"));
}
if (response.data) {
const user = response.data;
yield call(setToken, user.jwt);
yield put(authSuccess(user.jwt));
} else {
yield put(authFail(response.error));
}
} catch (e) {
yield put(authFail(e.toString()));
}
}
export function* authSaga() {
yield takeLatest(AuthTypes.AUTH_START, authStart);
}

View File

@@ -1,4 +1,8 @@
import { combineReducers } from "redux";
import { auth } from "~redux/auth/reducer";
import { auth, IAuthState } from "~redux/auth/reducer";
export interface IAppState {
auth: IAuthState;
}
export const rootReducer = combineReducers({ auth });

View File

@@ -1,4 +1,11 @@
import { createStore } from "redux";
import { applyMiddleware, createStore } from "redux";
import createSagaMiddlware from "redux-saga";
import { rootReducer } from "~redux/reducers";
export const store = createStore(rootReducer);
import { authSaga } from "./auth/sagas";
const sagaMiddleware = createSagaMiddlware();
export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(authSaga);

View File

@@ -5,7 +5,7 @@
"dom"
],
"jsx": "react",
"target": "es6",
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",