Feature: new "report a bug" modal (#13530)

* WIP: report a bug api/ui

* fix lint

* add USER_USERNAME

* extend sendTxn tests / checks + fix bug report email

* fix lint

* add more checks to sendTxn - fix bug-report variables

* fix lint / ci

* fix test: reset email config url

* fix test stub

* fix tests

* refactor the variables checks

* lint.

* move bug-report page as a modal

* send user_email to the email

* show true/false instead 1/0

* fix issues

* fix footer report bug email if not logged in

* fix styles/margins

* prefill user's email

* show facebook email if local email not existing

* bugReportSuccessModal.vue

* add BROWSER_UA to mail properties

* extract bugReportLogic to its own lib file for unit test

* test api validators

* fix lint
This commit is contained in:
negue
2021-12-15 02:16:50 +01:00
committed by GitHub
parent c37dac5568
commit a1cddcaf17
25 changed files with 833 additions and 64 deletions

View File

@@ -0,0 +1,57 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import { generateUser } from '../../../helpers/api-unit.helper';
import * as emailLib from '../../../../website/server/libs/email';
import { bugReportLogic } from '../../../../website/server/libs/bug-report';
describe('bug-report', () => {
beforeEach(() => {
sandbox.stub(emailLib, 'sendTxn').returns(Promise.resolve());
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('ADMIN_EMAIL').returns('true');
});
afterEach(() => {
sandbox.restore();
});
it('sends a mail using sendTxn', async () => {
const userId = '2b58daeb-bc50-4a83-b5d3-4ac52c7c0608';
const userMail = 'me@me.com';
const userMessage = 'The power is over 9000, please fix it';
const userAgent = 'The UserAgent with a bunch of weird browser engine levels';
const user = generateUser({
_id: userId,
});
const result = await bugReportLogic(
user, userMail, userMessage, userAgent,
);
expect(emailLib.sendTxn).to.be.called;
expect(result).to.deep.equal({
sendMailResult: undefined,
emailData: {
BROWSER_UA: userAgent,
REPORT_MSG: userMessage,
USER_CLASS: 'warrior',
USER_CONSECUTIVE_MONTHS: 0,
USER_COSTUME: 'false',
USER_CUSTOMER_ID: undefined,
USER_CUSTOM_DAY: 0,
USER_DAILIES_PAUSED: 'false',
USER_EMAIL: userMail,
USER_HOURGLASSES: 0,
USER_ID: userId,
USER_LEVEL: 1,
USER_OFFSET_MONTHS: 0,
USER_PAYMENT_PLATFORM: undefined,
USER_SUBSCRIPTION: undefined,
USER_TIMEZONE_OFFSET: 0,
USER_USERNAME: undefined,
},
});
});
});

View File

@@ -148,9 +148,18 @@ describe('emails', () => {
});
});
describe('sendTxnEmail', () => {
describe('sendTxn', () => {
let sendTxn = null;
beforeEach(() => {
sandbox.stub(got, 'post').returns(defer().promise);
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('IS_PROD').returns(true);
nconfGetStub.withArgs('BASE_URL').returns('BASE_URL');
const attachEmail = requireAgain(pathToEmailLib);
sendTxn = attachEmail.sendTxn;
});
afterEach(() => {
@@ -158,16 +167,14 @@ describe('emails', () => {
});
it('can send a txn email to one recipient', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachEmail = requireAgain(pathToEmailLib);
const sendTxnEmail = attachEmail.sendTxn;
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
email: 'my@email',
};
sendTxnEmail(mailingInfo, emailType);
sendTxn(mailingInfo, emailType);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
json: {
data: {
@@ -179,27 +186,77 @@ describe('emails', () => {
});
it('does not send email if address is missing', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachEmail = requireAgain(pathToEmailLib);
const sendTxnEmail = attachEmail.sendTxn;
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
// email: 'my@email',
};
sendTxnEmail(mailingInfo, emailType);
sendTxn(mailingInfo, emailType);
expect(got.post).not.to.be.called;
});
it('throws error when mail target is only a string', () => {
const emailType = 'an email type';
const mailingInfo = 'my email';
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when mail target has no _id or email', () => {
const emailType = 'an email type';
const mailingInfo = {
};
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when variables not an array', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
email: 'my@email',
};
const variables = {};
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws error when variables array not contain name/content', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
email: 'my@email',
};
const variables = [
{
},
];
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws no error when variables array contain name but no content', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
email: 'my@email',
};
const variables = [
{
name: 'MY_VAR',
},
];
expect(sendTxn(mailingInfo, emailType, variables)).to.not.throw;
});
it('uses getUserInfo in case of user data', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachEmail = requireAgain(pathToEmailLib);
const sendTxnEmail = attachEmail.sendTxn;
const emailType = 'an email type';
const mailingInfo = getUser();
sendTxnEmail(mailingInfo, emailType);
sendTxn(mailingInfo, emailType);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
json: {
data: {
@@ -211,17 +268,15 @@ describe('emails', () => {
});
it('sends email with some default variables', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachEmail = requireAgain(pathToEmailLib);
const sendTxnEmail = attachEmail.sendTxn;
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
email: 'my@email',
};
const variables = [1, 2, 3];
const variables = [];
sendTxnEmail(mailingInfo, emailType, variables);
sendTxn(mailingInfo, emailType, variables);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
json: {
data: {

View File

@@ -0,0 +1,49 @@
import {
generateUser,
} from '../../helpers/api-integration/v4';
describe('POST /bug-report', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when message is not added', async () => {
await expect(user.post('/bug-report', {
message: '',
}))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
// seems it is not possible to get the real error message
message: 'Invalid request parameters.',
});
});
it('returns an error when email is not added', async () => {
await expect(user.post('/bug-report', {
message: 'message',
email: '',
}))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
// seems it is not possible to get the real error message
message: 'Invalid request parameters.',
});
});
it('returns an error when email is not valid', async () => {
await expect(user.post('/bug-report', {
message: 'message',
email: 'notamail',
}))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
// seems it is not possible to get the real error message
message: 'Invalid request parameters.',
});
});
});

View File

@@ -40,6 +40,18 @@ store.state.user.data = {
preferences: {
},
auth: {
local: {
// email: 'example@example.com',
},
facebook: {
emails: [
{
value: 'test@test.de',
},
],
},
},
};
Vue.prototype.$store = store;

View File

@@ -33,6 +33,8 @@
<payments-success-modal />
<sub-cancel-modal-confirm v-if="isUserLoaded" />
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
@@ -177,6 +179,10 @@ import {
removeLocalSetting,
} from '@/libs/userlocalManager';
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
@@ -196,6 +202,8 @@ export default {
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
},
mixins: [notifications, spellsMixin],
data () {

View File

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<circle fill="#1CA372" cx="32" cy="32" r="32"/>
<path d="M27.277 46a3.34 3.34 0 0 1-2.367-.98L14 34.11l4.733-4.733 8.356 8.356L43.999 18l5.084 4.354L29.82 44.832a3.366 3.366 0 0 1-2.415 1.165c-.043.003-.086.003-.128.003" fill="#FFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -82,9 +82,17 @@
{{ $t('hall') }}
</router-link>
</li>
<li>
<li v-if="user">
<a
:href="bugReportMailto"
@click.prevent="openBugReportModal()"
target="_blank"
>
{{ $t('reportBug') }}
</a>
</li>
<li v-else>
<a
href="mailto:admin@habitica.com?subject=Habitica Web Bug Report"
target="_blank"
>
{{ $t('reportBug') }}

View File

@@ -0,0 +1,43 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import bugReportModal from '@/components/bugReportModal';
import bugReportSuccessModal from '@/components/bugReportSuccessModal';
const stories = storiesOf('Bug Report Modal', module);
stories.addDecorator(withKnobs);
stories
.add('bugReportModal', () => ({
components: { bugReportModal },
data () {
return {
};
},
template: `
<div>
<bug-report-modal></bug-report-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'bug-report-modal');
},
}))
.add('bugReportSuccessModal', () => ({
components: { bugReportSuccessModal },
data () {
return {
};
},
template: `
<div>
<bug-report-success-modal></bug-report-success-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'bug-report-success-modal');
},
}));

View File

@@ -0,0 +1,243 @@
<template>
<b-modal
:id="modalId"
size="md"
:hide-footer="true"
>
<div
slot="modal-header"
class="bug-report-modal-header"
>
<h2 v-once>
{{ $t('reportBug') }}
</h2>
<div v-once class="report-bug-header-describe">
{{ $t('reportBugHeaderDescribe') }}
</div>
<div class="dialog-close">
<close-icon @click="close()" :purple="true"/>
</div>
</div>
<div>
<form
class="form"
@submit.prevent.stop="sendBugReport()"
>
<div
class="form-group"
>
<label
v-once
for="emailInput"
>
{{ $t('email') }}
</label>
<div class="mb-2 description-label" v-once>
{{ $t('reportEmailText') }}
</div>
<input
id="emailInput"
v-model="email"
class="form-control"
type="email"
:required="true"
:placeholder="$t('reportEmailPlaceholder')"
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
>
<div class="error-label mt-2" v-if="emailInvalid">
{{ $t('reportEmailError') }}
</div>
</div>
<label v-once>
{{ $t('reportDescription') }}
</label>
<div class="mb-2 description-label" v-once>
{{ $t('reportDescriptionText') }}
</div>
<textarea
v-model="message"
class="form-control"
rows="5"
:required="true"
:placeholder="$t('reportDescriptionPlaceholder')"
:class="{'input-invalid': messageInvalid && this.message.length === 0}"
>
</textarea>
<button
class="btn btn-primary submit-button btn-block mx-auto mt-4"
type="submit"
:disabled="!message || !emailValid"
>
{{ $t('submitBugReport') }}
</button>
</form>
</div>
<div class="modal-footer">
<a
class="cancel-link mx-auto mb-4"
@click.prevent="close()"
>
{{ $t('cancel') }}
</a>
</div>
</b-modal>
</template>
<style lang="scss">
#bug-report-modal {
.modal-header {
padding: 0;
border: none;
}
.modal-dialog {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
overflow: auto;
}
.modal-body {
padding-top: 1rem;
padding-right: 1.5rem;
padding-left: 1.5rem;
padding-bottom: 0;
}
.modal-footer {
border-top: 0;
padding-bottom: 0;
}
}
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
h2 {
color: $white;
}
.bug-report-modal-header {
color: $white;
width: 100%;
padding: 2rem 3rem 1.5rem 1.5rem;
background-image: linear-gradient(288deg, #{$purple-200}, #{$purple-300});
}
.report-bug-header-describe {
font-size: 14px;
line-height: 1.71;
color: $purple-600;
}
label {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
.cancel-link {
color: $blue-10;
line-height: 1.71;
}
.submit-button {
width: auto;
}
.error-label {
font-size: 12px;
line-height: 1.33;
color: $maroon-10;
}
.description-label {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
}
</style>
<script>
import axios from 'axios';
import isEmail from 'validator/lib/isEmail';
import closeIcon from '@/components/shared/closeIcon';
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';
export default {
components: {
closeIcon,
},
data () {
return {
message: '',
email: '',
messageInvalid: false,
modalId: MODALS.BUG_REPORT,
};
},
methods: {
async sendBugReport () {
this.messageInvalid = false;
if (this.message.length === 0) {
this.messageInvalid = true;
return;
}
await axios.post('/api/v4/bug-report', {
message: this.message,
email: this.email,
});
this.message = '';
this.close();
this.$root.$emit('bv::show::modal', MODALS.BUG_REPORT_SUCCESS);
},
close () {
this.$root.$emit('bv::hide::modal', MODALS.BUG_REPORT);
},
},
computed: {
...mapState({ user: 'user.data' }),
emailValid () {
if (this.email.length <= 3) return false;
return isEmail(this.email);
},
emailInvalid () {
if (this.email.length <= 3) return false;
return !this.emailValid;
},
},
mounted () {
const { user } = this;
let email = user.auth?.local?.email;
if (!email && user.auth?.facebook?.emails) {
email = user.auth.facebook.emails?.[0]?.value;
}
if (!email && user.auth?.google?.emails) {
email = user.auth.google.emails?.[0]?.value;
}
if (!email && user.auth?.apple?.emails) {
email = user.auth.apple.emails?.[0]?.value;
}
this.email = email;
},
};
</script>

View File

@@ -0,0 +1,154 @@
<template>
<b-modal
:id="modalId"
size="md"
:hide-footer="true"
>
<div
slot="modal-header"
class="bug-report-modal-header"
>
<div class="green-bar"></div>
<div class="sub-header position-relative">
<div class="dialog-close">
<close-icon @click="close()" />
</div>
</div>
</div>
<div>
<span class="svg-icon check-icon"
v-html="icons.checkCircleIcon"
></span>
<div class="title" v-once>
{{ $t('reportSent') }}
</div>
<div class="text mt-3 mb-4" v-once>
{{ $t('reportSentDescription') }}
</div>
</div>
</b-modal>
</template>
<style lang="scss">
#bug-report-success-modal {
.modal-header {
padding: 0;
border: none;
}
.modal-content {
border: 0;
}
.modal-dialog {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
overflow: auto;
width: 330px;
}
.modal-body {
padding: 0 1.5rem;
}
.modal-footer {
border-top: 0;
padding-bottom: 0;
}
}
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
h2 {
color: $white;
}
.bug-report-modal-header {
width: 100%;
.green-bar {
height: 8px;
width: 100%;
background-color: $green-10;
}
}
.check-icon {
width: 64px;
height: 64px;
margin: 0 auto;
margin-top: -0.25rem;
}
.report-bug-header-describe {
font-size: 14px;
line-height: 1.71;
color: $purple-600;
}
label {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
.cancel-link {
color: $blue-10;
line-height: 1.71;
}
.submit-button {
width: auto;
}
.error-label {
font-size: 12px;
line-height: 1.33;
color: $maroon-10;
}
.description-label {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
}
.text {
min-height: 0;
}
</style>
<script>
import closeIcon from '@/components/shared/closeIcon';
import checkCircleIcon from '@/assets/svg/check_circle.svg';
import { MODALS } from '@/libs/consts';
export default {
components: {
closeIcon,
},
data () {
return {
icons: Object.freeze({
checkCircleIcon,
}),
modalId: MODALS.BUG_REPORT_SUCCESS,
};
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', MODALS.BUG_REPORT_SUCCESS);
},
},
computed: {},
mounted () {},
};
</script>

View File

@@ -311,6 +311,7 @@ import bronzeGuildBadgeIcon from '@/assets/svg/bronze-guild-badge-small.svg';
import QuestDetailModal from './questDetailModal';
import RightSidebar from '@/components/groups/rightSidebar';
import InvitationListModal from './invitationListModal';
import { PAGES } from '@/libs/consts';
export default {
components: {
@@ -554,7 +555,7 @@ export default {
this.$root.$emit('bv::show::modal', 'group-gems-modal');
},
messageLeader () {
window.open(`/private-messages?uuid=${this.group.leader.id}`);
window.open(`${PAGES.PRIVATE_MESSAGES}?uuid=${this.group.leader.id}`);
},
},
};

View File

@@ -389,6 +389,7 @@ import messageIcon from '@/assets/members/message.svg';
import starIcon from '@/assets/members/star.svg';
import dots from '@/assets/svg/dots.svg';
import SelectList from '@/components/ui/selectList';
import { PAGES } from '@/libs/consts';
export default {
components: {
@@ -558,7 +559,7 @@ export default {
});
this.$root.$emit('bv::hide::modal', 'members-modal');
this.$router.push('/private-messages');
this.$router.push(PAGES.PRIVATE_MESSAGES);
},
async searchMembers (searchTerm = '') {
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({

View File

@@ -361,7 +361,7 @@
</li>
<li>
<a
:href="bugReportMailto"
@click.prevent="openBugReportModal()"
target="_blank"
>
{{ $t('reportBug') }}

View File

@@ -311,7 +311,7 @@
</router-link>
<a
class="topbar-dropdown-item dropdown-item"
:href="bugReportMailto"
@click.prevent="openBugReportModal()"
target="_blank"
>
{{ $t('reportBug') }}

View File

@@ -17,6 +17,7 @@
<script>
import BaseNotification from './base';
import { PAGES } from '@/libs/consts';
export default {
components: {
@@ -25,7 +26,7 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push('/private-messages');
this.$router.push(PAGES.PRIVATE_MESSAGES);
},
},
};

View File

@@ -143,6 +143,7 @@ import userIcon from '@/assets/svg/user.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import { EVENTS } from '@/libs/events';
import { PAGES } from '@/libs/consts';
export default {
components: {
@@ -170,7 +171,7 @@ export default {
if (this.$router.history.current.name === 'privateMessages') {
this.$root.$emit(EVENTS.PM_REFRESH);
} else {
this.$router.push('/private-messages');
this.$router.push(PAGES.PRIVATE_MESSAGES);
}
},
showProfile (startingPage) {

View File

@@ -2,6 +2,11 @@
<button
title="close dialog"
@click="$emit('click', $event)"
:style="{
'--icon-color': iconColor,
'--icon-color-hover': iconColorHover,
}"
:class="{'purple': purple}"
>
<div
v-once
@@ -16,6 +21,7 @@ import svgClose from '@/assets/svg/close.svg';
export default {
name: 'CloseIcon',
props: ['iconColor', 'iconColorHover', 'purple'],
data () {
return {
icons: Object.freeze({
@@ -43,13 +49,18 @@ export default {
width: 1rem;
height: 1rem;
&.purple {
--icon-color: #{$purple-400};
--icon-color-hover: #{$purple-500};
}
::v-deep svg path {
stroke: $gray-200;
stroke: var(--icon-color, #{$gray-200});
}
&:hover {
::v-deep svg path {
stroke: $gray-100;
stroke: var(--icon-color-hover, #{$gray-100});
}
}
}

View File

@@ -9,13 +9,11 @@
<a href="mailto:admin@habitica.com">admin&commat;habitica&period;com</a>
<span v-if="user">
<br>
{{ $t('reportBug') }}
&colon;&nbsp;
<a
:href="bugReportMailto"
@click.prevent="openBugReportModal()"
target="_blank"
>
admin&commat;habitica&period;com
{{ $t('reportBug') }}
</a>
<br>
{{ $t('reportCommunityIssues') }}

View File

@@ -0,0 +1,8 @@
export const PAGES = {
PRIVATE_MESSAGES: '/private-messages',
};
export const MODALS = {
BUG_REPORT: 'bug-report-modal',
BUG_REPORT_SUCCESS: 'bug-report-success-modal',
};

View File

@@ -1,34 +1,9 @@
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';
export default {
computed: {
...mapState({ user: 'user.data' }),
bugReportMailto () {
let subscriptionInfo = 'Not Subscribed';
if (!this.user) {
return 'mailto:admin@habitica.com?subject=Habitica Web Bug Report';
}
if (this.user.purchased.plan.customerId) {
subscriptionInfo = `
Subscription: ${this.user.purchased.plan.planId}%0d%0a
Payment Platform: ${this.user.purchased.plan.paymentMethod}%0d%0a
Customer ID: ${this.user.purchased.plan.customerId}%0d%0a
Consecutive Months: ${this.user.purchased.plan.consecutive.count}%0d%0a
Offset Months: ${this.user.purchased.plan.consecutive.offset}%0d%0a
Mystic Hourglasses: ${this.user.purchased.plan.consecutive.trinkets}
`;
}
return `mailto:admin@habitica.com?subject=Habitica Web Bug Report&body=
Please describe the issue you encountered:%0d%0a%0d%0a
User ID: ${this.user._id}%0d%0a
Level: ${this.user.stats.lvl}%0d%0a
Class: ${this.user.stats.class}%0d%0a
Dailies Paused: ${this.user.preferences.sleep}%0d%0a
Uses Costume: ${this.user.preferences.costume}%0d%0a
Custom Day Start: ${this.user.preferences.dayStart}%0d%0a
Timezone Offset: ${this.user.preferences.timezoneOffset}%0d%0a
${subscriptionInfo}
`;
methods: {
openBugReportModal () {
this.$root.$emit('bv::show::modal', MODALS.BUG_REPORT);
},
},
};

View File

@@ -4,6 +4,7 @@ import getStore from '@/store';
import handleRedirect from './handleRedirect';
import ParentPage from '@/components/parentPage';
import { PAGES } from '@/libs/consts';
// NOTE: when adding a page make sure to implement setTitle
@@ -201,7 +202,7 @@ const router = new VueRouter({
},
],
},
{ path: '/private-messages', name: 'privateMessages', component: MessagesIndex },
{ path: PAGES.PRIVATE_MESSAGES, name: 'privateMessages', component: MessagesIndex },
{
name: 'challenges',
path: '/challenges',

View File

@@ -93,6 +93,16 @@
"audioTheme_pizildenTheme": "Pizilden's Theme",
"audioTheme_farvoidTheme": "Farvoid Theme",
"reportBug": "Report a Bug",
"reportBugHeaderDescribe": "Please describe the bug youre experiencing and our team will get back to you.",
"reportEmailText": "This will only be used to contact you regarding the bug report.",
"reportEmailPlaceholder": "Your email address",
"reportEmailError": "Please provide a valid email address",
"reportDescription": "Description",
"reportDescriptionText": "Include screenshots or Javascript console errors if helpful.",
"reportDescriptionPlaceholder": "Describe the bug in detail here",
"submitBugReport": "Submit Bug Report",
"reportSent": "Bug report sent!",
"reportSentDescription": "Well get back to you once our team has a chance to investigate. Thank you for reporting the issue.",
"overview": "Overview for New Users",
"dateFormat": "Date Format",
"achievementStressbeast": "Savior of Stoïkalm",
@@ -201,5 +211,6 @@
"howManyToBuy": "How many would you like to buy?",
"contactForm": "Contact the Moderation Team",
"loadEarlierMessages": "Load Earlier Messages",
"askQuestion": "Ask a Question"
"askQuestion": "Ask a Question",
"emptyReportBugMessage": "Report Bug Message missing"
}

View File

@@ -0,0 +1,53 @@
import { authWithHeaders } from '../../middlewares/auth';
import { bugReportLogic } from '../../libs/bug-report';
const api = {};
/**
* @api {post} /api/v4/bug-report Report an issue
* @apiName BugReport
* @apiGroup BugReport
* @apiDescription This POST method is used to send bug reports from the Website.
* Since it needs the Users Data, it requires authentication.
*
* @apiParam (Body) {String} message Bug Report Message to sent
* @apiParam (Body) {String} email User Email
*
* @apiSuccess {Object} data Result of this bug report
* @apiSuccess {Boolean} data.ok Status of this report
* @apiSuccess {String} data.message Status of this report
*
* @apiError (400) {BadRequest} emptyReportBugMessage The report message is missing.
* @apiUse UserNotFound
*/
api.bugReport = {
method: 'POST',
url: '/bug-report',
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkBody('message', res.t('emptyReportBugMessage')).notEmpty();
req.checkBody('email', res.t('missingEmail')).notEmpty();
req.checkBody('email', res.t('notAnEmail')).isEmail();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { message, email } = req.body;
const { user } = res.locals;
const BROWSER_UA = req.get('User-Agent');
const {
emailData, sendMailResult,
} = bugReportLogic(
user, email, message, BROWSER_UA,
);
res.status(200).send({
ok: true,
emailData,
sendMailResult,
});
},
};
export default api;

View File

@@ -0,0 +1,39 @@
import nconf from 'nconf';
import { convertVariableObjectToArray, sendTxn } from './email';
export async function bugReportLogic (
user, userEmail, message, BROWSER_UA,
) {
const emailData = {
USER_ID: user._id,
USER_EMAIL: userEmail,
USER_USERNAME: user.auth.local.username,
USER_LEVEL: user.stats.lvl,
USER_CLASS: user.stats.class,
USER_DAILIES_PAUSED: user.preferences.sleep === 1 ? 'true' : 'false',
USER_COSTUME: user.preferences.costume === 1 ? 'true' : 'false',
USER_CUSTOM_DAY: user.preferences.dayStart,
USER_TIMEZONE_OFFSET: user.preferences.timezoneOffset,
USER_SUBSCRIPTION: user.purchased.plan.planId,
USER_PAYMENT_PLATFORM: user.purchased.plan.paymentMethod,
USER_CUSTOMER_ID: user.purchased.plan.customerId,
USER_CONSECUTIVE_MONTHS: user.purchased.plan.consecutive.count,
USER_OFFSET_MONTHS: user.purchased.plan.consecutive.offset,
USER_HOURGLASSES: user.purchased.plan.consecutive.trinkets,
REPORT_MSG: message,
BROWSER_UA,
};
const adminMail = { email: nconf.get('ADMIN_EMAIL') };
const sendMailResult = await sendTxn(
adminMail,
'report-a-bug',
convertVariableObjectToArray(emailData),
);
return {
sendMailResult,
emailData,
};
}

View File

@@ -67,12 +67,32 @@ export function getGroupUrl (group) {
// Send a transactional email using Mandrill through the external email server
export async function sendTxn (mailingInfoArray, emailType, variables, personalVariables) {
mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; // eslint-disable-line no-param-reassign, max-len
if (!Array.isArray(mailingInfoArray)) {
mailingInfoArray = [mailingInfoArray]; // eslint-disable-line no-param-reassign
}
for (const entry of mailingInfoArray) {
if (typeof entry === 'string'
&& (typeof entry._id === 'undefined' && typeof entry.email === 'undefined')
) {
throw new Error('Argument Error mailingInfoArray: does not contain email or _id');
}
}
if (variables && !Array.isArray(variables)) {
throw new Error('Argument Error variables: is not an array');
}
variables = [ // eslint-disable-line no-param-reassign
{ name: 'BASE_URL', content: BASE_URL },
].concat(variables || []);
for (const variable of variables) {
if (typeof variable.name === 'undefined' && typeof variable.content === 'undefined') {
throw new Error('Argument Error variables: does not contain name or content');
}
}
// It's important to pass at least a user with its `preferences`
// as we need to check if he unsubscribed
mailingInfoArray = mailingInfoArray // eslint-disable-line no-param-reassign
@@ -157,3 +177,17 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
return null;
}
export function convertVariableObjectToArray (variableObject) {
const variablesArray = [];
const objectKeys = Object.keys(variableObject);
for (const propName of objectKeys) {
variablesArray.push({
name: propName,
content: variableObject[propName],
});
}
return variablesArray;
}