dashboard/shell/pages/auth/setup.vue

509 lines
14 KiB
Vue

<script>
import { randomStr } from '@shell/utils/string';
import { LabeledInput } from '@components/Form/LabeledInput';
import CopyToClipboard from '@shell/components/CopyToClipboard';
import AsyncButton from '@shell/components/AsyncButton';
import { LOGGED_OUT, SETUP } from '@shell/config/query-params';
import { NORMAN, MANAGEMENT } from '@shell/config/types';
import { findBy } from '@shell/utils/array';
import { Checkbox } from '@components/Form/Checkbox';
import { getVendor, getProduct, setVendor } from '@shell/config/private-label';
import { RadioGroup } from '@components/Form/Radio';
import { setSetting } from '@shell/utils/settings';
import { SETTING } from '@shell/config/settings';
import { _ALL_IF_AUTHED } from '@shell/plugins/dashboard-store/actions';
import { isDevBuild } from '@shell/utils/version';
import { exceptionToErrorsArray } from '@shell/utils/error';
import Password from '@shell/components/form/Password';
import { applyProducts } from '@shell/store/type-map';
import BrandImage from '@shell/components/BrandImage';
const calcIsFirstLogin = (store) => {
const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
return firstLoginSetting?.value === 'true';
};
const calcMustChangePassword = async(store) => {
await store.dispatch('auth/getUser');
const out = store.getters['auth/v3User']?.mustChangePassword;
return out;
};
export default {
layout: 'unauthenticated',
data() {
return {
passwordOptions: [
{ label: this.t('setup.useRandom'), value: true },
{ label: this.t('setup.useManual'), value: false }],
};
},
async middleware({ store, redirect, route } ) {
try {
await store.dispatch('management/findAll', {
type: MANAGEMENT.SETTING,
opt: {
load: _ALL_IF_AUTHED, url: `/v1/${ MANAGEMENT.SETTING }`, redirectUnauthorized: false
}
});
} catch (e) {
}
const isFirstLogin = await calcIsFirstLogin(store);
const mustChangePassword = await calcMustChangePassword(store);
if (isFirstLogin) {
// Always show setup if this is the first log in
return;
} else if (mustChangePassword) {
// If the password needs changing and this isn't the first log in ensure we have the password
if (!!store.getters['auth/initialPass']) {
// Got it... show setup
return;
}
// Haven't got it... redirect to log in so we get it
await store.dispatch('auth/logout', null, { root: true });
return redirect(302, `/auth/login?${ LOGGED_OUT }`);
}
// For all other cases we don't need to show setup
return redirect('/');
},
components: {
AsyncButton, LabeledInput, CopyToClipboard, Checkbox, RadioGroup, Password, BrandImage
},
async asyncData({ route, req, store }) {
const telemetrySetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.TELEMETRY);
const serverUrlSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SERVER_URL);
const rancherVersionSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER);
let telemetry = true;
if (telemetrySetting?.value && telemetrySetting.value !== 'prompt') {
telemetry = telemetrySetting.value !== 'out';
} else if (!rancherVersionSetting?.value || isDevBuild(rancherVersionSetting?.value)) {
telemetry = false;
}
let plSetting;
try {
await store.dispatch('management/findAll', {
type: MANAGEMENT.SETTING,
opt: {
load: _ALL_IF_AUTHED, url: `/v1/${ MANAGEMENT.SETTING }`, redirectUnauthorized: false
},
});
plSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.PL);
} catch (e) {
// Older versions used Norman API to get these
plSetting = await store.dispatch('rancher/find', {
type: 'setting',
id: SETTING.PL,
opt: { url: `/v3/settings/${ SETTING.PL }` }
});
}
if (plSetting.value?.length && plSetting.value !== getVendor()) {
setVendor(plSetting.value);
}
const productName = plSetting.default;
const principals = await store.dispatch('rancher/findAll', { type: NORMAN.PRINCIPAL, opt: { url: '/v3/principals' } });
const me = findBy(principals, 'me', true);
const current = route.query[SETUP] || store.getters['auth/initialPass'];
const v3User = store.getters['auth/v3User'] ?? {};
const mcmFeature = await store.dispatch('management/find', {
type: MANAGEMENT.FEATURE, id: 'multi-cluster-management', opt: { url: `/v1/${ MANAGEMENT.FEATURE }/multi-cluster-management` }
});
const mcmEnabled = mcmFeature?.spec?.value || mcmFeature?.status?.default;
let serverUrl;
if (serverUrlSetting?.value) {
serverUrl = serverUrlSetting.value;
} else if ( process.server ) {
serverUrl = req.headers.host;
} else {
serverUrl = window.location.origin;
}
const isFirstLogin = await calcIsFirstLogin(store);
const mustChangePassword = await calcMustChangePassword(store);
return {
productName,
vendor: getVendor(),
product: getProduct(),
step: parseInt(route.query.step, 10) || 1,
useRandom: true,
haveCurrent: !!current,
username: me?.loginName || 'admin',
isFirstLogin,
mustChangePassword,
current,
password: randomStr(),
confirm: '',
v3User,
serverUrl,
mcmEnabled,
telemetry,
eula: false,
principals,
errors: []
};
},
computed: {
saveEnabled() {
if ( !this.eula && this.isFirstLogin) {
return false;
}
if ( this.mustChangePassword ) {
if ( !this.current ) {
return false;
}
if ( !this.useRandom ) {
if ( !this.password || this.password !== this.confirm ) {
return false;
}
}
}
return true;
},
me() {
const out = findBy(this.principals, 'me', true);
return out;
}
},
watch: {
useRandom(neu) {
if (neu) {
this.password = randomStr();
} else {
this.password = '';
this.$nextTick(() => {
this.$refs.password.focus();
});
}
}
},
methods: {
async save(buttonCb) {
const promises = [];
try {
await applyProducts(this.$store, this.$plugin);
await this.$store.dispatch('loadManagement');
if ( this.mustChangePassword ) {
await this.$store.dispatch('rancher/request', {
url: '/v3/users?action=changepassword',
method: 'post',
data: {
currentPassword: this.current,
newPassword: this.password
},
});
} else {
promises.push(setSetting(this.$store, SETTING.FIRST_LOGIN, 'false'));
}
const user = this.v3User;
user.mustChangePassword = false;
this.$store.dispatch('auth/gotUser', user);
if (this.isFirstLogin) {
promises.push( setSetting(this.$store, SETTING.EULA_AGREED, (new Date()).toISOString()) );
promises.push( setSetting(this.$store, SETTING.TELEMETRY, this.telemetry ? 'in' : 'out') );
if ( this.mcmEnabled && this.serverUrl ) {
promises.push( setSetting(this.$store, SETTING.SERVER_URL, this.serverUrl) );
}
}
await Promise.all(promises);
setTimeout(() => {
buttonCb(true);
this.done();
}, 2000);
} catch (err) {
console.error(err) ; // eslint-disable-line no-console
buttonCb(false);
this.errors = exceptionToErrorsArray(err);
}
},
done() {
this.$router.replace('/');
},
},
};
</script>
<template>
<form class="setup">
<div class="row">
<div class="col span-6 form-col">
<div>
&nbsp;
</div>
<div>
<h1 class="text-center">
{{ t('setup.welcome', {product}) }}
</h1>
<template v-if="mustChangePassword">
<p
v-clean-html="t(isFirstLogin ? 'setup.setPassword' : 'setup.newUserSetPassword', { username }, true)"
class="text-center mb-20 mt-20 setup-title"
/>
<Password
v-if="!haveCurrent"
v-model.trim="current"
autocomplete="current-password"
type="password"
:label="t('setup.currentPassword')"
class="mb-20"
:required="true"
/>
<!-- For password managers... -->
<input
type="hidden"
name="username"
autocomplete="username"
:value="username"
>
<div class="mb-20">
<RadioGroup
v-model="useRandom"
data-testid="setup-password-mode"
name="password-mode"
:options="passwordOptions"
/>
</div>
<div class="mb-20">
<LabeledInput
v-if="useRandom"
ref="password"
v-model.trim="password"
:type="useRandom ? 'text' : 'password'"
:disabled="useRandom"
data-testid="setup-password-random"
label-key="setup.newPassword"
>
<template
v-if="useRandom"
#suffix
>
<div
class="addon"
style="padding: 0 0 0 12px;"
>
<CopyToClipboard
:text="password"
class="btn-sm"
/>
</div>
</template>
</LabeledInput>
<Password
v-else
ref="password"
v-model.trim="password"
:label="t('setup.newPassword')"
data-testid="setup-password"
:required="true"
/>
</div>
<Password
v-show="!useRandom"
v-model.trim="confirm"
autocomplete="new-password"
data-testid="setup-password-confirm"
:label="t('setup.confirmPassword')"
:required="true"
/>
</template>
<template v-if="isFirstLogin">
<template v-if="mcmEnabled">
<hr
v-if="mustChangePassword"
class="mt-20 mb-20"
>
<p>
<t
k="setup.serverUrl.tip"
:raw="true"
/>
</p>
<div class="mt-20">
<LabeledInput
v-model="serverUrl"
:label="t('setup.serverUrl.label')"
data-testid="setup-server-url"
/>
</div>
</template>
<div class="checkbox mt-40">
<Checkbox
id="checkbox-telemetry"
v-model="telemetry"
>
<template #label>
<t
k="setup.telemetry"
:raw="true"
:name="productName"
/>
</template>
</Checkbox>
</div>
<div class="checkbox pt-10 eula">
<Checkbox
id="checkbox-eula"
v-model="eula"
data-testid="setup-agreement"
>
<template #label>
<t
k="setup.eula"
:raw="true"
:name="productName"
/>
</template>
</Checkbox>
</div>
</template>
<div
id="submit"
class="text-center mt-20"
>
<AsyncButton
key="passwordSubmit"
type="submit"
mode="continue"
:disabled="!saveEnabled"
data-testid="setup-submit"
@click="save"
/>
</div>
<div class="setup-errors mt-20">
<h4
v-for="err in errors"
:key="err"
class="text-error text-center"
>
{{ err }}
</h4>
</div>
</div>
</div>
<BrandImage
class="col span-6 landscape"
file-name="login-landscape.svg"
/>
</div>
</form>
</template>
<style lang="scss" scoped>
.principal {
display: block;
background: var(--box-bg);
border: 1px solid var(--border);
border-radius: 3px;
margin: 10px 0;
padding: 10px;
line-height: 40px;
img {
vertical-align: middle;
margin: 0 10px;
}
}
.setup {
overflow: hidden;
.row {
& .checkbox {
margin: auto
}
.span-6 {
padding: 0 60px;
}
.landscape {
height: 100vh;
margin: 0;
object-fit: cover;
padding: 0;
}
}
.form-col {
display: flex;
flex-direction: column;
overflow-y: auto;
position: relative;
height: 100vh;
& > div:first-of-type {
flex:3;
}
& > div:nth-of-type(2) {
flex: 9;
}
}
.setup-title {
::v-deep code {
font-size: 12px;
padding: 0;
}
}
.setup-errors {
min-height: 50px;
}
p {
line-height: 20px;
}
}
</style>