mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat(server): sign-up restrictions for self-hosted
This commit is contained in:
@@ -72,6 +72,11 @@ services:
|
|||||||
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
|
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
|
||||||
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
|
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
|
||||||
|
|
||||||
|
# Sign-up restrictions
|
||||||
|
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
|
||||||
|
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
|
||||||
|
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
|
||||||
|
|
||||||
database_migration:
|
database_migration:
|
||||||
container_name: bigcapital-database-migration
|
container_name: bigcapital-database-migration
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
|
|||||||
BASE_URL= \
|
BASE_URL= \
|
||||||
# Agendash
|
# Agendash
|
||||||
AGENDASH_AUTH_USER=agendash \
|
AGENDASH_AUTH_USER=agendash \
|
||||||
AGENDASH_AUTH_PASSWORD=123123
|
AGENDASH_AUTH_PASSWORD=123123 \
|
||||||
|
# Sign-up restriction
|
||||||
|
SIGNUP_DISABLED= \
|
||||||
|
SIGNUP_ALLOWED_DOMAINS= \
|
||||||
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
ENV MAIL_HOST=$MAIL_HOST \
|
ENV MAIL_HOST=$MAIL_HOST \
|
||||||
MAIL_USERNAME=$MAIL_USERNAME \
|
MAIL_USERNAME=$MAIL_USERNAME \
|
||||||
@@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \
|
|||||||
# MongoDB
|
# MongoDB
|
||||||
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
|
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
|
||||||
# Application
|
# Application
|
||||||
BASE_URL=$BASE_URL
|
BASE_URL=$BASE_URL \
|
||||||
|
# Sign-up restriction
|
||||||
|
SIGNUP_DISABLED=$SIGNUP_DISABLED \
|
||||||
|
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
|
||||||
|
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
|
||||||
|
|
||||||
# Create app directory.
|
# Create app directory.
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController {
|
|||||||
asyncMiddleware(this.resetPassword.bind(this)),
|
asyncMiddleware(this.resetPassword.bind(this)),
|
||||||
this.handlerErrors
|
this.handlerErrors
|
||||||
);
|
);
|
||||||
|
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +208,23 @@ export default class AuthenticationController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the authentication meta for SPA.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
* @returns {Response|void}
|
||||||
|
*/
|
||||||
|
private async getAuthMeta(req: Request, res: Response, next: Function) {
|
||||||
|
try {
|
||||||
|
const meta = await this.authApplication.getAuthMeta();
|
||||||
|
|
||||||
|
return res.status(200).send({ meta });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the service errors.
|
* Handles the service errors.
|
||||||
*/
|
*/
|
||||||
@@ -247,6 +265,42 @@ export default class AuthenticationController extends BaseController {
|
|||||||
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (error.errorType === 'SIGNUP_RESTRICTED') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'SIGNUP_RESTRICTED',
|
||||||
|
message:
|
||||||
|
'Sign-up is restricted no one can sign-up to the system.',
|
||||||
|
code: 700,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'SIGNUP_NOT_ALLOWED_EMAIL_DOMAIN') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'SIGNUP_NOT_ALLOWED_EMAIL_DOMAIN',
|
||||||
|
message:
|
||||||
|
'Sign-up is restricted the given email domain is not allowed to sign-up.',
|
||||||
|
code: 710,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'SIGNUP_NOT_ALLOWED_EMAIL_ADDRESS') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'SIGNUP_NOT_ALLOWED_EMAIL_ADDRESS',
|
||||||
|
message:
|
||||||
|
'The sign-up restricted the given email address is not allowed to sign-up.',
|
||||||
|
code: 720,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -146,6 +147,19 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign-up restrictions
|
||||||
|
*/
|
||||||
|
signupRestrictions: {
|
||||||
|
disabled: parseBoolean<boolean>(process.env.SIGNUP_DISABLED, false),
|
||||||
|
allowedDomains: castCommaListEnvVarToArray(
|
||||||
|
process.env.SIGNUP_ALLOWED_DOMAINS
|
||||||
|
),
|
||||||
|
allowedEmails: castCommaListEnvVarToArray(
|
||||||
|
process.env.SIGNUP_ALLOWED_EMAILS
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puppeteer remote browserless connection.
|
* Puppeteer remote browserless connection.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword {
|
|||||||
export interface IAuthSendedResetPassword {
|
export interface IAuthSendedResetPassword {
|
||||||
user: ISystemUser,
|
user: ISystemUser,
|
||||||
token: string;
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthGetMetaPOJO {
|
||||||
|
signupDisabled: boolean;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Service, Inject, Container } from 'typedi';
|
import { Service, Inject, Container } from 'typedi';
|
||||||
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces';
|
import {
|
||||||
|
IRegisterDTO,
|
||||||
|
ISystemUser,
|
||||||
|
IPasswordReset,
|
||||||
|
IAuthGetMetaPOJO,
|
||||||
|
} from '@/interfaces';
|
||||||
import { AuthSigninService } from './AuthSignin';
|
import { AuthSigninService } from './AuthSignin';
|
||||||
import { AuthSignupService } from './AuthSignup';
|
import { AuthSignupService } from './AuthSignup';
|
||||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||||
|
import { GetAuthMeta } from './GetAuthMeta';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationApplication {
|
export default class AuthenticationApplication {
|
||||||
@@ -15,6 +21,9 @@ export default class AuthenticationApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private authResetPasswordService: AuthSendResetPassword;
|
private authResetPasswordService: AuthSendResetPassword;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authGetMeta: GetAuthMeta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signin and generates JWT token.
|
* Signin and generates JWT token.
|
||||||
* @throws {ServiceError}
|
* @throws {ServiceError}
|
||||||
@@ -53,4 +62,12 @@ export default class AuthenticationApplication {
|
|||||||
public async resetPassword(token: string, password: string): Promise<void> {
|
public async resetPassword(token: string, password: string): Promise<void> {
|
||||||
return this.authResetPasswordService.resetPassword(token, password);
|
return this.authResetPasswordService.resetPassword(token, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the authentication meta for SPA.
|
||||||
|
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||||
|
*/
|
||||||
|
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||||
|
return this.authGetMeta.getAuthMeta();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { omit } from 'lodash';
|
import { isEmpty, omit } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
|||||||
import TenantsManagerService from '../Tenancy/TenantsManager';
|
import TenantsManagerService from '../Tenancy/TenantsManager';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { hashPassword } from '@/utils';
|
import { hashPassword } from '@/utils';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
export class AuthSignupService {
|
export class AuthSignupService {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -33,6 +34,9 @@ export class AuthSignupService {
|
|||||||
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
|
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||||
const { systemUserRepository } = this.sysRepositories;
|
const { systemUserRepository } = this.sysRepositories;
|
||||||
|
|
||||||
|
// Validates the signup disable restrictions.
|
||||||
|
await this.validateSignupRestrictions(signupDTO.email);
|
||||||
|
|
||||||
// Validates the given email uniqiness.
|
// Validates the given email uniqiness.
|
||||||
await this.validateEmailUniqiness(signupDTO.email);
|
await this.validateEmailUniqiness(signupDTO.email);
|
||||||
|
|
||||||
@@ -74,4 +78,40 @@ export class AuthSignupService {
|
|||||||
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate sign-up disable restrictions.
|
||||||
|
* @param {string} email
|
||||||
|
*/
|
||||||
|
private async validateSignupRestrictions(email: string) {
|
||||||
|
// Can't continue if the signup is not disabled.
|
||||||
|
if (!config.signupRestrictions.disabled) return;
|
||||||
|
|
||||||
|
// Validate the allowed domains.
|
||||||
|
if (!isEmpty(config.signupRestrictions.allowedDomains)) {
|
||||||
|
const emailDomain = email.split('@').pop();
|
||||||
|
const isAllowed = config.signupRestrictions.allowedDomains.some(
|
||||||
|
(domain) => emailDomain === domain
|
||||||
|
);
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new ServiceError(ERRORS.SIGNUP_NOT_ALLOWED_EMAIL_DOMAIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate the allowed email addresses.
|
||||||
|
if (!isEmpty(config.signupRestrictions.allowedEmails)) {
|
||||||
|
const isAllowed =
|
||||||
|
config.signupRestrictions.allowedEmails.indexOf(email) !== -1;
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new ServiceError(ERRORS.SIGNUP_NOT_ALLOWED_EMAIL_ADDRESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Throw error if the signup is disabled with no exceptions.
|
||||||
|
if (
|
||||||
|
isEmpty(config.signupRestrictions.allowedDomains) &&
|
||||||
|
isEmpty(config.signupRestrictions.allowedEmails)
|
||||||
|
) {
|
||||||
|
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
packages/server/src/services/Authentication/GetAuthMeta.ts
Normal file
16
packages/server/src/services/Authentication/GetAuthMeta.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { IAuthGetMetaPOJO } from '@/interfaces';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetAuthMeta {
|
||||||
|
/**
|
||||||
|
* Retrieves the authentication meta for SPA.
|
||||||
|
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||||
|
*/
|
||||||
|
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||||
|
return {
|
||||||
|
signupDisabled: config.signupRestrictions.disabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,8 @@ export const ERRORS = {
|
|||||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||||
|
|
||||||
|
SIGNUP_NOT_ALLOWED_EMAIL_ADDRESS: 'SIGNUP_NOT_ALLOWED_EMAIL_ADDRESS',
|
||||||
|
SIGNUP_NOT_ALLOWED_EMAIL_DOMAIN: 'SIGNUP_NOT_ALLOWED_EMAIL_DOMAIN',
|
||||||
|
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -467,6 +467,10 @@ const assocDepthLevelToObjectTree = (
|
|||||||
return objects;
|
return objects;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
|
||||||
|
return envVar ? envVar?.split(',')?.map(_.trim) : [];
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
templateRender,
|
templateRender,
|
||||||
accumSum,
|
accumSum,
|
||||||
@@ -499,4 +503,5 @@ export {
|
|||||||
mergeObjectsBykey,
|
mergeObjectsBykey,
|
||||||
nestedArrayToFlatten,
|
nestedArrayToFlatten,
|
||||||
assocDepthLevelToObjectTree,
|
assocDepthLevelToObjectTree,
|
||||||
|
castCommaListEnvVarToArray
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user