diff --git a/backend/src/core/loaders/sequelize.loader.ts b/backend/src/core/loaders/sequelize.loader.ts index 4ea02ab03..4ed0c12da 100644 --- a/backend/src/core/loaders/sequelize.loader.ts +++ b/backend/src/core/loaders/sequelize.loader.ts @@ -14,6 +14,7 @@ import { Statistic, ProtectedMessage, Unsubscriber, + Agency, } from '@core/models' import { EmailMessage, @@ -86,6 +87,7 @@ const sequelizeLoader = async (): Promise => { UserDemo, Statistic, Unsubscriber, + Agency, ] const emailModels = [ EmailMessage, diff --git a/backend/src/core/models/agency.ts b/backend/src/core/models/agency.ts new file mode 100644 index 000000000..9374aecff --- /dev/null +++ b/backend/src/core/models/agency.ts @@ -0,0 +1,10 @@ +import { DataType, Model, Column, Table } from 'sequelize-typescript' + +@Table({ tableName: 'agencies', underscored: true, timestamps: true }) +export class Agency extends Model { + @Column({ + type: DataType.STRING, + primaryKey: true, + }) + domain!: string +} diff --git a/backend/src/core/models/index.ts b/backend/src/core/models/index.ts index 1430a1002..6b69773fb 100644 --- a/backend/src/core/models/index.ts +++ b/backend/src/core/models/index.ts @@ -9,3 +9,4 @@ export * from './user/user-demo' export * from './statistic' export * from './protected_message' export * from './unsubscriber' +export * from './agency' diff --git a/backend/src/core/services/auth.service.ts b/backend/src/core/services/auth.service.ts index 4be30b012..7c19b1810 100644 --- a/backend/src/core/services/auth.service.ts +++ b/backend/src/core/services/auth.service.ts @@ -127,7 +127,7 @@ const hasWaitTimeElapsed = async (email: string): Promise => { * @param email */ const isWhitelistedEmail = async (email: string): Promise => { - const endsInWhitelistedDomain = validateDomain(email) + const endsInWhitelistedDomain = await validateDomain(email) if (!endsInWhitelistedDomain) { // If the email does not end in a whitelisted domain, check that it was whitelisted by us manually const user = await User.findOne({ where: { email: email } }) diff --git a/backend/src/core/utils/validate-domain.ts b/backend/src/core/utils/validate-domain.ts index b667ef0c3..ca48f6bd0 100644 --- a/backend/src/core/utils/validate-domain.ts +++ b/backend/src/core/utils/validate-domain.ts @@ -1,48 +1,59 @@ import validator from 'validator' import { loggerWithLabel } from '@core/logger' import config from '@core/config' +import { Agency } from '@core/models' const logger = loggerWithLabel(module) -type ValidateDomainFunction = (email: string) => boolean -const getValidateDomain = (domains: string): ValidateDomainFunction => { - const domainsToWhitelist: string[] = domains - .split(';') - .map((domain) => { - if (domain.startsWith('.') && validator.isEmail(`user@agency${domain}`)) { - // wildcard domain - // example: .gov.sg - // allow any emails that have domains ending in .gov.sg to sign in - return domain - } else if (domain.startsWith('@') && validator.isEmail(`user${domain}`)) { - // specific domain - // example: @moe.edu.sg - // allow any emails that are @moe.edu.sg to sign in - return domain - } - return '' - }) - .filter(Boolean) +const isValidDomain = (domain: string): boolean => { + // wildcard domain + // example: .gov.sg + // allow any emails that have domains ending in .gov.sg to sign in + const isWildCardDomain = + domain.startsWith('.') && validator.isEmail(`user@agency${domain}`) + + // specific domain + // example: @moe.edu.sg + // allow any emails that are @moe.edu.sg to sign in + const isSpecificDomain = + domain.startsWith('@') && validator.isEmail(`user${domain}`) + + return isWildCardDomain || isSpecificDomain +} + +const validateDomain = async (email: string): Promise => { + const configDomains = config.get('domains').split(';') + const agencyDomains = (await Agency.findAll()).map((agency) => agency.domain) + + const domainsToWhitelist = configDomains + .concat(agencyDomains) + .filter(isValidDomain) if (domainsToWhitelist.length === 0) { throw new Error( - `No domains were whitelisted - the supplied DOMAIN_WHITELIST is ${domains}` + `No domains were whitelisted - the supplied DOMAIN_WHITELIST is ${config.get( + 'domains' + )}` ) } else { logger.info({ message: 'Domains whitelisted', domainsToWhitelist, - action: 'getValidateDomain', + action: 'validateDomain', }) } - return (email): boolean => { - return ( - validator.isEmail(email) && - domainsToWhitelist.some((domain) => email.endsWith(domain)) - ) + + const matched = domainsToWhitelist.filter((domain) => email.endsWith(domain)) + if (matched.length > 0) { + logger.info({ + message: 'Match for email found.', + matched, + email, + action: 'validateDomain', + }) } -} -const validateDomain = getValidateDomain(config.get('domains')) + return validator.isEmail(email) && matched.length > 0 +} export { validateDomain }