diff --git a/packages/server/src/api/controllers/Authentication.ts b/packages/server/src/api/controllers/Authentication.ts index c162508ba..87ac2166c 100644 --- a/packages/server/src/api/controllers/Authentication.ts +++ b/packages/server/src/api/controllers/Authentication.ts @@ -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); diff --git a/packages/server/src/services/Authentication/AuthApplication.ts b/packages/server/src/services/Authentication/AuthApplication.ts index bbc547f1f..47f2168a3 100644 --- a/packages/server/src/services/Authentication/AuthApplication.ts +++ b/packages/server/src/services/Authentication/AuthApplication.ts @@ -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; diff --git a/packages/server/src/services/Authentication/AuthSignupResend.ts b/packages/server/src/services/Authentication/AuthSignupResend.ts index 581a20236..5c764e80c 100644 --- a/packages/server/src/services/Authentication/AuthSignupResend.ts +++ b/packages/server/src/services/Authentication/AuthSignupResend.ts @@ -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); diff --git a/packages/webapp/src/components/App.tsx b/packages/webapp/src/components/App.tsx index 949a1861f..57effb72a 100644 --- a/packages/webapp/src/components/App.tsx +++ b/packages/webapp/src/components/App.tsx @@ -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 }) { - + + + + + + + + + + + + - + + + + + diff --git a/packages/webapp/src/components/Guards/EnsureAuthNotAuthenticated.tsx b/packages/webapp/src/components/Guards/EnsureAuthNotAuthenticated.tsx new file mode 100644 index 000000000..e849a572c --- /dev/null +++ b/packages/webapp/src/components/Guards/EnsureAuthNotAuthenticated.tsx @@ -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 : ; +} diff --git a/packages/webapp/src/components/Guards/EnsureUserEmailVerified.tsx b/packages/webapp/src/components/Guards/EnsureUserEmailVerified.tsx new file mode 100644 index 000000000..6fb57c4a5 --- /dev/null +++ b/packages/webapp/src/components/Guards/EnsureUserEmailVerified.tsx @@ -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 ; + } + return <>{children}; +} diff --git a/packages/webapp/src/components/Guards/PrivateRoute.tsx b/packages/webapp/src/components/Guards/PrivateRoute.tsx index 8e8e167b9..a4ef8d3b7 100644 --- a/packages/webapp/src/components/Guards/PrivateRoute.tsx +++ b/packages/webapp/src/components/Guards/PrivateRoute.tsx @@ -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 ( - - {isAuthenticated ? ( - - ) : ( - - )} - + return isAuthenticated ? ( + children + ) : ( + ); } diff --git a/packages/webapp/src/containers/Authentication/AuthContainer.tsx b/packages/webapp/src/containers/Authentication/AuthContainer.tsx new file mode 100644 index 000000000..6ba68c196 --- /dev/null +++ b/packages/webapp/src/containers/Authentication/AuthContainer.tsx @@ -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 ( + + + + + + + {children} + + + ); +} + +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; +`; diff --git a/packages/webapp/src/containers/Authentication/Authentication.tsx b/packages/webapp/src/containers/Authentication/Authentication.tsx index 8b05570c4..34a99f104 100644 --- a/packages/webapp/src/containers/Authentication/Authentication.tsx +++ b/packages/webapp/src/containers/Authentication/Authentication.tsx @@ -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 ; - } return ( diff --git a/packages/webapp/src/containers/Authentication/EmailConfirmation.tsx b/packages/webapp/src/containers/Authentication/EmailConfirmation.tsx new file mode 100644 index 000000000..c9aeb985e --- /dev/null +++ b/packages/webapp/src/containers/Authentication/EmailConfirmation.tsx @@ -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; +} diff --git a/packages/webapp/src/containers/Authentication/RegisterVerify.module.scss b/packages/webapp/src/containers/Authentication/RegisterVerify.module.scss new file mode 100644 index 000000000..f0754f470 --- /dev/null +++ b/packages/webapp/src/containers/Authentication/RegisterVerify.module.scss @@ -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; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Authentication/RegisterVerify.tsx b/packages/webapp/src/containers/Authentication/RegisterVerify.tsx new file mode 100644 index 000000000..2147a8f2e --- /dev/null +++ b/packages/webapp/src/containers/Authentication/RegisterVerify.tsx @@ -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 ( + + + +

Please verify your email

+

+ We sent an email to asdahmed@gmail.com Click the + link inside to get started. +

+ + + + + + +
+
+
+ ); +} diff --git a/packages/webapp/src/hooks/query/authentication.tsx b/packages/webapp/src/hooks/query/authentication.tsx index a604ec322..d43d244c1 100644 --- a/packages/webapp/src/hooks/query/authentication.tsx +++ b/packages/webapp/src/hooks/query/authentication.tsx @@ -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, + ); +}; \ No newline at end of file diff --git a/packages/webapp/src/hooks/state/authentication.tsx b/packages/webapp/src/hooks/state/authentication.tsx index 4fb20bf6c..b520e1ac1 100644 --- a/packages/webapp/src/hooks/state/authentication.tsx +++ b/packages/webapp/src/hooks/state/authentication.tsx @@ -64,3 +64,10 @@ export const useAuthUser = () => { export const useAuthOrganizationId = () => { return useSelector((state) => state.authentication.organizationId); }; + +/** + * + */ +export const useAuthUserVerified = () => { + return useSelector(() => false); +}; diff --git a/packages/webapp/src/routes/authentication.tsx b/packages/webapp/src/routes/authentication.tsx index 77b1fce6f..7ce79dbbc 100644 --- a/packages/webapp/src/routes/authentication.tsx +++ b/packages/webapp/src/routes/authentication.tsx @@ -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'), }), - } + }, ];