From fa1fef11d6e728a82055bad54053b0378547dcb9 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 13 Oct 2018 12:53:16 -0500 Subject: [PATCH] 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'));