mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
Merge pull request #426 from bigcapitalhq/big-163-user-email-verification-after-signing-up
feat: User email verification after signing-up.
This commit is contained in:
@@ -48,6 +48,9 @@ SIGNUP_DISABLED=false
|
||||
SIGNUP_ALLOWED_DOMAINS=
|
||||
SIGNUP_ALLOWED_EMAILS=
|
||||
|
||||
# Sign-up Email Confirmation
|
||||
SIGNUP_EMAIL_CONFIRMATION=false
|
||||
|
||||
# API rate limit (points,duration,block duration).
|
||||
API_RATE_LIMIT=120,60,600
|
||||
|
||||
|
||||
@@ -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,6 +30,20 @@ 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',
|
||||
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register/verify',
|
||||
this.signupVerifySchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.registerVerify.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
@@ -99,6 +115,17 @@ export default class AuthenticationController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
private get signupVerifySchema(): ValidationChain[] {
|
||||
return [
|
||||
check('email')
|
||||
.exists()
|
||||
.isString()
|
||||
.isEmail()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('token').exists().isString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
* @returns {ValidationChain[]}
|
||||
@@ -166,6 +193,58 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provider user's email after signin-up.
|
||||
* @param {Request} req
|
||||
* @param {Response}| res
|
||||
* @param {Function} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async registerVerify(req: Request, res: Response, next: Function) {
|
||||
const signUpVerifyDTO: { email: string; token: string } =
|
||||
this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const user = await this.authApplication.signUpConfirm(
|
||||
signUpVerifyDTO.email,
|
||||
signUpVerifyDTO.token
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
message: 'The given user has verified successfully',
|
||||
user,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends the confirmation email to the user.
|
||||
* @param {Request} req
|
||||
* @param {Response}| res
|
||||
* @param {Function} next
|
||||
*/
|
||||
private async registerVerifyResendMail(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const data = await this.authApplication.signUpConfirmResend(user.id);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
message: 'The given user has verified successfully',
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
|
||||
@@ -153,6 +153,13 @@ module.exports = {
|
||||
),
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign-up email confirmation
|
||||
*/
|
||||
signupConfirmation: {
|
||||
enabled: parseBoolean<boolean>(process.env.SIGNUP_EMAIL_CONFIRMATION, false),
|
||||
},
|
||||
|
||||
/**
|
||||
* Puppeteer remote browserless connection.
|
||||
*/
|
||||
|
||||
@@ -66,16 +66,27 @@ export interface IAuthResetedPasswordEventPayload {
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IAuthSendingResetPassword {
|
||||
user: ISystemUser,
|
||||
user: ISystemUser;
|
||||
token: string;
|
||||
}
|
||||
export interface IAuthSendedResetPassword {
|
||||
user: ISystemUser,
|
||||
user: ISystemUser;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifingEventPayload {
|
||||
email: string;
|
||||
verifyToken: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifiedEventPayload {
|
||||
email: string;
|
||||
verifyToken: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/s
|
||||
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
||||
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
|
||||
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
|
||||
import { SendVerfiyMailOnSignUp } from '@/services/Authentication/events/SendVerfiyMailOnSignUp';
|
||||
|
||||
|
||||
export default () => {
|
||||
@@ -222,6 +223,7 @@ export const susbcribers = () => {
|
||||
DeleteCashflowTransactionOnUncategorize,
|
||||
PreventDeleteTransactionOnDelete,
|
||||
|
||||
SubscribeFreeOnSignupCommunity
|
||||
SubscribeFreeOnSignupCommunity,
|
||||
SendVerfiyMailOnSignUp
|
||||
];
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleRe
|
||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
||||
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
||||
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new ResetPasswordMailJob(agenda);
|
||||
@@ -27,6 +28,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new PaymentReceiveMailNotificationJob(agenda);
|
||||
new PlaidFetchTransactionsJob(agenda);
|
||||
new ImportDeleteExpiredFilesJobs(agenda);
|
||||
new SendVerifyMailJob(agenda);
|
||||
|
||||
agenda.start().then(() => {
|
||||
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ISystemUser,
|
||||
@@ -9,6 +9,9 @@ import { AuthSigninService } from './AuthSignin';
|
||||
import { AuthSignupService } from './AuthSignup';
|
||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||
import { GetAuthMeta } from './GetAuthMeta';
|
||||
import { AuthSignupConfirmService } from './AuthSignupConfirm';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { AuthSignupConfirmResend } from './AuthSignupResend';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationApplication {
|
||||
@@ -18,6 +21,12 @@ export default class AuthenticationApplication {
|
||||
@Inject()
|
||||
private authSignupService: AuthSignupService;
|
||||
|
||||
@Inject()
|
||||
private authSignupConfirmService: AuthSignupConfirmService;
|
||||
|
||||
@Inject()
|
||||
private authSignUpConfirmResendService: AuthSignupConfirmResend;
|
||||
|
||||
@Inject()
|
||||
private authResetPasswordService: AuthSendResetPassword;
|
||||
|
||||
@@ -44,6 +53,28 @@ export default class AuthenticationApplication {
|
||||
return this.authSignupService.signUp(signupDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verfying the provided user's email after signin-up.
|
||||
* @param {string} email
|
||||
* @param {string} token
|
||||
* @returns {Promise<SystemUser>}
|
||||
*/
|
||||
public async signUpConfirm(
|
||||
email: string,
|
||||
token: string
|
||||
): Promise<SystemUser> {
|
||||
return this.authSignupConfirmService.signUpConfirm(email, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends the confirmation email of the given system user.
|
||||
* @param {number} userId - System user id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async signUpConfirmResend(userId: number) {
|
||||
return this.authSignUpConfirmResendService.signUpConfirmResend(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import { defaultTo, isEmpty, omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import crypto from 'crypto';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IAuthSignedUpEventPayload,
|
||||
@@ -42,6 +43,13 @@ export class AuthSignupService {
|
||||
|
||||
const hashedPassword = await hashPassword(signupDTO.password);
|
||||
|
||||
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
||||
const verifiedEnabed = defaultTo(config.signupConfirmation.enabled, false);
|
||||
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
||||
const verified = !verifiedEnabed;
|
||||
|
||||
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
|
||||
|
||||
// Triggers signin up event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
||||
signupDTO,
|
||||
@@ -50,10 +58,12 @@ export class AuthSignupService {
|
||||
const tenant = await this.tenantsManager.createTenant();
|
||||
const registeredUser = await systemUserRepository.create({
|
||||
...omit(signupDTO, 'country'),
|
||||
verifyToken,
|
||||
verified,
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenantId: tenant.id,
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
inviteAcceptedAt,
|
||||
});
|
||||
// Triggers signed up event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUp, {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { ERRORS } from './_constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IAuthSignUpVerifiedEventPayload,
|
||||
IAuthSignUpVerifingEventPayload,
|
||||
} from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class AuthSignupConfirmService {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Verifies the provided user's email after signing-up.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IRegisterDTO} signupDTO
|
||||
* @returns {Promise<ISystemUser>}
|
||||
*/
|
||||
public async signUpConfirm(
|
||||
email: string,
|
||||
verifyToken: string
|
||||
): Promise<SystemUser> {
|
||||
const foundUser = await SystemUser.query().findOne({ email, verifyToken });
|
||||
|
||||
if (!foundUser) {
|
||||
throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID);
|
||||
}
|
||||
const userId = foundUser.id;
|
||||
|
||||
// Triggers `signUpConfirming` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUpConfirming, {
|
||||
email,
|
||||
verifyToken,
|
||||
userId,
|
||||
} as IAuthSignUpVerifingEventPayload);
|
||||
|
||||
const updatedUser = await SystemUser.query().patchAndFetchById(
|
||||
foundUser.id,
|
||||
{
|
||||
verified: true,
|
||||
verifyToken: '',
|
||||
}
|
||||
);
|
||||
// Triggers `signUpConfirmed` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, {
|
||||
email,
|
||||
verifyToken,
|
||||
userId,
|
||||
} as IAuthSignUpVerifiedEventPayload);
|
||||
|
||||
return updatedUser as SystemUser;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { ERRORS } from './_constants';
|
||||
|
||||
@Service()
|
||||
export class AuthSignupConfirmResend {
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Resends the email confirmation of the given user.
|
||||
* @param {number} userId - User ID.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async signUpConfirmResend(userId: number) {
|
||||
const user = await SystemUser.query().findById(userId).throwIfNotFound();
|
||||
|
||||
// Throw error if the user is already verified.
|
||||
if (user.verified) {
|
||||
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||
}
|
||||
// Throw error if the verification token is not exist.
|
||||
if (!user.verifyToken) {
|
||||
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||
}
|
||||
const payload = {
|
||||
email: user.email,
|
||||
token: user.verifyToken,
|
||||
fullName: user.firstName,
|
||||
};
|
||||
await this.agenda.now('send-signup-verify-mail', payload);
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,33 @@ export default class AuthenticationMailMesssages {
|
||||
})
|
||||
.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends signup verification mail.
|
||||
* @param {string} email - Email address
|
||||
* @param {string} fullName - User name.
|
||||
* @param {string} token - Verification token.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendSignupVerificationMail(
|
||||
email: string,
|
||||
fullName: string,
|
||||
token: string
|
||||
) {
|
||||
const verifyUrl = `${config.baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
||||
|
||||
await new Mail()
|
||||
.setSubject('Bigcapital - Verify your email')
|
||||
.setView('mail/SignupVerifyEmail.html')
|
||||
.setTo(email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: `${global.__views_dir}/images/bigcapital.png`,
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
.setData({ verifyUrl, fullName })
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ export const ERRORS = {
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
||||
SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID',
|
||||
USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { Inject } from 'typedi';
|
||||
|
||||
export class SendVerfiyMailOnSignUp {
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Attaches events with handles.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.auth.signUp, this.handleSendVerifyMailOnSignup);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ITaxRateEditedPayload} payload -
|
||||
*/
|
||||
private handleSendVerifyMailOnSignup = async ({
|
||||
user,
|
||||
}: IAuthSignedUpEventPayload) => {
|
||||
const payload = {
|
||||
email: user.email,
|
||||
token: user.verifyToken,
|
||||
fullName: user.firstName,
|
||||
};
|
||||
await this.agenda.now('send-signup-verify-mail', payload);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Container } from 'typedi';
|
||||
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages';
|
||||
|
||||
export class SendVerifyMailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Agenda} agenda
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'send-signup-verify-mail',
|
||||
{ priority: 'high' },
|
||||
this.handler.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send welcome mail job.
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { data } = job.attrs;
|
||||
const { email, fullName, token } = data;
|
||||
const authService = Container.get(AuthenticationMailMesssages);
|
||||
|
||||
try {
|
||||
await authService.sendSignupVerificationMail(email, fullName, token);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ export default {
|
||||
signUp: 'onSignUp',
|
||||
signingUp: 'onSigningUp',
|
||||
|
||||
signUpConfirming: 'signUpConfirming',
|
||||
signUpConfirmed: 'signUpConfirmed',
|
||||
|
||||
sendingResetPassword: 'onSendingResetPassword',
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.table('users', (table) => {
|
||||
table.string('verify_token');
|
||||
table.boolean('verified').defaultTo(false);
|
||||
})
|
||||
.then(() => {
|
||||
return knex('USERS').update({ verified: true });
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {};
|
||||
@@ -4,6 +4,12 @@ import SystemModel from '@/system/models/SystemModel';
|
||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||
|
||||
export default class SystemUser extends SystemModel {
|
||||
firstName!: string;
|
||||
lastName!: string;
|
||||
verified!: boolean;
|
||||
inviteAcceptedAt!: Date | null;
|
||||
deletedAt!: Date | null;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -29,23 +35,33 @@ export default class SystemUser extends SystemModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['fullName', 'isDeleted', 'isInviteAccepted'];
|
||||
return ['fullName', 'isDeleted', 'isInviteAccepted', 'isVerified'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Detarmines whether the user is deleted.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isDeleted() {
|
||||
return !!this.deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Detarmines whether the sent invite is accepted.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isInviteAccepted() {
|
||||
return !!this.inviteAcceptedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the user's email is verified.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isVerified() {
|
||||
return !!this.verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full name attribute.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import _, { isEmpty } from 'lodash';
|
||||
import path from 'path';
|
||||
import * as R from 'ramda';
|
||||
|
||||
@@ -329,7 +329,7 @@ const booleanValuesRepresentingTrue: string[] = ['true', '1'];
|
||||
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
|
||||
|
||||
const normalizeValue = (value: any): string =>
|
||||
value.toString().trim().toLowerCase();
|
||||
value?.toString().trim().toLowerCase();
|
||||
|
||||
const booleanValues: string[] = [
|
||||
...booleanValuesRepresentingTrue,
|
||||
@@ -338,7 +338,7 @@ const booleanValues: string[] = [
|
||||
|
||||
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
|
||||
const normalizedValue = normalizeValue(value);
|
||||
if (booleanValues.indexOf(normalizedValue) === -1) {
|
||||
if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) {
|
||||
return defaultValue;
|
||||
}
|
||||
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;
|
||||
|
||||
424
packages/server/views/mail/SignupVerifyEmail.html
Normal file
424
packages/server/views/mail/SignupVerifyEmail.html
Normal file
@@ -0,0 +1,424 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Bigcapital | Reset your password</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
|
||||
/*All the styling goes here*/
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #2d95fd;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #1968F0;
|
||||
border-color: #1968F0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mb4{
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #004dd0 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #004dd0 !important;
|
||||
border-color: #004dd0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[data-icon="bigcapital"] path {
|
||||
fill: #004dd0;
|
||||
}
|
||||
|
||||
[data-icon='bigcapital'] .path-1,
|
||||
[data-icon='bigcapital'] .path-13 {
|
||||
fill: #2d95fd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
<span class="preheader">Verify your email.</span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="presentation" class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="align-center">
|
||||
<img src="cid:bigcapital_logo" />
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<p class="align-center">
|
||||
<h2>Verify your email</h2>
|
||||
</p>
|
||||
<p class="mgb-1x">Hi <strong>{{ fullName }}<strong>,</p>
|
||||
<p class="mgb-2-5x">To continue setting up your Bigcapital account, please verify that this is your email address.</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{ verifyUrl }}" target="_blank">Verify email address</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>If this was a mistake, just ignore this email and nothing will happen.</p>
|
||||
<p class="email-note">This is an automatically generated email please do not reply to this email.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block powered-by">
|
||||
Powered by <a href="https://Bigcapital.ly">Bigcapital.ly</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,13 +9,24 @@ import 'moment/locale/ar-ly';
|
||||
import 'moment/locale/es-us';
|
||||
|
||||
import AppIntlLoader from './AppIntlLoader';
|
||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
||||
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
|
||||
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';
|
||||
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
|
||||
|
||||
const EmailConfirmation = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||
});
|
||||
const RegisterVerify = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/RegisterVerify'),
|
||||
});
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
@@ -26,9 +37,30 @@ function AppInsider({ history }) {
|
||||
<DashboardThemeProvider>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'} component={Authentication} />
|
||||
<Route path={'/auth/register/verify'}>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailNotVerified>
|
||||
<RegisterVerify />
|
||||
</EnsureUserEmailNotVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth/email_confirmation'}>
|
||||
<EmailConfirmation />
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth'}>
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<Authentication />
|
||||
</EnsureAuthNotAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/'}>
|
||||
<PrivateRoute component={DashboardPrivatePages} />
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailVerified>
|
||||
<DashboardPrivatePages />
|
||||
</EnsureUserEmailVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
useAuthenticatedAccount,
|
||||
useCurrentOrganization,
|
||||
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
|
||||
isBooted.current = true;
|
||||
},
|
||||
);
|
||||
// Reset the loading states once the hook unmount.
|
||||
useEffect(
|
||||
() => () => {
|
||||
isAuthUserLoading && !isBooted.current && stopLoading();
|
||||
isOrgLoading && !isBooted.current && stopLoading();
|
||||
},
|
||||
[isAuthUserLoading, isOrgLoading, stopLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: isOrgLoading || isAuthUserLoading,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
interface EnsureAuthNotAuthenticatedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function EnsureAuthNotAuthenticated({
|
||||
children,
|
||||
redirectTo = '/',
|
||||
}: EnsureAuthNotAuthenticatedProps) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return !isAuthenticated ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Redirect to={{ pathname: redirectTo }} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
interface EnsureAuthenticatedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function EnsureAuthenticated({
|
||||
children,
|
||||
redirectTo = '/auth/login',
|
||||
}: EnsureAuthenticatedProps) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return isAuthenticated ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Redirect to={{ pathname: redirectTo }} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useAuthUserVerified } from '@/hooks/state';
|
||||
|
||||
interface EnsureUserEmailNotVerifiedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher Order Component to ensure that the user's email is not verified.
|
||||
* If is verified, redirects to the inner setup page.
|
||||
*/
|
||||
export function EnsureUserEmailNotVerified({
|
||||
children,
|
||||
redirectTo = '/',
|
||||
}: EnsureUserEmailNotVerifiedProps) {
|
||||
const isAuthVerified = useAuthUserVerified();
|
||||
|
||||
if (isAuthVerified) {
|
||||
return <Redirect to={{ pathname: redirectTo }} />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useAuthUserVerified } from '@/hooks/state';
|
||||
|
||||
interface EnsureUserEmailVerifiedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
redirectTo = '/auth/register/verify',
|
||||
}: EnsureUserEmailVerifiedProps) {
|
||||
const isAuthVerified = useAuthUserVerified();
|
||||
|
||||
if (!isAuthVerified) {
|
||||
return <Redirect to={{ pathname: redirectTo }} />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
export default function PrivateRoute({ component: Component, ...rest }) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return (
|
||||
<BodyClassName className={''}>
|
||||
{isAuthenticated ? (
|
||||
<Component />
|
||||
) : (
|
||||
<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,16 @@
|
||||
// @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,46 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { useAuthSignUpVerify } from '@/hooks/query';
|
||||
import { AppToaster } from '@/components';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
export default function EmailConfirmation() {
|
||||
const { mutateAsync: authSignupVerify } = useAuthSignUpVerify();
|
||||
const history = useHistory();
|
||||
const query = useQuery();
|
||||
|
||||
const token = query.get('token');
|
||||
const email = query.get('email');
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !email) {
|
||||
history.push('/auth/login');
|
||||
}
|
||||
}, [history, token, email]);
|
||||
|
||||
useEffect(() => {
|
||||
authSignupVerify({ token, email })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'Your email has been verified, Congrats!',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
history.push('/');
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
history.push('/');
|
||||
});
|
||||
}, [token, email, authSignupVerify, history]);
|
||||
|
||||
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,70 @@
|
||||
// @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';
|
||||
|
||||
export default function RegisterVerify() {
|
||||
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.',
|
||||
});
|
||||
});
|
||||
};
|
||||
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
|
||||
minimal
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleSignOutBtnClick}
|
||||
>
|
||||
Not my email
|
||||
</Button>
|
||||
</Stack>
|
||||
</AuthInsiderCard>
|
||||
</AuthInsider>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export const useAuthResetPassword = (props) => {
|
||||
*/
|
||||
export const useAuthMetadata = (props) => {
|
||||
return useRequestQuery(
|
||||
[t.AUTH_METADATA_PAGE,],
|
||||
[t.AUTH_METADATA_PAGE],
|
||||
{
|
||||
method: 'get',
|
||||
url: `auth/meta`,
|
||||
@@ -88,5 +88,35 @@ export const useAuthMetadata = (props) => {
|
||||
defaultData: {},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useAuthSignUpVerifyResendMail = (props) => {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
() => apiRequest.post('auth/register/verify/resend'),
|
||||
props,
|
||||
);
|
||||
};
|
||||
|
||||
interface AuthSignUpVerifyValues {
|
||||
token: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useAuthSignUpVerify = (props) => {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
(values: AuthSignUpVerifyValues) =>
|
||||
apiRequest.post('auth/register/verify', values),
|
||||
props,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useQueryTenant, useRequestQuery } from '../useQueryRequest';
|
||||
import useApiRequest from '../useRequest';
|
||||
import { useSetFeatureDashboardMeta } from '../state/feature';
|
||||
import t from './types';
|
||||
import { useSetAuthEmailConfirmed } from '../state';
|
||||
|
||||
// Common invalidate queries.
|
||||
const commonInvalidateQueries = (queryClient) => {
|
||||
@@ -130,6 +131,8 @@ export function useUser(id, props) {
|
||||
}
|
||||
|
||||
export function useAuthenticatedAccount(props) {
|
||||
const setEmailConfirmed = useSetAuthEmailConfirmed();
|
||||
|
||||
return useRequestQuery(
|
||||
['AuthenticatedAccount'],
|
||||
{
|
||||
@@ -139,6 +142,9 @@ export function useAuthenticatedAccount(props) {
|
||||
{
|
||||
select: (response) => response.data.data,
|
||||
defaultData: {},
|
||||
onSuccess: (data) => {
|
||||
setEmailConfirmed(data.is_verified);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
@@ -166,4 +172,3 @@ export const useDashboardMeta = (props) => {
|
||||
}, [state.isSuccess, state.data, setFeatureDashboardMeta]);
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCallback } from 'react';
|
||||
import { isAuthenticated } from '@/store/authentication/authentication.reducer';
|
||||
import { setLogin } from '@/store/authentication/authentication.actions';
|
||||
import {
|
||||
setEmailConfirmed,
|
||||
setLogin,
|
||||
} from '@/store/authentication/authentication.actions';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { removeCookie } from '@/utils';
|
||||
|
||||
@@ -64,3 +67,22 @@ export const useAuthUser = () => {
|
||||
export const useAuthOrganizationId = () => {
|
||||
return useSelector((state) => state.authentication.organizationId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the user's email verification status.
|
||||
*/
|
||||
export const useAuthUserVerified = () => {
|
||||
return useSelector((state) => state.authentication.verified);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user's email verification status.
|
||||
*/
|
||||
export const useSetAuthEmailConfirmed = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useCallback(
|
||||
(verified?: boolean = true) => dispatch(setEmailConfirmed(verified)),
|
||||
[dispatch],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,4 +3,8 @@ import t from '@/store/types';
|
||||
|
||||
export const setLogin = () => ({ type: t.LOGIN_SUCCESS });
|
||||
export const setLogout = () => ({ type: t.LOGOUT });
|
||||
export const setStoreReset = () => ({ type: t.RESET });
|
||||
export const setStoreReset = () => ({ type: t.RESET });
|
||||
export const setEmailConfirmed = (verified?: boolean) => ({
|
||||
type: t.SET_EMAIL_VERIFIED,
|
||||
action: { verified },
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { PayloadAction, createReducer } from '@reduxjs/toolkit';
|
||||
import { persistReducer } from 'redux-persist';
|
||||
import purgeStoredState from 'redux-persist/es/purgeStoredState';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { getCookie } from '@/utils';
|
||||
import t from '@/store/types';
|
||||
|
||||
@@ -13,6 +14,7 @@ const initialState = {
|
||||
tenantId: getCookie('tenant_id'),
|
||||
userId: getCookie('authenticated_user_id'),
|
||||
locale: getCookie('locale'),
|
||||
verified: true, // Let's be optimistic and assume the user's email is confirmed.
|
||||
errors: [],
|
||||
};
|
||||
|
||||
@@ -32,6 +34,15 @@ const reducerInstance = createReducer(initialState, {
|
||||
state.errors = [];
|
||||
},
|
||||
|
||||
[t.SET_EMAIL_VERIFIED]: (
|
||||
state,
|
||||
payload: PayloadAction<{ verified?: boolean }>,
|
||||
) => {
|
||||
state.verified = !isUndefined(payload.action.verified)
|
||||
? payload.action.verified
|
||||
: true;
|
||||
},
|
||||
|
||||
[t.RESET]: (state) => {
|
||||
purgeStoredState(CONFIG);
|
||||
},
|
||||
|
||||
@@ -7,4 +7,5 @@ export default {
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS',
|
||||
RESET: 'RESET',
|
||||
SET_EMAIL_VERIFIED: 'SET_EMAIL_VERIFIED'
|
||||
};
|
||||
Reference in New Issue
Block a user