@tapni/auth 0.0.109 → 0.0.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/.vite/manifest.json +32 -0
  2. package/dist/Account-5DndxBQe.js +111 -0
  3. package/dist/QR-rB8KdJig.js +41 -0
  4. package/dist/TapniAuth.es.js +2 -16733
  5. package/dist/TapniAuth.umd.js +39 -39
  6. package/dist/install-QOxCq9FJ.js +18408 -0
  7. package/dist/style.css +1 -1
  8. package/dist/web-AImUTDQQ.js +54 -0
  9. package/package.json +6 -3
  10. package/src/App.vue +55 -11
  11. package/src/components/Language.vue +1 -1
  12. package/src/components/ModalOverlay.vue +1 -0
  13. package/src/components/OTP.vue +120 -0
  14. package/src/install.js +0 -3
  15. package/src/main.js +3 -11
  16. package/src/mixins/apple.mixin.js +2 -2
  17. package/src/mixins/auth.mixin.js +52 -509
  18. package/src/mixins/facebook.mixin.js +2 -2
  19. package/src/mixins/global.mixin.js +5 -4
  20. package/src/mixins/google.mixin.js +2 -2
  21. package/src/mixins/mfa-auth.mixin.js +76 -0
  22. package/src/mixins/microsoft.mixin.js +3 -7
  23. package/src/mixins/okta.mixin.js +1 -1
  24. package/src/mixins/qr-auth.mixin.js +6 -1
  25. package/src/mixins/saml.mixin.js +2 -1
  26. package/src/routes.js +13 -0
  27. package/src/services/Api.js +15 -28
  28. package/src/services/AuthService.js +42 -37
  29. package/src/services/CompanyService.js +7 -8
  30. package/src/services/DeviceService.js +5 -6
  31. package/src/services/UserService.js +22 -23
  32. package/src/services/UtilService.js +1 -1
  33. package/src/store/locales/en.js +8 -1
  34. package/src/store/store.js +563 -0
  35. package/src/views/Account.vue +236 -0
  36. package/src/views/Login.vue +29 -15
  37. package/src/views/MFA.vue +109 -0
  38. package/src/views/Register.vue +10 -10
  39. package/src/views/Reset.vue +3 -3
  40. package/src/views/Verify.vue +4 -4
  41. package/src/views/Welcome.vue +3 -2
  42. package/src/styles/framework.css +0 -4012
  43. package/src/styles/inter.ttf +0 -0
  44. package/src/styles/style.css +0 -618
@@ -0,0 +1,236 @@
1
+ <template>
2
+ <div class="page-login content-boxed content-boxed-padding center-text" style="margin-top: -1px; overflow: hidden; border: solid 0px #ffffff;">
3
+ <br>
4
+ <h1 class="bold full-top no-bottom center-text">{{ssoLang[appLanguage].welcome}}, {{account.name}}</h1>
5
+ <p class="full-bottom half-top center-text color-black font-16">{{account.email}}</p>
6
+
7
+ <div v-if="display !== 'npm' && false" class="full-top full-bottom">
8
+ <a @click="continueTo('app')" class="button-center button button-90 google-button bg-tapni-grey pointer">
9
+ <span> {{ssoLang[appLanguage].continue}} </span>
10
+ </a>
11
+
12
+ <div class="page-login-links center-text">
13
+ <a @click="continueTo('admin')">Looking for Admin Dashboard? <b>{{ssoLang[appLanguage].click_here}}</b></a>
14
+ <div class="clear"></div>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="container">
19
+ <div v-if="false" class="accountCard pointer" @click="expanded = !expanded">
20
+ <div class="activeAccount">
21
+ <div class="left" v-if="$storex.username">
22
+ <img
23
+ :src="loggedInAccounts[$storex.username]?.photo"
24
+ :alt="loggedInAccounts[$storex.username]?.name"
25
+ style="width: 50px; border-radius: 20px"
26
+ />
27
+ </div>
28
+ <div class="middle" v-if="$storex.username">
29
+ <h4>{{ loggedInAccounts[$storex.username]?.name }}</h4>
30
+ <div>@{{ $storex.username }}</div>
31
+ </div>
32
+ <div class="right" v-show="addAccountReady">
33
+ <img
34
+ src="https://cdn.tapni.co/icons/newArrowRight.svg"
35
+ alt="Right arrow"
36
+ :style="{ transform: expanded ? 'rotate(90deg)' : '' }"
37
+ />
38
+ </div>
39
+ </div>
40
+ <div class="otherAccounts" v-if="expanded && loggedInAccounts">
41
+ <template v-for="(username, index) in Object.keys(loggedInAccounts)">
42
+ <div
43
+ class="activeAccount"
44
+ :key="index"
45
+ v-if="username !== $storex.username"
46
+ @click="switchAccount(username)"
47
+ >
48
+ <div class="left">
49
+ <img
50
+ :src="loggedInAccounts[username].photo"
51
+ :alt="loggedInAccounts[username].name"
52
+ style="width: 50px; border-radius: 20px"
53
+ />
54
+ </div>
55
+ <div class="middle">
56
+ <h4>{{ loggedInAccounts[username].name }}</h4>
57
+ <div>@ {{ username }}</div>
58
+ </div>
59
+ </div>
60
+ </template>
61
+ <div class="activeAccount newAccount" @click="addAccount">
62
+ <div class="left">
63
+ <img
64
+ src="https://cdn.tapni.co/icons/Add.png"
65
+ style="width: 50px; border-radius: 20px"
66
+ />
67
+ </div>
68
+ <div class="middle">
69
+ <h4>{{ssoLang[appLanguage].add_account }}</h4>
70
+ </div>
71
+ <div class="right">
72
+ <img
73
+ src="https://cdn.tapni.co/icons/Plus.png"
74
+ width="30px"
75
+ />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="settingsCard pointer left-text">
82
+ <div class="device" @click="toggleAccountSettingsModal">
83
+ <div class="left">
84
+ <img
85
+ src="https://cdn.tapni.co/icons/newSettings.svg"
86
+ style="width: 50px; border-radius: 20px"
87
+ />
88
+ </div>
89
+ <div class="middle">
90
+ <h4>{{ssoLang[appLanguage].mfa }}</h4>
91
+ </div>
92
+ <div class="right">
93
+ <img
94
+ src="https://cdn.tapni.co/icons/newArrowRight.svg"
95
+ />
96
+ </div>
97
+ </div>
98
+
99
+ <div class="divider"></div>
100
+
101
+ <div class="device" @click="toggleLanguageModal">
102
+ <div class="left">
103
+ <img
104
+ src="https://cdn.tapni.co/icons/newPublicProfile.svg"
105
+ style="width: 50px; border-radius: 20px"
106
+ />
107
+ </div>
108
+ <div class="middle">
109
+ <h4>{{ssoLang[appLanguage].change_language }}</h4>
110
+ </div>
111
+ <div class="right">
112
+ <img
113
+ src="https://cdn.tapni.co/icons/newArrowRight.svg"
114
+ />
115
+ </div>
116
+ </div>
117
+
118
+ <div class="divider"></div>
119
+
120
+ <div class="device" @click="logout">
121
+ <div class="left">
122
+ <img
123
+ src="https://cdn.tapni.co/icons/newLogout.svg"
124
+ style="width: 50px; border-radius: 20px"
125
+ />
126
+ </div>
127
+ <div class="middle">
128
+ <h4>{{ssoLang[appLanguage].logout }}</h4>
129
+ </div>
130
+ <div class="right">
131
+ <img
132
+ src="https://cdn.tapni.co/icons/newArrowRight.svg"
133
+ />
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Additional items can be added here in the future -->
138
+ <div v-if="false" class="moreFooter">
139
+ <p class="app-version" style="margin-top: 10px">
140
+ {{ssoLang[appLanguage].version }}: {{ appVersion }}
141
+ </p>
142
+ <h4 v-if="false">{{ssoLang[appLanguage].terms_and_privacy }}</h4>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script>
150
+ import AuthMixin from "../mixins/auth.mixin";
151
+ import { EventBus } from "../store/event-bus.js";
152
+ import AuthService from "../services/AuthService.js";
153
+ export default {
154
+ mixins: [AuthMixin],
155
+ data () {
156
+ return {
157
+ expanded: false,
158
+ addAccountReady: false,
159
+ appVersion: import.meta.env.APP_VERSION,
160
+ }
161
+ },
162
+ async mounted () {
163
+ this.getAccountSettings();
164
+ },
165
+ methods: {
166
+ continueTo (realm) {
167
+ console.log('continue to ' + realm);
168
+ },
169
+ addAccount () {
170
+ this.$router.push('/login');
171
+ },
172
+ switchAccount (username) {
173
+ console.log('switch', username);
174
+ },
175
+ toggleAccountSettingsModal () {
176
+ this.$router.push('/mfa');
177
+ // EventBus.$emit('toggleAccountSettingsModal')
178
+ },
179
+ toggleLanguageModal () {
180
+ EventBus.$emit('toggleSSOLanguageModal')
181
+ },
182
+ logout () {
183
+ this.logout()
184
+ },
185
+ }
186
+ };
187
+ </script>
188
+
189
+ <style scoped>
190
+ .container {
191
+ padding: 20px !important;
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 20px;
195
+ padding-bottom: 100px !important;
196
+ }
197
+ .settingsCard {
198
+ background-color: white;
199
+ padding: 20px;
200
+ border-radius: 26px;
201
+ align-items: center;
202
+ }
203
+
204
+ .device {
205
+ display: flex;
206
+ align-items: center;
207
+ }
208
+
209
+ h4 {
210
+ margin-left: 20px;
211
+ font-weight: bold;
212
+ }
213
+
214
+ .middle {
215
+ flex: 1;
216
+ }
217
+
218
+ .left img {
219
+ background-color: #f7f8f9;
220
+ border-radius: 20px;
221
+ padding: 10px;
222
+ }
223
+
224
+ .divider {
225
+ height: 1px;
226
+ background-color: #f0f0f0;
227
+ margin: 20px 0;
228
+ }
229
+
230
+ .moreFooter {
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: space-between;
234
+ padding: 0 10px;
235
+ }
236
+ </style>
@@ -226,15 +226,33 @@
226
226
  <span>{{ ssoLang[appLanguage].sign_in_with }} Email</span>
227
227
  </a>
228
228
 
229
+ <!-- Google Login Button -->
230
+ <a
231
+ @click="$router.push('/qr')"
232
+ v-if="displayQRLogin"
233
+ class="button-center button button-90 google-button pointer"
234
+ >
235
+ <img
236
+ src="https://cdn.tapni.co/icons/qr.png"
237
+ style="
238
+ position: absolute;
239
+ margin-left: -5px;
240
+ padding: 15px 0;
241
+ height: 100%;
242
+ "
243
+ />
244
+ <span>{{ ssoLang[appLanguage].sign_in_with }} QR</span>
245
+ </a>
246
+
229
247
  <div class="decoration decoration-lines-thin no-bottom"></div>
230
248
 
231
249
  <p class="close-text center-text half-top color-black">
232
250
  {{ ssoLang[appLanguage].terms_by_signing_in }} <br />
233
- <a class="" href="https://tapni.co/policies/terms-of-service">{{
251
+ <a class="" href="https://tapni.com/policies/terms-of-service">{{
234
252
  ssoLang[this.appLanguage].terms_of_service
235
253
  }}</a>
236
254
  {{ ssoLang[appLanguage].and }}
237
- <a href="https://tapni.co/policies/privacy-policy">{{
255
+ <a href="https://tapni.com/policies/privacy-policy">{{
238
256
  ssoLang[this.appLanguage].privacy_policy
239
257
  }}</a>
240
258
  </p>
@@ -272,13 +290,13 @@ import MicrosoftMixin from "../mixins/microsoft.mixin";
272
290
  import OktaMixin from "../mixins/okta.mixin";
273
291
  import SamlMixin from "../mixins/saml.mixin";
274
292
  import AuthMixin from "../mixins/auth.mixin";
293
+ import MFAMixin from "../mixins/mfa-auth.mixin";
275
294
  import QRAuthMixin from "../mixins/qr-auth.mixin";
276
295
  import { EventBus } from "../store/event-bus";
277
296
  import to from "await-to-js";
278
- import AuthService from "@/services/AuthService";
279
297
  export default {
280
298
  name: "AuthLogin",
281
- mixins: [GoogleMixin, FacebookMixin, AppleMixin, MicrosoftMixin, OktaMixin, SamlMixin, AuthMixin, QRAuthMixin],
299
+ mixins: [GoogleMixin, FacebookMixin, AppleMixin, MicrosoftMixin, OktaMixin, SamlMixin, AuthMixin, QRAuthMixin, MFAMixin],
282
300
  props: {
283
301
  isModal: {
284
302
  type: Boolean,
@@ -299,25 +317,24 @@ export default {
299
317
  },
300
318
  computed: {
301
319
  displayFormLogin() {
302
- return (
303
- (this.ssoCompany?.login?.form_login && !this.isModal) ?? this.emailLogin
304
- );
320
+ return this.emailLogin;
305
321
  },
306
322
  displayResetPasswordLogin() {
307
- return (this.ssoCompany?.login?.reset_password && !this.isModal) ?? true;
323
+ return true;
308
324
  },
309
325
  displayRegisterLogin() {
310
- return (this.ssoCompany?.login?.create_account && !this.isModal) ?? true;
326
+ return true;
311
327
  },
312
328
  },
313
329
  async mounted() {
314
330
  if (!this.isModal) {
315
331
  if(this.$route.path === '/login/callback/okta') return await this.handleOktaRedirect();
316
332
  if(this.$route.path === '/login/callback/saml') return await this.handleSamlRedirect();
333
+ if (this.$route.query.qrLogin) this.changeLoginToQr();
317
334
 
318
335
 
319
336
  setTimeout(() => {
320
- if (this.$storage && this.$storage.token) {
337
+ if (this.$storex && this.$storex.token) {
321
338
  if (import.meta.env.VITE_APP_MODE === 'npm') this.$router.push(this.home);
322
339
  else if (import.meta.env.VITE_APP_MODE === 'redirect') location.href = this.home
323
340
  } else {
@@ -336,15 +353,12 @@ export default {
336
353
 
337
354
  let response_type = this.$route.query.response_type;
338
355
 
339
- if (import.meta.env.VITE_APP_MODE === 'redirect') {
340
- response_type = 'code'
341
- }
342
-
343
356
  let loginData = {
344
357
  response_type: response_type || 'token',
345
358
  isModal: this.isModal,
346
359
  captcha: this.captcha,
347
360
  password: this.password,
361
+ otpToken: this.otpToken,
348
362
  // captchaToken: await this.getCaptchaToken('account_verify')
349
363
  };
350
364
  if (this.emailOrUsername.indexOf("@") !== -1) {
@@ -373,7 +387,7 @@ export default {
373
387
  watch: {
374
388
  emailOrUsername(nv) {
375
389
  if (nv.includes("@")) {
376
- this.$storage.verifyEmail = nv;
390
+ this.$storex.verifyEmail = nv;
377
391
  }
378
392
  },
379
393
  '$route.path': async function(routePath) {
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div class="page-login content-boxed content-boxed-padding">
3
+ <h4 style="text-align: center; width: 80%; margin: 0 auto; margin-top: 50px;">
4
+ 2-Step Verification
5
+ </h4>
6
+ <br>
7
+
8
+ <div class="qrCodeLoginContainer center-text">
9
+
10
+ <div>
11
+ Enable Multi-Factor Authentication
12
+ <div class="ios-switch" style="display: inline-block;float: right">
13
+ <input
14
+ v-model="mfaEnable"
15
+ type="checkbox"
16
+ name="ios-switch"
17
+ class="ios-switch-checkbox"
18
+ :id="'mfa-open-switch'"
19
+ />
20
+ <label class="ios-switch-label" :for="'mfa-open-switch'"></label>
21
+ </div>
22
+ </div>
23
+ <br>
24
+ <div v-show="mfaEnable && !account.mfaEnabled" style="text-align: center;">
25
+ <div id="qrCodeContainer"
26
+ class="qrCodeRounded"
27
+ style="max-width: 100%;"/>
28
+ <br>
29
+ <p class="bold pointer font-16 underline" @click="copy(otpSecret)"> {{ otpSecret }}</p>
30
+ <br>
31
+ <br>
32
+ <div style="margin: 0 auto; width: 85%; text-align: center;">
33
+ <h5 class="">Scan the QR code above or enter the secret key into your authenticator app.</h5>
34
+ <br>
35
+ </div>
36
+ </div>
37
+ <form @submit.prevent="validateMFA" v-if="account.mfaEnabled !== mfaEnable">
38
+ <button
39
+ type="submit"
40
+ class="button black-button google-button bg-tapni-grey button-90 button-center uppercase bold full-top pointer"
41
+ >
42
+ <span v-if="!loading">{{ ssoLang[appLanguage].continue }}</span>
43
+ <span v-else class="button--loading button__loader"></span>
44
+ </button>
45
+ <a href="javascript:;" @click="cancelMFA">Cancel</a>
46
+ </form>
47
+ <div v-else style="margin: 0 auto; width: 100%; text-align: center;">
48
+ <div>
49
+ <h5 v-if="mfaEnable" class="">
50
+ Your account is protected via <br> multi-factor authentication (MFA) ✅
51
+ </h5>
52
+ <h5 v-else class="">Protect your account from unauthorized access by enabling multi-factor authentication (MFA)</h5>
53
+
54
+ <a class="half-top" href="javascript:;" @click="cancelMFA">Go Back to Account Settings</a>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+
62
+ <script>
63
+ import MFAAuthMixin from "../mixins/mfa-auth.mixin";
64
+ import AuthMixin from "../mixins/auth.mixin";
65
+ import AuthService from '../services/AuthService'
66
+ import to from "await-to-js";
67
+ import {EventBus} from "@/store/event-bus.js";
68
+
69
+ export default {
70
+ name: "AuthMFA",
71
+ mixins: [AuthMixin, MFAAuthMixin],
72
+ props: {
73
+ payload: {
74
+ type: Object,
75
+ default: {}
76
+ },
77
+ },
78
+ mounted() {
79
+ if (!this.isLoggedIn) this.$router.push('/login');
80
+ },
81
+ methods: {
82
+ async submit() {
83
+ let data = {
84
+ otpToken: this.otpToken,
85
+ otpSecret: this.otpSecret,
86
+ enable: this.mfaEnable,
87
+ };
88
+
89
+ if (!this.mfaEnable) {
90
+ delete data.otpSecret;
91
+ }
92
+
93
+ this.loading = true;
94
+ const [err, response] = await to(AuthService.setMfa(data, this.$storex))
95
+ this.loading = false;
96
+ if (err) {
97
+ EventBus.$emit('ssoEvent', {name: 'setLoading', data: false})
98
+ return this.errorHandler(err)
99
+ }
100
+ if (response.data.success) {
101
+ this.getAccountSettings();
102
+ this.successSnack(response.data.message);
103
+ }
104
+ },
105
+ }
106
+ };
107
+ </script>
108
+
109
+ <style scoped></style>
@@ -114,7 +114,7 @@ export default {
114
114
  props: {
115
115
  isModal: {
116
116
  type: Boolean,
117
- required: false,
117
+ required: false,
118
118
  default: false
119
119
  },
120
120
  },
@@ -128,7 +128,7 @@ export default {
128
128
  email: '',
129
129
  password: '',
130
130
  revealPassword: false,
131
- emailLogin: true,
131
+ emailLogin: true,
132
132
  invitationCode: '',
133
133
  referral: ''
134
134
  }
@@ -146,15 +146,15 @@ export default {
146
146
  // Username Registration for Link-In-Bio
147
147
  if (this.$route.query.username) {
148
148
  this.usernameRegister = true
149
- this.emailLogin = true
149
+ this.emailLogin = true
150
150
  this.username = this.$route.query.username
151
151
  }
152
152
 
153
153
  // Track Referrals from Query or Local Storage
154
154
  if (this.$route.query.ref) {
155
- this.referral = this.$route.query.ref
156
- } else if (this.$storage.referral) {
157
- this.referral = this.$storage.referral
155
+ this.referral = this.$route.query.ref
156
+ } else if (this.$storex.referral) {
157
+ this.referral = this.$storex.referral
158
158
  }
159
159
  }
160
160
  },
@@ -172,13 +172,13 @@ export default {
172
172
  this.username = this.username.trim()
173
173
 
174
174
  let data = {
175
- response_type: this.response_type,
175
+ response_type: this.response_type,
176
176
  email: this.email,
177
177
  username: this.username,
178
178
  password: this.password,
179
179
  ic: this.invitationCode,
180
180
  captchaToken: await this.getCaptchaToken('account_verify'),
181
- ref: this.referral
181
+ ref: this.referral
182
182
  };
183
183
 
184
184
  let response = await this.register(data);
@@ -207,8 +207,8 @@ export default {
207
207
  }
208
208
  },
209
209
  watch: {
210
- email(nv) { this.$storage.verifyEmail = nv },
211
- referral(nv) { this.$storage.referral = nv }
210
+ email(nv) { this.$storex.verifyEmail = nv },
211
+ referral(nv) { this.$storex.referral = nv }
212
212
  }
213
213
  }
214
214
  </script>
@@ -82,8 +82,8 @@ export default {
82
82
  }
83
83
  },
84
84
  mounted () {
85
- if (this.$storage.verifyEmail) {
86
- this.email = this.$storage.verifyEmail
85
+ if (this.$storex.verifyEmail) {
86
+ this.email = this.$storex.verifyEmail
87
87
  }
88
88
  if (this.$route.query.c) {
89
89
  this.code = this.$route.query.c;
@@ -141,7 +141,7 @@ export default {
141
141
  },
142
142
  },
143
143
  watch: {
144
- email(nv) { this.$storage.verifyEmail = nv }
144
+ email(nv) { this.$storex.verifyEmail = nv }
145
145
  }
146
146
  }
147
147
  </script>
@@ -69,7 +69,7 @@ export default {
69
69
  }
70
70
  },
71
71
  mounted () {
72
- if (this.$storage.verifyEmail) this.email = this.$storage.verifyEmail
72
+ if (this.$storex.verifyEmail) this.email = this.$storex.verifyEmail
73
73
  if (this.$route) this.init()
74
74
  },
75
75
  methods: {
@@ -77,8 +77,8 @@ export default {
77
77
  if (this.isLoggedIn) {
78
78
  this.$router.push(this.home)
79
79
  } else {
80
- if (this.$storage.verifyEmail) {
81
- this.email = this.$storage.verifyEmail
80
+ if (this.$storex.verifyEmail) {
81
+ this.email = this.$storex.verifyEmail
82
82
  }
83
83
  if (this.$route.hash === '#reset') {
84
84
  this.action = 'reset'
@@ -125,7 +125,7 @@ export default {
125
125
  }
126
126
  await this.loginSetup(response)
127
127
  await this.getLoggedInAccounts()
128
- this.$storage.verifyEmail = '';
128
+ this.$storex.verifyEmail = '';
129
129
  this.$router.push('/' + response.data.data.username + '#edit')
130
130
  this.successSnack(this.ssoLang[this.appLanguage].verify_account_success2)
131
131
  }
@@ -42,6 +42,7 @@ export default {
42
42
  }
43
43
  },
44
44
  async mounted () {
45
+ console.log('yoo');
45
46
  // Autoplay video - iOS 12 controls fix
46
47
  let videoElement = document.getElementById('tapniVideo');
47
48
  // Ensure the video is ready to play
@@ -57,13 +58,13 @@ export default {
57
58
  });
58
59
 
59
60
  setTimeout(() => {
60
- if (this.$storage && this.$storage.token) {
61
+ if (this.$storex && this.$storex.token) {
61
62
  if (import.meta.env.VITE_APP_MODE === 'npm') this.$router.push(this.home);
62
63
  else if (import.meta.env.VITE_APP_MODE === 'redirect') location.href = this.home
63
64
  } else {
64
65
  EventBus.$emit('ssoEvent', {name: 'setLoading', data: false})
65
66
  }
66
- }, 2500)
67
+ }, 500)
67
68
  },
68
69
  methods: {
69
70
  }