Feat/login

This commit is contained in:
Valentino 2022-01-18 10:14:29 +00:00
parent 4803c4d8b6
commit 3361d4c043
7 changed files with 91 additions and 167 deletions

View file

@ -1,28 +1,29 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useMatch, Router, navigate, RouteComponentProps, Redirect } from '@reach/router'; import { Router, RouteComponentProps } from '@reach/router';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { isValid } from 'src/services/api'; import { isValid } from 'src/services/api';
import { useAuth } from 'src/services/auth'; import { useAuth } from 'src/services/auth';
import { Apps, Dashboard, Users, Login, AppSingle } from './modules'; import { Apps, Dashboard, Users, Login, AppSingle } from './modules';
import { Layout } from './components'; import { Layout } from './components';
import { LoginCallback } from './modules/login/LoginCallback';
type AppProps = RouteComponentProps; type AppProps = RouteComponentProps;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function App(_: AppProps) { function App(_: AppProps) {
const { auth } = useAuth(); const { auth } = useAuth();
const isLoginPage = useMatch('/login'); // const isLoginPage = useMatch('/login');
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [initializedToken, setInitializedToken] = useState<string | null>(null); const [initializedToken, setInitializedToken] = useState<string | null>(null);
useEffect(() => { // useEffect(() => {
if (!isValid(auth) && !isLoginPage) { // if (!isValid(auth) && !isLoginPage) {
navigate('/login'); // navigate('/login');
} // }
}, [auth, isLoginPage]); // }, [auth, isLoginPage]);
useEffect(() => { useEffect(() => {
if (isValid(auth) && (!initialized || initializedToken !== auth.token)) { if (isValid(auth) && (!initialized || initializedToken !== auth.token)) {
@ -48,6 +49,7 @@ function App(_: AppProps) {
{!isValid(auth) ? ( {!isValid(auth) ? (
<Router> <Router>
<Login path="/login" /> <Login path="/login" />
<LoginCallback path="/login-callback" />
</Router> </Router>
) : ( ) : (
<Layout> <Layout>
@ -60,7 +62,7 @@ function App(_: AppProps) {
</Layout> </Layout>
)} )}
{isValid(auth) ? <Redirect from="/" to="/dashboard" noThrow /> : <Redirect from="/" to="/login" noThrow />} {/* {isValid(auth) ? <Redirect from="/" to="/dashboard" noThrow /> : <Redirect from="/" to="/" noThrow />} */}
{/* Place to load notifications */} {/* Place to load notifications */}
<div <div

View file

@ -1,133 +1,65 @@
import React, { useEffect } from 'react'; import React from 'react';
import { navigate, RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import * as yup from 'yup';
import clsx from 'clsx'; import clsx from 'clsx';
import { Controller, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { LockClosedIcon } from '@heroicons/react/solid'; import { LockClosedIcon } from '@heroicons/react/solid';
import { useAuth } from 'src/services/auth'; import { performApiCall } from 'src/services/api';
import { isValid } from 'src/services/api';
import { VALIDATION_MESSAGES } from 'src/common/const';
import { showToast, ToastType } from 'src/common/util/show-toast';
const validationSchema = yup.object({
email: yup.string().required(VALIDATION_MESSAGES.required('Email')),
password: yup.string().required(VALIDATION_MESSAGES.required('Password')),
});
type LoginProps = RouteComponentProps; type LoginProps = RouteComponentProps;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Login(_: LoginProps) { export function Login(_: LoginProps) {
const { auth, logIn } = useAuth(); const handleSubmit = async () => {
const { handleSubmit, formState, control } = useForm<{ try {
email: string; const { data } = await performApiCall({
password: string; path: '/login',
remember: boolean; method: 'POST',
}>({ });
resolver: yupResolver(validationSchema),
mode: 'all',
defaultValues: { email: process.env.REACT_APP_USERNAME, password: process.env.REACT_APP_PASSWORD },
});
const onSubmit = handleSubmit(async (values) => { if (data.authorizationUrl) {
const res = await logIn(values.email, values.password); window.location.href = data.authorizationUrl;
}
} catch (e: any) {
// continue
}
// @ts-ignore // @ts-ignore
if (res.ok) { // if (res.ok) {
navigate('/dashboard'); // showToast('Logged in!');
showToast('Logged in!'); // } else {
} else { // showToast('Username or password incorrect', ToastType.Error);
showToast('Username or password incorrect', ToastType.Error); // }
} };
});
useEffect(() => { // useEffect(() => {
if (isValid(auth)) { // if (isValid(auth)) {
navigate('/dashboard'); // navigate('/dashboard');
} // }
}, [auth]); // }, [auth]);
if (isValid(auth)) { // if (isValid(auth)) {
return null; // return null;
} // }
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8"> <div className="max-w-md w-full space-y-8">
<div className="flex justify-center"> <div className="flex justify-center">
<img className="hidden lg:block" src="assets/logo.svg" alt="Stackspin" /> <img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2> <h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
</div> </div>
<form className="mt-8 space-y-6" action="#" onSubmit={onSubmit} noValidate> <button
<input type="hidden" name="remember" defaultValue="true" /> onClick={handleSubmit}
<div className="rounded-md shadow-sm -space-y-px"> type="button"
<div> className={clsx(
<Controller 'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
control={control} )}
name="email" >
defaultValue="" <span className="absolute left-0 inset-y-0 flex items-center pl-3">
render={({ field }) => { <LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
return ( </span>
<> Sign in
<label htmlFor="email-address" className="sr-only"> </button>
Username
</label>
<input
{...field}
id="email-address"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-dark focus:border-primary-dark focus:z-10 sm:text-sm"
placeholder="Username"
/>
</>
);
}}
/>
</div>
<div>
<Controller
control={control}
name="password"
defaultValue=""
render={({ field }) => {
return (
<>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...field}
id="password"
type="password"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-dark focus:border-primary-dark focus:z-10 sm:text-sm"
placeholder="Password"
/>
</>
);
}}
/>
</div>
</div>
<div>
<button
type="submit"
className={clsx(
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
{
'is-loading': formState.isSubmitting,
},
)}
disabled={formState.isSubmitting || !formState.isValid}
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
</span>
Sign in
</button>
</div>
</form>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,31 @@
import React, { useEffect } from 'react';
import { RouteComponentProps } from '@reach/router';
import { useAuth } from 'src/services/auth';
import { showToast, ToastType } from 'src/common/util/show-toast';
type LoginCallbackProps = RouteComponentProps;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function LoginCallback(_: LoginCallbackProps) {
const { logIn } = useAuth();
useEffect(() => {
const res = logIn();
// @ts-ignore
if (res.ok) {
showToast('Logged in');
} else {
showToast('Something went wrong', ToastType.Error);
// navigate('/login');
}
}, [logIn]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="flex justify-center">Loading . . .</div>
</div>
</div>
);
}

View file

@ -1,38 +1,22 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getAuth, signIn, signOut, register as apiRegister, refreshUser as apiRefreshUser } from '../redux'; import { getAuth, signIn, signOut } from '../redux';
export function useAuth() { export function useAuth() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const auth = useSelector(getAuth); const auth = useSelector(getAuth);
const logIn = useCallback( const logIn = useCallback(() => {
(email: string, password: string) => { return dispatch(signIn());
return dispatch(signIn(email, password)); }, [dispatch]);
},
[dispatch],
);
const register = useCallback(
(email: string, password: string, firstName: string, lastName: string) => {
return dispatch(apiRegister(email, password, firstName, lastName));
},
[dispatch],
);
const logOut = useCallback(() => { const logOut = useCallback(() => {
return dispatch(signOut()); return dispatch(signOut());
}, [dispatch]); }, [dispatch]);
const refreshUser = useCallback(() => {
return dispatch(apiRefreshUser());
}, [dispatch]);
return { return {
auth, auth,
logIn, logIn,
logOut, logOut,
register,
refreshUser,
}; };
} }

View file

@ -1,3 +1,3 @@
export { useAuth } from './hooks'; export { useAuth } from './hooks';
export { getAuth, reducer, signIn, signOut, register, AuthActionTypes, getIsAuthLoading } from './redux'; export { getAuth, reducer, signIn, signOut, AuthActionTypes, getIsAuthLoading } from './redux';
export * from './types'; export * from './types';

View file

@ -15,20 +15,10 @@ const signOutAction = (): SuccessAction => ({
payload: null, payload: null,
}); });
export const signIn = (email: string, password: string) => export const signIn = () =>
createApiAction( createApiAction(
{ {
path: '/login', path: '/hydra/callback',
method: 'POST',
body: { username: email, password },
},
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
);
export const refreshUser = () =>
createApiAction(
{
path: '/dashboard',
method: 'GET', method: 'GET',
}, },
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE], [AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
@ -39,18 +29,3 @@ export function signOut() {
dispatch(signOutAction()); dispatch(signOutAction());
}; };
} }
export const register = (email: string, password: string, firstName: string, lastName: string) =>
createApiAction(
{
path: '/auth/register',
method: 'POST',
body: {
email,
firstName,
lastName,
password,
},
},
[AuthActionTypes.REGISTRATION_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.REGISTRATION_FAILURE],
);

View file

@ -1,4 +1,4 @@
export { signIn, signOut, register, refreshUser, AuthActionTypes } from './actions'; export { signIn, signOut, AuthActionTypes } from './actions';
export { default as reducer } from './reducers'; export { default as reducer } from './reducers';
export { getAuth, getIsAuthLoading } from './selectors'; export { getAuth, getIsAuthLoading } from './selectors';
export * from './types'; export * from './types';