Feat/login
This commit is contained in:
parent
4803c4d8b6
commit
3361d4c043
7 changed files with 91 additions and 167 deletions
18
src/App.tsx
18
src/App.tsx
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
31
src/modules/login/LoginCallback.tsx
Normal file
31
src/modules/login/LoginCallback.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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],
|
|
||||||
);
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue