diff --git a/test/api/unit/middlewares/blocker.test.js b/test/api/unit/middlewares/blocker.test.js index bd5c5c443f..7e3e167caf 100644 --- a/test/api/unit/middlewares/blocker.test.js +++ b/test/api/unit/middlewares/blocker.test.js @@ -29,7 +29,7 @@ function checkErrorNotThrown (next) { expect(typeof calledWith[0] === 'undefined').to.equal(true); } -describe('Blocker middleware', () => { +describe.only('Blocker middleware', () => { const pathToBlocker = '../../../../website/server/middlewares/blocker'; let res; let req; let next; diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index c5b6054c73..09575a8bf3 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -1,9 +1,13 @@ import moment from 'moment'; +import requireAgain from 'require-again'; import { model as User } from '../../../../website/server/models/user'; import { model as NewsPost } from '../../../../website/server/models/newsPost'; import { model as Group } from '../../../../website/server/models/group'; +import { model as Blocker } from '../../../../website/server/models/blocker'; import common from '../../../../website/common'; +const pathToUserSchema = '../../../../website/server/models/user/schema'; + describe('User Model', () => { describe('.toJSON()', () => { it('keeps user._tmp when calling .toJSON', () => { @@ -912,4 +916,73 @@ describe('User Model', () => { expect(user.toJSON().flags.newStuff).to.equal(true); }); }); + + describe.only('validates email', () => { + it('does not throw an error for a valid email', () => { + const user = new User(); + user.auth.local.email = 'hello@example.com'; + const errors = user.validateSync(); + expect(errors.errors['auth.local.email']).to.not.exist; + }); + + it('throws an error if email is not valid', () => { + const user = new User(); + user.auth.local.email = 'invalid-email'; + const errors = user.validateSync(); + expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmail')); + }); + + it('throws an error if email is using a restricted domain', () => { + const user = new User(); + user.auth.local.email = 'scammer@habitica.com'; + const errors = user.validateSync(); + expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmailDomain', { domains: 'habitica.com, habitrpg.com' })); + }); + + it('throws an error if email was blocked specifically', () => { + sandbox.stub(Blocker, 'watchBlockers').returns({ + on: (event, callback) => { + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@example.com' } }); + }, + }); + const schema = requireAgain(pathToUserSchema).UserSchema; + const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com')) + expect(valid).to.equal(false); + }); + + it('throws an error if email domain was blocked', () => { + sandbox.stub(Blocker, 'watchBlockers').returns({ + on: (event, callback) => { + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } }); + }, + }); + const schema = requireAgain(pathToUserSchema).UserSchema; + const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com')); + expect(valid).to.equal(false); + }); + + it('throws an error if user portion of email was blocked', () => { + sandbox.stub(Blocker, 'watchBlockers').returns({ + on: (event, callback) => { + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } }); + }, + }); + const schema = requireAgain(pathToUserSchema).UserSchema; + const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com')); + expect(valid).to.equal(false); + }); + + it('throws an error if email is not blocked', () => { + sandbox.stub(Blocker, 'watchBlockers').returns({ + on: (event, callback) => { + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } }); + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } }); + callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'bad@test.com' } }); + }, + }); + const schema = requireAgain(pathToUserSchema).UserSchema; + const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('good@test.com')); + expect(valid).to.equal(true); + }); + }); }); diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index efffc176c0..0277d66de8 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -9,9 +9,31 @@ import { schema as SubscriptionPlanSchema } from '../subscriptionPlan'; import { schema as TagSchema } from '../tag'; import { schema as UserNotificationSchema } from '../userNotification'; import { schema as WebhookSchema } from '../webhook'; +import { model as Blocker } from '../blocker'; const RESTRICTED_EMAIL_DOMAINS = Object.freeze(['habitica.com', 'habitrpg.com']); +const BLOCKED_EMAILS = []; + +Blocker.watchBlockers({ + type: 'email', + area: 'full', +}, { + initial: true, +}).on('change', async change => { + const { operation, blocker: { value } } = change; + if (operation === 'add') { + if (value && !BLOCKED_EMAILS.includes(value)) { + BLOCKED_EMAILS.push(value); + } + } else if (operation === 'delete') { + const index = BLOCKED_EMAILS.indexOf(value); + if (index !== -1) { + BLOCKED_EMAILS.splice(index, 1); + } + } +}); + // User schema definition export const UserSchema = new Schema({ apiToken: { @@ -43,6 +65,12 @@ export const UserSchema = new Schema({ return RESTRICTED_EMAIL_DOMAINS.every(domain => !lowercaseEmail.endsWith(`@${domain}`)); }, message: shared.i18n.t('invalidEmailDomain', { domains: RESTRICTED_EMAIL_DOMAINS.join(', ') }), + }, { + validator (email) { + const lowercaseEmail = email.toLowerCase(); + return BLOCKED_EMAILS.every(block => lowercaseEmail.indexOf(block) === -1); + }, + message: shared.i18n.t('emailBlockedRegistration'), }], }, username: {