mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'sabrecat/force-username-modal' into sabrecat/usernames-master
This commit is contained in:
57
test/api/v4/user/auth/POST-user_verify_display_name.test.js
Normal file
57
test/api/v4/user/auth/POST-user_verify_display_name.test.js
Normal 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')] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
34
website/client/assets/svg/hello-habitican.svg
Normal file
34
website/client/assets/svg/hello-habitican.svg
Normal 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 |
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
270
website/client/components/settings/verifyUsername.vue
Normal file
270
website/client/components/settings/verifyUsername.vue
Normal 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>
|
||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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 didn’t 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 didn’t 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",
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
website/raw_sprites/spritesmith_large/scene_veteran_pets.png
Normal file
BIN
website/raw_sprites/spritesmith_large/scene_veteran_pets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
Reference in New Issue
Block a user