Add SetupAccount view
This commit is contained in:
228
dashboard/src/components/SetupAccount.vue
Normal file
228
dashboard/src/components/SetupAccount.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { Button, PasswordInput, FormGroup, TextInput, fetcher } from 'pankow';
|
||||
import { API_ORIGIN } from '../constants.js';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
|
||||
const profileModel = ProfileModel.create();
|
||||
|
||||
const ready = ref(false);
|
||||
const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const mode = ref('');
|
||||
const footer = ref('');
|
||||
const cloudronName = ref('');
|
||||
|
||||
const profileLocked = ref(false);
|
||||
const existingUsername = ref(false);
|
||||
const username = ref('');
|
||||
const displayName = ref('');
|
||||
const password = ref('');
|
||||
const passwordRepeat = ref('');
|
||||
const dashboardUrl = ref('');
|
||||
const inviteToken = ref('');
|
||||
|
||||
const MODE = {
|
||||
SETUP: 'setup',
|
||||
NO_USERNAME: 'noUsername',
|
||||
INVALID_TOKEN: 'invalidToken',
|
||||
DONE: 'done',
|
||||
};
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
formError.value = {};
|
||||
|
||||
const data = {
|
||||
inviteToken: inviteToken.value,
|
||||
password: password.value,
|
||||
};
|
||||
|
||||
if (!profileLocked.value) {
|
||||
if (!existingUsername.value) data.username = username.value;
|
||||
data.displayName = displayName.value;
|
||||
}
|
||||
|
||||
const [error, result] = await profileModel.setupAccount(data);
|
||||
if (error) {
|
||||
if (error.status === 401) {
|
||||
mode.value = MODE.INVALID_TOKEN;
|
||||
} else if (error.status === 409) {
|
||||
formError.value.username = 'Username already taken';
|
||||
} else if (error.status === 400) {
|
||||
if (error.body && error.body.message.indexOf('Username') === 0) {
|
||||
formError.value.username = error.body.message;
|
||||
} else if (error.body && error.body.message.indexOf('Password') === 0) {
|
||||
formError.value.password = error.body.message;
|
||||
passwordRepeat.value = '';
|
||||
} else {
|
||||
formError.value.generic = error.body ? error.body.message : 'Interal error';
|
||||
}
|
||||
} else {
|
||||
formError.value.generic = error.body ? error.body.message : 'Interal error';
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = result.accessToken;
|
||||
|
||||
dashboardUrl.value = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
|
||||
busy.value = false;
|
||||
mode.value = MODE.DONE;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
profileLocked.value = !!search.profileLocked;
|
||||
existingUsername.value = !!search.username;
|
||||
username.value = search.username || '';
|
||||
displayName.value = search.displayName || '';
|
||||
inviteToken.value = search.inviteToken;
|
||||
|
||||
// Init into the correct view
|
||||
mode.value = (!existingUsername.value && profileLocked.value) ? MODE.NO_USERNAME : MODE.SETUP;
|
||||
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- TODO mobile layout -->
|
||||
<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-if="mode === MODE.SETUP">
|
||||
<small>{{ $t('setupAccount.welcomeTo') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
<div>{{ $t('setupAccount.description') }}</div>
|
||||
|
||||
<div class="text-danger" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<!-- prevents autofill -->
|
||||
<input type="password" style="display: none;"/>
|
||||
|
||||
<input type="submit" style="display: none;"/>
|
||||
|
||||
<FormGroup :has-error="formError.username">
|
||||
<label for="inputUsername">{{ $t('setupAccount.username') }}</label>
|
||||
<TextInput id="inputUsername" v-model="username" :disabled="profileLocked || existingUsername" required/>
|
||||
<div class="text-danger" v-if="formError.username">{{ formError.username }}</div>
|
||||
<!-- TODO -->
|
||||
<!-- <small ng-show="setupAccountForm.username.$error.minlength">{{ 'setupAccount.errorUsernameTooShort' | tr }}</small> -->
|
||||
<!-- <small ng-show="setupAccountForm.username.$error.maxlength">{{ 'setupAccount.errorUsernameTooLong' | tr }}</small> -->
|
||||
<!-- <small ng-show="setupAccountForm.username.$dirty && setupAccountForm.username.$invalid">{{ 'setupAccount.errorUsernameInvalid' | tr }}</small> -->
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="inputDisplayName">{{ $t('setupAccount.fullName') }}</label>
|
||||
<TextInput id="inputDisplayName" v-model="displayName" :disabled="profileLocked" required/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup :has-error="formError.password">
|
||||
<label for="inputPassword">{{ $t('setupAccount.password') }}</label>
|
||||
<PasswordInput id="inputPassword" v-model="password" required/>
|
||||
<div class="text-danger" v-if="formError.password">{{ formError.password }}</div>
|
||||
<!-- TODO -->
|
||||
<!-- <small ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">{{ 'setupAccount.errorPassword' | tr }}</small> -->
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup :has-error="password !== '' && passwordRepeat !== '' && password !== passwordRepeat">
|
||||
<label for="inputPasswordRepeat">{{ $t('setupAccount.passwordRepeat') }}</label>
|
||||
<PasswordInput id="inputPasswordRepeat" v-model="passwordRepeat" required/>
|
||||
<div class="text-danger" v-if="password !== '' && passwordRepeat !== '' && password !== passwordRepeat">{{ $t('setupAccount.errorPasswordNoMatch') }}</div>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
<Button :disabled="busy || password !== passwordRepeat" :loading="busy" @click="onSubmit()">{{ $t('setupAccount.setupAction') }}</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === MODE.NO_USERNAME">
|
||||
<small>{{ $t('setupAccount.welcomeTo') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
<h3>{{ $t('setupAccount.noUsername.title') }}</h3>
|
||||
<div>{{ $t('setupAccount.noUsername.description') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === MODE.INVALID_TOKEN">
|
||||
<small>{{ $t('setupAccount.welcomeTo') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
<h3 class="text-danger">{{ $t('setupAccount.invalidToken.title') }}</h3>
|
||||
<div>{{ $t('setupAccount.invalidToken.description') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === MODE.DONE">
|
||||
<small>{{ $t('setupAccount.welcomeTo') }}</small>
|
||||
<h1>{{ cloudronName }}</h1>
|
||||
<br/>
|
||||
<h3>{{ $t('setupAccount.success.title') }}</h3>
|
||||
<Button :href="dashboardUrl">{{ $t('setupAccount.success.openDashboardAction') }}</Button>
|
||||
</div>
|
||||
</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 {
|
||||
max-width: 400px;
|
||||
padding-left: 20px;
|
||||
flex-basis: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user