Files
cloudron-box/dashboard/src/components/Login.vue
T
2025-03-25 18:05:29 +01:00

184 lines
5.1 KiB
Vue

<script>
import { Button, PasswordInput, TextInput, fetcher } from 'pankow';
export default {
name: 'Login',
components: {
Button,
PasswordInput,
TextInput
},
data() {
return {
ready: false,
busy: false,
passwordError: null,
totpError: null,
internalError: null,
username: '',
password: '',
totpToken: '',
totpTokenRequired: false,
// coming from login.html template
name: window.cloudron.name,
note: window.cloudron.note,
iconUrl: window.cloudron.iconUrl,
footer: window.cloudron.footer,
submitUrl: window.cloudron.submitUrl
};
},
async mounted() {
// placed optionally in local storage by setupaccount.js
const autoLoginToken = localStorage.cloudronFirstTimeToken;
if (autoLoginToken) {
try {
const res = await fetch.post(this.submitUrl, { autoLoginToken });
localStorage.removeItem('cloudronFirstTimeToken');
if (res.body.redirectTo) window.location.href = res.body.redirectTo;
else console.log('login success but missing redirectTo in data:', res.body);
} catch (error) {
console.error('Failed to use autologin token', error);
}
}
this.ready = true;
},
methods: {
async onSubmit() {
this.busy = true;
this.passwordError = false;
this.totpError = false;
this.internalError = false;
const body = {
username: this.username,
password: this.password,
totpToken: this.totpToken
};
try {
const res = await fetcher.post(this.submitUrl, body);
if (res.status === 410) {
// the oidc login session is old
window.location.reload();
} else if (res.status === 401) {
if (res.body.message.indexOf('totpToken') !== -1) {
this.totpError = this.totpTokenRequired; // only set on second try coming from login
this.totpTokenRequired = true;
this.totpToken = '';
setTimeout(() => document.getElementById('inputTotpToken').focus(), 0);
} else {
this.password = '';
this.passwordError = true;
document.getElementById('inputPassword').focus();
}
}
if (res.body.redirectTo) window.location.href = res.body.redirectTo;
else console.log('login success but missing redirectTo in data:', res.body);
} catch (error) {
this.internalError = true;
console.error('Login failed:', error);
}
this.busy = false;
}
}
};
</script>
<template>
<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>
<br/>
<div :html="note"></div>
<p class="has-error" v-show="passwordError">{{ $t('login.errorIncorrectCredentials') }}</p>
<p class="has-error" v-show="internalError">{{ $t('login.errorInternal') }}</p>
<form @submit.prevent="onSubmit" v-show="!totpTokenRequired">
<input type="submit" style="display: none;"/>
<div class="form-element">
<label for="inputUsername">{{ $t('login.username') }}</label>
<TextInput id="inputUsername" v-model="username" autofocus required/>
</div>
<div class="form-element">
<label for="inputPassword">{{ $t('login.password') }}</label>
<PasswordInput id="inputPassword" v-model="password" required/>
</div>
<Button id="loginSubmitButton" style="margin-top: 12px" @click.prevent="onSubmit" :loading="busy">{{ $t('login.signInAction') }}</Button>
<a href="/passwordreset.html" style="margin-left: 10px;">{{ $t('login.resetPasswordAction') }}</a>
</form>
<form @submit.prevent="onSubmit" v-show="totpTokenRequired">
<input type="submit" style="display: none;"/>
<div class="form-element">
<label for="inputTotpToken">{{ $t('login.2faToken') }}</label>
<TextInput id="inputTotpToken" v-model="totpToken"/>
<p class="has-error" v-show="totpError">{{ $t('login.errorIncorrect2FAToken') }}</p>
</div>
<Button id="totpTokenSubmitButton" style="margin-top: 12px" type="submit" @click.prevent="onSubmit" :loading="busy">{{ $t('login.signInAction') }}</Button>
</form>
</div>
</div>
<footer v-show="footer" v-html="footer"></footer>
</template>
<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-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>