mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +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 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 { 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' }} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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>
|
||||
|
||||
@@ -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 = () => {
|
||||
return useSelector((state) => state.authentication.organizationId);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useAuthUserVerified = () => {
|
||||
return useSelector(() => false);
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user