Fixup password reset view
This commit is contained in:
@@ -66,8 +66,8 @@ fetcher.globalOptions.errorHook = (error) => {
|
||||
return offlineOverlay.value.open();
|
||||
}
|
||||
|
||||
// // re-login will make the code get a new token
|
||||
// if (status === 401) return client.login();
|
||||
// re-login will make the code get a new token
|
||||
if (error.status === 401) return profileModel.logout();
|
||||
|
||||
if (error.status === 500 || error.status === 501) {
|
||||
// actual internal server error, most likely a bug or timeout log to console only to not alert the user
|
||||
@@ -140,7 +140,7 @@ function onHashChange() {
|
||||
view.value = VIEWS.EVENTLOG;
|
||||
} else if (v === VIEWS.NETWORK && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.NETWORK;
|
||||
} else if (v === VIEWS.PROFILE) {
|
||||
} else if (v.indexOf(VIEWS.PROFILE) === 0) {
|
||||
view.value = VIEWS.PROFILE;
|
||||
} else if (v === VIEWS.SERVICES && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.SERVICES;
|
||||
@@ -175,9 +175,6 @@ onMounted(async () => {
|
||||
if (error) return console.error(error);
|
||||
profile.value = result;
|
||||
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
onHashChange();
|
||||
|
||||
[error, result] = await dashboardModel.config();
|
||||
if (error) return console.error(error);
|
||||
config.value = result;
|
||||
@@ -191,6 +188,11 @@ onMounted(async () => {
|
||||
window.document.body.classList.add('has-background');
|
||||
}
|
||||
|
||||
if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled) window.location.hash = `/${VIEWS.PROFILE}?setup2fa`;
|
||||
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
onHashChange();
|
||||
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -92,10 +92,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-root" v-show="ready">
|
||||
<div class="layout-left">
|
||||
<img width="128" height="128" class="icon" :src="iconUrl"/>
|
||||
<div class="layout-root" v-if="ready">
|
||||
<div class="layout-left" :style="{ 'background-image': `url('${API_ORIGIN}/api/v1/cloudron/background')` }">
|
||||
<img width="128" height="128" class="icon" :src="`${API_ORIGIN}/api/v1/cloudron/avatar`"/>
|
||||
</div>
|
||||
|
||||
<div class="layout-right">
|
||||
<small>{{ $t('login.loginTo') }}</small>
|
||||
<h1>{{ name }}</h1>
|
||||
@@ -154,7 +155,6 @@ export default {
|
||||
|
||||
.layout-left {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
background-image: url('/api/v1/cloudron/background');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
flex-basis: 30%;
|
||||
|
||||
@@ -1,113 +1,123 @@
|
||||
<script>
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { Button, PasswordInput, TextInput, fetcher } from 'pankow';
|
||||
import { API_ORIGIN } from '../constants.js';
|
||||
|
||||
export default {
|
||||
name: 'PasswordReset',
|
||||
components: {
|
||||
Button,
|
||||
PasswordInput,
|
||||
TextInput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
busy: false,
|
||||
error: '',
|
||||
footer: '',
|
||||
mode: '',
|
||||
resetToken: '',
|
||||
passwordResetIdentifier: '',
|
||||
newPassword: '',
|
||||
newPasswordRepeat: '',
|
||||
totpToken: ''
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
const ready = ref(false);
|
||||
const busy = ref(false);
|
||||
const error = ref({});
|
||||
const mode = ref('');
|
||||
const footer = ref('');
|
||||
const cloudronName = ref('');
|
||||
const resetToken = ref('');
|
||||
const passwordResetIdentifier = ref('');
|
||||
const newPassword = ref('');
|
||||
const newPasswordRepeat = ref('');
|
||||
const totpToken = ref('');
|
||||
|
||||
try {
|
||||
const res = await fetcher.get(`${API_ORIGIN}/api/v1/auth/branding`);
|
||||
this.footer = marked.parse(res.body.footer);
|
||||
} catch (error) {
|
||||
console.error('Failed to get branding info.', error);
|
||||
}
|
||||
|
||||
// Init into the correct view
|
||||
if (search.resetToken) {
|
||||
this.resetToken = search.resetToken;
|
||||
window.document.title = 'Set New Password';
|
||||
this.mode = 'newPassword';
|
||||
setTimeout(() => document.getElementById('inputNewPassword').focus(), 200);
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else { // also search.passwordReset
|
||||
window.document.title = 'Password Reset Request';
|
||||
this.mode = 'passwordReset';
|
||||
this.passwordResetIdentifier = '';
|
||||
setTimeout(() => document.getElementById('inputPasswordResetIdentifier').focus(), 200);
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
},
|
||||
methods: {
|
||||
async onPasswordReset() {
|
||||
this.busy = true;
|
||||
|
||||
try {
|
||||
await fetcher.post(`${API_ORIGIN}/api/v1/auth/password_reset_request`, { identifier: this.passwordResetIdentifier });
|
||||
} catch (error) {
|
||||
console.error('Failed to reset password.', error);
|
||||
}
|
||||
|
||||
this.busy = 'false';
|
||||
this.mode = 'passwordResetDone';
|
||||
},
|
||||
async onNewPassword() {
|
||||
this.busy = true;
|
||||
|
||||
const data = {
|
||||
resetToken: this.resetToken,
|
||||
password: this.newPassword,
|
||||
totpToken: this.totpToken
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetcher.post(`${API_ORIGIN}/api/v1/auth/password_reset`, data);
|
||||
if (res.status === 400 || res.status === 401) {
|
||||
this.error = res.body.message;
|
||||
this.newPasswordRepeat = '';
|
||||
} else if (res.status === 409) {
|
||||
this.error = 'Ask your admin for an invite link first';
|
||||
} else if (res.status === 202) {
|
||||
// set token to autologin
|
||||
localStorage.token = data.accessToken;
|
||||
this.mode = 'newPasswordDone';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set new password.', error);
|
||||
}
|
||||
|
||||
this.busy = false;
|
||||
}
|
||||
}
|
||||
const MODE = {
|
||||
NEW_PASSWORD: 'newPassword',
|
||||
NEW_PASSWORD_DONE: 'newPasswordDone',
|
||||
RESET_PASSWORD: 'passwordReset',
|
||||
RESET_PASSWORD_DONE: 'passwordResetDone',
|
||||
};
|
||||
|
||||
async function onPasswordReset() {
|
||||
busy.value = true;
|
||||
error.value = {};
|
||||
|
||||
try {
|
||||
await fetcher.post(`${API_ORIGIN}/api/v1/auth/password_reset_request`, { identifier: passwordResetIdentifier.value });
|
||||
} catch (error) {
|
||||
error.value.generic = error;
|
||||
console.error('Failed to reset password.', error);
|
||||
}
|
||||
|
||||
busy.value = 'false';
|
||||
mode.value = MODE.RESET_PASSWORD_DONE;
|
||||
}
|
||||
|
||||
async function onNewPassword() {
|
||||
busy.value = true;
|
||||
error.value = {};
|
||||
|
||||
const data = {
|
||||
resetToken: resetToken.value,
|
||||
password: newPassword.value,
|
||||
totpToken: totpToken.value
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetcher.post(`${API_ORIGIN}/api/v1/auth/password_reset`, data);
|
||||
if (res.status === 400 || res.status === 401) {
|
||||
if (res.body.message.indexOf('totpToken') !== -1) {
|
||||
error.value.totpToken = res.body.message;
|
||||
totpToken.value = '';
|
||||
} else {
|
||||
error.value.generic = res.body.message;
|
||||
newPasswordRepeat.value = '';
|
||||
}
|
||||
} else if (res.status === 409) {
|
||||
error.value.generic = 'Ask your admin for an invite link first';
|
||||
} else if (res.status === 202) {
|
||||
// set token to autologin
|
||||
localStorage.token = res.body.accessToken;
|
||||
mode.value = MODE.NEW_PASSWORD_DONE;
|
||||
}
|
||||
} catch (error) {
|
||||
error.value.generic = 'Internal error';
|
||||
console.error('Failed to set new password.', error);
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
try {
|
||||
const res = await fetcher.get(`${API_ORIGIN}/api/v1/auth/branding`);
|
||||
footer.value = marked.parse(res.body.footer);
|
||||
cloudronName.value = res.body.cloudronName;
|
||||
} catch (error) {
|
||||
console.error('Failed to get branding info.', error);
|
||||
}
|
||||
|
||||
// Init into the correct view
|
||||
if (search.resetToken) {
|
||||
resetToken.value = search.resetToken;
|
||||
window.document.title = 'Set New Password';
|
||||
mode.value = MODE.NEW_PASSWORD;
|
||||
setTimeout(() => document.getElementById('inputNewPassword').focus(), 200);
|
||||
} else if (search.accessToken || search.access_token) { // auto-login feature
|
||||
localStorage.token = search.accessToken || search.access_token;
|
||||
window.location.href = '/';
|
||||
} else { // also search.passwordReset
|
||||
window.document.title = 'Password Reset Request';
|
||||
mode.value = MODE.RESET_PASSWORD;
|
||||
passwordResetIdentifier.value = '';
|
||||
setTimeout(() => document.getElementById('inputPasswordResetIdentifier').focus(), 200);
|
||||
}
|
||||
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-root">
|
||||
<div class="layout-left">
|
||||
<img width="128" height="128" class="icon" :src="'/api/v1/cloudron/avatar'"/>
|
||||
<div class="layout-root" v-if="ready">
|
||||
<div class="layout-left" :style="{ 'background-image': `url('${API_ORIGIN}/api/v1/cloudron/background')` }">
|
||||
<img width="128" height="128" class="icon" :src="`${API_ORIGIN}/api/v1/cloudron/avatar`"/>
|
||||
</div>
|
||||
|
||||
<div class="layout-right">
|
||||
|
||||
<div v-show="mode === 'passwordReset'">
|
||||
<h2>{{ $t('passwordReset.title') }}</h2>
|
||||
<div v-if="mode === MODE.RESET_PASSWORD">
|
||||
<small>{{ $t('passwordReset.title') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
|
||||
<form name="passwordResetForm" @submit.prevent="onPasswordReset()">
|
||||
<input type="submit" style="display: none;"/>
|
||||
@@ -122,34 +132,37 @@ export default {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'passwordResetDone'">
|
||||
<h2 v-show="!error">{{ $t('passwordReset.emailSent.title') }}</h2>
|
||||
<h4 v-show="error" class="has-error">{{ error }}</h4>
|
||||
<div v-if="mode === MODE.RESET_PASSWORD_DONE">
|
||||
<h4 v-if="error.generic" class="has-error">{{ error.generic }}</h4>
|
||||
<h2 v-else>{{ $t('passwordReset.emailSent.title') }}</h2>
|
||||
<Button href="/">{{ $t('passwordReset.backToLoginAction') }}</Button>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'newPassword'">
|
||||
<h2>{{ $t('passwordReset.newPassword.title') }}</h2>
|
||||
<div v-if="mode === MODE.NEW_PASSWORD">
|
||||
<small>{{ $t('passwordReset.newPassword.title') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
|
||||
<p class="has-error" v-show="error">{{ error }}</p>
|
||||
<p class="has-error" v-if="error.generic">{{ error.generic }}</p>
|
||||
|
||||
<form name="newPasswordForm" @submit.prevent="onNewPassword()">
|
||||
<input type="submit" style="display: none;"/>
|
||||
<input type="password" style="display: none;"/>
|
||||
|
||||
<div class="form-element" :class="{'has-error': newPasswordRepeat && newPassword !== newPasswordRepeat}">
|
||||
<div class="form-element" :class="{'has-error': newPasswordRepeat && newPassword !== newPasswordRepeat }">
|
||||
<label for="inputNewPassword">{{ $t('passwordReset.newPassword.password') }}</label>
|
||||
<PasswordInput id="inputNewPassword" v-model="newPassword" autofocus required />
|
||||
</div>
|
||||
|
||||
<div class="form-element" :class="{'has-error': newPasswordRepeat && newPassword !== newPasswordRepeat}">
|
||||
<div class="form-element" :class="{'has-error': newPasswordRepeat && newPassword !== newPasswordRepeat }">
|
||||
<label for="inputNewPasswordRepeat">{{ $t('passwordReset.newPassword.passwordRepeat') }}</label>
|
||||
<PasswordInput id="inputNewPasswordRepeat" v-model="newPasswordRepeat" required />
|
||||
</div>
|
||||
|
||||
<div class="form-element">
|
||||
<div class="form-element" :class="{'has-error': error.totpToken }">
|
||||
<label for="inputPasswordResetTotpToken">{{ $t('login.2faToken') }}</label>
|
||||
<TextInput id="inputPasswordResetTotpToken" v-model="totpToken" :disabled="busy" />
|
||||
<p class="has-error" v-if="error.totpToken">{{ error.totpToken }}</p>
|
||||
</div>
|
||||
|
||||
<Button style="margin-top: 12px" @click="onNewPassword()" :disabled="busy || !newPassword || newPassword !== newPasswordRepeat" :loading="busy">{{ $t('passwordReset.passwordChanged.submitAction') }}</Button>
|
||||
@@ -157,8 +170,10 @@ export default {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'newPasswordDone'">
|
||||
<h2>{{ $t('passwordReset.success.title') }}</h2>
|
||||
<div v-if="mode === MODE.NEW_PASSWORD_DONE">
|
||||
<small>{{ $t('passwordReset.success.title') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
<Button href="/">{{ $t('passwordReset.success.openDashboardAction') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +197,6 @@ export default {
|
||||
|
||||
.layout-left {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
background-image: url('/api/v1/cloudron/background');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
flex-basis: 30%;
|
||||
|
||||
@@ -5,6 +5,8 @@ import '@fontsource/noto-sans';
|
||||
import i18n from './i18n.js';
|
||||
import PasswordReset from './components/PasswordReset.vue';
|
||||
|
||||
import './style.css';
|
||||
|
||||
(async function init() {
|
||||
const app = createApp(PasswordReset);
|
||||
|
||||
|
||||
@@ -170,16 +170,18 @@ async function onRevokeAllWebAndCliTokens() {
|
||||
|
||||
// 2fa
|
||||
const mandatory2FAHelp = ref('');
|
||||
const twoFAModal = ref(false);
|
||||
const twoFASecret = ref('');
|
||||
const twoFATotpToken = ref('');
|
||||
const twoFAQRCode = ref('');
|
||||
const twoFAEnableError = ref('');
|
||||
const twoFADialog = useTemplateRef('twoFADialog');
|
||||
|
||||
async function onOpenTwoFASetupDialog() {
|
||||
async function onOpenTwoFASetupDialog(modal = false) {
|
||||
const [error, result] = await profileModel.setTwoFASecret();
|
||||
if (error) return console.error(error);
|
||||
|
||||
twoFAModal.value = modal;
|
||||
twoFAEnableError.value = '';
|
||||
twoFATotpToken.value = '';
|
||||
twoFASecret.value = result.secret;
|
||||
@@ -244,6 +246,9 @@ onMounted(async () => {
|
||||
// dashboard and development clientIds were issued with 7.5.0
|
||||
webadminTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; });
|
||||
cliTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; });
|
||||
|
||||
// check if we should show the 2fa setup
|
||||
if (window.location.hash.indexOf('setup2fa') !== -1) onOpenTwoFASetupDialog(true /* modal */);
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -251,7 +256,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<InputDialog ref="inputDialog" />
|
||||
|
||||
<Dialog ref="twoFADialog" :title="$t('profile.enable2FA.title')" :show-x="true">
|
||||
<Dialog ref="twoFADialog" :title="$t('profile.enable2FA.title')" :show-x="!twoFAModal" :modal="twoFAModal">
|
||||
<div style="text-align: center; max-width: 420px">
|
||||
<p v-show="mandatory2FAHelp">{{ $t('profile.enable2FA.description') }}</p>
|
||||
<p v-html="$t('profile.enable2FA.authenticatorAppDescription', { googleAuthenticatorPlayStoreLink: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', googleAuthenticatorITunesLink: 'https://itunes.apple.com/us/app/google-authenticator/id388497605', freeOTPPlayStoreLink: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', freeOTPITunesLink: 'https://itunes.apple.com/us/app/freeotp-authenticator/id872559395'})"></p>
|
||||
|
||||
Reference in New Issue
Block a user