2025-03-28 21:48:52 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
2025-04-24 22:48:34 +02:00
|
|
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
2025-03-28 21:48:52 +01:00
|
|
|
import { marked } from 'marked';
|
|
|
|
|
import { Button, PasswordInput, FormGroup, TextInput, fetcher } from 'pankow';
|
|
|
|
|
import { API_ORIGIN } from '../constants.js';
|
2025-04-09 22:49:41 +02:00
|
|
|
import PublicPageLayout from '../components/PublicPageLayout.vue';
|
2025-03-28 21:48:52 +01:00
|
|
|
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('');
|
2025-04-24 22:48:34 +02:00
|
|
|
const form = useTemplateRef('form');
|
2025-03-28 21:48:52 +01:00
|
|
|
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() {
|
2025-04-24 22:48:34 +02:00
|
|
|
if (!form.value.reportValidity()) return;
|
|
|
|
|
|
2025-03-28 21:48:52 +01:00
|
|
|
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>
|
2025-04-09 22:49:41 +02:00
|
|
|
<PublicPageLayout :footerHtml="footer">
|
|
|
|
|
<div>
|
2025-03-28 21:48:52 +01:00
|
|
|
<div v-if="mode === MODE.SETUP">
|
|
|
|
|
<small>{{ $t('setupAccount.welcomeTo') }}</small>
|
|
|
|
|
<h1>{{ cloudronName }}</h1>
|
|
|
|
|
<br/>
|
|
|
|
|
<div>{{ $t('setupAccount.description') }}</div>
|
|
|
|
|
|
2025-04-24 22:48:34 +02:00
|
|
|
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
|
2025-03-28 21:48:52 +01:00
|
|
|
|
2025-04-24 22:48:34 +02:00
|
|
|
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form">
|
2025-03-28 21:48:52 +01:00
|
|
|
<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/>
|
2025-04-24 22:48:34 +02:00
|
|
|
<div class="error-label" v-if="formError.username">{{ formError.username }}</div>
|
2025-03-28 21:48:52 +01:00
|
|
|
</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/>
|
2025-04-24 22:48:34 +02:00
|
|
|
<div class="error-label" v-if="formError.password">{{ formError.password }}</div>
|
2025-03-28 21:48:52 +01:00
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<FormGroup :has-error="password !== '' && passwordRepeat !== '' && password !== passwordRepeat">
|
|
|
|
|
<label for="inputPasswordRepeat">{{ $t('setupAccount.passwordRepeat') }}</label>
|
|
|
|
|
<PasswordInput id="inputPasswordRepeat" v-model="passwordRepeat" required/>
|
2025-04-24 22:48:34 +02:00
|
|
|
<div class="error-label" v-if="password !== '' && passwordRepeat !== '' && password !== passwordRepeat">{{ $t('setupAccount.errorPasswordNoMatch') }}</div>
|
2025-03-28 21:48:52 +01:00
|
|
|
</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/>
|
2025-04-24 22:48:34 +02:00
|
|
|
<h3 class="error-label">{{ $t('setupAccount.invalidToken.title') }}</h3>
|
2025-03-28 21:48:52 +01:00
|
|
|
<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>
|
2025-04-09 22:49:41 +02:00
|
|
|
</PublicPageLayout>
|
2025-03-28 21:48:52 +01:00
|
|
|
</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>
|
|
|
|
|
|