502 lines
16 KiB
Vue
502 lines
16 KiB
Vue
<template>
|
|
<div>
|
|
<div>
|
|
<v-row justify="center">
|
|
<v-dialog v-model="tfaDialog" persistent max-width="600px">
|
|
<v-card>
|
|
<v-card-title>
|
|
<span class="text-h5">{{ authTwoFactor }}</span>
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-text-field
|
|
v-model="pin"
|
|
:label="authEnterPin"
|
|
required
|
|
:error-messages="pinError"
|
|
autofocus
|
|
@keyup.enter="tfaVerify"
|
|
></v-text-field>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-btn color="blue darken-1" text @click="cancelTfaVerify()">{{
|
|
cancel
|
|
}}</v-btn>
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
color="blue darken-1"
|
|
:disabled="pin == null || pin.length == 0"
|
|
text
|
|
@click="tfaVerify()"
|
|
>{{ authVerifyCode }}</v-btn
|
|
>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-row>
|
|
<v-row align="center" justify="center" class="mx-auto mt-sm-12 mb-16">
|
|
<v-col cols="12" offset-md="4">
|
|
<form>
|
|
<v-row>
|
|
<!-- Customer logo -->
|
|
<v-col v-if="showCustomSmallLogo()" cols="12">
|
|
<div class="text-center">
|
|
<img :src="mediumLogoUrl" />
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col v-if="showCustomMediumLogo()" cols="7">
|
|
<div class="text-center">
|
|
<img :src="largeLogoUrl" />
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col v-if="showCustomLargeLogo()" cols="7">
|
|
<div class="text-center">
|
|
<img :src="largeLogoUrl" />
|
|
</div>
|
|
</v-col>
|
|
|
|
<!-- Small Sockeye logo -->
|
|
<v-col v-if="showSmallBrandLogo()" cols="12">
|
|
<!-- <v-img
|
|
:src="require('@/assets/img/sockeye-64.png')"
|
|
contain
|
|
height="64"
|
|
></v-img> -->
|
|
<v-img
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
height="64"
|
|
></v-img>
|
|
</v-col>
|
|
<!-- Large Sockeye logo. -->
|
|
<v-col v-if="showLargeBrandLogo()" cols="6" class="ml-16">
|
|
<!-- <v-img
|
|
:src="require('@/assets/img/sockey-128.png')"
|
|
contain
|
|
height="128"
|
|
></v-img> -->
|
|
<v-img
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
height="128"
|
|
></v-img>
|
|
</v-col>
|
|
<v-col v-if="formState.errorBoxMessage" cols="12" md="7">
|
|
<gz-error
|
|
:error-box-message="formState.errorBoxMessage"
|
|
></gz-error>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="7">
|
|
<v-text-field
|
|
id="username"
|
|
v-model="input.username"
|
|
v-focus
|
|
name="username"
|
|
prepend-icon="$sockiUser"
|
|
autofocus
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
:error="errorBadCreds"
|
|
@keyup.enter="onEnterUserName"
|
|
></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" md="7">
|
|
<v-text-field
|
|
id="password"
|
|
v-model="input.password"
|
|
name="password"
|
|
:append-outer-icon="reveal ? '$sockiEye' : '$sockiEyeSlash'"
|
|
prepend-icon="$sockiKey"
|
|
:type="reveal ? 'text' : 'password'"
|
|
:error="errorBadCreds"
|
|
@keyup.enter="login"
|
|
@click:append-outer="reveal = !reveal"
|
|
></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" md="7" mt-1 mb-5>
|
|
<v-btn
|
|
color="primary"
|
|
value="LOGIN"
|
|
:data-cy="`loginbutton${errorBadCreds ? '_failedcreds' : ''}`"
|
|
@click="login()"
|
|
>
|
|
<v-icon>$sockiSignIn</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</form>
|
|
</v-col>
|
|
</v-row>
|
|
<v-footer color="primary" padless absolute>
|
|
<div
|
|
v-if="showFooterLogo()"
|
|
style="text-align: center"
|
|
class="mx-auto pa-4 mb-10 mb-sm-1 mt-n8"
|
|
>
|
|
<a
|
|
href="https://ayanova.com"
|
|
target="_blank"
|
|
style="text-decoration: none"
|
|
class="primary white--text text-caption"
|
|
>
|
|
<div v-if="showFooterLogo()" style="width: 100%" class="mx-auto">
|
|
<v-img
|
|
style="margin: 0 auto"
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
></v-img>
|
|
</div>
|
|
<div class="mx-auto">Sockeye {{ version }}</div>
|
|
<div class="mx-auto">
|
|
<span class="primary white--text text-caption">{{
|
|
copyright
|
|
}}</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div
|
|
v-else
|
|
style="text-align: center"
|
|
class="mx-auto pa-4 mb-1 mb-sm-1"
|
|
>
|
|
<a
|
|
href="https://ayanova.com"
|
|
target="_blank"
|
|
style="text-decoration: none"
|
|
class="primary white--text text-caption"
|
|
>
|
|
<div class="mx-auto">Sockeye {{ version }}</div>
|
|
<div class="mx-auto">
|
|
<span class="primary white--text text-caption">{{
|
|
copyright
|
|
}}</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</v-footer>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import { processLogin, processLogout } from "../api/authutil";
|
|
import sockeyeVersion from "../api/sockeye-version";
|
|
export default {
|
|
data() {
|
|
return {
|
|
input: {
|
|
username: null,
|
|
password: null
|
|
},
|
|
tfaDialog: false,
|
|
authTwoFactor: null,
|
|
authEnterPin: null,
|
|
authVerifyCode: null,
|
|
authPinInvalid: null,
|
|
cancel: null,
|
|
pin: null,
|
|
tt: null,
|
|
pinError: null,
|
|
loggedInWithKnownPassword: false,
|
|
hasSmallLogo: false,
|
|
hasMediumLogo: false,
|
|
hasLargeLogo: false,
|
|
smallLogoUrl: null,
|
|
mediumLogoUrl: null,
|
|
largeLogoUrl: null,
|
|
errorBadCreds: false,
|
|
reveal: false,
|
|
formState: { errorBoxMessage: null }
|
|
};
|
|
},
|
|
computed: {
|
|
copyright() {
|
|
return sockeyeVersion.copyright;
|
|
},
|
|
version() {
|
|
return sockeyeVersion.version;
|
|
}
|
|
},
|
|
async created() {
|
|
const vm = this;
|
|
window.$gz.eventBus.$emit("menu-change", {
|
|
isMain: true,
|
|
icon: "",
|
|
title: null
|
|
});
|
|
//reset password redirects here with preset creds
|
|
//this will help the user identify it and hopefully remember it
|
|
if (vm.$route.params.presetLogin) {
|
|
vm.input.username = vm.$route.params.presetLogin;
|
|
vm.input.password = vm.$route.params.presetPassword;
|
|
}
|
|
|
|
vm.smallLogoUrl = window.$gz.api.logoUrl("small");
|
|
vm.mediumLogoUrl = window.$gz.api.logoUrl("medium");
|
|
vm.largeLogoUrl = window.$gz.api.logoUrl("large");
|
|
try {
|
|
let res = await window.$gz.api.get("notify/hello");
|
|
if (res.data != null) {
|
|
vm.input.username = "ss";
|
|
vm.input.password = "ss";
|
|
vm.reveal = true; //might as well show it since it's the default anyway
|
|
|
|
//However, if the eval users exist then
|
|
vm.hasSmallLogo = res.data.sl;
|
|
vm.hasMediumLogo = res.data.ml;
|
|
vm.hasLargeLogo = res.data.ll;
|
|
}
|
|
|
|
//Live eval from website redirects here with generated creds set
|
|
// ...login?usr={username}&pw={pw}
|
|
//anything in window.location.search is placed in the 'search' route parameter by app.vue for parsing here because they are lost when the redirect happens to the login form as it's actually app.vue that gets hit first not login
|
|
//Note that the $route.query system does not work no matter what I tried and seems to be broken in vue router so have to do it ourselves from the
|
|
//Handle search query params
|
|
if (vm.$route.params.search != undefined) {
|
|
let params = new URLSearchParams(vm.$route.params.search);
|
|
if (params.has("usr")) {
|
|
vm.input.username = params.get("usr");
|
|
vm.input.password = params.get("pw");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
//squash it, this isn't critical
|
|
}
|
|
},
|
|
methods: {
|
|
async tfaVerify() {
|
|
//
|
|
//send 2fa code to server if ok, then proceed as normal
|
|
const vm = this;
|
|
if (vm.pin && vm.pin != "") {
|
|
vm.pinError = null;
|
|
|
|
try {
|
|
const res = await window.$gz.api.upsert(
|
|
"auth/tfa-authenticate",
|
|
{
|
|
pin: vm.pin,
|
|
tempToken: vm.tt
|
|
},
|
|
true
|
|
);
|
|
|
|
if (res.error) {
|
|
//don't expect this to ever get called but just in case
|
|
// throw new Error(res.error);
|
|
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
|
|
}
|
|
await this.step2(res);
|
|
} catch (error) {
|
|
//bad PIN?
|
|
if (
|
|
error.message &&
|
|
error.message.includes("ErrorUserNotAuthenticated")
|
|
) {
|
|
vm.pinError = vm.authPinInvalid;
|
|
return;
|
|
}
|
|
//server closed by server state setting?
|
|
if (error.code == 2000 || error.code == 2001 || error.code == 2006) {
|
|
vm.formState.errorBoxMessage = error.message;
|
|
return;
|
|
}
|
|
//probably here because server unresponsive.
|
|
if (error.message) {
|
|
let msg = error.message;
|
|
if (
|
|
msg.includes("NetworkError") ||
|
|
msg.includes("Failed to fetch")
|
|
) {
|
|
msg =
|
|
"Could not connect to Sockeye server at " +
|
|
window.$gz.api.APIUrl("") +
|
|
"\r\nError: " +
|
|
error.message;
|
|
}
|
|
//this just makes the error a little cleaner to remove the extraneous typeerror
|
|
msg = msg.replace(" TypeError:", "");
|
|
vm.formState.errorBoxMessage = msg;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
cancelTfaVerify() {
|
|
const vm = this;
|
|
vm.tt = null;
|
|
vm.pin = null;
|
|
vm.errorBadCreds = false;
|
|
vm.pinError = [];
|
|
vm.input.username = null;
|
|
vm.input.password = null;
|
|
vm.tfaDialog = false;
|
|
},
|
|
showFooterLogo() {
|
|
return (
|
|
this.showCustomSmallLogo() ||
|
|
this.showCustomMediumLogo() ||
|
|
this.showCustomLargeLogo()
|
|
);
|
|
},
|
|
showSmallBrandLogo() {
|
|
return !this.hasMediumLogo && this.$vuetify.breakpoint.smAndDown;
|
|
},
|
|
showLargeBrandLogo() {
|
|
if (this.showSmallBrandLogo()) {
|
|
return false;
|
|
}
|
|
if (this.showCustomSmallLogo()) {
|
|
return false;
|
|
}
|
|
if (this.showCustomMediumLogo()) {
|
|
return false;
|
|
}
|
|
if (this.showCustomLargeLogo()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
showCustomSmallLogo() {
|
|
return this.hasMediumLogo && this.$vuetify.breakpoint.smAndDown;
|
|
},
|
|
showCustomMediumLogo() {
|
|
return this.hasMediumLogo && this.$vuetify.breakpoint.mdOnly;
|
|
},
|
|
showCustomLargeLogo() {
|
|
return this.hasLargeLogo && this.$vuetify.breakpoint.lgAndUp;
|
|
},
|
|
trialUserSelected(item) {
|
|
this.input.password = item.p;
|
|
this.input.username = item.l;
|
|
},
|
|
trialHelpClick: function() {
|
|
//open help nav for trial login
|
|
window.$gz.eventBus.$emit("menu-click", {
|
|
key: "app:help",
|
|
data: "sock-evaluate#sample-users"
|
|
});
|
|
},
|
|
onEnterUserName: function() {
|
|
//move focus to password
|
|
document.getElementsByName("password")[0].focus();
|
|
},
|
|
async login() {
|
|
const vm = this;
|
|
|
|
if (vm.input.username != "" && vm.input.password != "") {
|
|
vm.errorBadCreds = false;
|
|
|
|
try {
|
|
const res = await window.$gz.api.upsert(
|
|
"auth",
|
|
{
|
|
login: vm.input.username,
|
|
password: vm.input.password
|
|
},
|
|
true
|
|
);
|
|
if (res.error) {
|
|
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
|
|
}
|
|
//check for 2fa enabled, if so then need to do one more step before process login can be called
|
|
if (res.data.tfa) {
|
|
this.authTwoFactor = res.data.authTwoFactor;
|
|
this.authEnterPin = res.data.authEnterPin;
|
|
this.authVerifyCode = res.data.authVerifyCode;
|
|
this.authPinInvalid = res.data.authPinInvalid;
|
|
this.tt = res.data.tt;
|
|
this.cancel = res.data.cancel;
|
|
this.pin = null;
|
|
//prompt for 2fa
|
|
this.tfaDialog = true;
|
|
return;
|
|
}
|
|
|
|
await this.step2(res);
|
|
} catch (error) {
|
|
if (
|
|
error.message &&
|
|
error.message.includes("License agreement consent required")
|
|
) {
|
|
window.location.reload();
|
|
}
|
|
//bad creds?
|
|
if (
|
|
error.message &&
|
|
error.message.includes("ErrorUserNotAuthenticated")
|
|
) {
|
|
vm.errorBadCreds = true;
|
|
return;
|
|
}
|
|
//server closed by server state setting?
|
|
if (error.code == 2000 || error.code == 2001 || error.code == 2006) {
|
|
vm.formState.errorBoxMessage = error.message;
|
|
return;
|
|
}
|
|
//probably here because server unresponsive.
|
|
if (error.message) {
|
|
let msg = error.message;
|
|
|
|
if (
|
|
msg.includes("NetworkError") ||
|
|
msg.includes("Failed to fetch")
|
|
) {
|
|
msg =
|
|
"Could not connect to Sockeye server at " +
|
|
window.$gz.api.APIUrl("") +
|
|
"\r\nError: " +
|
|
error.message;
|
|
}
|
|
//this just makes the error a little cleaner to remove the extraneous typeerror
|
|
msg = msg.replace(" TypeError:", "");
|
|
vm.formState.errorBoxMessage = msg;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async step2(res) {
|
|
const vm = this;
|
|
await processLogin(res.data, vm.loggedInWithKnownPassword);
|
|
//check if support and updates has expired and is paid for license and show warning if so
|
|
//but not to customer logins, that's just shaming and will anger people
|
|
if (
|
|
!vm.$store.getters.isCustomerUser &&
|
|
vm.$store.state.globalSettings.maintenanceExpired &&
|
|
(vm.$store.state.globalSettings.licenseStatus == 3 ||
|
|
vm.$store.state.globalSettings.licenseStatus == 4)
|
|
) {
|
|
(async function() {
|
|
await window.$gz.dialog.displayLTModalNotificationMessage(
|
|
"MaintenanceExpiredNote",
|
|
"MaintenanceExpired",
|
|
"error",
|
|
"https://ayanova.com/docs/adm-license/#maintenance-expired-message"
|
|
);
|
|
})();
|
|
}
|
|
|
|
const toPath = vm.$route.params.topath; //set in app.vue::mounted
|
|
if (toPath != undefined) {
|
|
//open the url indicated
|
|
vm.$router.push(vm.$route.params.topath);
|
|
} else {
|
|
vm.$router.push(vm.$store.state.homePage);
|
|
}
|
|
}
|
|
},
|
|
beforeRouteEnter(to, from, next) {
|
|
//very important as this in conjunction with the menu options means
|
|
//navigation guards work properly by just sending people here
|
|
next(() => {
|
|
processLogout();
|
|
next();
|
|
});
|
|
}
|
|
};
|
|
</script>
|