2024-12-14 23:20:21 +01:00
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
import { marked } from 'marked';
|
|
|
|
|
import { Button, PasswordInput, TextInput, fetcher } from 'pankow';
|
2025-03-03 11:22:56 +01:00
|
|
|
import { API_ORIGIN } from '../constants.js';
|
2024-12-14 23:20:21 +01:00
|
|
|
|
|
|
|
|
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; }, {});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
2025-01-19 12:00:22 +01:00
|
|
|
<template>
|
|
|
|
|
<div class="layout-root">
|
|
|
|
|
<div class="layout-left">
|
|
|
|
|
<img width="128" height="128" class="icon" :src="'/api/v1/cloudron/avatar'"/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="layout-right">
|
|
|
|
|
|
|
|
|
|
<div v-show="mode === 'passwordReset'">
|
|
|
|
|
<h2>{{ $t('passwordReset.title') }}</h2>
|
|
|
|
|
|
|
|
|
|
<form name="passwordResetForm" @submit.prevent="onPasswordReset()">
|
|
|
|
|
<input type="submit" style="display: none;"/>
|
|
|
|
|
|
|
|
|
|
<div class="form-element">
|
|
|
|
|
<label for="inputPasswordResetIdentifier">{{ $t('passwordReset.usernameOrEmail') }}</label>
|
|
|
|
|
<TextInput id="inputPasswordResetIdentifier" name="passwordResetIdentifier" v-model="passwordResetIdentifier" :disabled="busy" autofocus required />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button style="margin-top: 12px" @click="onPasswordReset()" :disabled="busy || !passwordResetIdentifier" :loading="busy">{{ $t('passwordReset.resetAction') }}</Button>
|
|
|
|
|
<a href="/" style="margin-left: 10px;">{{ $t('passwordReset.backToLoginAction') }}</a>
|
|
|
|
|
</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>
|
|
|
|
|
<Button href="/">{{ $t('passwordReset.backToLoginAction') }}</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-show="mode === 'newPassword'">
|
|
|
|
|
<h2>{{ $t('passwordReset.newPassword.title') }}</h2>
|
|
|
|
|
|
|
|
|
|
<p class="has-error" v-show="error">{{ error }}</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}">
|
|
|
|
|
<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}">
|
|
|
|
|
<label for="inputNewPasswordRepeat">{{ $t('passwordReset.newPassword.passwordRepeat') }}</label>
|
|
|
|
|
<PasswordInput id="inputNewPasswordRepeat" v-model="newPasswordRepeat" required />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-element">
|
|
|
|
|
<label for="inputPasswordResetTotpToken">{{ $t('login.2faToken') }}</label>
|
|
|
|
|
<TextInput id="inputPasswordResetTotpToken" v-model="totpToken" :disabled="busy" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button style="margin-top: 12px" @click="onNewPassword()" :disabled="busy || !newPassword || newPassword !== newPasswordRepeat" :loading="busy">{{ $t('passwordReset.passwordChanged.submitAction') }}</Button>
|
|
|
|
|
<a href="/" style="margin-left: 10px;">{{ $t('passwordReset.backToLoginAction') }}</a>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-show="mode === 'newPasswordDone'">
|
|
|
|
|
<h2>{{ $t('passwordReset.success.title') }}</h2>
|
|
|
|
|
<Button href="/">{{ $t('passwordReset.success.openDashboardAction') }}</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<footer v-show="footer" v-html="footer"></footer>
|
|
|
|
|
</template>
|
|
|
|
|
|
2024-12-14 23:20:21 +01:00
|
|
|
<style>
|
|
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
|
margin-bottom: 20%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.layout-root {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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%;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.layout-right {
|
|
|
|
|
padding-left: 20px;
|
|
|
|
|
flex-basis: 70%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-element {
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|