mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +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_DOMAINS=
|
||||||
SIGNUP_ALLOWED_EMAILS=
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
|
# Sign-up Email Confirmation
|
||||||
|
SIGNUP_EMAIL_CONFIRMATION=false
|
||||||
|
|
||||||
# API rate limit (points,duration,block duration).
|
# API rate limit (points,duration,block duration).
|
||||||
API_RATE_LIMIT=120,60,600
|
API_RATE_LIMIT=120,60,600
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +30,20 @@ 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(
|
||||||
|
'/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(
|
router.post(
|
||||||
'/register',
|
'/register',
|
||||||
this.registerSchema,
|
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.
|
* Reset password schema.
|
||||||
* @returns {ValidationChain[]}
|
* @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
|
* Send reset password handler
|
||||||
* @param {Request} req
|
* @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.
|
* Puppeteer remote browserless connection.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -66,16 +66,27 @@ export interface IAuthResetedPasswordEventPayload {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IAuthSendingResetPassword {
|
export interface IAuthSendingResetPassword {
|
||||||
user: ISystemUser,
|
user: ISystemUser;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
export interface IAuthSendedResetPassword {
|
export interface IAuthSendedResetPassword {
|
||||||
user: ISystemUser,
|
user: ISystemUser;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthGetMetaPOJO {
|
export interface IAuthGetMetaPOJO {
|
||||||
signupDisabled: boolean;
|
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 { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
||||||
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
|
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
|
||||||
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
|
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
|
||||||
|
import { SendVerfiyMailOnSignUp } from '@/services/Authentication/events/SendVerfiyMailOnSignUp';
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@@ -222,6 +223,7 @@ export const susbcribers = () => {
|
|||||||
DeleteCashflowTransactionOnUncategorize,
|
DeleteCashflowTransactionOnUncategorize,
|
||||||
PreventDeleteTransactionOnDelete,
|
PreventDeleteTransactionOnDelete,
|
||||||
|
|
||||||
SubscribeFreeOnSignupCommunity
|
SubscribeFreeOnSignupCommunity,
|
||||||
|
SendVerfiyMailOnSignUp
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleRe
|
|||||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||||
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
||||||
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
||||||
|
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
new ResetPasswordMailJob(agenda);
|
new ResetPasswordMailJob(agenda);
|
||||||
@@ -27,6 +28,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new PaymentReceiveMailNotificationJob(agenda);
|
new PaymentReceiveMailNotificationJob(agenda);
|
||||||
new PlaidFetchTransactionsJob(agenda);
|
new PlaidFetchTransactionsJob(agenda);
|
||||||
new ImportDeleteExpiredFilesJobs(agenda);
|
new ImportDeleteExpiredFilesJobs(agenda);
|
||||||
|
new SendVerifyMailJob(agenda);
|
||||||
|
|
||||||
agenda.start().then(() => {
|
agenda.start().then(() => {
|
||||||
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Service, Inject, Container } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import {
|
import {
|
||||||
IRegisterDTO,
|
IRegisterDTO,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
@@ -9,6 +9,9 @@ import { AuthSigninService } from './AuthSignin';
|
|||||||
import { AuthSignupService } from './AuthSignup';
|
import { AuthSignupService } from './AuthSignup';
|
||||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||||
import { GetAuthMeta } from './GetAuthMeta';
|
import { GetAuthMeta } from './GetAuthMeta';
|
||||||
|
import { AuthSignupConfirmService } from './AuthSignupConfirm';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { AuthSignupConfirmResend } from './AuthSignupResend';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationApplication {
|
export default class AuthenticationApplication {
|
||||||
@@ -18,6 +21,12 @@ export default class AuthenticationApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private authSignupService: AuthSignupService;
|
private authSignupService: AuthSignupService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authSignupConfirmService: AuthSignupConfirmService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authSignUpConfirmResendService: AuthSignupConfirmResend;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private authResetPasswordService: AuthSendResetPassword;
|
private authResetPasswordService: AuthSendResetPassword;
|
||||||
|
|
||||||
@@ -44,6 +53,28 @@ export default class AuthenticationApplication {
|
|||||||
return this.authSignupService.signUp(signupDTO);
|
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.
|
* Generates and retrieve password reset token for the given user email.
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isEmpty, omit } from 'lodash';
|
import { defaultTo, isEmpty, omit } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import crypto from 'crypto';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import {
|
||||||
IAuthSignedUpEventPayload,
|
IAuthSignedUpEventPayload,
|
||||||
@@ -42,6 +43,13 @@ export class AuthSignupService {
|
|||||||
|
|
||||||
const hashedPassword = await hashPassword(signupDTO.password);
|
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.
|
// Triggers signin up event.
|
||||||
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
||||||
signupDTO,
|
signupDTO,
|
||||||
@@ -50,10 +58,12 @@ export class AuthSignupService {
|
|||||||
const tenant = await this.tenantsManager.createTenant();
|
const tenant = await this.tenantsManager.createTenant();
|
||||||
const registeredUser = await systemUserRepository.create({
|
const registeredUser = await systemUserRepository.create({
|
||||||
...omit(signupDTO, 'country'),
|
...omit(signupDTO, 'country'),
|
||||||
|
verifyToken,
|
||||||
|
verified,
|
||||||
active: true,
|
active: true,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
inviteAcceptedAt,
|
||||||
});
|
});
|
||||||
// Triggers signed up event.
|
// Triggers signed up event.
|
||||||
await this.eventPublisher.emitAsync(events.auth.signUp, {
|
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();
|
.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',
|
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||||
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||||
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
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',
|
signUp: 'onSignUp',
|
||||||
signingUp: 'onSigningUp',
|
signingUp: 'onSigningUp',
|
||||||
|
|
||||||
|
signUpConfirming: 'signUpConfirming',
|
||||||
|
signUpConfirmed: 'signUpConfirmed',
|
||||||
|
|
||||||
sendingResetPassword: 'onSendingResetPassword',
|
sendingResetPassword: 'onSendingResetPassword',
|
||||||
sendResetPassword: 'onSendResetPassword',
|
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';
|
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||||
|
|
||||||
export default class SystemUser extends SystemModel {
|
export default class SystemUser extends SystemModel {
|
||||||
|
firstName!: string;
|
||||||
|
lastName!: string;
|
||||||
|
verified!: boolean;
|
||||||
|
inviteAcceptedAt!: Date | null;
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -29,23 +35,33 @@ export default class SystemUser extends SystemModel {
|
|||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['fullName', 'isDeleted', 'isInviteAccepted'];
|
return ['fullName', 'isDeleted', 'isInviteAccepted', 'isVerified'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Detarmines whether the user is deleted.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
get isDeleted() {
|
get isDeleted() {
|
||||||
return !!this.deletedAt;
|
return !!this.deletedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Detarmines whether the sent invite is accepted.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
get isInviteAccepted() {
|
get isInviteAccepted() {
|
||||||
return !!this.inviteAcceptedAt;
|
return !!this.inviteAcceptedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the user's email is verified.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isVerified() {
|
||||||
|
return !!this.verified;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full name attribute.
|
* Full name attribute.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _, { isEmpty } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ const booleanValuesRepresentingTrue: string[] = ['true', '1'];
|
|||||||
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
|
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
|
||||||
|
|
||||||
const normalizeValue = (value: any): string =>
|
const normalizeValue = (value: any): string =>
|
||||||
value.toString().trim().toLowerCase();
|
value?.toString().trim().toLowerCase();
|
||||||
|
|
||||||
const booleanValues: string[] = [
|
const booleanValues: string[] = [
|
||||||
...booleanValuesRepresentingTrue,
|
...booleanValuesRepresentingTrue,
|
||||||
@@ -338,7 +338,7 @@ const booleanValues: string[] = [
|
|||||||
|
|
||||||
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
|
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
|
||||||
const normalizedValue = normalizeValue(value);
|
const normalizedValue = normalizeValue(value);
|
||||||
if (booleanValues.indexOf(normalizedValue) === -1) {
|
if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;
|
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 'moment/locale/es-us';
|
||||||
|
|
||||||
import AppIntlLoader from './AppIntlLoader';
|
import AppIntlLoader from './AppIntlLoader';
|
||||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
|
||||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
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';
|
||||||
|
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
|
||||||
|
|
||||||
|
const EmailConfirmation = LazyLoader({
|
||||||
|
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||||
|
});
|
||||||
|
const RegisterVerify = LazyLoader({
|
||||||
|
loader: () => import('@/containers/Authentication/RegisterVerify'),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App inner.
|
* App inner.
|
||||||
@@ -26,9 +37,30 @@ function AppInsider({ history }) {
|
|||||||
<DashboardThemeProvider>
|
<DashboardThemeProvider>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Switch>
|
<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={'/'}>
|
<Route path={'/'}>
|
||||||
<PrivateRoute component={DashboardPrivatePages} />
|
<EnsureAuthenticated>
|
||||||
|
<EnsureUserEmailVerified>
|
||||||
|
<DashboardPrivatePages />
|
||||||
|
</EnsureUserEmailVerified>
|
||||||
|
</EnsureAuthenticated>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
useAuthenticatedAccount,
|
useAuthenticatedAccount,
|
||||||
useCurrentOrganization,
|
useCurrentOrganization,
|
||||||
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
|
|||||||
isBooted.current = true;
|
isBooted.current = true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// Reset the loading states once the hook unmount.
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
isAuthUserLoading && !isBooted.current && stopLoading();
|
||||||
|
isOrgLoading && !isBooted.current && stopLoading();
|
||||||
|
},
|
||||||
|
[isAuthUserLoading, isOrgLoading, stopLoading],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: isOrgLoading || isAuthUserLoading,
|
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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import { Route, Switch, useLocation } from 'react-router-dom';
|
||||||
import { Redirect, 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,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) => {
|
export const useAuthMetadata = (props) => {
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
[t.AUTH_METADATA_PAGE,],
|
[t.AUTH_METADATA_PAGE],
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `auth/meta`,
|
url: `auth/meta`,
|
||||||
@@ -88,5 +88,35 @@ export const useAuthMetadata = (props) => {
|
|||||||
defaultData: {},
|
defaultData: {},
|
||||||
...props,
|
...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 useApiRequest from '../useRequest';
|
||||||
import { useSetFeatureDashboardMeta } from '../state/feature';
|
import { useSetFeatureDashboardMeta } from '../state/feature';
|
||||||
import t from './types';
|
import t from './types';
|
||||||
|
import { useSetAuthEmailConfirmed } from '../state';
|
||||||
|
|
||||||
// Common invalidate queries.
|
// Common invalidate queries.
|
||||||
const commonInvalidateQueries = (queryClient) => {
|
const commonInvalidateQueries = (queryClient) => {
|
||||||
@@ -130,6 +131,8 @@ export function useUser(id, props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAuthenticatedAccount(props) {
|
export function useAuthenticatedAccount(props) {
|
||||||
|
const setEmailConfirmed = useSetAuthEmailConfirmed();
|
||||||
|
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
['AuthenticatedAccount'],
|
['AuthenticatedAccount'],
|
||||||
{
|
{
|
||||||
@@ -139,6 +142,9 @@ export function useAuthenticatedAccount(props) {
|
|||||||
{
|
{
|
||||||
select: (response) => response.data.data,
|
select: (response) => response.data.data,
|
||||||
defaultData: {},
|
defaultData: {},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setEmailConfirmed(data.is_verified);
|
||||||
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -166,4 +172,3 @@ export const useDashboardMeta = (props) => {
|
|||||||
}, [state.isSuccess, state.data, setFeatureDashboardMeta]);
|
}, [state.isSuccess, state.data, setFeatureDashboardMeta]);
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { isAuthenticated } from '@/store/authentication/authentication.reducer';
|
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 { useQueryClient } from 'react-query';
|
||||||
import { removeCookie } from '@/utils';
|
import { removeCookie } from '@/utils';
|
||||||
|
|
||||||
@@ -64,3 +67,22 @@ export const useAuthUser = () => {
|
|||||||
export const useAuthOrganizationId = () => {
|
export const useAuthOrganizationId = () => {
|
||||||
return useSelector((state) => state.authentication.organizationId);
|
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'),
|
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'),
|
||||||
}),
|
}),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,4 +3,8 @@ import t from '@/store/types';
|
|||||||
|
|
||||||
export const setLogin = () => ({ type: t.LOGIN_SUCCESS });
|
export const setLogin = () => ({ type: t.LOGIN_SUCCESS });
|
||||||
export const setLogout = () => ({ type: t.LOGOUT });
|
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
|
// @ts-nocheck
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
import { PayloadAction, createReducer } from '@reduxjs/toolkit';
|
||||||
import { persistReducer } from 'redux-persist';
|
import { persistReducer } from 'redux-persist';
|
||||||
import purgeStoredState from 'redux-persist/es/purgeStoredState';
|
import purgeStoredState from 'redux-persist/es/purgeStoredState';
|
||||||
import storage from 'redux-persist/lib/storage';
|
import storage from 'redux-persist/lib/storage';
|
||||||
|
import { isUndefined } from 'lodash';
|
||||||
import { getCookie } from '@/utils';
|
import { getCookie } from '@/utils';
|
||||||
import t from '@/store/types';
|
import t from '@/store/types';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ const initialState = {
|
|||||||
tenantId: getCookie('tenant_id'),
|
tenantId: getCookie('tenant_id'),
|
||||||
userId: getCookie('authenticated_user_id'),
|
userId: getCookie('authenticated_user_id'),
|
||||||
locale: getCookie('locale'),
|
locale: getCookie('locale'),
|
||||||
|
verified: true, // Let's be optimistic and assume the user's email is confirmed.
|
||||||
errors: [],
|
errors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +34,15 @@ const reducerInstance = createReducer(initialState, {
|
|||||||
state.errors = [];
|
state.errors = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[t.SET_EMAIL_VERIFIED]: (
|
||||||
|
state,
|
||||||
|
payload: PayloadAction<{ verified?: boolean }>,
|
||||||
|
) => {
|
||||||
|
state.verified = !isUndefined(payload.action.verified)
|
||||||
|
? payload.action.verified
|
||||||
|
: true;
|
||||||
|
},
|
||||||
|
|
||||||
[t.RESET]: (state) => {
|
[t.RESET]: (state) => {
|
||||||
purgeStoredState(CONFIG);
|
purgeStoredState(CONFIG);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export default {
|
|||||||
LOGOUT: 'LOGOUT',
|
LOGOUT: 'LOGOUT',
|
||||||
LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS',
|
LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS',
|
||||||
RESET: 'RESET',
|
RESET: 'RESET',
|
||||||
|
SET_EMAIL_VERIFIED: 'SET_EMAIL_VERIFIED'
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user