feat: wip email confirmation

This commit is contained in:
Ahmed Bouhuolia
2024-04-28 17:51:11 +02:00
parent 4368c18479
commit b9fc0cdd9e
15 changed files with 283 additions and 36 deletions

View File

@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
@Service()
export default class AuthenticationController extends BaseController {
@Inject()
@@ -28,10 +30,10 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.login.bind(this)),
this.handlerErrors
);
router.use('/register/verify/resend', JWTAuth);
router.use('/register/verify/resend', AttachCurrentTenantUser);
router.post(
'register/verify/resend',
[check('email').exists().isEmail()],
this.validationResult,
'/register/verify/resend',
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
this.handlerErrors
);
@@ -199,7 +201,8 @@ export default class AuthenticationController extends BaseController {
* @returns {Response|void}
*/
private async registerVerify(req: Request, res: Response, next: Function) {
const signUpVerifyDTO = this.matchedBodyData(req);
const signUpVerifyDTO: { email: string; token: string } =
this.matchedBodyData(req);
try {
const user = await this.authApplication.signUpConfirm(
@@ -228,17 +231,15 @@ export default class AuthenticationController extends BaseController {
res: Response,
next: Function
) {
const signUpVerifyDTO = this.matchedBodyData(req);
const { user } = req;
try {
const user = await this.authApplication.signUpConfirm(
signUpVerifyDTO.email,
signUpVerifyDTO.token
);
const data = await this.authApplication.signUpConfirm(user.id);
return res.status(200).send({
type: 'success',
message: 'The given user has verified successfully',
user,
data,
});
} catch (error) {
next(error);

View File

@@ -11,6 +11,7 @@ import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
import { AuthSignupConfirmService } from './AuthSignupConfirm';
import { SystemUser } from '@/system/models';
import { AuthSignupConfirmResend } from './AuthSignupResend';
interface ISignupConfirmDTO {
token: string;
@@ -28,6 +29,9 @@ export default class AuthenticationApplication {
@Inject()
private authSignupConfirmService: AuthSignupConfirmService;
@Inject()
private authSignUpConfirmResendService: AuthSignupConfirmResend;
@Inject()
private authResetPasswordService: AuthSendResetPassword;

View File

@@ -13,14 +13,12 @@ export class AuthSignupConfirmResend {
* @param {number} tenantId
* @param {string} email
*/
public async signUpConfirmResend(email: string) {
const user = await SystemUser.query()
.findOne({ email })
.throwIfNotFound();
public async signUpConfirmResend(userId: number) {
const user = await SystemUser.query().findById(userId).throwIfNotFound();
//
//
if (user.verified) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED)
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
}
if (user.verifyToken) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);

View File

@@ -14,8 +14,15 @@ import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import { Authentication } from '@/containers/Authentication/Authentication';
import LazyLoader from '@/components/LazyLoader';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
const RegisterVerify = LazyLoader({
loader: () => import('@/containers/Authentication/RegisterVerify'),
});
/**
* App inner.
@@ -26,9 +33,24 @@ function AppInsider({ history }) {
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/auth'}>
<EnsureAuthNotAuthenticated>
<Authentication />
</EnsureAuthNotAuthenticated>
</Route>
<Route path={'/register/verify'}>
<PrivateRoute>
<RegisterVerify />
</PrivateRoute>
</Route>
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
<PrivateRoute>
<EnsureUserEmailVerified>
<DashboardPrivatePages />
</EnsureUserEmailVerified>
</PrivateRoute>
</Route>
</Switch>
</Router>

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
interface PrivateRouteProps {
children: React.ReactNode;
}
export function EnsureAuthNotAuthenticated({ children }: PrivateRouteProps) {
const isAuthenticated = useIsAuthenticated();
return !isAuthenticated ? children : <Redirect to={{ pathname: '/' }} />;
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useAuthUserVerified } from '@/hooks/state';
interface EnsureUserEmailVerifiedProps {
children: React.ReactNode;
}
/**
* Higher Order Component to ensure that the user's email is verified.
* If not verified, redirects to the email verification page.
*/
export function EnsureUserEmailVerified({
children,
}: EnsureUserEmailVerifiedProps) {
const isAuthVerified = useAuthUserVerified();
if (!isAuthVerified) {
return <Redirect to={{ pathname: '/register/verify' }} />;
}
return <>{children}</>;
}

View File

@@ -4,16 +4,16 @@ import BodyClassName from 'react-body-classname';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
export default function PrivateRoute({ component: Component, ...rest }) {
interface PrivateRouteProps {
children: React.ReactNode;
}
export default function PrivateRoute({ children }: PrivateRouteProps) {
const isAuthenticated = useIsAuthenticated();
return (
<BodyClassName className={''}>
{isAuthenticated ? (
<Component />
) : (
<Redirect to={{ pathname: '/auth/login' }} />
)}
</BodyClassName>
return isAuthenticated ? (
children
) : (
<Redirect to={{ pathname: '/auth/login' }} />
);
}

View File

@@ -0,0 +1,34 @@
// @ts-nocheck
import styled from 'styled-components';
import { Icon, FormattedMessage as T } from '@/components';
interface AuthContainerProps {
children: React.ReactNode;
}
export function AuthContainer({ children }: AuthContainerProps) {
return (
<AuthPage>
<AuthInsider>
<AuthLogo>
<Icon icon="bigcapital" height={37} width={214} />
</AuthLogo>
{children}
</AuthInsider>
</AuthPage>
);
}
const AuthPage = styled.div``;
const AuthInsider = styled.div`
width: 384px;
margin: 0 auto;
margin-bottom: 40px;
padding-top: 80px;
`;
const AuthLogo = styled.div`
text-align: center;
margin-bottom: 40px;
`;

View File

@@ -1,24 +1,17 @@
// @ts-nocheck
import React from 'react';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { Route, Switch, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import styled from 'styled-components';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state';
import { AuthMetaBootProvider } from './AuthMetaBoot';
import '@/style/pages/Authentication/Auth.scss';
export function Authentication() {
const to = { pathname: '/' };
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <Redirect to={to} />;
}
return (
<BodyClassName className={'authentication'}>
<AuthPage>

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import { useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useAuthSignUpVerify } from '@/hooks/query';
export default function EmailConfirmation() {
const { mutateAsync: authSignupVerify } = useAuthSignUpVerify();
const params = useParams();
const history = useHistory();
const token = params.token;
const email = params.email;
useEffect(() => {
if (!token || !email) {
history.push('register/email_confirmation');
}
}, [history, token, email]);
useEffect(() => {
authSignupVerify(token, email)
.then(() => {})
.catch((error) => {});
}, [token, email, authSignupVerify]);
return null;
}

View File

@@ -0,0 +1,18 @@
.root {
text-align: center;
}
.title{
font-size: 18px;
font-weight: 600;
margin-bottom: 0.5rem;
color: #252A31;
}
.description{
margin-bottom: 1rem;
font-size: 15px;
line-height: 1.45;
color: #404854;
}

View File

@@ -0,0 +1,74 @@
// @ts-nocheck
import { Button, Intent } from '@blueprintjs/core';
import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components';
import { useAuthActions } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer';
import { useHistory } from 'react-router-dom';
export default function RegisterVerify() {
const history = useHistory();
const { setLogout } = useAuthActions();
const { mutateAsync: resendSignUpVerifyMail, isLoading } =
useAuthSignUpVerifyResendMail();
const handleResendMailBtnClick = () => {
resendSignUpVerifyMail()
.then(() => {
AppToaster.show({
intent: Intent.SUCCESS,
message: 'The verification mail has sent successfully.',
});
})
.catch(() => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
});
});
};
// Handle logout link click.
const handleSignOutBtnClick = () => {
setLogout();
};
return (
<AuthContainer>
<AuthInsider>
<AuthInsiderCard className={styles.root}>
<h2 className={styles.title}>Please verify your email</h2>
<p className={styles.description}>
We sent an email to <strong>asdahmed@gmail.com</strong> Click the
link inside to get started.
</p>
<Stack spacing={4}>
<Button
large
fill
loading={isLoading}
intent={Intent.NONE}
onClick={handleResendMailBtnClick}
>
Resend email
</Button>
<Button
large
fill
intent={Intent.DANGER}
minimal
onClick={handleSignOutBtnClick}
>
Signout
</Button>
</Stack>
</AuthInsiderCard>
</AuthInsider>
</AuthContainer>
);
}

View File

@@ -90,3 +90,30 @@ export const useAuthMetadata = (props) => {
},
);
}
/**
*
*/
export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useApiRequest();
return useMutation(
() => apiRequest.post('auth/register/verify/resend'),
props,
);
};
/**
*
*/
export const useAuthSignUpVerify = (props) => {
const apiRequest = useApiRequest();
return useMutation(
(token: string, email: string) => apiRequest.post('auth/register/verify'),
props,
);
};

View File

@@ -64,3 +64,10 @@ export const useAuthUser = () => {
export const useAuthOrganizationId = () => {
return useSelector((state) => state.authentication.organizationId);
};
/**
*
*/
export const useAuthUserVerified = () => {
return useSelector(() => false);
};

View File

@@ -28,10 +28,16 @@ export default [
loader: () => import('@/containers/Authentication/InviteAccept'),
}),
},
{
path: `${BASE_URL}/register/email_confirmation`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
}),
},
{
path: `${BASE_URL}/register`,
component: LazyLoader({
loader: () => import('@/containers/Authentication/Register'),
}),
}
},
];