diff --git a/VRCX.csproj.DotSettings b/VRCX.csproj.DotSettings new file mode 100644 index 00000000..6e7fff86 --- /dev/null +++ b/VRCX.csproj.DotSettings @@ -0,0 +1,2 @@ + + No \ No newline at end of file diff --git a/html/src/app.js b/html/src/app.js index 31812221..3bc68a2c 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -6540,7 +6540,17 @@ speechSynthesis.getVoices(); } }; + $app.data.twoFactorAuthDialogVisible = false; + + API.$on('LOGIN', function () { + $app.twoFactorAuthDialogVisible = false; + }); + $app.methods.promptTOTP = function () { + if (this.twoFactorAuthDialogVisible) { + return; + } + this.twoFactorAuthDialogVisible = true; this.$prompt($t('prompt.totp.description'), $t('prompt.totp.header'), { distinguishCancelAndClose: true, cancelButtonText: $t('prompt.totp.use_otp'), @@ -6564,11 +6574,19 @@ speechSynthesis.getVoices(); } else if (action === 'cancel') { this.promptOTP(); } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); } }); }; $app.methods.promptOTP = function () { + if (this.twoFactorAuthDialogVisible) { + return; + } + this.twoFactorAuthDialogVisible = true; this.$prompt($t('prompt.otp.description'), $t('prompt.otp.header'), { distinguishCancelAndClose: true, cancelButtonText: $t('prompt.otp.use_otp'), @@ -6592,17 +6610,25 @@ speechSynthesis.getVoices(); } else if (action === 'cancel') { this.promptTOTP(); } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); } }); }; $app.methods.promptEmailOTP = function () { + if (this.twoFactorAuthDialogVisible) { + return; + } + this.twoFactorAuthDialogVisible = true; this.$prompt( $t('prompt.email_otp.description'), $t('prompt.email_otp.header'), { distinguishCancelAndClose: true, - cancelButtonText: $t('prompt.email_otp.cancel'), + cancelButtonText: $t('prompt.email_otp.resend'), confirmButtonText: $t('prompt.email_otp.verify'), inputPlaceholder: $t('prompt.email_otp.input_placeholder'), inputPattern: /^[0-9]{6}$/, @@ -6620,12 +6646,42 @@ speechSynthesis.getVoices(); API.getCurrentUser(); return args; }); + } else if (action === 'cancel') { + this.resendEmail2fa(); } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); } } ); }; + $app.methods.resendEmail2fa = function () { + if (this.loginForm.lastUserLoggedIn) { + var user = + this.loginForm.savedCredentials[ + this.loginForm.lastUserLoggedIn + ]; + if (typeof user !== 'undefined') { + webApiService.clearCookies(); + this.relogin(user).then(() => { + new Noty({ + type: 'success', + text: 'Successfully relogged in.' + }).show(); + }); + return; + } + } + new Noty({ + type: 'error', + text: 'Cannot send 2FA email without saved credentials. Please login again.' + }).show(); + this.promptEmailOTP(); + }; + $app.methods.showExportFriendsListDialog = function () { var {friends} = API.currentUser; if (Array.isArray(friends) === false) { @@ -6734,6 +6790,8 @@ speechSynthesis.getVoices(); API.$on('LOGOUT', async function () { await $app.updateStoredUser(this.currentUser); webApiService.clearCookies(); + // eslint-disable-next-line require-atomic-updates + $app.loginForm.lastUserLoggedIn = ''; configRepository.remove('lastUserLoggedIn'); }); @@ -6901,7 +6959,9 @@ speechSynthesis.getVoices(); API.login({ username: loginParmas.username, password: pwd, - cipher: loginParmas.password + cipher: loginParmas.password, + endpoint: loginParmas.endpoint, + websocket: loginParmas.websocket }) .catch((err2) => { this.loginForm.loading = false; @@ -6934,9 +6994,10 @@ speechSynthesis.getVoices(); endpoint: loginParmas.endpoint, websocket: loginParmas.websocket }) - .catch(() => { + .catch((err2) => { this.loginForm.loading = false; API.logout(); + reject(err2); }) .then(() => { this.loginForm.loading = false; @@ -21984,7 +22045,7 @@ speechSynthesis.getVoices(); API.$on('LOGIN', async function () { $app.avatarHistory = new Set(); var historyArray = await database.getAvatarHistory(); - $app.avatarHistoryArray = historyArray.reverse(); + $app.avatarHistoryArray = historyArray; for (var i = 0; i < historyArray.length; i++) { $app.avatarHistory.add(historyArray[i].id); this.applyAvatar(historyArray[i]); diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index a71460b4..6395d358 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -1073,7 +1073,7 @@ "email_otp": { "header": "Two-factor Authentication", "description": "Enter a numeric code that was sent to your email", - "cancel": "Cancel", + "resend": "Resend Email", "verify": "Verify", "input_placeholder": "Code", "input_error": "Invalid Code" diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 04b71258..52422fb8 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -1583,7 +1583,7 @@ class Database { updated_at: dbRow[12], version: dbRow[13] }; - data.unshift(row); + data.push(row); }, `SELECT * FROM ${Database.userPrefix}_avatar_history INNER JOIN cache_avatar ON cache_avatar.id = ${Database.userPrefix}_avatar_history.avatar_id ORDER BY ${Database.userPrefix}_avatar_history.created_at DESC LIMIT 100`); return data; }