From f049d29d1bc40a386f899271e7f8ecde93340ee0 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 16 Aug 2018 17:40:53 -0500 Subject: [PATCH 01/51] Added username invite --- .../groups/POST-groups_invite.test.js | 67 ++++++ website/common/locales/en/groups.json | 1 + website/server/controllers/api-v3/groups.js | 185 +++------------- website/server/libs/invites/index.js | 209 ++++++++++++++++++ website/server/middlewares/errorHandler.js | 2 +- website/server/models/group.js | 52 +++-- 6 files changed, 342 insertions(+), 174 deletions(-) create mode 100644 website/server/libs/invites/index.js diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js index ec62946350..4db475ccb7 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -23,6 +23,73 @@ describe('Post /groups/:groupId/invite', () => { }); }); + describe('username invites', () => { + it('returns an error when invited user is not found', async () => { + const fakeID = 'fakeuserid'; + + await expect(inviter.post(`/groups/${group._id}/invite`, { + usernames: [fakeID], + })) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithUsernameNotFound', {username: fakeID}), + }); + }); + + it('returns an error when inviting yourself to a group', async () => { + await expect(inviter.post(`/groups/${group._id}/invite`, { + usernames: [inviter.auth.local.lowerCaseUsername], + })) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('cannotInviteSelfToGroup'), + }); + }); + + it('invites a user to a group by username', async () => { + const userToInvite = await generateUser(); + + await expect(inviter.post(`/groups/${group._id}/invite`, { + usernames: [userToInvite.auth.local.lowerCaseUsername], + })).to.eventually.deep.equal([{ + id: group._id, + name: groupName, + inviter: inviter._id, + publicGuild: false, + }]); + + await expect(userToInvite.get('/user')) + .to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + }); + + it('invites multiple users to a group by uuid', async () => { + const userToInvite = await generateUser(); + const userToInvite2 = await generateUser(); + + await expect(inviter.post(`/groups/${group._id}/invite`, { + usernames: [userToInvite.auth.local.lowerCaseUsername, userToInvite2.auth.local.lowerCaseUsername], + })).to.eventually.deep.equal([ + { + id: group._id, + name: groupName, + inviter: inviter._id, + publicGuild: false, + }, + { + id: group._id, + name: groupName, + inviter: inviter._id, + publicGuild: false, + }, + ]); + + await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + }); + }); + describe('user id invites', () => { it('returns an error when inviter has no chat privileges', async () => { let inviterMuted = await inviter.update({'flags.chatRevoked': true}); diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 75bc3e23cd..aeebe2a1d5 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -240,6 +240,7 @@ "userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.", "userAlreadyInAParty": "UserID: <%= userId %>, User \"<%= username %>\" already in a party. ", "userWithIDNotFound": "User with id \"<%= userId %>\" not found.", + "userWithUsernameNotFound": "User with username \"<%= username %>\" not found.", "userHasNoLocalRegistration": "User does not have a local registration (username, email, password).", "uuidsMustBeAnArray": "User ID invites must be an array.", "emailsMustBeAnArray": "Email address invites must be an array.", diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 16d586a5ca..15ae6a6b24 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -9,7 +9,6 @@ import { model as User, nameFields, } from '../../models/user'; -import { model as EmailUnsubscription } from '../../models/emailUnsubscription'; import { NotFound, BadRequest, @@ -17,9 +16,12 @@ import { } from '../../libs/errors'; import { removeFromArray } from '../../libs/collectionManipulators'; import { sendTxn as sendTxnEmail } from '../../libs/email'; -import { encrypt } from '../../libs/encryption'; -import { sendNotification as sendPushNotification } from '../../libs/pushNotifications'; import pusher from '../../libs/pusher'; +import { + _inviteByUUID, + _inviteByEmail, + _inviteByUserName, +} from '../../libs/invites'; import common from '../../../common'; import payments from '../../libs/payments/payments'; import stripePayments from '../../libs/payments/stripe'; @@ -948,148 +950,6 @@ api.removeGroupMember = { }, }; -async function _inviteByUUID (uuid, group, inviter, req, res) { - let userToInvite = await User.findById(uuid).exec(); - const publicGuild = group.type === 'guild' && group.privacy === 'public'; - - if (!userToInvite) { - throw new NotFound(res.t('userWithIDNotFound', {userId: uuid})); - } else if (inviter._id === userToInvite._id) { - throw new BadRequest(res.t('cannotInviteSelfToGroup')); - } - - const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite); - if (objections.length > 0) { - throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name})); - } - - if (group.type === 'guild') { - if (_.includes(userToInvite.guilds, group._id)) { - throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name})); - } - if (_.find(userToInvite.invitations.guilds, {id: group._id})) { - throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name})); - } - - let guildInvite = { - id: group._id, - name: group.name, - inviter: inviter._id, - publicGuild, - }; - if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true; - userToInvite.invitations.guilds.push(guildInvite); - } else if (group.type === 'party') { - // Do not add to invitations.parties array if the user is already invited to that party - if (_.find(userToInvite.invitations.parties, {id: group._id})) { - throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name})); - } - - if (userToInvite.party._id) { - let userParty = await Group.getGroup({user: userToInvite, groupId: 'party', fields: 'memberCount'}); - - // Allow user to be invited to a new party when they're partying solo - if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name})); - } - - let partyInvite = {id: group._id, name: group.name, inviter: inviter._id}; - if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true; - - userToInvite.invitations.parties.push(partyInvite); - userToInvite.invitations.party = partyInvite; - } - - let groupLabel = group.type === 'guild' ? 'Guild' : 'Party'; - let groupTemplate = group.type === 'guild' ? 'guild' : 'party'; - if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] !== false) { - let emailVars = [ - {name: 'INVITER', content: inviter.profile.name}, - ]; - - if (group.type === 'guild') { - emailVars.push( - {name: 'GUILD_NAME', content: group.name}, - {name: 'GUILD_URL', content: '/groups/discovery'} - ); - } else { - emailVars.push( - {name: 'PARTY_NAME', content: group.name}, - {name: 'PARTY_URL', content: '/party'} - ); - } - - sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars); - } - - if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] !== false) { - let identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty'; - sendPushNotification( - userToInvite, - { - title: group.name, - message: res.t(identifier), - identifier, - payload: {groupID: group._id, publicGuild}, - } - ); - } - - let userInvited = await userToInvite.save(); - if (group.type === 'guild') { - return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1]; - } else if (group.type === 'party') { - return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1]; - } -} - -async function _inviteByEmail (invite, group, inviter, req, res) { - let userReturnInfo; - - if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail')); - - let userToContact = await User.findOne({$or: [ - {'auth.local.email': invite.email}, - {'auth.facebook.emails.value': invite.email}, - {'auth.google.emails.value': invite.email}, - ]}) - .select({_id: true, 'preferences.emailNotifications': true}) - .exec(); - - if (userToContact) { - userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res); - } else { - userReturnInfo = invite.email; - - let cancelledPlan = false; - if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true; - - const groupQueryString = JSON.stringify({ - id: group._id, - inviter: inviter._id, - publicGuild: group.type === 'guild' && group.privacy === 'public', - sentAt: Date.now(), // so we can let it expire - cancelledPlan, - }); - let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`; - - let variables = [ - {name: 'LINK', content: link}, - {name: 'INVITER', content: req.body.inviter || inviter.profile.name}, - ]; - - if (group.type === 'guild') { - variables.push({name: 'GUILD_NAME', content: group.name}); - } - - // Check for the email address not to be unsubscribed - let userIsUnsubscribed = await EmailUnsubscription.findOne({email: invite.email}).exec(); - let groupLabel = group.type === 'guild' ? '-guild' : ''; - if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables); - } - - return userReturnInfo; -} - /** * @api {post} /api/v3/groups/:groupId/invite Invite users to a group * @apiName InviteToGroup @@ -1176,7 +1036,7 @@ api.inviteToGroup = { url: '/groups/:groupId/invite', middlewares: [authWithHeaders({})], async handler (req, res) { - let user = res.locals.user; + const user = res.locals.user; if (user.flags.chatRevoked) throw new NotAuthorized(res.t('cannotInviteWhenMuted')); @@ -1184,35 +1044,48 @@ api.inviteToGroup = { if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL })); - let validationErrors = req.validationErrors(); + const validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; - let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); + const group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); if (!group) throw new NotFound(res.t('groupNotFound')); if (group.purchased && group.purchased.plan.customerId && user._id !== group.leader) throw new NotAuthorized(res.t('onlyGroupLeaderCanInviteToGroupPlan')); - let uuids = req.body.uuids; - let emails = req.body.emails; + const { + uuids, + emails, + usernames, + } = req.body; - await Group.validateInvitations(uuids, emails, res, group); + await Group.validateInvitations({ + uuids, + emails, + usernames, + }, res, group); - let results = []; + const results = []; if (uuids) { - let uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res)); - let uuidResults = await Promise.all(uuidInvites); + const uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res)); + const uuidResults = await Promise.all(uuidInvites); results.push(...uuidResults); } if (emails) { - let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res)); + const emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res)); user.invitesSent += emails.length; await user.save(); - let emailResults = await Promise.all(emailInvites); + const emailResults = await Promise.all(emailInvites); results.push(...emailResults); } + if (usernames) { + const usernameInvites = usernames.map((username) => _inviteByUserName(username, group, user, req, res)); + const usernameResults = await Promise.all(usernameInvites); + results.push(...usernameResults); + } + res.respond(200, results); }, }; diff --git a/website/server/libs/invites/index.js b/website/server/libs/invites/index.js new file mode 100644 index 0000000000..ef5a51b1f3 --- /dev/null +++ b/website/server/libs/invites/index.js @@ -0,0 +1,209 @@ +import _ from 'lodash'; + +import { encrypt } from '../encryption'; +import { sendNotification as sendPushNotification } from '../pushNotifications'; +import { + NotFound, + BadRequest, + NotAuthorized, +} from '../errors'; +import { sendTxn as sendTxnEmail } from '../email'; +import { model as EmailUnsubscription } from '../../models/emailUnsubscription'; +import { + model as User, +} from '../../models/user'; +import { + model as Group, +} from '../../models/group'; + +function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) { + if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return; + + const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty'; + + sendPushNotification( + userToInvite, + { + title: group.name, + message: res.t(identifier), + identifier, + payload: {groupID: group._id, publicGuild}, + } + ); +} + +function sendInviteEmail (userToInvite, groupLabel, group, inviter) { + if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] === false) return; + const groupTemplate = group.type === 'guild' ? 'guild' : 'party'; + + const emailVars = [ + {name: 'INVITER', content: inviter.profile.name}, + ]; + + if (group.type === 'guild') { + emailVars.push( + {name: 'GUILD_NAME', content: group.name}, + {name: 'GUILD_URL', content: '/groups/discovery'} + ); + } else { + emailVars.push( + {name: 'PARTY_NAME', content: group.name}, + {name: 'PARTY_URL', content: '/party'} + ); + } + + sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars); +} + +function inviteUserToGuild (userToInvite, group, inviter, publicGuild, res) { + const uuid = userToInvite._id; + + if (_.includes(userToInvite.guilds, group._id)) { + throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name})); + } + + if (_.find(userToInvite.invitations.guilds, {id: group._id})) { + throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name})); + } + + const guildInvite = { + id: group._id, + name: group.name, + inviter: inviter._id, + publicGuild, + }; + + if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true; + + userToInvite.invitations.guilds.push(guildInvite); +} + +async function inviteUserToParty (userToInvite, group, inviter, res) { + const uuid = userToInvite._id; + + // Do not add to invitations.parties array if the user is already invited to that party + if (_.find(userToInvite.invitations.parties, {id: group._id})) { + throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name})); + } + + if (userToInvite.party._id) { + let userParty = await Group.getGroup({user: userToInvite, groupId: 'party', fields: 'memberCount'}); + + // Allow user to be invited to a new party when they're partying solo + if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name})); + } + + let partyInvite = {id: group._id, name: group.name, inviter: inviter._id}; + if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true; + + userToInvite.invitations.parties.push(partyInvite); + userToInvite.invitations.party = partyInvite; +} + +async function addInvitiationToUser (userToInvite, group, inviter, res) { + const publicGuild = group.type === 'guild' && group.privacy === 'public'; + + if (group.type === 'guild') { + inviteUserToGuild(userToInvite, group, inviter, publicGuild, res); + } else if (group.type === 'party') { + await inviteUserToParty(userToInvite, group, inviter, res); + } + + const groupLabel = group.type === 'guild' ? 'Guild' : 'Party'; + sendInviteEmail(userToInvite, groupLabel, group, inviter); + sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res); + + const userInvited = await userToInvite.save(); + if (group.type === 'guild') { + return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1]; + } + + if (group.type === 'party') { + return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1]; + } +} + +async function _inviteByUUID (uuid, group, inviter, req, res) { + const userToInvite = await User.findById(uuid).exec(); + + if (!userToInvite) { + throw new NotFound(res.t('userWithIDNotFound', {userId: uuid})); + } else if (inviter._id === userToInvite._id) { + throw new BadRequest(res.t('cannotInviteSelfToGroup')); + } + + const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite); + if (objections.length > 0) { + throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name})); + } + + return await addInvitiationToUser(userToInvite, group, inviter, res); +} + +async function _inviteByEmail (invite, group, inviter, req, res) { + let userReturnInfo; + + if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail')); + + let userToContact = await User.findOne({$or: [ + {'auth.local.email': invite.email}, + {'auth.facebook.emails.value': invite.email}, + {'auth.google.emails.value': invite.email}, + ]}) + .select({_id: true, 'preferences.emailNotifications': true}) + .exec(); + + if (userToContact) { + userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res); + } else { + userReturnInfo = invite.email; + + let cancelledPlan = false; + if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true; + + const groupQueryString = JSON.stringify({ + id: group._id, + inviter: inviter._id, + publicGuild: group.type === 'guild' && group.privacy === 'public', + sentAt: Date.now(), // so we can let it expire + cancelledPlan, + }); + let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`; + + let variables = [ + {name: 'LINK', content: link}, + {name: 'INVITER', content: req.body.inviter || inviter.profile.name}, + ]; + + if (group.type === 'guild') { + variables.push({name: 'GUILD_NAME', content: group.name}); + } + + // Check for the email address not to be unsubscribed + let userIsUnsubscribed = await EmailUnsubscription.findOne({email: invite.email}).exec(); + let groupLabel = group.type === 'guild' ? '-guild' : ''; + if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables); + } + + return userReturnInfo; +} + +async function _inviteByUserName (username, group, inviter, req, res) { + const userToInvite = await User.findOne({'auth.local.lowerCaseUsername': username}).exec(); + + if (!userToInvite) { + throw new NotFound(res.t('userWithUsernameNotFound', { username })); + } + + if (inviter._id === userToInvite._id) { + throw new BadRequest(res.t('cannotInviteSelfToGroup')); + } + + return await addInvitiationToUser(userToInvite, group, inviter, res); +} + +module.exports = { + _inviteByUUID, + _inviteByEmail, + _inviteByUserName, +}; diff --git a/website/server/middlewares/errorHandler.js b/website/server/middlewares/errorHandler.js index 5b3cdb7390..429f8251a0 100644 --- a/website/server/middlewares/errorHandler.js +++ b/website/server/middlewares/errorHandler.js @@ -16,7 +16,7 @@ module.exports = function errorHandler (err, req, res, next) { // eslint-disable // Otherwise try to identify the type of error (mongoose validation, mongodb unique, ...) // If we can't identify it, respond with a generic 500 error let responseErr = err instanceof CustomError ? err : null; - + console.log(err) // Handle errors created with 'http-errors' or similar that have a status/statusCode property if (err.statusCode && typeof err.statusCode === 'number') { responseErr = new CustomError(); diff --git a/website/server/models/group.js b/website/server/models/group.js index f1612af3e8..2fbcb888d5 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -356,23 +356,17 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use return toJSON; }; -/** - * Checks invitation uuids and emails for possible errors. - * - * @param uuids An array of user ids - * @param emails An array of emails - * @param res Express res object for use with translations - * @throws BadRequest An error describing the issue with the invitations - */ -schema.statics.validateInvitations = async function getInvitationError (uuids, emails, res, group = null) { - let uuidsIsArray = Array.isArray(uuids); - let emailsIsArray = Array.isArray(emails); - let emptyEmails = emailsIsArray && emails.length < 1; - let emptyUuids = uuidsIsArray && uuids.length < 1; +function getIniviteError (uuids, emails, usernames) { + const uuidsIsArray = Array.isArray(uuids); + const emailsIsArray = Array.isArray(emails); + const usernamesIsArray = Array.isArray(usernames); + const emptyEmails = emailsIsArray && emails.length < 1; + const emptyUuids = uuidsIsArray && uuids.length < 1; + const emptyUsernames = usernamesIsArray && usernames.length < 1; let errorString; - if (!uuids && !emails) { + if (!uuids && !emails && !usernames) { errorString = 'canOnlyInviteEmailUuid'; } else if (uuids && !uuidsIsArray) { errorString = 'uuidsMustBeAnArray'; @@ -386,10 +380,12 @@ schema.statics.validateInvitations = async function getInvitationError (uuids, e errorString = 'inviteMustNotBeEmpty'; } - if (errorString) { - throw new BadRequest(res.t(errorString)); - } + if (usernames && emptyUsernames) errorString = 'usernamesMustNotBeEmpty'; + return errorString; +} + +function getInviteCount (uuids, emails) { let totalInvites = 0; if (uuids) { @@ -400,6 +396,28 @@ schema.statics.validateInvitations = async function getInvitationError (uuids, e totalInvites += emails.length; } + return totalInvites; +} + +/** + * Checks invitation uuids and emails for possible errors. + * + * @param uuids An array of user ids + * @param emails An array of emails + * @param res Express res object for use with translations + * @throws BadRequest An error describing the issue with the invitations + */ +schema.statics.validateInvitations = async function getInvitationError (invites, res, group = null) { + const { + uuids, + emails, + usernames, + } = invites; + + const errorString = getIniviteError(uuids, emails, usernames); + if (errorString) throw new BadRequest(res.t(errorString)); + + const totalInvites = getInviteCount(uuids, emails); if (totalInvites > INVITES_LIMIT) { throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT})); } From fa1fef11d6e728a82055bad54053b0378547dcb9 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 13 Oct 2018 12:53:16 -0500 Subject: [PATCH 02/51] feat(usernames): modal to force verification --- .../POST-user_verify_display_name.test.js | 57 +++++ website/client/assets/svg/hello-habitican.svg | 34 +++ website/client/components/notifications.vue | 13 +- .../components/settings/verifyUsername.vue | 237 ++++++++++++++++++ website/client/store/actions/auth.js | 9 + website/common/locales/en/front.json | 7 +- website/common/locales/en/messages.json | 4 +- website/common/locales/en/settings.json | 2 +- website/server/controllers/api-v4/user.js | 29 +++ website/server/libs/user/validation.js | 8 + 10 files changed, 392 insertions(+), 8 deletions(-) create mode 100644 test/api/v4/user/auth/POST-user_verify_display_name.test.js create mode 100644 website/client/assets/svg/hello-habitican.svg create mode 100644 website/client/components/settings/verifyUsername.vue diff --git a/test/api/v4/user/auth/POST-user_verify_display_name.test.js b/test/api/v4/user/auth/POST-user_verify_display_name.test.js new file mode 100644 index 0000000000..cb0c5e17d4 --- /dev/null +++ b/test/api/v4/user/auth/POST-user_verify_display_name.test.js @@ -0,0 +1,57 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v4'; + +const ENDPOINT = '/user/auth/verify-display-name'; + +describe('POST /user/auth/verify-display-name', async () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('successfully verifies display name including funky characters', async () => { + let newDisplayName = 'Sabé 🤬'; + let response = await user.post(ENDPOINT, { + displayName: newDisplayName, + }); + expect(response).to.eql({ isUsable: true }); + }); + + context('errors', async () => { + it('errors if display name is not provided', async () => { + await expect(user.post(ENDPOINT, { + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('errors if display name is a slur', async () => { + await expect(user.post(ENDPOINT, { + username: 'TESTPLACEHOLDERSLURWORDHERE', + })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueSlur')] }); + }); + + it('errors if display name contains a slur', async () => { + await expect(user.post(ENDPOINT, { + username: 'TESTPLACEHOLDERSLURWORDHERE_otherword', + })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + await expect(user.post(ENDPOINT, { + username: 'something_TESTPLACEHOLDERSLURWORDHERE', + })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + await expect(user.post(ENDPOINT, { + username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword', + })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + }); + + it('errors if display name has incorrect length', async () => { + await expect(user.post(ENDPOINT, { + username: 'this is a very long display name over 30 characters', + })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength')] }); + }); + }); +}); diff --git a/website/client/assets/svg/hello-habitican.svg b/website/client/assets/svg/hello-habitican.svg new file mode 100644 index 0000000000..292b792a79 --- /dev/null +++ b/website/client/assets/svg/hello-habitican.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/client/components/notifications.vue b/website/client/components/notifications.vue index 96d097ac9f..45c11bad01 100644 --- a/website/client/components/notifications.vue +++ b/website/client/components/notifications.vue @@ -25,6 +25,7 @@ div login-incentives(:data='notificationData') quest-completed quest-invitation + verify-username + + + + diff --git a/website/client/store/actions/auth.js b/website/client/store/actions/auth.js index 968c92766f..fad46d295f 100644 --- a/website/client/store/actions/auth.js +++ b/website/client/store/actions/auth.js @@ -55,6 +55,15 @@ export async function verifyUsername (store, params) { return result.data.data; } +export async function verifyDisplayName (store, params) { + let url = '/api/v4/user/auth/verify-display-name'; + let result = await axios.post(url, { + displayName: params.displayName, + }); + + return result.data.data; +} + export async function socialAuth (store, params) { let url = '/api/v4/user/auth/social'; let result = await axios.post(url, { diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json index 056f5754fc..9b5fbd29c6 100644 --- a/website/common/locales/en/front.json +++ b/website/common/locales/en/front.json @@ -271,15 +271,12 @@ "emailTaken": "Email address is already used in an account.", "newEmailRequired": "Missing new email address.", "usernameTime": "It's time to set your username!", - "usernameInfo": "Your display name hasn't changed, but your old login name will now become your public username. This username will be used for invitations, @mentions in chat, and messaging.

If you'd like to learn more about this change, visit the wiki's Player Names page.", - "usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.", + "usernameInfo": "Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.

If you'd like to learn more about this change, visit our wiki.", + "usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.", "usernameTaken": "Username already taken.", "usernameWrongLength": "Username must be between 1 and 20 characters long.", "displayNameWrongLength": "Display names must be between 1 and 30 characters long.", "usernameBadCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.", - "nameBadWords": "Names cannot include any inappropriate words.", - "confirmUsername": "Confirm Username", - "usernameConfirmed": "Username Confirmed", "passwordConfirmationMatch": "Password confirmation doesn't match password.", "invalidLoginCredentials": "Incorrect username and/or email and/or password.", "passwordResetPage": "Reset Password", diff --git a/website/common/locales/en/messages.json b/website/common/locales/en/messages.json index 2ddeef84fc..0a0e9b0c15 100644 --- a/website/common/locales/en/messages.json +++ b/website/common/locales/en/messages.json @@ -70,5 +70,7 @@ "beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!", - "messageDeletedUser": "Sorry, this user has deleted their account." + "messageDeletedUser": "Sorry, this user has deleted their account.", + + "messageMissingDisplayName": "Missing display name." } diff --git a/website/common/locales/en/settings.json b/website/common/locales/en/settings.json index 789294db6e..06ba78df64 100644 --- a/website/common/locales/en/settings.json +++ b/website/common/locales/en/settings.json @@ -199,7 +199,7 @@ "usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.", "currentUsername": "Current username:", "displaynameIssueLength": "Display Names must be between 1 and 30 characters.", - "displaynameIssueSlur": "Display Names may not contain inappropriate language", + "displaynameIssueSlur": "Display Names may not contain inappropriate language.", "goToSettings": "Go to Settings", "usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!", "usernameNotVerified": "Please confirm your username.", diff --git a/website/server/controllers/api-v4/user.js b/website/server/controllers/api-v4/user.js index 01cc9e7034..e0922db63f 100644 --- a/website/server/controllers/api-v4/user.js +++ b/website/server/controllers/api-v4/user.js @@ -1,5 +1,6 @@ import { authWithHeaders } from '../../middlewares/auth'; import * as userLib from '../../libs/user'; +import { verifyDisplayName } from '../../libs/user/validation'; const api = {}; @@ -206,4 +207,32 @@ api.userReset = { }, }; +api.verifyDisplayName = { + method: 'POST', + url: '/user/auth/verify-display-name', + middlewares: [authWithHeaders({ + optional: true, + })], + async handler (req, res) { + req.checkBody({ + displayName: { + notEmpty: {errorMessage: res.t('messageMissingDisplayName')}, + }, + }); + + const validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + const chosenDisplayName = req.body.displayName; + + const issues = verifyDisplayName(chosenDisplayName, res); + + if (issues.length > 0) { + res.respond(200, { isUsable: false, issues }); + } else { + res.respond(200, { isUsable: true }); + } + }, +}; + module.exports = api; diff --git a/website/server/libs/user/validation.js b/website/server/libs/user/validation.js index a56446bc91..a064ef1a0a 100644 --- a/website/server/libs/user/validation.js +++ b/website/server/libs/user/validation.js @@ -26,6 +26,14 @@ function usernameContainsInvalidCharacters (username) { return match !== null && match[0] !== null; } +export function verifyDisplayName (displayName, res) { + let issues = []; + if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displayNameWrongLength')); + if (nameContainsSlur(displayName)) issues.push(res.t('displaynameIssueSlur')); + + return issues; +} + export function verifyUsername (username, res) { let issues = []; if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength')); From 60b26d4ec053335863b1d50f5470e52079135f0d Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 13 Oct 2018 20:56:09 -0500 Subject: [PATCH 03/51] fix(test): string is miscapitalized :( --- test/api/v4/user/auth/POST-user_verify_display_name.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/v4/user/auth/POST-user_verify_display_name.test.js b/test/api/v4/user/auth/POST-user_verify_display_name.test.js index cb0c5e17d4..619bf696e3 100644 --- a/test/api/v4/user/auth/POST-user_verify_display_name.test.js +++ b/test/api/v4/user/auth/POST-user_verify_display_name.test.js @@ -39,13 +39,13 @@ describe('POST /user/auth/verify-display-name', async () => { it('errors if display name contains a slur', async () => { await expect(user.post(ENDPOINT, { username: 'TESTPLACEHOLDERSLURWORDHERE_otherword', - })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); await expect(user.post(ENDPOINT, { username: 'something_TESTPLACEHOLDERSLURWORDHERE', - })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); await expect(user.post(ENDPOINT, { username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword', - })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength'), t('displaynameIssueSlur')] }); + })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); }); it('errors if display name has incorrect length', async () => { From 392b54aa7b55733ca6a5258caec1388e6b75801f Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 13 Oct 2018 21:05:07 -0500 Subject: [PATCH 04/51] fix(usernames): remove more ratzen fratzen dupe strings --- website/common/locales/en/front.json | 3 --- website/server/libs/auth/index.js | 4 ++-- website/server/libs/user/validation.js | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json index 9b5fbd29c6..6c70e575f3 100644 --- a/website/common/locales/en/front.json +++ b/website/common/locales/en/front.json @@ -274,9 +274,6 @@ "usernameInfo": "Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.

If you'd like to learn more about this change, visit our wiki.", "usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.", "usernameTaken": "Username already taken.", - "usernameWrongLength": "Username must be between 1 and 20 characters long.", - "displayNameWrongLength": "Display names must be between 1 and 30 characters long.", - "usernameBadCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.", "passwordConfirmationMatch": "Password confirmation doesn't match password.", "invalidLoginCredentials": "Incorrect username and/or email and/or password.", "passwordResetPage": "Reset Password", diff --git a/website/server/libs/auth/index.js b/website/server/libs/auth/index.js index f783c59b40..d78c381b2c 100644 --- a/website/server/libs/auth/index.js +++ b/website/server/libs/auth/index.js @@ -79,8 +79,8 @@ async function registerLocal (req, res, { isV3 = false }) { notEmpty: true, errorMessage: res.t('missingUsername'), // TODO use the constants in the error message above - isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')}, - matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')}, + isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameIssueLength')}, + matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameIssueInvalidCharacters')}, }, email: { notEmpty: true, diff --git a/website/server/libs/user/validation.js b/website/server/libs/user/validation.js index a064ef1a0a..097ef70cdf 100644 --- a/website/server/libs/user/validation.js +++ b/website/server/libs/user/validation.js @@ -28,7 +28,7 @@ function usernameContainsInvalidCharacters (username) { export function verifyDisplayName (displayName, res) { let issues = []; - if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displayNameWrongLength')); + if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displaynameIssueLength')); if (nameContainsSlur(displayName)) issues.push(res.t('displaynameIssueSlur')); return issues; From bbd98517ff44f628c3a7543db9a977193204fc9a Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 13 Oct 2018 21:36:14 -0500 Subject: [PATCH 05/51] fix(test): copypasta and string trouble --- .../user/auth/POST-user_verify_display_name.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/api/v4/user/auth/POST-user_verify_display_name.test.js b/test/api/v4/user/auth/POST-user_verify_display_name.test.js index 619bf696e3..8fa1b9d5b9 100644 --- a/test/api/v4/user/auth/POST-user_verify_display_name.test.js +++ b/test/api/v4/user/auth/POST-user_verify_display_name.test.js @@ -32,26 +32,26 @@ describe('POST /user/auth/verify-display-name', async () => { it('errors if display name is a slur', async () => { await expect(user.post(ENDPOINT, { - username: 'TESTPLACEHOLDERSLURWORDHERE', + displayName: 'TESTPLACEHOLDERSLURWORDHERE', })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueSlur')] }); }); it('errors if display name contains a slur', async () => { await expect(user.post(ENDPOINT, { - username: 'TESTPLACEHOLDERSLURWORDHERE_otherword', + displayName: 'TESTPLACEHOLDERSLURWORDHERE_otherword', })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); await expect(user.post(ENDPOINT, { - username: 'something_TESTPLACEHOLDERSLURWORDHERE', + displayName: 'something_TESTPLACEHOLDERSLURWORDHERE', })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); await expect(user.post(ENDPOINT, { - username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword', + displayName: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword', })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] }); }); it('errors if display name has incorrect length', async () => { await expect(user.post(ENDPOINT, { - username: 'this is a very long display name over 30 characters', - })).to.eventually.eql({ isUsable: false, issues: [t('displayNameIssueLength')] }); + displayName: 'this is a very long display name over 30 characters', + })).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength')] }); }); }); }); From c576c5261e2053b343722f050e93a757b5c2b391 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sun, 14 Oct 2018 12:21:52 -0500 Subject: [PATCH 06/51] fix(username): Add @ prepend --- .../components/settings/verifyUsername.vue | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/website/client/components/settings/verifyUsername.vue b/website/client/components/settings/verifyUsername.vue index cc909d9d17..639611d6ea 100644 --- a/website/client/components/settings/verifyUsername.vue +++ b/website/client/components/settings/verifyUsername.vue @@ -28,12 +28,13 @@ .col-3 label(for='username') {{ $t('username') }} .col-9 - input#username.form-control( - type='text', - :placeholder="$t('newUsername')", - v-model='temporaryUsername', - @blur='restoreEmptyUsername()', - :class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid}') + .input-group-prepend.input-group-text @ + input#username.form-control( + type='text', + :placeholder="$t('newUsername')", + v-model='temporaryUsername', + @blur='restoreEmptyUsername()', + :class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid}') .mb-3(v-if="usernameIssues.length > 0") .input-error.text-center(v-for="issue in usernameIssues") {{ issue }} .small.text-center {{ $t('usernameLimitations') }} @@ -86,6 +87,17 @@ margin-bottom: 1rem; } + .input-group-prepend { + margin-right: 0px; + } + + .input-group-text { + border: 0px; + background-color: $white; + color: $gray-300; + padding: 0rem 0rem 0rem 0.75rem; + } + label { font-weight: bold; margin-bottom: 0rem; @@ -115,6 +127,11 @@ margin-right: -3rem; padding: 1rem 4rem 1rem 4rem; } + + #username { + padding-left: 0.25rem; + } + diff --git a/website/client/components/chat/chatCard.vue b/website/client/components/chat/chatCard.vue index a52824ec37..4200a718a9 100644 --- a/website/client/components/chat/chatCard.vue +++ b/website/client/components/chat/chatCard.vue @@ -4,36 +4,48 @@ div .message-hidden(v-if='msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden .message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden .card-body - h3.leader( - :class='userLevelStyle(msg)', - @click="showMemberModal(msg.uuid)", - v-b-tooltip.hover.top="tierTitle", - ) - | {{msg.user}} - .svg-icon(v-html="tierIcon", v-if='showShowTierStyle') - p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}} - .text(v-markdown='msg.text') - hr - div(v-if='msg.id') - .action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}') + h3.leader( + :class='userLevelStyle(msg)', + @click="showMemberModal(msg.uuid)", + v-b-tooltip.hover.top="tierTitle", + v-if="msg.user" + ) + | {{msg.user}} + .svg-icon(v-html="tierIcon", v-if='showShowTierStyle') + p.time(v-b-tooltip="", :title="msg.timestamp | date") + span(v-if="msg.username") @{{ msg.username }} • + span {{ msg.timestamp | timeAgo }} + .text(v-html='atHighlight(parseMarkdown(msg.text))') + hr + .d-flex(v-if='msg.id') + .action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)') + .svg-icon(v-html="icons.copy") + div {{$t('copyAsTodo')}} + .action.d-flex.align-items-center(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)') + .svg-icon(v-html="icons.report") + div {{$t('report')}} + // @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys + .action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()') + .svg-icon(v-html="icons.delete") + | {{$t('delete')}} + .ml-auto.d-flex + .action.liked.d-flex.align-items-center(v-if='likeCount > 0') + .svg-icon(v-html="icons.liked") + | + {{ likeCount }} + .action.d-flex.align-items-center(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}') .svg-icon(v-html="icons.like") span(v-if='!msg.likes[user._id]') {{ $t('like') }} span(v-if='msg.likes[user._id]') {{ $t('liked') }} - span.action(v-if='!inbox', @click='copyAsTodo(msg)') - .svg-icon(v-html="icons.copy") - | {{$t('copyAsTodo')}} - span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)') - .svg-icon(v-html="icons.report") - | {{$t('report')}} - // @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys - span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()') - .svg-icon(v-html="icons.delete") - | {{$t('delete')}} - span.action.float-right.liked(v-if='likeCount > 0') - .svg-icon(v-html="icons.liked") - | + {{ likeCount }} + + diff --git a/website/client/components/groups/chat.vue b/website/client/components/groups/chat.vue index 9c89526fbf..f19ceeb517 100644 --- a/website/client/components/groups/chat.vue +++ b/website/client/components/groups/chat.vue @@ -194,7 +194,6 @@ width: 100%; background-color: $white; border: solid 1px $gray-400; - font-size: 16px; font-style: italic; line-height: 1.43; color: $gray-300; diff --git a/website/server/models/message.js b/website/server/models/message.js index 5e7089677c..68ced421ab 100644 --- a/website/server/models/message.js +++ b/website/server/models/message.js @@ -9,7 +9,8 @@ const defaultSchema = () => ({ text: String, // sender properties - user: String, // profile name + user: String, // profile name (unfortunately) + username: String, contributor: {type: mongoose.Schema.Types.Mixed}, backer: {type: mongoose.Schema.Types.Mixed}, uuid: String, // sender uuid @@ -115,6 +116,7 @@ export function messageDefaults (msg, user) { contributor: user.contributor && user.contributor.toObject(), backer: user.backer && user.backer.toObject(), user: user.profile.name, + username: user.auth.local.username, }); } else { message.uuid = 'system'; From ad0ede8d01a4eaeee4ca1cd63a7f73b96c620136 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 17 Oct 2018 13:59:39 -0500 Subject: [PATCH 11/51] fix(model): don't break if auth.local undefined --- website/server/models/message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/models/message.js b/website/server/models/message.js index 68ced421ab..bd87e0093e 100644 --- a/website/server/models/message.js +++ b/website/server/models/message.js @@ -116,7 +116,7 @@ export function messageDefaults (msg, user) { contributor: user.contributor && user.contributor.toObject(), backer: user.backer && user.backer.toObject(), user: user.profile.name, - username: user.auth.local.username, + username: user.auth && user.auth.local && user.auth.local.username, }); } else { message.uuid = 'system'; From 044fe1775731acf3b444b0f208d629be2114c028 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 17 Oct 2018 14:43:33 -0500 Subject: [PATCH 12/51] feat(usernames): Veteran Pet ladder award for affected users --- .../2018}/20181002_username_email.js | 0 .../users/20181019_veteran_pet_ladder.js | 91 ++++++++++++++++++ website/common/locales/en/pets.json | 1 + website/common/script/content/stable.js | 1 + .../stable/pets/Pet-Fox-Veteran.png | Bin 0 -> 3913 bytes .../spritesmith_large/promo_veteran_pets.png | Bin 0 -> 19350 bytes website/server/controllers/api-v4/auth.js | 15 ++- website/server/models/user/hooks.js | 1 + 8 files changed, 108 insertions(+), 1 deletion(-) rename migrations/{users => archive/2018}/20181002_username_email.js (100%) create mode 100644 migrations/users/20181019_veteran_pet_ladder.js create mode 100644 website/raw_sprites/spritesmith/stable/pets/Pet-Fox-Veteran.png create mode 100644 website/raw_sprites/spritesmith_large/promo_veteran_pets.png diff --git a/migrations/users/20181002_username_email.js b/migrations/archive/2018/20181002_username_email.js similarity index 100% rename from migrations/users/20181002_username_email.js rename to migrations/archive/2018/20181002_username_email.js diff --git a/migrations/users/20181019_veteran_pet_ladder.js b/migrations/users/20181019_veteran_pet_ladder.js new file mode 100644 index 0000000000..e8e9039570 --- /dev/null +++ b/migrations/users/20181019_veteran_pet_ladder.js @@ -0,0 +1,91 @@ +/* eslint-disable no-console */ +import { model as User } from '../website/server/models/user'; + +function processUsers (lastId) { + let query = { + 'flags.verifiedUsername': true, + }; + + let fields = { + 'items.pets': 1, + }; + + if (lastId) { + query._id = { + $gt: lastId, + }; + } + + return User.find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .then(updateUsers) + .catch((err) => { + console.log(err); + return exiting(1, `ERROR! ${err}`); + }); +} + +let progressCount = 1000; +let count = 0; + +function updateUsers (users) { + if (!users || users.length === 0) { + console.warn('All appropriate users found and modified.'); + displayData(); + return; + } + + let userPromises = users.map(updateUser); + let lastUser = users[users.length - 1]; + + return Promise.all(userPromises) + .then(() => { + processUsers(lastUser._id); + }); +} + +function updateUser (user) { + count++; + + let set = {}; + + if (user.items.pets['Bear-Veteran']) { + set['items.pets.Fox-Veteran'] = 5; + } else if (user.items.pets['Lion-Veteran']) { + set['items.pets.Bear-Veteran'] = 5; + } else if (user.items.pets['Tiger-Veteran']) { + set['items.pets.Lion-Veteran'] = 5; + } else if (user.items.pets['Wolf-Veteran']) { + set['items.pets.Tiger-Veteran'] = 5; + } else { + set['items.pets.Wolf-Veteran'] = 5; + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return User.update({_id: user._id}, {$set: set}).exec(); +} + +function displayData () { + console.warn(`\n${count} users processed\n`); + return exiting(0); +} + +function exiting (code, msg) { + code = code || 0; // 0 = success + if (code && !msg) { + msg = 'ERROR!'; + } + if (msg) { + if (code) { + console.error(msg); + } else { + console.log(msg); + } + } + process.exit(code); +} + +module.exports = processUsers; diff --git a/website/common/locales/en/pets.json b/website/common/locales/en/pets.json index 2298efc77f..15d21f403c 100644 --- a/website/common/locales/en/pets.json +++ b/website/common/locales/en/pets.json @@ -19,6 +19,7 @@ "veteranTiger": "Veteran Tiger", "veteranLion": "Veteran Lion", "veteranBear": "Veteran Bear", + "veteranFox": "Veteran Fox", "cerberusPup": "Cerberus Pup", "hydra": "Hydra", "mantisShrimp": "Mantis Shrimp", diff --git a/website/common/script/content/stable.js b/website/common/script/content/stable.js index 72507a0049..2b788813cf 100644 --- a/website/common/script/content/stable.js +++ b/website/common/script/content/stable.js @@ -71,6 +71,7 @@ let specialPets = { 'Orca-Base': 'orca', 'Bear-Veteran': 'veteranBear', 'Hippogriff-Hopeful': 'hopefulHippogriffPet', + 'Fox-Veteran': 'veteranFox', }; let specialMounts = { diff --git a/website/raw_sprites/spritesmith/stable/pets/Pet-Fox-Veteran.png b/website/raw_sprites/spritesmith/stable/pets/Pet-Fox-Veteran.png new file mode 100644 index 0000000000000000000000000000000000000000..0677ba3afbf37ceb4cc1ecd5293923b00997455f GIT binary patch literal 3913 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 Xm_gnG^xZBy00000NkvXXu0mjfOHz!2 literal 0 HcmV?d00001 diff --git a/website/raw_sprites/spritesmith_large/promo_veteran_pets.png b/website/raw_sprites/spritesmith_large/promo_veteran_pets.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2db4bddfaaf3bdd2b7f536be6ed6f4670076fa GIT binary patch literal 19350 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 ddw}!gf^XnJr6D9| Date: Wed, 17 Oct 2018 15:27:54 -0500 Subject: [PATCH 13/51] feat(usernames): vet pet announcement --- .../components/settings/verifyUsername.vue | 10 ++++++++-- website/common/locales/en/settings.json | 3 ++- .../spritesmith_large/scene_veteran_pets.png | Bin 0 -> 4966 bytes 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 website/raw_sprites/spritesmith_large/scene_veteran_pets.png diff --git a/website/client/components/settings/verifyUsername.vue b/website/client/components/settings/verifyUsername.vue index 5ef311fc24..991ad6ff4a 100644 --- a/website/client/components/settings/verifyUsername.vue +++ b/website/client/components/settings/verifyUsername.vue @@ -40,13 +40,15 @@ .small.text-center {{ $t('usernameLimitations') }} .row.justify-content-center button.btn.btn-primary(type='submit', @click='submitNames()' :disabled='usernameCannotSubmit') {{ $t('saveAndConfirm') }} + .scene_veteran_pets.center-block + .small.text-center.mb-3 {{ $t('verifyUsernameVeteranPet') }} .small.text-center.tos-footer(v-html="$t('usernameTOSRequirements')") From 5cd0f56811eb7507d8e38daf1f08ad7040b3ea9d Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 19 Oct 2018 16:03:48 -0500 Subject: [PATCH 16/51] fix(usernames): let verify modal grow to content --- website/client/components/settings/verifyUsername.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/components/settings/verifyUsername.vue b/website/client/components/settings/verifyUsername.vue index 991ad6ff4a..f15f322779 100644 --- a/website/client/components/settings/verifyUsername.vue +++ b/website/client/components/settings/verifyUsername.vue @@ -48,7 +48,7 @@ diff --git a/website/client/components/userMenu/profile.vue b/website/client/components/userMenu/profile.vue index f3d1e65ea6..2237d1af9b 100644 --- a/website/client/components/userMenu/profile.vue +++ b/website/client/components/userMenu/profile.vue @@ -40,11 +40,12 @@ div #userProfile.standard-page(v-show='selectedPage === "profile"', v-if='user.profile') .row .col-12.col-md-8 - .header + .header.mb-3 h1 {{user.profile.name}} - h4 - strong {{ $t('userId') }}:  - | {{user._id}} + div + strong(v-if='user.auth && user.auth.local && user.auth.local.username') @{{ user.auth.local.username }} + div + strong(v-if='this.userLoggedIn.contributor.admin') {{ user._id }} .col-12.col-md-4 button.btn.btn-secondary(v-if='user._id === userLoggedIn._id', @click='editing = !editing') {{ $t('edit') }} .row(v-if='!editing') @@ -146,7 +147,7 @@ div #profile { .member-details { .character-name, small, .small-text { - color: #878190 + color: #878190; } } @@ -193,7 +194,7 @@ div .gift-icon { width: 14px; margin: auto; - color: #686274; + color: $gray-100; } .gift-icon { @@ -201,13 +202,13 @@ div } .remove-icon { - width:16px; - color: #686274; + width: 16px; + color: $gray-100; } .positive-icon { width: 14px; - color: #686274; + color: $gray-100; } .photo img { @@ -216,11 +217,12 @@ div .header { h1 { - color: #4f2a93; + color: $purple-200; + margin-bottom: 0rem; } h4 { - color: #686274; + color: $gray-100; } } diff --git a/website/server/models/user/index.js b/website/server/models/user/index.js index 5521f21290..4866a1eef3 100644 --- a/website/server/models/user/index.js +++ b/website/server/models/user/index.js @@ -8,7 +8,7 @@ require('./methods'); // A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private) export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats - achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected`; + achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected auth.local.username`; // The minimum amount of data needed when populating multiple users export let nameFields = 'profile.name'; From 804fe1c6d564c594a6ea0e6b63745e535752742e Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 24 Oct 2018 18:39:54 -0500 Subject: [PATCH 20/51] fix(usernames): various z-index modals above Resting banner force reload after verify username add missing e-mail validation on frontpage let Yesterdaily modal float behind username modal --- website/client/assets/scss/modal.scss | 6 +++++- website/client/components/auth/authForm.vue | 4 ++++ website/client/components/auth/registerLoginReset.vue | 4 ++++ website/client/components/settings/verifyUsername.vue | 1 + website/client/components/snackbars/notifications.vue | 2 +- website/client/components/yesterdailyModal.vue | 11 +++++++++++ 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/website/client/assets/scss/modal.scss b/website/client/assets/scss/modal.scss index e61bfccb30..64d9820818 100644 --- a/website/client/assets/scss/modal.scss +++ b/website/client/assets/scss/modal.scss @@ -11,6 +11,10 @@ } } +.modal { + z-index: 1350; +} + .modal-dialog { width: auto; @@ -117,4 +121,4 @@ } } } -} \ No newline at end of file +} diff --git a/website/client/components/auth/authForm.vue b/website/client/components/auth/authForm.vue index 5c55c800cb..5f4854d78e 100644 --- a/website/client/components/auth/authForm.vue +++ b/website/client/components/auth/authForm.vue @@ -156,6 +156,10 @@ export default { } }); }, 500), + validateEmail (email) { + let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); + }, // @TODO: Abstract hello in to action or lib async socialAuth (network) { try { diff --git a/website/client/components/auth/registerLoginReset.vue b/website/client/components/auth/registerLoginReset.vue index ffaa3b6c93..88ea5b249d 100644 --- a/website/client/components/auth/registerLoginReset.vue +++ b/website/client/components/auth/registerLoginReset.vue @@ -415,6 +415,10 @@ export default { } }); }, 500), + validateEmail (email) { + let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); + }, async register () { // @TODO do not use alert if (!this.email) { diff --git a/website/client/components/settings/verifyUsername.vue b/website/client/components/settings/verifyUsername.vue index f15f322779..da3babcf85 100644 --- a/website/client/components/settings/verifyUsername.vue +++ b/website/client/components/settings/verifyUsername.vue @@ -206,6 +206,7 @@ await this.$store.dispatch('user:fetch', {forceLoad: true}); this.$root.$emit('habitica::resync-completed'); this.$root.$emit('bv::hide::modal', 'verify-username'); + this.$router.go(0); }, restoreEmptyDisplayName () { if (this.temporaryDisplayName.length < 1) { diff --git a/website/client/components/snackbars/notifications.vue b/website/client/components/snackbars/notifications.vue index 22b0d78030..af44e65a39 100644 --- a/website/client/components/snackbars/notifications.vue +++ b/website/client/components/snackbars/notifications.vue @@ -9,7 +9,7 @@ position: fixed; right: 10px; width: 350px; - z-index: 1070; // 1070 is above modal backgrounds + z-index: 1400; // 1400 is above modal backgrounds &-top-pos { &-normal { diff --git a/website/client/components/yesterdailyModal.vue b/website/client/components/yesterdailyModal.vue index 698b98c1ec..cff077257c 100644 --- a/website/client/components/yesterdailyModal.vue +++ b/website/client/components/yesterdailyModal.vue @@ -22,6 +22,17 @@ button.btn.btn-primary(@click='close()') {{ $t('yesterDailiesCallToAction') }} + + + + diff --git a/website/client/components/settings/verifyUsername.vue b/website/client/components/settings/verifyUsername.vue index 024b1f190e..5fc0ec6ceb 100644 --- a/website/client/components/settings/verifyUsername.vue +++ b/website/client/components/settings/verifyUsername.vue @@ -10,36 +10,7 @@ div.nametag-header(v-html='icons.helloNametag') h2.text-center {{ $t('usernameTime') }} p.text-center(v-html="$t('usernameInfo')") - .form-group - .row.align-items-center - .col-3 - label(for='displayName') {{ $t('displayName') }} - .col-9 - input#displayName.form-control( - type='text', - :placeholder="$t('newDisplayName')", - v-model='temporaryDisplayName', - @blur='restoreEmptyDisplayName()', - :class='{"is-invalid input-invalid": displayNameInvalid, "input-valid": displayNameValid, "text-darker": temporaryDisplayName.length > 0}') - .mb-3(v-if="displayNameIssues.length > 0") - .input-error.text-center(v-for="issue in displayNameIssues") {{ issue }} - .form-group - .row.align-items-center - .col-3 - label(for='username') {{ $t('username') }} - .col-9 - .input-group-prepend.input-group-text @ - input#username.form-control( - type='text', - :placeholder="$t('newUsername')", - v-model='temporaryUsername', - @blur='restoreEmptyUsername()', - :class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid, "text-darker": temporaryUsername.length > 0}') - .mb-3(v-if="usernameIssues.length > 0") - .input-error.text-center(v-for="issue in usernameIssues") {{ issue }} - .small.text-center {{ $t('usernameLimitations') }} - .row.justify-content-center - button.btn.btn-primary(type='submit', @click='submitNames()' :disabled='usernameCannotSubmit') {{ $t('saveAndConfirm') }} + username-form .scene_veteran_pets.center-block .small.text-center.mb-3 {{ $t('verifyUsernameVeteranPet') }} .small.text-center.tos-footer(v-html="$t('usernameTOSRequirements')") @@ -62,57 +33,15 @@ diff --git a/website/common/locales/en/character.json b/website/common/locales/en/character.json index 75b255069b..919594aa21 100644 --- a/website/common/locales/en/character.json +++ b/website/common/locales/en/character.json @@ -7,7 +7,7 @@ "noPhoto": "This Habitican hasn't added a photo.", "other": "Other", "fullName": "Full Name", - "displayName": "Display Name", + "displayName": "Display name", "changeDisplayName": "Change Display Name", "newDisplayName": "New Display Name", "displayPhoto": "Photo", diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json index 6c70e575f3..35b55ea0f8 100644 --- a/website/common/locales/en/front.json +++ b/website/common/locales/en/front.json @@ -329,7 +329,7 @@ "joinToday": "Join Habitica Today", "featuredIn": "Featured in", "signup": "Sign Up", - "getStarted": "Get Started", + "getStarted": "Get Started!", "mobileApps": "Mobile Apps", "learnMore": "Learn More" } diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json index 202b0e0ba6..4e967ee950 100644 --- a/website/common/locales/en/generic.json +++ b/website/common/locales/en/generic.json @@ -254,6 +254,7 @@ "resetFilters": "Clear all filters", "applyFilters": "Apply Filters", + "wantToWorkOn": "I want to work on:", "categories": "Categories", "habiticaOfficial": "Habitica Official", "animals": "Animals", diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index 5c404f64d4..f9e862ccd3 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -6,9 +6,11 @@ "welcomeTo": "Welcome to", "welcomeBack": "Welcome back!", "justin": "Justin", - "justinIntroMessage1": "Hello there! You must be new here. My name is Justin, your guide to Habitica.", + "justinIntroMessage1": "Hello there! You must be new here. My name is Justin, and I'll be your guide in Habitica.", "justinIntroMessage2": "To start, you'll need to create an avatar.", "justinIntroMessage3": "Great! Now, what are you interested in working on throughout this journey?", + "justinIntroMessageUsername": "Before we begin, let’s figure out what to call you. Below you’ll find a display name and username I’ve generated for you. After you’ve picked a display name and username, we’ll get started by creating an avatar!", + "justinIntroMessageAppearance": "So how would you like to look? Don’t worry, you can change this later.", "introTour": "Here we are! I've filled out some Tasks for you based on your interests, so you can get started right away. Click a Task to edit or add new Tasks to fit your routine!", "prev": "Prev", "next": "Next", diff --git a/website/common/locales/en/subscriber.json b/website/common/locales/en/subscriber.json index 046341dd3b..973ba65172 100644 --- a/website/common/locales/en/subscriber.json +++ b/website/common/locales/en/subscriber.json @@ -210,7 +210,7 @@ "haveCouponCode": "Do you have a coupon code?", "subscriptionAlreadySubscribedLeadIn": "Thanks for subscribing!", "subscriptionAlreadySubscribed1": "To see your subscription details and cancel, renew, or change your subscription, please go to User icon > Settings > Subscription.", - "purchaseAll": "Purchase All", + "purchaseAll": "Purchase Set", "gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.", "gemsRemaining": "gems remaining", "notEnoughGemsToBuy": "You are unable to buy that amount of gems" From b54f031acd1da14f1da3ec1119e43c0a6a2e3f79 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 1 Nov 2018 15:17:01 -0500 Subject: [PATCH 28/51] fix(chat): replace autocomplete at @ --- website/client/components/chat/autoComplete.vue | 4 ++-- website/client/components/groups/chat.vue | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/website/client/components/chat/autoComplete.vue b/website/client/components/chat/autoComplete.vue index db72d2ad47..052d90ec96 100644 --- a/website/client/components/chat/autoComplete.vue +++ b/website/client/components/chat/autoComplete.vue @@ -65,7 +65,7 @@ import tier9 from 'assets/svg/tier-staff.svg'; import tierNPC from 'assets/svg/tier-npc.svg'; export default { - props: ['selections', 'text', 'coords', 'chat', 'textbox'], + props: ['selections', 'text', 'caretPosition', 'coords', 'chat', 'textbox'], data () { return { atRegex: new RegExp(/@[\w-]+$/), @@ -142,7 +142,7 @@ export default { if (!this.atRegex.test(newText)) return; this.searchActive = true; - this.currentSearchPosition = newText.length - 2; + this.currentSearchPosition = newText.lastIndexOf('@', this.caretPosition); }, chat () { this.resetDefaults(); diff --git a/website/client/components/groups/chat.vue b/website/client/components/groups/chat.vue index f19ceeb517..0f6b23164c 100644 --- a/website/client/components/groups/chat.vue +++ b/website/client/components/groups/chat.vue @@ -19,6 +19,7 @@ v-on:select="selectedAutocomplete", :textbox='textbox', :coords='coords', + :caretPosition = 'caretPosition', :chat='group.chat') .row.chat-actions @@ -56,6 +57,7 @@ data () { return { newMessage: '', + caretPosition: 0, chat: { submitDisable: false, submitTimeout: null, @@ -75,7 +77,7 @@ methods: { // https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a getCoord (e, text) { - let carPos = text.selectionEnd; + this.caretPosition = text.selectionEnd; let div = document.createElement('div'); let span = document.createElement('span'); let copyStyle = getComputedStyle(text); @@ -86,8 +88,8 @@ div.style.position = 'absolute'; document.body.appendChild(div); - div.textContent = text.value.substr(0, carPos); - span.textContent = text.value.substr(carPos) || '.'; + div.textContent = text.value.substr(0, this.caretPosition); + span.textContent = text.value.substr(this.caretPosition) || '.'; div.appendChild(span); this.coords = { TOP: span.offsetTop, From dc46127fc7ca5c4d38d1d92132c21794874fe358 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 1 Nov 2018 15:22:20 -0500 Subject: [PATCH 29/51] refactor(auth): only import needed validator module --- website/client/components/auth/authForm.vue | 4 ++-- website/client/components/auth/registerLoginReset.vue | 4 ++-- website/client/components/static/home.vue | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/website/client/components/auth/authForm.vue b/website/client/components/auth/authForm.vue index 471474156c..5489cf4f10 100644 --- a/website/client/components/auth/authForm.vue +++ b/website/client/components/auth/authForm.vue @@ -82,7 +82,7 @@ import hello from 'hellojs'; import { setUpAxios } from 'client/libs/auth'; import debounce from 'lodash/debounce'; -import validator from 'validator'; +import isEmail from 'validator/lib/isEmail'; import facebookSquareIcon from 'assets/svg/facebook-square.svg'; import googleIcon from 'assets/svg/google.svg'; @@ -116,7 +116,7 @@ export default { computed: { emailValid () { if (this.email.length <= 3) return false; - return validator.isEmail(this.email); + return isEmail(this.email); }, emailInvalid () { return !this.emailValid; diff --git a/website/client/components/auth/registerLoginReset.vue b/website/client/components/auth/registerLoginReset.vue index c0f42c2870..d131dc76a5 100644 --- a/website/client/components/auth/registerLoginReset.vue +++ b/website/client/components/auth/registerLoginReset.vue @@ -295,7 +295,7 @@ import axios from 'axios'; import hello from 'hellojs'; import debounce from 'lodash/debounce'; -import validator from 'validator'; +import isEmail from 'validator/lib/isEmail'; import gryphon from 'assets/svg/gryphon.svg'; import habiticaIcon from 'assets/svg/habitica-logo.svg'; @@ -341,7 +341,7 @@ export default { }, emailValid () { if (this.email.length <= 3) return false; - return validator.isEmail(this.email); + return isEmail(this.email); }, emailInvalid () { if (this.email.length <= 3) return false; diff --git a/website/client/components/static/home.vue b/website/client/components/static/home.vue index 27dfdc702b..a6acf92a55 100644 --- a/website/client/components/static/home.vue +++ b/website/client/components/static/home.vue @@ -560,7 +560,7 @@ diff --git a/website/client/components/groups/inviteModalOld.vue b/website/client/components/groups/inviteModalOld.vue new file mode 100644 index 0000000000..8d8869f00d --- /dev/null +++ b/website/client/components/groups/inviteModalOld.vue @@ -0,0 +1,152 @@ + + + diff --git a/website/client/components/header/index.vue b/website/client/components/header/index.vue index dffdef7046..af6f3ad020 100644 --- a/website/client/components/header/index.vue +++ b/website/client/components/header/index.vue @@ -1,6 +1,6 @@