Add SetupAccount view

This commit is contained in:
Johannes Zellner
2025-03-28 21:48:52 +01:00
parent ec07334d14
commit 9a6995343b
5 changed files with 277 additions and 151 deletions

View 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>