mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: wip email confirmation
This commit is contained in:
@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
|||||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||||
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationController extends BaseController {
|
export default class AuthenticationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -28,10 +30,10 @@ export default class AuthenticationController extends BaseController {
|
|||||||
asyncMiddleware(this.login.bind(this)),
|
asyncMiddleware(this.login.bind(this)),
|
||||||
this.handlerErrors
|
this.handlerErrors
|
||||||
);
|
);
|
||||||
|
router.use('/register/verify/resend', JWTAuth);
|
||||||
|
router.use('/register/verify/resend', AttachCurrentTenantUser);
|
||||||
router.post(
|
router.post(
|
||||||
'register/verify/resend',
|
'/register/verify/resend',
|
||||||
[check('email').exists().isEmail()],
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
|
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
|
||||||
this.handlerErrors
|
this.handlerErrors
|
||||||
);
|
);
|
||||||
@@ -199,7 +201,8 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @returns {Response|void}
|
* @returns {Response|void}
|
||||||
*/
|
*/
|
||||||
private async registerVerify(req: Request, res: Response, next: Function) {
|
private async registerVerify(req: Request, res: Response, next: Function) {
|
||||||
const signUpVerifyDTO = this.matchedBodyData(req);
|
const signUpVerifyDTO: { email: string; token: string } =
|
||||||
|
this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.authApplication.signUpConfirm(
|
const user = await this.authApplication.signUpConfirm(
|
||||||
@@ -228,17 +231,15 @@ export default class AuthenticationController extends BaseController {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: Function
|
next: Function
|
||||||
) {
|
) {
|
||||||
const signUpVerifyDTO = this.matchedBodyData(req);
|
const { user } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.authApplication.signUpConfirm(
|
const data = await this.authApplication.signUpConfirm(user.id);
|
||||||
signUpVerifyDTO.email,
|
|
||||||
signUpVerifyDTO.token
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'The given user has verified successfully',
|
message: 'The given user has verified successfully',
|
||||||
user,
|
data,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { AuthSendResetPassword } from './AuthSendResetPassword';
|
|||||||
import { GetAuthMeta } from './GetAuthMeta';
|
import { GetAuthMeta } from './GetAuthMeta';
|
||||||
import { AuthSignupConfirmService } from './AuthSignupConfirm';
|
import { AuthSignupConfirmService } from './AuthSignupConfirm';
|
||||||
import { SystemUser } from '@/system/models';
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { AuthSignupConfirmResend } from './AuthSignupResend';
|
||||||
|
|
||||||
interface ISignupConfirmDTO {
|
interface ISignupConfirmDTO {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -28,6 +29,9 @@ export default class AuthenticationApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private authSignupConfirmService: AuthSignupConfirmService;
|
private authSignupConfirmService: AuthSignupConfirmService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authSignUpConfirmResendService: AuthSignupConfirmResend;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private authResetPasswordService: AuthSendResetPassword;
|
private authResetPasswordService: AuthSendResetPassword;
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ export class AuthSignupConfirmResend {
|
|||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
*/
|
*/
|
||||||
public async signUpConfirmResend(email: string) {
|
public async signUpConfirmResend(userId: number) {
|
||||||
const user = await SystemUser.query()
|
const user = await SystemUser.query().findById(userId).throwIfNotFound();
|
||||||
.findOne({ email })
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
if (user.verified) {
|
if (user.verified) {
|
||||||
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED)
|
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||||
}
|
}
|
||||||
if (user.verifyToken) {
|
if (user.verifyToken) {
|
||||||
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
|||||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||||
import { Authentication } from '@/containers/Authentication/Authentication';
|
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||||
|
|
||||||
|
import LazyLoader from '@/components/LazyLoader';
|
||||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||||
import { queryConfig } from '../hooks/query/base';
|
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.
|
* App inner.
|
||||||
@@ -26,9 +33,24 @@ function AppInsider({ history }) {
|
|||||||
<DashboardThemeProvider>
|
<DashboardThemeProvider>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={'/auth'} component={Authentication} />
|
<Route path={'/auth'}>
|
||||||
|
<EnsureAuthNotAuthenticated>
|
||||||
|
<Authentication />
|
||||||
|
</EnsureAuthNotAuthenticated>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={'/register/verify'}>
|
||||||
|
<PrivateRoute>
|
||||||
|
<RegisterVerify />
|
||||||
|
</PrivateRoute>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path={'/'}>
|
<Route path={'/'}>
|
||||||
<PrivateRoute component={DashboardPrivatePages} />
|
<PrivateRoute>
|
||||||
|
<EnsureUserEmailVerified>
|
||||||
|
<DashboardPrivatePages />
|
||||||
|
</EnsureUserEmailVerified>
|
||||||
|
</PrivateRoute>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -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: '/' }} />;
|
||||||
|
}
|
||||||
@@ -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}</>;
|
||||||
|
}
|
||||||
@@ -4,16 +4,16 @@ import BodyClassName from 'react-body-classname';
|
|||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useIsAuthenticated } from '@/hooks/state';
|
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();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
|
||||||
return (
|
return isAuthenticated ? (
|
||||||
<BodyClassName className={''}>
|
children
|
||||||
{isAuthenticated ? (
|
|
||||||
<Component />
|
|
||||||
) : (
|
) : (
|
||||||
<Redirect to={{ pathname: '/auth/login' }} />
|
<Redirect to={{ pathname: '/auth/login' }} />
|
||||||
)}
|
|
||||||
</BodyClassName>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
`;
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
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 BodyClassName from 'react-body-classname';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
|
|
||||||
import authenticationRoutes from '@/routes/authentication';
|
import authenticationRoutes from '@/routes/authentication';
|
||||||
import { Icon, FormattedMessage as T } from '@/components';
|
import { Icon, FormattedMessage as T } from '@/components';
|
||||||
import { useIsAuthenticated } from '@/hooks/state';
|
|
||||||
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
||||||
|
|
||||||
import '@/style/pages/Authentication/Auth.scss';
|
import '@/style/pages/Authentication/Auth.scss';
|
||||||
|
|
||||||
export function Authentication() {
|
export function Authentication() {
|
||||||
const to = { pathname: '/' };
|
|
||||||
const isAuthenticated = useIsAuthenticated();
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <Redirect to={to} />;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<BodyClassName className={'authentication'}>
|
<BodyClassName className={'authentication'}>
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -64,3 +64,10 @@ export const useAuthUser = () => {
|
|||||||
export const useAuthOrganizationId = () => {
|
export const useAuthOrganizationId = () => {
|
||||||
return useSelector((state) => state.authentication.organizationId);
|
return useSelector((state) => state.authentication.organizationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const useAuthUserVerified = () => {
|
||||||
|
return useSelector(() => false);
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,10 +28,16 @@ export default [
|
|||||||
loader: () => import('@/containers/Authentication/InviteAccept'),
|
loader: () => import('@/containers/Authentication/InviteAccept'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${BASE_URL}/register/email_confirmation`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `${BASE_URL}/register`,
|
path: `${BASE_URL}/register`,
|
||||||
component: LazyLoader({
|
component: LazyLoader({
|
||||||
loader: () => import('@/containers/Authentication/Register'),
|
loader: () => import('@/containers/Authentication/Register'),
|
||||||
}),
|
}),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user