mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
login form
This commit is contained in:
@@ -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);
|
||||
|
||||
8
frontend/src/redux/api/auth/index.ts
Normal file
8
frontend/src/redux/api/auth/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { fetchJSON } from "../utils";
|
||||
|
||||
export async function login(username: string, password: string) {
|
||||
return fetchJSON("/users/login", "POST", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
50
frontend/src/redux/api/utils.ts
Normal file
50
frontend/src/redux/api/utils.ts
Normal 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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
38
frontend/src/redux/auth/sagas.ts
Normal file
38
frontend/src/redux/auth/sagas.ts
Normal 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);
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"dom"
|
||||
],
|
||||
"jsx": "react",
|
||||
"target": "es6",
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
|
||||
Reference in New Issue
Block a user