Files
habitica/website/client/components/settings/site.vue
Sabe Jones 8b610d771c fix(usernames): various
Reword invalid characters error
Correct typo in slur error
Remove extraneous Confirm button
Reset username field if empty on blur
Restore ability to add local auth to social login
2018-10-03 13:13:47 -05:00

481 lines
19 KiB
Vue

<template lang="pug">
.row.standard-page
restore-modal
reset-modal
delete-modal
h1.col-12 {{ $t('settings') }}
.col-sm-6
.form-horizontal
h5 {{ $t('language') }}
select.form-control(:value='user.preferences.language',
@change='changeLanguage($event)')
option(v-for='lang in availableLanguages', :value='lang.code') {{lang.name}}
small
| {{ $t('americanEnglishGovern') }}
br
strong(v-html="$t('helpWithTranslation')")
hr
.form-horizontal
h5 {{ $t('dateFormat') }}
select.form-control(v-model='user.preferences.dateFormat',
@change='set("dateFormat")')
option(v-for='dateFormat in availableFormats', :value='dateFormat') {{dateFormat}}
hr
.form-horizontal
h5 {{ $t('audioTheme') }}
select.form-control(v-model='user.preferences.sound',
@change='set("sound")')
option(v-for='sound in availableAudioThemes', :value='sound') {{ $t(`audioTheme_${sound}`) }}
hr
.form-horizontal(v-if='hasClass')
h5 {{ $t('characterBuild') }}
h6(v-once) {{ $t('class') + ': ' }}
// @TODO: what is classText
// span(v-if='classText') {{ classText }}&nbsp;
button.btn.btn-danger.btn-xs(@click='changeClassForUser(true)', v-once) {{ $t('changeClass') }}
small.cost &nbsp; 3 {{ $t('gems') }}
// @TODO add icon span.Pet_Currency_Gem1x.inline-gems
hr
div
.checkbox
label
input(type='checkbox', v-model='user.preferences.advancedCollapsed', @change='set("advancedCollapsed")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('startAdvCollapsedPop')") {{ $t('startAdvCollapsed') }}
.checkbox
label
input(type='checkbox', v-model='user.preferences.dailyDueDefaultView', @change='set("dailyDueDefaultView")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('dailyDueDefaultViewPop')") {{ $t('dailyDueDefaultView') }}
.checkbox(v-if='party.memberCount === 1')
label
input(type='checkbox', v-model='user.preferences.displayInviteToPartyWhenPartyIs1', @change='set("displayInviteToPartyWhenPartyIs1")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('displayInviteToPartyWhenPartyIs1')") {{ $t('displayInviteToPartyWhenPartyIs1') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.levelUp', @change='set("suppressModals", "levelUp")')
label {{ $t('suppressLevelUpModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.hatchPet', @change='set("suppressModals", "hatchPet")')
label {{ $t('suppressHatchPetModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.raisePet', @change='set("suppressModals", "raisePet")')
label {{ $t('suppressRaisePetModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.streak', @change='set("suppressModals", "streak")')
label {{ $t('suppressStreakModal') }}
//- .checkbox
//- label {{ $t('confirmScoreNotes') }}
//- input(type='checkbox', v-model='user.preferences.tasks.confirmScoreNotes', @change='set({"preferences.tasks.confirmScoreNotes": user.preferences.tasks.confirmScoreNotes ? true: false})')
//- .checkbox
//- label {{ $t('groupTasksByChallenge') }}
//- input(type='checkbox', v-model='user.preferences.tasks.groupByChallenge', @change='set({"preferences.tasks.groupByChallenge": user.preferences.tasks.groupByChallenge ? true: false})')
hr
button.btn.btn-primary.mr-2.mb-2(@click='showBailey()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('showBaileyPop')") {{ $t('showBailey') }}
button.btn.btn-primary.mr-2.mb-2(@click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('fixValPop')") {{ $t('fixVal') }}
button.btn.btn-primary.mb-2(v-if='user.preferences.disableClasses == true', @click='changeClassForUser(false)',
popover-trigger='mouseenter', popover-placement='right', :popover="$t('enableClassPop')") {{ $t('enableClass') }}
hr
div
h5 {{ $t('customDayStart') }}
.alert.alert-warning {{ $t('customDayStartInfo1') }}
.form-horizontal
.form-group
.col-7
select.form-control(v-model='newDayStart')
option(v-for='option in dayStartOptions' :value='option.value') {{option.name}}
.col-5
button.btn.btn-block.btn-primary.mt-1(@click='openDayStartModal()',
:disabled='newDayStart === user.preferences.dayStart')
| {{ $t('saveCustomDayStart') }}
hr
h5 {{ $t('timezone') }}
.form-horizontal
.form-group
.col-12
p(v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})")
p(v-html="$t('timezoneInfo')")
.col-sm-6
h2 {{ $t('registration') }}
.panel-body
div
ul.list-inline
li(v-for='network in SOCIAL_AUTH_NETWORKS')
button.btn.btn-primary.mb-2(v-if='!user.auth[network.key].id', @click='socialAuth(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
button.btn.btn-primary.mb-2(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
button.btn.btn-danger(@click='deleteSocialAuth(network)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
hr
div(v-if='!user.auth.local.email')
p {{ $t('addLocalAuth') }}
.form(name='localAuth', novalidate)
.form-group
input.form-control(type='text', :placeholder="$t('email')", v-model='localAuth.email', required)
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='localAuth.password', required)
.form-group
input.form-control(type='password', :placeholder="$t('confirmPass')", v-model='localAuth.confirmPassword', required)
button.btn.btn-primary(type='submit', @click='addLocalAuth()') {{ $t('submit') }}
.usersettings
h5 {{ $t('changeDisplayName') }}
.form(name='changeDisplayName', novalidate)
.form-group
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName')
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }}
h5 {{ $t('changeUsername') }}
.form(name='changeUsername', novalidate)
.iconalert.iconalert-success(v-if='verifiedUsername') {{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
.iconalert.iconalert-warning(v-else)
div.align-middle
span {{ $t('usernameNotVerified') }}
.form-group
input#changeUsername.form-control(@blur='restoreEmptyUsername()',type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username', :class='{"is-invalid input-invalid": usernameInvalid}')
.input-error(v-for="issue in usernameIssues") {{ issue }}
small.form-text.text-muted {{ $t('changeUsernameDisclaimer') }}
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)', :disabled='usernameCannotSubmit') {{ $t('saveAndConfirm') }}
h5(v-if='user.auth.local.email') {{ $t('changeEmail') }}
.form(v-if='user.auth.local.email', name='changeEmail', novalidate)
.form-group
input#changeEmail.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail')
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='emailUpdates.password')
button.btn.btn-primary(type='submit', @click='changeUser("email", emailUpdates)') {{ $t('submit') }}
h5(v-if='user.auth.local.email') {{ $t('changePass') }}
.form(v-if='user.auth.local.email', name='changePassword', novalidate)
.form-group
input#changePassword.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password')
.form-group
input.form-control(type='password', :placeholder="$t('newPass')", v-model='passwordUpdates.newPassword')
.form-group
input.form-control(type='password', :placeholder="$t('confirmPass')", v-model='passwordUpdates.confirmPassword')
button.btn.btn-primary(type='submit', @click='changeUser("password", passwordUpdates)') {{ $t('submit') }}
hr
div
h5 {{ $t('dangerZone') }}
div
button.btn.btn-danger.mr-2.mb-2(@click='openResetModal()',
popover-trigger='mouseenter', popover-placement='right', v-b-popover.hover.auto="$t('resetAccPop')") {{ $t('resetAccount') }}
button.btn.btn-danger.mb-2(@click='openDeleteModal()',
popover-trigger='mouseenter', v-b-popover.hover.auto="$t('deleteAccPop')") {{ $t('deleteAccount') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
input {
color: $gray-50;
}
.usersettings h5 {
margin-top: 1em;
}
.iconalert > div > span {
line-height: 25px;
}
.iconalert > div:after {
clear: both;
content: '';
display: table;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
margin-top: 5px;
}
</style>
<script>
import hello from 'hellojs';
import moment from 'moment';
import axios from 'axios';
import { mapState } from 'client/libs/store';
import debounce from 'lodash/debounce';
import restoreModal from './restoreModal';
import resetModal from './resetModal';
import deleteModal from './deleteModal';
import { SUPPORTED_SOCIAL_NETWORKS } from '../../../common/script/constants';
import changeClass from '../../../common/script/ops/changeClass';
import notificationsMixin from '../../mixins/notifications';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
export default {
mixins: [notificationsMixin],
components: {
restoreModal,
resetModal,
deleteModal,
},
data () {
let dayStartOptions = [];
for (let number = 0; number < 24; number += 1) {
let meridian = number < 12 ? 'AM' : 'PM';
let hour = number % 12;
let option = {
value: number,
name: `${hour ? hour : 12}:00 ${meridian}`,
};
dayStartOptions.push(option);
}
return {
SOCIAL_AUTH_NETWORKS: [],
party: {},
// Made available by the server as a script
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
dayStartOptions,
newDayStart: 0,
temporaryDisplayName: '',
usernameUpdates: {username: ''},
emailUpdates: {},
passwordUpdates: {},
localAuth: {
username: '',
email: '',
password: '',
confirmPassword: '',
},
usernameIssues: [],
};
},
mounted () {
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
// @TODO: We may need to request the party here
this.party = this.$store.state.party;
this.newDayStart = this.user.preferences.dayStart;
this.usernameUpdates.username = this.user.auth.local.username || null;
this.temporaryDisplayName = this.user.profile.name;
this.emailUpdates.newEmail = this.user.auth.local.email || null;
this.localAuth.username = this.user.auth.local.username || null;
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
}, {
redirect_uri: '', // eslint-disable-line
});
const focusID = this.$route.query.focus;
if (focusID !== undefined && focusID !== null) {
this.$nextTick(() => {
const element = document.getElementById(focusID);
if (element !== undefined && element !== null) {
element.focus();
}
});
}
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
availableAudioThemes () {
return ['off', ...this.content.audioThemes];
},
timezoneOffsetToUtc () {
let offset = this.user.preferences.timezoneOffset;
let sign = offset > 0 ? '-' : '+';
offset = Math.abs(offset) / 60;
let hour = Math.floor(offset);
let minutesInt = (offset - hour) * 60;
let minutes = minutesInt < 10 ? `0${minutesInt}` : minutesInt;
return `UTC${sign}${hour}:${minutes}`;
},
dayStart () {
return this.user.preferences.dayStart;
},
hasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
verifiedUsername () {
return this.user.flags.verifiedUsername;
},
usernameValid () {
if (this.usernameUpdates.username.length <= 1) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.usernameUpdates.username.length <= 1) return false;
return !this.usernameValid;
},
usernameCannotSubmit () {
if (this.usernameUpdates.username.length <= 1) return true;
return !this.usernameValid;
},
},
watch: {
usernameUpdates: {
handler () {
this.validateUsername(this.usernameUpdates.username);
},
deep: true,
},
},
methods: {
// eslint-disable-next-line func-names
validateUsername: debounce(function (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),
set (preferenceType, subtype) {
let settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
return this.$store.dispatch('user:set', settings);
},
hideHeader () {
this.set('hideHeader');
if (!this.user.preferences.hideHeader || !this.user.preferences.stickyHeader) return;
this.user.preferences.hideHeader = false;
this.set('stickyHeader');
},
toggleStickyHeader () {
this.set('stickyHeader');
},
showTour () {
// @TODO: Do we still use this?
// User.set({'flags.showTour':true});
// Guide.goto('intro', 0, true);
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
hasBackupAuthOption (networkKeyToCheck) {
if (this.user.auth.local.username) {
return true;
}
return find(this.SOCIAL_AUTH_NETWORKS, (network) => {
if (network.key !== networkKeyToCheck) {
if (this.user.auth.hasOwnProperty(network.key)) {
return this.user.auth[network.key].id;
}
}
});
},
calculateNextCron () {
let nextCron = moment().hours(this.newDayStart).minutes(0).seconds(0).milliseconds(0);
let currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
},
openDayStartModal () {
let nextCron = this.calculateNextCron();
// @TODO: Add generic modal
if (!confirm(this.$t('sureChangeCustomDayStartTime', {time: nextCron}))) return;
this.saveDayStart();
// $rootScope.openModal('change-day-start', { scope: $scope });
},
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v4/user/custom-day-start', {
dayStart: this.newDayStart,
});
// @TODO
// Notification.text(response.data.data.message);
},
async changeLanguage (e) {
const newLang = e.target.value;
this.user.preferences.language = newLang;
await this.set('language');
setTimeout(() => window.location.reload(true));
},
async changeUser (attribute, updates) {
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
if (attribute === 'username') {
this.user.auth.local.username = updates[attribute];
this.localAuth.username = this.user.auth.local.username;
this.user.flags.verifiedUsername = true;
} else if (attribute === 'email') {
this.user.auth.local.email = updates[attribute];
}
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', {'profile.name': newName});
alert(this.$t('displayNameSuccess'));
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
},
openRestoreModal () {
this.$root.$emit('bv::show::modal', 'restore');
},
openResetModal () {
this.$root.$emit('bv::show::modal', 'reset');
},
openDeleteModal () {
this.$root.$emit('bv::show::modal', 'delete');
},
async deleteSocialAuth (network) {
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
this.text(this.$t('detachedSocial', {network: network.name}));
},
async socialAuth (network) {
let auth = await hello(network).login({scope: 'email'});
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
},
async changeClassForUser (confirmationNeeded) {
if (confirmationNeeded && !confirm(this.$t('changeClassConfirmCost'))) return;
try {
changeClass(this.user);
await axios.post('/api/v4/user/change-class');
} catch (e) {
alert(e.message);
}
},
addLocalAuth () {
axios.post('/api/v4/user/auth/local/register', this.localAuth, 'addedLocalAuth');
},
restoreEmptyUsername () {
if (this.usernameUpdates.username.length < 1) {
this.usernameUpdates.username = this.user.auth.local.username;
}
},
},
};
</script>