Merge branch 'sabrecat/force-username-modal' into sabrecat/usernames-master

This commit is contained in:
Sabe Jones
2018-10-17 15:29:23 -05:00
14 changed files with 475 additions and 23 deletions

View File

@@ -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, {
displayName: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueSlur')] });
});
it('errors if display name contains a slur', async () => {
await expect(user.post(ENDPOINT, {
displayName: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
await expect(user.post(ENDPOINT, {
displayName: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
await expect(user.post(ENDPOINT, {
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, {
displayName: 'this is a very long display name over 30 characters',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength')] });
});
});
});

View File

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="104" viewBox="0 0 256 104">
<defs>
<rect id="b" width="96" height="56" rx="6"/>
<filter id="a" width="112.5%" height="128.6%" x="-6.2%" y="-10.7%" filterUnits="objectBoundingBox">
<feMorphology in="SourceAlpha" operator="dilate" radius="4" result="shadowSpreadOuter1"/>
<feOffset dy="4" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/>
<feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.407843137 0 0 0 0 0.384313725 0 0 0 0 0.454901961 0 0 0 0.24 0"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<g opacity=".64">
<path fill="#3FDAA2" d="M194.359 74.179l3.074-1.341-2.917-1.655-1.341-3.075-1.655 2.918-3.075 1.34 2.918 1.656 1.34 3.074z" opacity=".96"/>
<path fill="#FF6165" d="M127.439 4.105l-.023 2.982 2.399-1.771 2.981.023-1.77-2.4.022-2.98-2.399 1.77-2.98-.022z" opacity=".84"/>
<path fill="#3FDAA2" d="M70.501 38.126l2.574.428-1.202-2.315.428-2.574-2.316 1.202-2.573-.427 1.202 2.315-.428 2.573z" opacity=".83"/>
<path fill="#50B5E9" d="M240.929 73.34l.173 6.333 4.962-3.939 6.334-.173-3.939-4.962-.173-6.334-4.962 3.939-6.334.173z" opacity=".73"/>
<path fill="#FFBE5D" d="M198.881 41.724l-3.984 5.397 6.708-.05 5.397 3.983-.05-6.708 3.983-5.397-6.708.051-5.397-3.984z" opacity=".82"/>
<path fill="#50B5E9" d="M81.165 96.829l-.589-4.433-3.193 3.13-4.433.59 3.13 3.193.59 4.433 3.193-3.13 4.433-.59z" opacity=".99"/>
<path fill="#50B5E9" d="M119.702 40.186l-3.901 5.91 7.068-.425 5.91 3.902-.425-7.069 3.901-5.909-7.068.425-5.909-3.902z"/>
<path fill="#FF6165" d="M162.14 91.367l3.404 2.9.278-4.463 2.9-3.404-4.463-.278-3.404-2.9-.278 4.463-2.9 3.404z" opacity=".84"/>
<path fill="#FF944C" d="M6.708 37.066l.62 3.675 2.568-2.7 3.675-.62-2.7-2.568-.62-3.675-2.568 2.7-3.675.62zM253.486 43.18l-.037-3.727-2.96 2.265-3.726.037 2.265 2.959.037 3.727 2.96-2.266 3.726-.037z" opacity=".93"/>
<path fill="#9A62FF" d="M51.481 70.952l5.13-.957-3.843-3.53-.957-5.128-3.53 3.842-5.128.957 3.843 3.53.956 5.128z" opacity=".99"/>
<path fill="#FFBE5D" d="M78.061 8.656l-.952 3.987 3.761-1.63 3.987.952-1.63-3.761.953-3.988-3.762 1.63-3.987-.952z"/>
<path fill="#3FDAA2" d="M5.863 70.74l4.692 1.207-1.849-4.478 1.208-4.692-4.478 1.849-4.692-1.208 1.849 4.478-1.208 4.692z" opacity=".96"/>
<path fill="#9A62FF" d="M182.63 14.447l5.026-1.4-4.135-3.18-1.4-5.027-3.181 4.136-5.026 1.4 4.135 3.18 1.4 5.027z" opacity=".99"/>
</g>
<g transform="translate(79 24)">
<use fill="#000" filter="url(#a)" xlink:href="#b"/>
<rect width="100" height="60" x="-2" y="-2" fill="#6133B4" stroke="#4F2A93" stroke-width="4" rx="6"/>
</g>
<path fill="#FFF" d="M99.91 39v-9.8h3.024v3.528h2.366V29.2h3.024V39H105.3v-3.584h-2.366V39H99.91zm12.138 0v-9.8h7.182v2.576h-4.186v1.12h3.878v2.352h-3.878v1.176h4.256V39h-7.252zm10.794 0v-9.8h3.024v7.14h3.738V39h-6.762zm10.178 0v-9.8h3.024v7.14h3.738V39h-6.762zm14.14.21c-2.688 0-4.634-2.03-4.634-4.97v-.252c0-2.94 1.96-4.998 4.648-4.998 2.702 0 4.648 2.03 4.648 4.97v.252c0 2.94-1.974 4.998-4.662 4.998zm.014-2.702c.98 0 1.582-.84 1.582-2.296v-.21c0-1.456-.616-2.31-1.596-2.31-.966 0-1.582.84-1.582 2.296v.21c0 1.456.616 2.31 1.596 2.31zM79 44h96v28H79z"/>
<path fill="#4E4A57" d="M102.99 64.82c.04-1.6.04-3.34-.12-4.96-.8-.18-1.76-.26-2.88-.1v5.18c0 .18-.38.28-.5.28-.24 0-.5-.14-.54-.4l-.24-14.22c0-.42.06-.76.52-.76.1 0 .72.1.72.28l.02 8.54c.74.1 2.34.06 2.98.14v-.84c0-2.74-.16-5.1-.16-7.48 0-.24.04-.64.34-.64.3 0 .74.14.78.52l.26 14.36c0 .34-.3.62-.68.62h-.1c-.14-.02-.36-.4-.4-.52zm7.64-3.18h-2.96l-.12.12-.34 2.26c-.04.3-.02 1.16-.6 1.16-.26 0-.44-.52-.44-.72 2.08-14.5 2.1-14.54 2.1-14.54.1-.38.24-.94.74-.94.22 0 .5.1.62.3l.16.42c1.16 4.58 1.7 9.28 2.24 14l.12 1.3c0 .02-.02.02-.02.04-.14.38-.22.54-.6.54h-.06c-.12 0-.32 0-.34-.14l-.5-3.8zm-1.5-10.52l-1.16 9.28 2.5.06-1.34-9.34zm4.3-.68c1.3-1.48 4.24-1.06 4.24 1.4 0 1.04-.4 1.98-1.08 2.74 2.28 1.42 3.18 3.12 3.18 5.8 0 2.86-1.58 5.3-4.72 5.3-.14 0-.54-.02-.7-.3-.22-3.08-.22-6.14-.52-9.76l-.4-4.76v-.42zm1.98 14.02c.12.02.24.02.36.02 2.12 0 2.82-2.18 2.82-4.1 0-2.22-1.02-4.92-3.64-4.92 0 3.02.22 6 .46 9zm-.82-13.58l.3 3.3c1.02 0 1.7-1.16 1.7-2.26 0-1.14-.94-1.54-2-1.04zm6.54 14.74l.54-14.84c.14-.4.24-.42.66-.42.58 0 .58.26.58.84 0 .06-.02.32-.02.36l-.6 13.68c-.04.88-1.16.96-1.16.38zm5.08-1.86l-.4-12.66c-1.24.08-1.8.06-1.84-.28.04-.68.04-.92 2.12-.8h1.68c.44.02 1.18.08 1.18.66 0 .18-.22.38-.4.38h-1.68l.38 12.7c0 .18-.4.28-.52.28s-.52-.1-.52-.28zm4.12 1.86l.54-14.84c.14-.4.24-.42.66-.42.58 0 .58.26.58.84 0 .06-.02.32-.02.36l-.6 13.68c-.04.88-1.16.96-1.16.38zm7.66-15.56c.42 0 .94.28.94.74 0 .7-1.16.56-1.44.56h-.12c-1.94 0-2.8 4.02-2.8 6.28 0 2.56.16 4.38 1.92 5.84.86.72 2.18-.02 2.48.52.4.7-.34.94-.98.94-4.54-.1-5.24-6.12-4.32-10.02.48-2.08 1.72-4.86 4.32-4.86zm7.4 11.58h-2.96l-.12.12-.34 2.26c-.04.3-.02 1.16-.6 1.16-.26 0-.44-.52-.44-.72 2.08-14.5 2.1-14.54 2.1-14.54.1-.38.24-.94.74-.94.22 0 .5.1.62.3l.16.42c1.16 4.58 1.7 9.28 2.24 14l.12 1.3c0 .02-.02.02-.02.04-.14.38-.22.54-.6.54h-.06c-.12 0-.32 0-.34-.14l-.5-3.8zm-1.5-10.52l-1.16 9.28 2.5.06-1.34-9.34zm5.02 14.24l.24-14.08c-.04-.8 1.06-1.28 1.48-.12l4.08 11.06c.02-.88.18-5.8.3-7.76.06-1.2-.02-2.42.24-3.54 0-.22.22-.22.52-.22.48.02.52.04.5.48-.06 2.08-.36 4.16-.44 6.24-.04 1.16-.24 6.7-.24 7.86-.14.42-.76.18-.92-.16-.2-.42-.36-.82-4.6-12.16l-.14 12.28c0 .6-1.02.54-1.02.12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -25,6 +25,7 @@ div
login-incentives(:data='notificationData') login-incentives(:data='notificationData')
quest-completed quest-completed
quest-invitation quest-invitation
verify-username
</template> </template>
<style lang='scss'> <style lang='scss'>
@@ -118,6 +119,7 @@ import streak from './achievements/streak';
import ultimateGear from './achievements/ultimateGear'; import ultimateGear from './achievements/ultimateGear';
import wonChallenge from './achievements/wonChallenge'; import wonChallenge from './achievements/wonChallenge';
import loginIncentives from './achievements/login-incentives'; import loginIncentives from './achievements/login-incentives';
import verifyUsername from './settings/verifyUsername';
const NOTIFICATIONS = { const NOTIFICATIONS = {
CHALLENGE_JOINED_ACHIEVEMENT: { CHALLENGE_JOINED_ACHIEVEMENT: {
@@ -178,6 +180,7 @@ export default {
dropsEnabled, dropsEnabled,
contributor, contributor,
loginIncentives, loginIncentives,
verifyUsername,
}, },
data () { data () {
// Levels that already display modals and should not trigger generic Level Up // Levels that already display modals and should not trigger generic Level Up
@@ -323,6 +326,8 @@ export default {
this.goto('intro', 0); this.goto('intro', 0);
}, 2000); }, 2000);
this.forceVerifyUsername();
this.runYesterDailies(); this.runYesterDailies();
// Do not remove the event listener as it's live for the entire app lifetime // Do not remove the event listener as it's live for the entire app lifetime
@@ -445,7 +450,8 @@ export default {
Promise.all([ Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}), this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}), this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]).then(() => this.runYesterDailies()); ]).then(() => this.forceVerifyUsername())
.then(() => this.runYesterDailies());
} }
}, 1000), }, 1000),
scheduleNextCron () { scheduleNextCron () {
@@ -465,6 +471,11 @@ export default {
this.nextCron = Number(nextCron.format('x')); this.nextCron = Number(nextCron.format('x'));
this.$store.state.isRunningYesterdailies = false; this.$store.state.isRunningYesterdailies = false;
}, },
forceVerifyUsername () {
if (this.user.flags.verifiedUsername) return;
this.$root.$emit('bv::show::modal', 'verify-username');
},
async runYesterDailies () { async runYesterDailies () {
if (this.$store.state.isRunningYesterdailies) return; if (this.$store.state.isRunningYesterdailies) return;
this.$store.state.isRunningYesterdailies = true; this.$store.state.isRunningYesterdailies = true;

View File

@@ -130,8 +130,10 @@
h5 {{ $t('changeDisplayName') }} h5 {{ $t('changeDisplayName') }}
.form(name='changeDisplayName', novalidate) .form(name='changeDisplayName', novalidate)
.form-group .form-group
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName') input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName', :class='{"is-invalid input-invalid": displayNameInvalid}')
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }} .mb-3(v-if="displayNameIssues.length > 0")
.input-error(v-for="issue in displayNameIssues") {{ issue }}
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)', :disabled='displayNameCannotSubmit') {{ $t('submit') }}
h5 {{ $t('changeUsername') }} h5 {{ $t('changeUsername') }}
.form(name='changeUsername', novalidate) .form(name='changeUsername', novalidate)
@@ -252,6 +254,7 @@ export default {
password: '', password: '',
confirmPassword: '', confirmPassword: '',
}, },
displayNameIssues: [],
usernameIssues: [], usernameIssues: [],
}; };
}, },
@@ -312,6 +315,18 @@ export default {
verifiedUsername () { verifiedUsername () {
return this.user.flags.verifiedUsername; return this.user.flags.verifiedUsername;
}, },
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length <= 1) return false;
return this.displayNameIssues.length === 0;
},
displayNameCannotSubmit () {
if (this.temporaryDisplayName.length <= 1) return true;
return !this.displayNameValid;
},
usernameValid () { usernameValid () {
if (this.usernameUpdates.username.length <= 1) return false; if (this.usernameUpdates.username.length <= 1) return false;
return this.usernameIssues.length === 0; return this.usernameIssues.length === 0;
@@ -332,10 +347,30 @@ export default {
}, },
deep: true, deep: true,
}, },
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
}, },
methods: { methods: {
// eslint-disable-next-line func-names validateDisplayName: debounce(function checkName (displayName) {
validateUsername: debounce(function (username) { if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) { if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = []; this.usernameIssues = [];
return; return;

View File

@@ -0,0 +1,270 @@
<template lang="pug">
b-modal#verify-username(
size="m",
:no-close-on-backdrop="true",
:no-close-on-esc="true",
:hide-header="true",
:hide-footer="true",
@hide="$emit('hide')",
).d-flex
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') }}
.scene_veteran_pets.center-block
.small.text-center.mb-3 {{ $t('verifyUsernameVeteranPet') }}
.small.text-center.tos-footer(v-html="$t('usernameTOSRequirements')")
</template>
<style lang="scss">
#verify-username___BV_modal_outer_ {
.modal-content {
height: 650px;
width: 566px;
padding-left: 2rem;
padding-right: 2rem;
}
.modal-dialog {
-webkit-transform: translate(-5%, calc(50vh - 60%));
-ms-transform: translate(0, 50vh) translate(-5%, -60%);
-o-transform: translate(-5%, calc(50vh - 60%));
transform: translate(0, 50vh) translate(-5%, -60%);
}
}
</style>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
button {
margin: 1rem;
}
.center-block {
margin: 0 auto 1em auto;
}
.col-3 {
padding-right: 0rem;
}
.form-group {
background-color: $gray-700;
border-radius: 2px;
border: solid 1px $gray-500;
}
h2 {
color: $purple-200;
margin-top: 1.5rem;
}
input {
border: 0px;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}
.input-group-prepend {
margin-right: 0px;
}
.input-group-text {
border: 0px;
background-color: $white;
color: $gray-300;
padding: 0rem 0rem 0rem 0.75rem;
}
label {
color: $gray-100;
font-weight: bold;
margin-bottom: 0rem;
margin-left: 1rem;
}
.nametag-header {
background-color: $gray-700;
border-radius: 0.3rem 0.3rem 0rem 0rem;
margin-left: -3rem;
margin-right: -3rem;
padding: 1rem 9rem 1rem 9rem;
}
p {
color: #686274;
}
.small {
color: $gray-200;
}
.text-darker {
color: $gray-50;
}
.tos-footer {
background-color: $gray-700;
border-radius: 0rem 0rem 0.3rem 0.3rem;
margin-left: -3rem;
margin-right: -3rem;
padding: 1rem 4rem 1rem 4rem;
}
#username {
padding-left: 0.25rem;
}
</style>
<script>
import axios from 'axios';
import debounce from 'lodash/debounce';
import helloNametag from 'assets/svg/hello-habitican.svg';
import { mapState } from 'client/libs/store';
export default {
computed: {
...mapState({
user: 'user.data',
}),
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length <= 1) return false;
return this.displayNameIssues.length === 0;
},
usernameCannotSubmit () {
if (this.temporaryUsername.length <= 1) return true;
return !this.usernameValid || !this.displayNameValid;
},
usernameInvalid () {
if (this.temporaryUsername.length <= 1) return false;
return !this.usernameValid;
},
usernameValid () {
if (this.temporaryUsername.length <= 1) return false;
return this.usernameIssues.length === 0;
},
},
data () {
return {
icons: Object.freeze({
helloNametag,
}),
displayNameIssues: [],
temporaryDisplayName: '',
temporaryUsername: '',
usernameIssues: [],
};
},
methods: {
async submitNames () {
if (this.temporaryDisplayName !== this.user.profile.name) {
await axios.put('/api/v4/user/', {'profile.name': this.temporaryDisplayName});
}
await axios.put('/api/v4/user/auth/update-username', {username: this.temporaryUsername});
this.close();
},
async close () {
this.$root.$emit('habitica::resync-requested');
await this.$store.dispatch('user:fetch', {forceLoad: true});
this.$root.$emit('habitica::resync-completed');
this.$root.$emit('bv::hide::modal', 'verify-username');
},
restoreEmptyDisplayName () {
if (this.temporaryDisplayName.length < 1) {
this.temporaryDisplayName = this.user.profile.name;
}
},
restoreEmptyUsername () {
if (this.temporaryUsername.length < 1) {
this.temporaryUsername = this.user.auth.local.username;
}
},
validateDisplayName: debounce(function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
},
mounted () {
this.temporaryDisplayName = this.user.profile.name;
this.temporaryUsername = this.user.auth.local.username;
},
watch: {
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
temporaryUsername: {
handler () {
this.validateUsername(this.temporaryUsername);
},
deep: true,
},
},
};
</script>

View File

@@ -55,6 +55,15 @@ export async function verifyUsername (store, params) {
return result.data.data; 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) { export async function socialAuth (store, params) {
let url = '/api/v4/user/auth/social'; let url = '/api/v4/user/auth/social';
let result = await axios.post(url, { let result = await axios.post(url, {

View File

@@ -271,15 +271,9 @@
"emailTaken": "Email address is already used in an account.", "emailTaken": "Email address is already used in an account.",
"newEmailRequired": "Missing new email address.", "newEmailRequired": "Missing new email address.",
"usernameTime": "It's time to set your username!", "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.<br><br>If you'd like to learn more about this change, visit the wiki's <a href='http://habitica.wikia.com/wiki/Player_Names' target='_blank'>Player Names</a> page.", "usernameInfo": "Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.<br><br>If you'd like to learn more about this change, <a href='http://habitica.wikia.com/wiki/Player_Names' target='_blank'>visit our wiki</a>.",
"usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didnt previously set a login name, your username was auto-generated.", "usernameTOSRequirements": "Usernames must conform to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>. If you didnt previously set a login name, your username was auto-generated.",
"usernameTaken": "Username already taken.", "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.", "passwordConfirmationMatch": "Password confirmation doesn't match password.",
"invalidLoginCredentials": "Incorrect username and/or email and/or password.", "invalidLoginCredentials": "Incorrect username and/or email and/or password.",
"passwordResetPage": "Reset Password", "passwordResetPage": "Reset Password",

View File

@@ -70,5 +70,7 @@
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!", "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."
} }

View File

@@ -199,9 +199,10 @@
"usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.", "usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
"currentUsername": "Current username:", "currentUsername": "Current username:",
"displaynameIssueLength": "Display Names must be between 1 and 30 characters.", "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", "goToSettings": "Go to Settings",
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!", "usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
"usernameNotVerified": "Please confirm your username.", "usernameNotVerified": "Please confirm your username.",
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging." "changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging.",
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -90,12 +90,14 @@ api.verifyUsername = {
const issues = verifyUsername(chosenUsername, res); const issues = verifyUsername(chosenUsername, res);
const existingUser = await User.findOne({ if (issues.length < 1) {
'auth.local.lowerCaseUsername': chosenUsername.toLowerCase(), const existingUser = await User.findOne({
}, {auth: 1}).exec(); 'auth.local.lowerCaseUsername': chosenUsername.toLowerCase(),
}, {auth: 1}).exec();
if (existingUser) { if (existingUser) {
if (!user || existingUser._id !== user._id) issues.push(res.t('usernameTaken')); if (!user || existingUser._id !== user._id) issues.push(res.t('usernameTaken'));
}
} }
if (issues.length > 0) { if (issues.length > 0) {

View File

@@ -1,5 +1,6 @@
import { authWithHeaders } from '../../middlewares/auth'; import { authWithHeaders } from '../../middlewares/auth';
import * as userLib from '../../libs/user'; import * as userLib from '../../libs/user';
import { verifyDisplayName } from '../../libs/user/validation';
const api = {}; 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; module.exports = api;

View File

@@ -79,8 +79,8 @@ async function registerLocal (req, res, { isV3 = false }) {
notEmpty: true, notEmpty: true,
errorMessage: res.t('missingUsername'), errorMessage: res.t('missingUsername'),
// TODO use the constants in the error message above // TODO use the constants in the error message above
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')}, isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameIssueLength')},
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')}, matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameIssueInvalidCharacters')},
}, },
email: { email: {
notEmpty: true, notEmpty: true,

View File

@@ -26,6 +26,14 @@ function usernameContainsInvalidCharacters (username) {
return match !== null && match[0] !== null; return match !== null && match[0] !== null;
} }
export function verifyDisplayName (displayName, res) {
let issues = [];
if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displaynameIssueLength'));
if (nameContainsSlur(displayName)) issues.push(res.t('displaynameIssueSlur'));
return issues;
}
export function verifyUsername (username, res) { export function verifyUsername (username, res) {
let issues = []; let issues = [];
if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength')); if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength'));