Compare commits

...

6 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
8c2b9fba29 fix: add the signup email confirmation env var (#458) 2024-05-22 21:10:56 +02:00
Ahmed Bouhuolia
7208b8fab5 feat: add the missing Newrelic env vars to docker-compose.prod file (#457) 2024-05-22 20:42:00 +02:00
Ahmed Bouhuolia
0836fe14e0 feat: handle http exceptions (#456) 2024-05-22 19:30:41 +02:00
Ahmed Bouhuolia
1227111fae fix: typo 2024-05-18 15:44:53 +02:00
Ahmed Bouhuolia
2ada57a2b4 fix: auto-increment setting parsing. (#453) 2024-05-17 12:49:04 +02:00
Ahmed Bouhuolia
e380c598d3 fix: Showing the real mail address on email confirmation view (#445)
* fix: Showing the real mail address on email confirmation view

* chore: remove the unused hook
2024-05-15 14:03:58 +02:00
28 changed files with 290 additions and 112 deletions

View File

@@ -82,6 +82,9 @@ services:
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS} - SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS} - SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
# Sign-up email confirmation
- SIGNUP_EMAIL_CONFIRMATION=${SIGNUP_EMAIL_CONFIRMATION}
# Gotenberg (Pdf generator) # Gotenberg (Pdf generator)
- GOTENBERG_URL=${GOTENBERG_URL} - GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL} - GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
@@ -108,7 +111,8 @@ services:
- NEW_RELIC_AI_MONITORING_ENABLED=${NEW_RELIC_AI_MONITORING_ENABLED} - NEW_RELIC_AI_MONITORING_ENABLED=${NEW_RELIC_AI_MONITORING_ENABLED}
- NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED} - NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED}
- NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED} - NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED}
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
- NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration

View File

@@ -207,7 +207,6 @@ export default class AccountsController extends BaseController {
tenantId, tenantId,
accountDTO accountDTO
); );
return res.status(200).send({ return res.status(200).send({
id: account.id, id: account.id,
message: 'The account has been created successfully.', message: 'The account has been created successfully.',

View File

@@ -14,7 +14,7 @@ export class ExportController extends BaseController {
/** /**
* Router constructor method. * Router constructor method.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -53,6 +53,7 @@ export class ExportController extends BaseController {
query.resource, query.resource,
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
); );
// Retrieves the csv format.
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Type', 'text/csv');

View File

@@ -16,7 +16,7 @@ export class ImportController extends BaseController {
/** /**
* Router constructor method. * Router constructor method.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -240,11 +240,7 @@ export class ImportController extends BaseController {
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }], errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
}); });
} }
return res.status(400).send({
errors: [{ type: error.errorType }],
});
} }
next(error); next(error);
} }
} }

View File

@@ -77,14 +77,14 @@ export default class ManualJournalsController extends BaseController {
/** /**
* Specific manual journal id param validation schema. * Specific manual journal id param validation schema.
*/ */
get manualJournalParamSchema() { private get manualJournalParamSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Manual journal DTO schema. * Manual journal DTO schema.
*/ */
get manualJournalValidationSchema() { private get manualJournalValidationSchema() {
return [ return [
check('date').exists().isISO8601(), check('date').exists().isISO8601(),
check('currency_code').optional(), check('currency_code').optional(),
@@ -154,7 +154,7 @@ export default class ManualJournalsController extends BaseController {
/** /**
* Manual journals list validation schema. * Manual journals list validation schema.
*/ */
get manualJournalsListSchema() { private get manualJournalsListSchema() {
return [ return [
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
@@ -320,7 +320,7 @@ export default class ManualJournalsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
getManualJournalsList = async ( private getManualJournalsList = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction

View File

@@ -33,17 +33,17 @@ export default class OrganizationDashboardController extends BaseController {
} }
/** /**
* * Detarmines whether the current authed organization to able to change its currency/.
* @param req * @param {Request} req
* @param res * @param {Response} res
* @param next * @param {NextFunction} next
* @returns * @returns {Response|void}
*/ */
private async baseCurrencyMutateAbility( private async baseCurrencyMutateAbility(
req: Request, req: Request,
res: Response, res: Response,
next: Function next: Function
) { ): Promise<Response|void> {
const { tenantId } = req; const { tenantId } = req;
try { try {

View File

@@ -29,8 +29,7 @@ export class ProjectsController extends BaseController {
check('cost_estimate').exists().isDecimal(), check('cost_estimate').exists().isDecimal(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.createProject.bind(this)), asyncMiddleware(this.createProject.bind(this))
this.catchServiceErrors
); );
router.post( router.post(
'/:id', '/:id',
@@ -43,8 +42,7 @@ export class ProjectsController extends BaseController {
check('cost_estimate').exists().isDecimal(), check('cost_estimate').exists().isDecimal(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editProject.bind(this)), asyncMiddleware(this.editProject.bind(this))
this.catchServiceErrors
); );
router.patch( router.patch(
'/:projectId/status', '/:projectId/status',
@@ -56,16 +54,14 @@ export class ProjectsController extends BaseController {
.isIn([IProjectStatus.InProgress, IProjectStatus.Closed]), .isIn([IProjectStatus.InProgress, IProjectStatus.Closed]),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editProject.bind(this)), asyncMiddleware(this.editProject.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/:id', '/:id',
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project), CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
[param('id').exists().isInt().toInt()], [param('id').exists().isInt().toInt()],
this.validationResult, this.validationResult,
asyncMiddleware(this.getProject.bind(this)), asyncMiddleware(this.getProject.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/:projectId/billable/entries', '/:projectId/billable/entries',
@@ -76,24 +72,21 @@ export class ProjectsController extends BaseController {
query('to_date').optional().isISO8601(), query('to_date').optional().isISO8601(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.projectBillableEntries.bind(this)), asyncMiddleware(this.projectBillableEntries.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/', '/',
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project), CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
[], [],
this.validationResult, this.validationResult,
asyncMiddleware(this.getProjects.bind(this)), asyncMiddleware(this.getProjects.bind(this))
this.catchServiceErrors
); );
router.delete( router.delete(
'/:id', '/:id',
CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project), CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project),
[param('id').exists().isInt().toInt()], [param('id').exists().isInt().toInt()],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteProject.bind(this)), asyncMiddleware(this.deleteProject.bind(this))
this.catchServiceErrors
); );
return router; return router;
} }
@@ -252,22 +245,4 @@ export class ProjectsController extends BaseController {
next(error); next(error);
} }
}; };
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
}
next(error);
}
} }

View File

@@ -155,6 +155,7 @@ export default class UsersController extends BaseController {
try { try {
const user = await this.usersService.getUser(tenantId, userId); const user = await this.usersService.getUser(tenantId, userId);
return res.status(200).send({ user }); return res.status(200).send({ user });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -229,7 +230,7 @@ export default class UsersController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
catchServiceErrors( private catchServiceErrors(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -17,7 +17,7 @@ export class WarehousesController extends BaseController {
* *
* @returns * @returns
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(

View File

@@ -34,14 +34,21 @@ export class Webhooks extends BaseController {
* @param {Response} res * @param {Response} res
* @returns {Response} * @returns {Response}
*/ */
public async lemonWebhooks(req: Request, res: Response) { public async lemonWebhooks(req: Request, res: Response, next: any) {
const data = req.body; const data = req.body;
const signature = req.headers['x-signature'] ?? ''; const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody; const rawBody = req.rawBody;
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature); try {
await this.lemonWebhooksService.handlePostWebhook(
return res.status(200).send(); rawBody,
data,
signature
);
return res.status(200).send();
} catch (error) {
next(error);
}
} }
/** /**

View File

@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from 'express';
/**
* Global error handler.
* @param {Error} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function GlobalErrorException(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error(err.stack);
res.status(500);
res.boom.badImplementation('', { stack: err.stack });
}

View File

@@ -10,8 +10,14 @@ import {
DataError, DataError,
} from 'objection'; } from 'objection';
// In this example `res` is an express response object. /**
export default function ObjectionErrorHandlerMiddleware( * Handles the Objection error exception.
* @param {Error} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function ObjectionErrorException(
err: Error, err: Error,
req: Request, req: Request,
res: Response, res: Response,
@@ -108,6 +114,7 @@ export default function ObjectionErrorHandlerMiddleware(
type: 'UnknownDatabaseError', type: 'UnknownDatabaseError',
data: {}, data: {},
}); });
} else {
next(err);
} }
next(err);
} }

View File

@@ -0,0 +1,25 @@
import { NextFunction, Request, Response } from 'express';
import { ServiceError } from '@/exceptions';
/**
* Handles service error exception.
* @param {Error | ServiceError} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function ServiceErrorException(
err: Error | ServiceError,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof ServiceError) {
res.boom.badRequest('', {
errors: [{ type: err.errorType, message: err.message }],
type: 'ServiceError',
});
} else {
next(err);
}
}

View File

@@ -17,7 +17,9 @@ import {
} from '@/api/middleware/JSONResponseTransformer'; } from '@/api/middleware/JSONResponseTransformer';
import config from '@/config'; import config from '@/config';
import path from 'path'; import path from 'path';
import ObjectionErrorHandlerMiddleware from '@/api/middleware/ObjectionErrorHandlerMiddleware'; import { ObjectionErrorException } from '@/api/exceptions/ObjectionErrorException';
import { ServiceErrorException } from '@/api/exceptions/ServiceErrorException';
import { GlobalErrorException } from '@/api/exceptions/GlobalErrorException';
export default ({ app }) => { export default ({ app }) => {
// Express configuration. // Express configuration.
@@ -30,9 +32,6 @@ export default ({ app }) => {
// Helmet helps you secure your Express apps by setting various HTTP headers. // Helmet helps you secure your Express apps by setting various HTTP headers.
app.use(helmet()); app.use(helmet());
// Allow to full error stack traces and internal details
app.use(errorHandler());
// Boom response objects. // Boom response objects.
app.use(boom()); app.use(boom());
@@ -65,8 +64,10 @@ export default ({ app }) => {
// Agendash application load. // Agendash application load.
app.use('/agendash', AgendashController.router()); app.use('/agendash', AgendashController.router());
// Handles objectionjs errors. // Handles errors.
app.use(ObjectionErrorHandlerMiddleware); app.use(ObjectionErrorException);
app.use(ServiceErrorException);
app.use(GlobalErrorException);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req: Request, res: Response, next: NextFunction) => { app.use((req: Request, res: Response, next: NextFunction) => {

View File

@@ -1,4 +1,4 @@
import { difference } from 'lodash'; import { difference, isEmpty } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
@@ -244,16 +244,12 @@ export class CommandManualJournalValidators {
/** /**
* Validates the manual journal number require. * Validates the manual journal number require.
* @param {string} journalNumber * @param {string} journalNumber
* @throws {ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED)}
*/ */
public validateJournalNoRequireWhenAutoNotEnabled = ( public validateJournalNoRequireWhenAutoNotEnabled = (
tenantId: number,
journalNumber: string journalNumber: string
) => { ) => {
// Retrieve the next manual journal number. if (isEmpty(journalNumber)) {
const autoIncrmenetEnabled =
this.autoIncrement.autoIncrementEnabled(tenantId);
if (!journalNumber || !autoIncrmenetEnabled) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED); throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
} }
}; };

View File

@@ -99,7 +99,6 @@ export class CreateManualJournalService {
// Validate manual journal number require when auto-increment not enabled. // Validate manual journal number require when auto-increment not enabled.
this.validator.validateJournalNoRequireWhenAutoNotEnabled( this.validator.validateJournalNoRequireWhenAutoNotEnabled(
tenantId,
manualJournalDTO.journalNumber manualJournalDTO.journalNumber
); );
// Validate manual journal uniquiness on the storage. // Validate manual journal uniquiness on the storage.

View File

@@ -15,10 +15,8 @@ export default class AutoIncrementOrdersService {
const group = settingsGroup; const group = settingsGroup;
// Settings service transaction number and prefix. // Settings service transaction number and prefix.
const autoIncrement = settings.get({ group, key: 'auto_increment' }, false); return settings.get({ group, key: 'auto_increment' }, false);
};
return parseBoolean(autoIncrement, false);
}
/** /**
* Retrieve the next service transaction number. * Retrieve the next service transaction number.
@@ -27,17 +25,16 @@ export default class AutoIncrementOrdersService {
* @param {Function} getMaxTransactionNo * @param {Function} getMaxTransactionNo
* @return {Promise<string>} * @return {Promise<string>}
*/ */
getNextTransactionNumber(tenantId: number, settingsGroup: string): string { getNextTransactionNumber(tenantId: number, group: string): string {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const group = settingsGroup;
// Settings service transaction number and prefix. // Settings service transaction number and prefix.
const autoIncrement = settings.get({ group, key: 'auto_increment' }, false); const autoIncrement = this.autoIncrementEnabled(tenantId, group);
const settingNo = settings.get({ group, key: 'next_number' }, ''); const settingNo = settings.get({ group, key: 'next_number' }, '');
const settingPrefix = settings.get({ group, key: 'number_prefix' }, ''); const settingPrefix = settings.get({ group, key: 'number_prefix' }, '');
return parseBoolean(autoIncrement, false) ? `${settingPrefix}${settingNo}` : ''; return autoIncrement ? `${settingPrefix}${settingNo}` : '';
} }
/** /**
@@ -53,7 +50,9 @@ export default class AutoIncrementOrdersService {
const autoIncrement = settings.get({ group, key: 'auto_increment' }); const autoIncrement = settings.get({ group, key: 'auto_increment' });
// Can't continue if the auto-increment of the service was disabled. // Can't continue if the auto-increment of the service was disabled.
if (!autoIncrement) { return; } if (!autoIncrement) {
return;
}
settings.set( settings.set(
{ group, key: 'next_number' }, { group, key: 'next_number' },

View File

@@ -10,6 +10,7 @@ import {
} from './utils'; } from './utils';
import { Plan } from '@/system/models'; import { Plan } from '@/system/models';
import { Subscription } from './Subscription'; import { Subscription } from './Subscription';
import { isEmpty } from 'lodash';
@Service() @Service()
export class LemonSqueezyWebhooks { export class LemonSqueezyWebhooks {
@@ -32,6 +33,9 @@ export class LemonSqueezyWebhooks {
if (!config.lemonSqueezy.webhookSecret) { if (!config.lemonSqueezy.webhookSecret) {
throw new Error('Lemon Squeezy Webhook Secret not set in .env'); throw new Error('Lemon Squeezy Webhook Secret not set in .env');
} }
if (!signature) {
throw new Error('Request signature is required.');
}
const secret = config.lemonSqueezy.webhookSecret; const secret = config.lemonSqueezy.webhookSecret;
const hmacSignature = createHmacSignature(secret, rawData); const hmacSignature = createHmacSignature(secret, rawData);
@@ -96,7 +100,7 @@ export class LemonSqueezyWebhooks {
if (webhookEvent === 'subscription_created') { if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion( await this.subscriptionService.newSubscribtion(
tenantId, tenantId,
'early-adaptor', 'early-adaptor'
); );
} }
} }

View File

@@ -337,6 +337,9 @@ const booleanValues: string[] = [
].map((value) => normalizeValue(value)); ].map((value) => normalizeValue(value));
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => { export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
if (typeof value === 'boolean') {
return value; // Retrun early we have nothing to parse.
}
const normalizedValue = normalizeValue(value); const normalizedValue = normalizeValue(value);
if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) { if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) {
return defaultValue; return defaultValue;

View File

@@ -9,7 +9,11 @@ import AuthInsider from '@/containers/Authentication/AuthInsider';
import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication'; import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication';
import RegisterForm from './RegisterForm'; import RegisterForm from './RegisterForm';
import { RegisterSchema, transformRegisterErrorsToForm, transformRegisterToastMessages } from './utils'; import {
RegisterSchema,
transformRegisterErrorsToForm,
transformRegisterToastMessages,
} from './utils';
import { import {
AuthFooterLinks, AuthFooterLinks,
AuthFooterLink, AuthFooterLink,
@@ -32,7 +36,7 @@ export default function RegisterUserForm() {
const handleSubmit = (values, { setSubmitting, setErrors }) => { const handleSubmit = (values, { setSubmitting, setErrors }) => {
authRegisterMutate(values) authRegisterMutate(values)
.then((response) => { .then(() => {
authLoginMutate({ authLoginMutate({
crediential: values.email, crediential: values.email,
password: values.password, password: values.password,
@@ -87,7 +91,10 @@ function RegisterFooterLinks() {
return ( return (
<AuthFooterLinks> <AuthFooterLinks>
<AuthFooterLink> <AuthFooterLink>
<T id={'return_to'} /> <Link to={'/auth/login'}><T id={'sign_in'} /></Link> <T id={'return_to'} />{' '}
<Link to={'/auth/login'}>
<T id={'sign_in'} />
</Link>
</AuthFooterLink> </AuthFooterLink>
<AuthFooterLink> <AuthFooterLink>

View File

@@ -4,7 +4,7 @@ import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components'; import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss'; import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components'; import { AppToaster, Stack } from '@/components';
import { useAuthActions } from '@/hooks/state'; import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query'; import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer'; import { AuthContainer } from './AuthContainer';
@@ -13,6 +13,8 @@ export default function RegisterVerify() {
const { mutateAsync: resendSignUpVerifyMail, isLoading } = const { mutateAsync: resendSignUpVerifyMail, isLoading } =
useAuthSignUpVerifyResendMail(); useAuthSignUpVerifyResendMail();
const emailAddress = useAuthUserVerifyEmail();
const handleResendMailBtnClick = () => { const handleResendMailBtnClick = () => {
resendSignUpVerifyMail() resendSignUpVerifyMail()
.then(() => { .then(() => {
@@ -38,8 +40,8 @@ export default function RegisterVerify() {
<AuthInsiderCard className={styles.root}> <AuthInsiderCard className={styles.root}>
<h2 className={styles.title}>Please verify your email</h2> <h2 className={styles.title}>Please verify your email</h2>
<p className={styles.description}> <p className={styles.description}>
We sent an email to <strong>asdahmed@gmail.com</strong> Click the We sent an email to <strong>{emailAddress}</strong> Click the link
link inside to get started. inside to get started.
</p> </p>
<Stack spacing={4}> <Stack spacing={4}>

View File

@@ -1,10 +1,8 @@
// @ts-nocheck // @ts-nocheck
import { T } from '@/components'; import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlan'; import { SubscriptionPlans } from './SubscriptionPlan';
import withPlans from '../../Subscriptions/withPlans'; import withPlans from '../../Subscriptions/withPlans';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { Callout, Intent } from '@blueprintjs/core';
/** /**
* Billing plans. * Billing plans.
@@ -15,10 +13,11 @@ function SubscriptionPlansSectionRoot({ plans }) {
<Callout <Callout
style={{ marginBottom: '1.5rem' }} style={{ marginBottom: '1.5rem' }}
icon={null} icon={null}
title={'Early Adaptors Plan'} title={'Early Adopter Plan'}
> >
We're looking for 200 early adaptors, when you subscribe you'll get We're looking for 200 early adopters, when you subscribe you'll get the
the full features and unlimited users for a year regardless of the subscribed plan. full features and unlimited users for a year regardless of the
subscribed plan.
</Callout> </Callout>
<SubscriptionPlans plans={plans} /> <SubscriptionPlans plans={plans} />
</section> </section>

View File

@@ -1,9 +1,17 @@
// @ts-nocheck // @ts-nocheck
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { batch } from 'react-redux';
import useApiRequest from '../useRequest'; import useApiRequest from '../useRequest';
import { setCookie } from '../../utils'; import { setCookie } from '../../utils';
import { useRequestQuery } from '../useQueryRequest'; import { useRequestQuery } from '../useQueryRequest';
import t from './types'; import t from './types';
import {
useSetAuthToken,
useSetAuthUserId,
useSetLocale,
useSetOrganizationId,
useSetTenantId,
} from '../state';
/** /**
* Saves the response data to cookies. * Saves the response data to cookies.
@@ -24,14 +32,30 @@ function setAuthLoginCookies(data) {
export const useAuthLogin = (props) => { export const useAuthLogin = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
const setAuthToken = useSetAuthToken();
const setOrganizationId = useSetOrganizationId();
const setUserId = useSetAuthUserId();
const setTenantId = useSetTenantId();
const setLocale = useSetLocale();
return useMutation((values) => apiRequest.post('auth/login', values), { return useMutation((values) => apiRequest.post('auth/login', values), {
select: (res) => res.data, select: (res) => res.data,
onSuccess: (data) => { onSuccess: (res) => {
// Set authentication cookies. // Set authentication cookies.
setAuthLoginCookies(data.data); setAuthLoginCookies(res.data);
// Reboot the application. batch(() => {
window.location.reload(); // Sets the auth metadata to global state.
setAuthToken(res.data.token);
setOrganizationId(res.data.tenant.organization_id);
setUserId(res.data.user.id);
setTenantId(res.data.tenant.id);
if (res.data?.tenant?.metadata?.language) {
setLocale(res.data?.tenant?.metadata?.language);
}
});
props?.onSuccess && props?.onSuccess(...args);
}, },
...props, ...props,
}); });

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import { useQueryTenant, useRequestQuery } from '../useQueryRequest'; import { 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';
@@ -143,7 +143,7 @@ export function useAuthenticatedAccount(props) {
select: (response) => response.data.data, select: (response) => response.data.data,
defaultData: {}, defaultData: {},
onSuccess: (data) => { onSuccess: (data) => {
setEmailConfirmed(data.is_verified); setEmailConfirmed(data.is_verified, data.email);
}, },
...props, ...props,
}, },

View File

@@ -3,8 +3,13 @@ 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 { import {
setAuthTenantId,
setAuthToken,
setAuthUserId,
setEmailConfirmed, setEmailConfirmed,
setLogin, setLogin,
setOrganizationId,
setLocale,
} from '@/store/authentication/authentication.actions'; } from '@/store/authentication/authentication.actions';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { removeCookie } from '@/utils'; import { removeCookie } from '@/utils';
@@ -75,6 +80,14 @@ export const useAuthUserVerified = () => {
return useSelector((state) => state.authentication.verified); return useSelector((state) => state.authentication.verified);
}; };
/**
* Retrieves the user's email address.
* @returns {string}
*/
export const useAuthUserVerifyEmail = () => {
return useSelector((state) => state.authentication.verifyEmail);
};
/** /**
* Sets the user's email verification status. * Sets the user's email verification status.
*/ */
@@ -82,7 +95,53 @@ export const useSetAuthEmailConfirmed = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
return useCallback( return useCallback(
(verified?: boolean = true) => dispatch(setEmailConfirmed(verified)), (verified?: boolean = true, email: string) =>
dispatch(setEmailConfirmed(verified, email)),
[dispatch],
);
};
export const useSetOrganizationId = () => {
const dispatch = useDispatch();
return useCallback(
(organizationId: string) => dispatch(setOrganizationId(organizationId)),
[dispatch],
);
};
export const useSetAuthToken = () => {
const dispatch = useDispatch();
return useCallback(
(authToken: string) => dispatch(setAuthToken(authToken)),
[dispatch],
);
};
export const useSetTenantId = () => {
const dispatch = useDispatch();
return useCallback(
(tenantId: string) => dispatch(setAuthTenantId(tenantId)),
[dispatch],
);
};
export const useSetAuthUserId = () => {
const dispatch = useDispatch();
return useCallback(
(userId: string) => dispatch(setAuthUserId(userId)),
[dispatch],
);
};
export const useSetLocale = () => {
const dispatch = useDispatch();
return useCallback(
(locale: string) => dispatch(setLocale(locale)),
[dispatch], [dispatch],
); );
}; };

View File

@@ -4,7 +4,27 @@ 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) => ({ export const setEmailConfirmed = (verified?: boolean, email?: string) => ({
type: t.SET_EMAIL_VERIFIED, type: t.SET_EMAIL_VERIFIED,
action: { verified }, action: { verified, email },
});
export const setOrganizationId = (organizationId: string) => ({
type: t.SET_ORGANIZATIOIN_ID,
action: { organizationId },
});
export const setAuthToken = (token: string) => ({
type: t.SET_AUTH_TOKEN,
action: { token },
});
export const setAuthTenantId = (tenantId: string) => ({
type: t.SET_TENANT_ID,
action: { tenantId },
});
export const setAuthUserId = (userId: string) => ({
type: t.SET_USER_ID,
action: { userId },
});
export const setLocale = (locale: string) => ({
type: t.SET_LOCALE,
action: { locale },
}); });

View File

@@ -9,12 +9,13 @@ import t from '@/store/types';
// Read stored data in cookies and merge it with the initial state. // Read stored data in cookies and merge it with the initial state.
const initialState = { const initialState = {
token: getCookie('token'), token: getCookie('token') || null,
organizationId: getCookie('organization_id'), organizationId: getCookie('organization_id') || null,
tenantId: getCookie('tenant_id'), tenantId: getCookie('tenant_id') || null,
userId: getCookie('authenticated_user_id'), userId: getCookie('authenticated_user_id') || null,
locale: getCookie('locale'), locale: getCookie('locale') || 'en',
verified: true, // Let's be optimistic and assume the user's email is confirmed. verified: true, // Let's be optimistic and assume the user's email is confirmed.
verifyEmail: null,
errors: [], errors: [],
}; };
@@ -36,11 +37,35 @@ const reducerInstance = createReducer(initialState, {
[t.SET_EMAIL_VERIFIED]: ( [t.SET_EMAIL_VERIFIED]: (
state, state,
payload: PayloadAction<{ verified?: boolean }>, payload: PayloadAction<{ verified?: boolean; email?: string }>,
) => { ) => {
state.verified = !isUndefined(payload.action.verified) state.verified = !isUndefined(payload.action.verified)
? payload.action.verified ? payload.action.verified
: true; : true;
state.verifyEmail = payload.action.email || null;
if (state.verified) {
state.verifyEmail = null;
}
},
[t.SET_AUTH_TOKEN]: (state, payload: PayloadAction<{ token: string }>) => {
state.token = payload.action.token;
},
[t.SET_ORGANIZATIOIN_ID]: (
state,
payload: PayloadAction<{ organizationId: string }>,
) => {
state.organizationId = payload.action.organizationId;
},
[t.SET_TENANT_ID]: (state, payload: PayloadAction<{ tenantId: string }>) => {
state.tenantId = payload.action.tenantId;
},
[t.SET_USER_ID]: (state, payload: PayloadAction<{ userId: string }>) => {
state.userId = payload.action.userId;
}, },
[t.RESET]: (state) => { [t.RESET]: (state) => {

View File

@@ -7,5 +7,10 @@ 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' SET_EMAIL_VERIFIED: 'SET_EMAIL_VERIFIED',
SET_AUTH_TOKEN: 'SET_AUTH_TOKEN',
SET_ORGANIZATIOIN_ID: 'SET_ORGANIZATIOIN_ID',
SET_TENANT_ID: 'SET_TENANT_ID',
SET_USER_ID: 'SET_USER_ID',
SET_LOCALE: 'SET_LOCALE',
}; };