blob: 8503f71082b5e0e0ea75f6e8a16f32b082d14652 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<a-form
id="formLogin"
class="user-layout-login"
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
v-ctrl-enter="handleSubmit"
>
<a-tabs
class="tab-center"
:activeKey="customActiveKey"
size="large"
:tabBarStyle="{ textAlign: 'center', borderBottom: 'unset' }"
@change="handleTabClick"
:animated="false"
>
<a-tab-pane key="cs">
<template #tab>
<span>
<safety-outlined />
{{ $t('label.login.portal') }}
</span>
</template>
<a-form-item v-if="$config.multipleServer" name="server" ref="server">
<a-select
size="large"
:placeholder="$t('server')"
v-model:value="form.server"
@change="onChangeServer"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<a-select-option v-for="item in $config.servers" :key="(item.apiHost || '') + item.apiBase" :label="item.name">
<template #prefix>
<database-outlined />
</template>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item ref="username" name="username">
<a-input
size="large"
type="text"
v-focus="true"
:placeholder="$t('label.username')"
v-model:value="form.username"
>
<template #prefix>
<user-outlined />
</template>
</a-input>
</a-form-item>
<a-form-item ref="password" name="password">
<a-input-password
size="large"
type="password"
autocomplete="false"
:placeholder="$t('label.password')"
v-model:value="form.password"
>
<template #prefix>
<lock-outlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item ref="domain" name="domain">
<a-input
size="large"
type="text"
:placeholder="$t('label.domain')"
v-model:value="form.domain"
>
<template #prefix>
<block-outlined />
</template>
</a-input>
</a-form-item>
</a-tab-pane>
<a-tab-pane key="saml" :disabled="idps.length === 0">
<template #tab>
<span>
<audit-outlined />
{{ $t('label.login.single.signon') }}
</span>
</template>
<a-form-item v-if="$config.multipleServer" name="server" ref="server">
<a-select
size="large"
:placeholder="$t('server')"
v-model:value="form.server"
@change="onChangeServer"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="item in $config.servers" :key="(item.apiHost || '') + item.apiBase" :label="item.name">
<template #prefix>
<database-outlined />
</template>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="idp" ref="idp">
<a-select
v-model:value="form.idp"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="(idp, idx) in idps" :key="idx" :value="idp.id" :label="idp.orgName">
{{ idp.orgName }}
</a-select-option>
</a-select>
</a-form-item>
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-button
size="large"
type="primary"
html-type="submit"
class="login-button"
:loading="state.loginBtn"
:disabled="state.loginBtn"
ref="submit"
@click="handleSubmit"
>{{ $t('label.login') }}</a-button>
</a-form-item>
<translation-menu/>
<div class="content" v-if="socialLogin">
<p class="or">or</p>
</div>
<div class="center">
<div class="social-auth" v-if="githubprovider">
<a-button
@click="handleGithubProviderAndDomain"
tag="a"
color="primary"
:href="getGitHubUrl(from)"
class="auth-btn github-auth"
style="height: 38px; width: 185px; padding: 0; margin-bottom: 5px;" >
<img src="/assets/github.svg" style="width: 32px; padding: 5px" />
<a-text>Sign in with Github</a-text>
</a-button>
</div>
<div class="social-auth" v-if="googleprovider">
<a-button
@click="handleGoogleProviderAndDomain"
tag="a"
color="primary"
:href="getGoogleUrl(from)"
class="auth-btn google-auth"
style="height: 38px; width: 185px; padding: 0" >
<img src="/assets/google.svg" style="width: 32px; padding: 5px" />
<a-text>Sign in with Google</a-text>
</a-button>
</div>
</div>
</a-form>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { api } from '@/api'
import store from '@/store'
import { mapActions } from 'vuex'
import { sourceToken } from '@/utils/request'
import { SERVER_MANAGER } from '@/store/mutation-types'
import TranslationMenu from '@/components/header/TranslationMenu'
export default {
components: {
TranslationMenu
},
data () {
return {
idps: [],
customActiveKey: 'cs',
customActiveKeyOauth: false,
loginBtn: false,
email: '',
secretcode: '',
oauthexclude: '',
socialLogin: false,
googleprovider: false,
githubprovider: false,
googleredirecturi: '',
githubredirecturi: '',
googleclientid: '',
githubclientid: '',
loginType: 0,
state: {
time: 60,
loginBtn: false,
loginType: 0
},
server: ''
}
},
created () {
if (this.$config.multipleServer) {
this.server = this.$localStorage.get(SERVER_MANAGER) || this.$config.servers[0]
}
this.initForm()
if (store.getters.logoutFlag) {
if (store.getters.readyForShutdownPollingJob !== '' || store.getters.readyForShutdownPollingJob !== undefined) {
clearInterval(store.getters.readyForShutdownPollingJob)
}
sourceToken.init()
this.fetchData()
} else {
this.fetchData()
}
},
methods: {
...mapActions(['Login', 'Logout', 'OauthLogin']),
initForm () {
this.formRef = ref()
this.form = reactive({
server: (this.server.apiHost || '') + this.server.apiBase
})
this.rules = reactive({})
this.setRules()
},
setRules () {
if (this.customActiveKey === 'cs' && this.customActiveKeyOauth === false) {
this.rules.username = [
{
required: true,
message: this.$t('message.error.username'),
trigger: 'change'
},
{
validator: this.handleUsernameOrEmail,
trigger: 'change'
}
]
this.rules.password = [
{
required: true,
message: this.$t('message.error.password'),
trigger: 'change'
}
]
} else {
this.rules.username = []
this.rules.password = []
}
},
fetchData () {
api('listIdps').then(response => {
if (response) {
this.idps = response.listidpsresponse.idp || []
this.idps.sort(function (a, b) {
if (a.orgName < b.orgName) { return -1 }
if (a.orgName > b.orgName) { return 1 }
return 0
})
this.form.idp = this.idps[0].id || ''
}
})
api('listOauthProvider', {}).then(response => {
if (response) {
const oauthproviders = response.listoauthproviderresponse.oauthprovider || []
oauthproviders.forEach(item => {
this.socialLogin = true
if (item.provider === 'google') {
this.googleprovider = item.enabled
this.googleclientid = item.clientid
this.googleredirecturi = item.redirecturi
}
if (item.provider === 'github') {
this.githubprovider = item.enabled
this.githubclientid = item.clientid
this.githubredirecturi = item.redirecturi
}
})
}
})
},
// handler
async handleUsernameOrEmail (rule, value) {
const { state } = this
const regex = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/
if (regex.test(value)) {
state.loginType = 0
} else {
state.loginType = 1
}
return Promise.resolve()
},
handleTabClick (key) {
this.customActiveKey = key
this.setRules()
},
handleGithubProviderAndDomain () {
this.handleDomain()
this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'github')
},
handleGoogleProviderAndDomain () {
this.handleDomain()
this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'google')
},
handleDomain () {
const values = toRaw(this.form)
if (!values.domain) {
this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', '/')
} else {
this.$store.commit('SET_DOMAIN_USED_TO_LOGIN', values.domain)
}
},
getGitHubUrl (from) {
const rootURl = 'https://github.com/login/oauth/authorize'
const options = {
client_id: this.githubclientid,
scope: 'user:email',
state: 'cloudstack'
}
const qs = new URLSearchParams(options)
return `${rootURl}?${qs.toString()}`
},
getGoogleUrl (from) {
const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth'
const options = {
redirect_uri: this.googleredirecturi,
client_id: this.googleclientid,
access_type: 'offline',
response_type: 'code',
prompt: 'consent',
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
].join(' '),
state: from
}
const qs = new URLSearchParams(options)
return `${rootUrl}?${qs.toString()}`
},
handleSubmit (e) {
e.preventDefault()
if (this.state.loginBtn) return
this.formRef.value.validate().then(() => {
this.state.loginBtn = true
const values = toRaw(this.form)
if (this.$config.multipleServer) {
this.axios.defaults.baseURL = (this.server.apiHost || '') + this.server.apiBase
store.dispatch('SetServer', this.server)
}
if (this.customActiveKey === 'cs') {
const loginParams = { ...values }
delete loginParams.username
loginParams[!this.state.loginType ? 'email' : 'username'] = values.username
loginParams.password = values.password
loginParams.domain = values.domain
if (!loginParams.domain) {
loginParams.domain = '/'
}
this.Login(loginParams)
.then((res) => this.loginSuccess(res))
.catch(err => {
this.requestFailed(err)
this.state.loginBtn = false
})
} else if (this.customActiveKey === 'saml') {
this.state.loginBtn = false
var samlUrl = this.$config.apiBase + '?command=samlSso'
if (values.idp) {
samlUrl += ('&idpid=' + values.idp)
}
window.location.href = samlUrl
}
}).catch(error => {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
handleSubmitOauth (provider) {
this.customActiveKeyOauth = true
this.setRules()
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
const loginParams = { ...values }
delete loginParams.username
loginParams.email = this.email
loginParams.provider = provider
loginParams.secretcode = this.secretcode
loginParams.domain = values.domain
if (!loginParams.domain) {
loginParams.domain = '/'
}
this.OauthLogin(loginParams)
.then((res) => this.loginSuccess(res))
.catch(err => {
this.requestFailed(err)
this.state.loginBtn = false
})
})
},
loginSuccess (res) {
this.$notification.destroy()
this.$store.commit('SET_COUNT_NOTIFY', 0)
if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider !== '' && store.getters.twoFaProvider !== undefined) {
this.$router.push({ path: '/verify2FA' }).catch(() => {})
} else if (store.getters.twoFaEnabled === true && (store.getters.twoFaProvider === '' || store.getters.twoFaProvider === undefined)) {
this.$router.push({ path: '/setup2FA' }).catch(() => {})
} else {
this.$store.commit('SET_LOGIN_FLAG', true)
this.$router.push({ path: '/dashboard' }).catch(() => {})
}
},
requestFailed (err) {
if (err && err.response && err.response.data && err.response.data.loginresponse) {
const error = err.response.data.loginresponse.errorcode + ': ' + err.response.data.loginresponse.errortext
this.$message.error(`${this.$t('label.error')} ${error}`)
} else if (err && err.response && err.response.data && err.response.data.oauthloginresponse) {
const error = err.response.data.oauthloginresponse.errorcode + ': ' + err.response.data.oauthloginresponse.errortext
this.$message.error(`${this.$t('label.error')} ${error}`)
} else {
this.$message.error(this.$t('message.login.failed'))
}
},
onChangeServer (server) {
const servers = this.$config.servers || []
const serverFilter = servers.filter(ser => (ser.apiHost || '') + ser.apiBase === server)
this.server = serverFilter[0] || {}
}
}
}
</script>
<style lang="less" scoped>
.user-layout-login {
min-width: 260px;
width: 368px;
margin: 0 auto;
.mobile & {
max-width: 368px;
width: 98%;
}
label {
font-size: 14px;
}
button.login-button {
margin-top: 8px;
padding: 0 15px;
font-size: 16px;
height: 40px;
width: 100%;
}
.user-login-other {
text-align: left;
margin-top: 24px;
line-height: 22px;
.item-icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
.register {
float: right;
}
.g-btn-wrapper {
background-color: rgb(221, 75, 57);
height: 40px;
width: 80px;
}
}
.center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100px;
}
.content {
margin: 10px auto;
width: 300px;
}
.or {
text-align: center;
font-size: 16px;
background:
linear-gradient(#CCC 0 0) left,
linear-gradient(#CCC 0 0) right;
background-size: 40% 1px;
background-repeat: no-repeat;
}
}
</style>