From ad3c9ebfe9be3a79f2f20dd86551227f8bc2257c Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 7 May 2023 17:22:18 +0200 Subject: [PATCH] feat(server): sign-up restrictions for self-hosted --- docker-compose.prod.yml | 5 ++ packages/server/Dockerfile | 12 ++++- .../src/api/controllers/Authentication.ts | 54 +++++++++++++++++++ packages/server/src/config/index.ts | 14 +++++ .../server/src/interfaces/Authentication.ts | 4 ++ .../Authentication/AuthApplication.ts | 19 ++++++- .../src/services/Authentication/AuthSignup.ts | 42 ++++++++++++++- .../services/Authentication/GetAuthMeta.ts | 16 ++++++ .../src/services/Authentication/_constants.ts | 4 ++ packages/server/src/utils/index.ts | 5 ++ 10 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/services/Authentication/GetAuthMeta.ts diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ac8938ff3..157d4ab1b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -72,6 +72,11 @@ services: - AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER} - 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: container_name: bigcapital-database-migration build: diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index aebf36981..af2de4263 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -34,7 +34,11 @@ ARG MAIL_HOST= \ BASE_URL= \ # 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 \ MAIL_USERNAME=$MAIL_USERNAME \ @@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \ # MongoDB MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \ # 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. WORKDIR /app diff --git a/packages/server/src/api/controllers/Authentication.ts b/packages/server/src/api/controllers/Authentication.ts index e0f9073ea..a8bb1b4d5 100644 --- a/packages/server/src/api/controllers/Authentication.ts +++ b/packages/server/src/api/controllers/Authentication.ts @@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController { asyncMiddleware(this.resetPassword.bind(this)), this.handlerErrors ); + router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this))); 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. */ @@ -247,6 +265,42 @@ export default class AuthenticationController extends BaseController { 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); } diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index 928eac2a8..c32be4bac 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -1,5 +1,6 @@ import dotenv from 'dotenv'; import path from 'path'; +import { castCommaListEnvVarToArray, parseBoolean } from '@/utils'; dotenv.config(); @@ -146,6 +147,19 @@ module.exports = { }, }, + /** + * Sign-up restrictions + */ + signupRestrictions: { + disabled: parseBoolean(process.env.SIGNUP_DISABLED, false), + allowedDomains: castCommaListEnvVarToArray( + process.env.SIGNUP_ALLOWED_DOMAINS + ), + allowedEmails: castCommaListEnvVarToArray( + process.env.SIGNUP_ALLOWED_EMAILS + ), + }, + /** * Puppeteer remote browserless connection. */ diff --git a/packages/server/src/interfaces/Authentication.ts b/packages/server/src/interfaces/Authentication.ts index fb26c4fa6..253c178f9 100644 --- a/packages/server/src/interfaces/Authentication.ts +++ b/packages/server/src/interfaces/Authentication.ts @@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword { export interface IAuthSendedResetPassword { user: ISystemUser, token: string; +} + +export interface IAuthGetMetaPOJO { + signupDisabled: boolean; } \ No newline at end of file diff --git a/packages/server/src/services/Authentication/AuthApplication.ts b/packages/server/src/services/Authentication/AuthApplication.ts index 5c077530d..9fa74c973 100644 --- a/packages/server/src/services/Authentication/AuthApplication.ts +++ b/packages/server/src/services/Authentication/AuthApplication.ts @@ -1,8 +1,14 @@ import { Service, Inject, Container } from 'typedi'; -import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces'; +import { + IRegisterDTO, + ISystemUser, + IPasswordReset, + IAuthGetMetaPOJO, +} from '@/interfaces'; import { AuthSigninService } from './AuthSignin'; import { AuthSignupService } from './AuthSignup'; import { AuthSendResetPassword } from './AuthSendResetPassword'; +import { GetAuthMeta } from './GetAuthMeta'; @Service() export default class AuthenticationApplication { @@ -15,6 +21,9 @@ export default class AuthenticationApplication { @Inject() private authResetPasswordService: AuthSendResetPassword; + @Inject() + private authGetMeta: GetAuthMeta; + /** * Signin and generates JWT token. * @throws {ServiceError} @@ -53,4 +62,12 @@ export default class AuthenticationApplication { public async resetPassword(token: string, password: string): Promise { return this.authResetPasswordService.resetPassword(token, password); } + + /** + * Retrieves the authentication meta for SPA. + * @returns {Promise} + */ + public async getAuthMeta(): Promise { + return this.authGetMeta.getAuthMeta(); + } } diff --git a/packages/server/src/services/Authentication/AuthSignup.ts b/packages/server/src/services/Authentication/AuthSignup.ts index b2d11ac22..0adbdb9fe 100644 --- a/packages/server/src/services/Authentication/AuthSignup.ts +++ b/packages/server/src/services/Authentication/AuthSignup.ts @@ -1,4 +1,4 @@ -import { omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import moment from 'moment'; import { ServiceError } from '@/exceptions'; import { @@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import TenantsManagerService from '../Tenancy/TenantsManager'; import events from '@/subscribers/events'; import { hashPassword } from '@/utils'; +import config from '@/config'; export class AuthSignupService { @Inject() @@ -33,6 +34,9 @@ export class AuthSignupService { public async signUp(signupDTO: IRegisterDTO): Promise { const { systemUserRepository } = this.sysRepositories; + // Validates the signup disable restrictions. + await this.validateSignupRestrictions(signupDTO.email); + // Validates the given email uniqiness. await this.validateEmailUniqiness(signupDTO.email); @@ -74,4 +78,40 @@ export class AuthSignupService { 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); + } + } } diff --git a/packages/server/src/services/Authentication/GetAuthMeta.ts b/packages/server/src/services/Authentication/GetAuthMeta.ts new file mode 100644 index 000000000..80e68d682 --- /dev/null +++ b/packages/server/src/services/Authentication/GetAuthMeta.ts @@ -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} + */ + public async getAuthMeta(): Promise { + return { + signupDisabled: config.signupRestrictions.disabled, + }; + } +} diff --git a/packages/server/src/services/Authentication/_constants.ts b/packages/server/src/services/Authentication/_constants.ts index 16a3a5831..fc2718d4b 100644 --- a/packages/server/src/services/Authentication/_constants.ts +++ b/packages/server/src/services/Authentication/_constants.ts @@ -7,4 +7,8 @@ export const ERRORS = { TOKEN_EXPIRED: 'TOKEN_EXPIRED', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_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', }; diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index e1178a2f6..fa2bc6772 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -467,6 +467,10 @@ const assocDepthLevelToObjectTree = ( return objects; }; +const castCommaListEnvVarToArray = (envVar: string): Array => { + return envVar ? envVar?.split(',')?.map(_.trim) : []; +}; + export { templateRender, accumSum, @@ -499,4 +503,5 @@ export { mergeObjectsBykey, nestedArrayToFlatten, assocDepthLevelToObjectTree, + castCommaListEnvVarToArray };