670 lines
21 KiB
Vue
670 lines
21 KiB
Vue
<template>
|
|
<div>
|
|
<div v-if="!lcr">
|
|
<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 AyaNova logo -->
|
|
<v-col v-if="showSmallBrandLogo()" cols="12">
|
|
<v-img
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
height="64"
|
|
></v-img>
|
|
</v-col>
|
|
<!-- Large AyaNova logo. -->
|
|
<v-col v-if="showLargeBrandLogo()" cols="7" class="ml-16">
|
|
<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 v-if="showEvalUsers == true" cols="12" md="7">
|
|
<v-select
|
|
v-model="selectedTrialUserId"
|
|
:items="selectLists.trialUsers"
|
|
item-text="name"
|
|
item-value="l"
|
|
label="Trial mode example users"
|
|
prepend-icon="$ayiQuestionCircle"
|
|
return-object
|
|
data-cy="selecttrialuser"
|
|
@click:prepend="trialHelpClick"
|
|
@input="trialUserSelected"
|
|
>
|
|
</v-select>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="7">
|
|
<v-text-field
|
|
id="username"
|
|
v-model="input.username"
|
|
v-focus
|
|
name="username"
|
|
prepend-icon="$ayiUser"
|
|
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 ? '$ayiEye' : '$ayiEyeSlash'"
|
|
prepend-icon="$ayiKey"
|
|
: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>$ayiSignIn</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')"
|
|
width="48px"
|
|
height="48px"
|
|
max-height="48px"
|
|
max-width="48px"
|
|
contain
|
|
></v-img>
|
|
</div>
|
|
<div class="mx-auto">AyaNova {{ 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">AyaNova {{ version }}</div>
|
|
<div class="mx-auto">
|
|
<span class="primary white--text text-caption">{{
|
|
copyright
|
|
}}</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</v-footer>
|
|
</div>
|
|
<div v-else>
|
|
<LICR :lcr="lcr" :pp="pp" @accepted="lcr = false"></LICR>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import { processLogin, processLogout } from "../api/authutil";
|
|
import ayaNovaVersion from "../api/ayanova-version";
|
|
import LICR from "../components/lcr-control.vue";
|
|
export default {
|
|
components: {
|
|
LICR
|
|
},
|
|
data() {
|
|
return {
|
|
input: {
|
|
username: null,
|
|
password: null
|
|
},
|
|
tfaDialog: false,
|
|
authTwoFactor: null,
|
|
authEnterPin: null,
|
|
authVerifyCode: null,
|
|
authPinInvalid: null,
|
|
cancel: null,
|
|
pin: null,
|
|
tt: null,
|
|
lcr: false,
|
|
pp: false, //case 4205
|
|
pinError: null,
|
|
loggedInWithKnownPassword: false,
|
|
hasSmallLogo: false,
|
|
hasMediumLogo: false,
|
|
hasLargeLogo: false,
|
|
smallLogoUrl: null,
|
|
mediumLogoUrl: null,
|
|
largeLogoUrl: null,
|
|
errorBadCreds: false,
|
|
reveal: false,
|
|
formState: { errorBoxMessage: null },
|
|
showEvalUsers: false,
|
|
selectedTrialUserId: 1,
|
|
selectLists: {
|
|
trialUsers: [
|
|
{
|
|
name: "SuperUser",
|
|
l: "superuser",
|
|
p: "l3tm3in"
|
|
},
|
|
{
|
|
name: "Accounting",
|
|
l: "Accounting",
|
|
p: "Accounting"
|
|
},
|
|
{
|
|
name: "Business admin",
|
|
l: "BizAdmin",
|
|
p: "BizAdmin"
|
|
},
|
|
{
|
|
name: "Business admin - restricted",
|
|
l: "BizAdminRestricted",
|
|
p: "BizAdminRestricted"
|
|
},
|
|
{
|
|
name: "Customer",
|
|
l: "Customer",
|
|
p: "Customer"
|
|
},
|
|
{
|
|
name: "Customer - restricted",
|
|
l: "CustomerRestricted",
|
|
p: "CustomerRestricted"
|
|
},
|
|
{
|
|
name: "Head office",
|
|
l: "HeadOffice",
|
|
p: "HeadOffice"
|
|
},
|
|
{
|
|
name: "Inventory",
|
|
l: "Inventory",
|
|
p: "Inventory"
|
|
},
|
|
{
|
|
name: "Inventory - restricted",
|
|
l: "InventoryRestricted",
|
|
p: "InventoryRestricted"
|
|
},
|
|
{
|
|
name: "Sales",
|
|
l: "Sales",
|
|
p: "Sales"
|
|
},
|
|
{
|
|
name: "Sales - restricted",
|
|
l: "SalesRestricted",
|
|
p: "SalesRestricted"
|
|
},
|
|
{
|
|
name: "Service manager",
|
|
l: "Service",
|
|
p: "Service"
|
|
},
|
|
{
|
|
name: "Service manager - restricted",
|
|
l: "ServiceRestricted",
|
|
p: "ServiceRestricted"
|
|
},
|
|
{
|
|
name: "Service Technician",
|
|
l: "Tech",
|
|
p: "Tech"
|
|
},
|
|
{
|
|
name: "Service Technician - restricted",
|
|
l: "TechRestricted",
|
|
p: "TechRestricted"
|
|
},
|
|
{
|
|
name: "Subcontractor",
|
|
l: "SubContractor",
|
|
p: "SubContractor"
|
|
},
|
|
{
|
|
name: "Subcontractor - restricted",
|
|
l: "SubContractorRestricted",
|
|
p: "SubContractorRestricted"
|
|
},
|
|
{
|
|
name: "System Operations",
|
|
l: "OpsAdmin",
|
|
p: "OpsAdmin"
|
|
},
|
|
{
|
|
name: "System Operations - restricted",
|
|
l: "OpsAdminRestricted",
|
|
p: "OpsAdminRestricted"
|
|
},
|
|
{
|
|
name: "Translation - Deutsch / German",
|
|
l: "de",
|
|
p: "de"
|
|
},
|
|
{
|
|
name: "Translation - Español / Spanish",
|
|
l: "es",
|
|
p: "es"
|
|
},
|
|
{
|
|
name: "Translation - Français / French",
|
|
l: "fr",
|
|
p: "fr"
|
|
}
|
|
]
|
|
}
|
|
};
|
|
},
|
|
computed: {
|
|
copyright() {
|
|
return ayaNovaVersion.copyright;
|
|
},
|
|
version() {
|
|
return ayaNovaVersion.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) {
|
|
//if the superuser exists with default credentials then use it to get them going unless the trial eval users exist in which case use bizadmin as the default
|
|
//as it has more rights to view things
|
|
if (res.data.eval == true) {
|
|
vm.input.username = "BizAdmin";
|
|
vm.input.password = "BizAdmin";
|
|
vm.reveal = true; //might as well show it since it's the default anyway
|
|
} else if (res.data.sudf == true) {
|
|
//superuser is default creds
|
|
vm.input.username = "superuser";
|
|
vm.input.password = "l3tm3in";
|
|
vm.reveal = true; //might as well show it since it's the default anyway
|
|
}
|
|
//However, if the eval users exist then
|
|
vm.showEvalUsers = res.data.eval;
|
|
vm.hasSmallLogo = res.data.sl;
|
|
vm.hasMediumLogo = res.data.ml;
|
|
vm.hasLargeLogo = res.data.ll;
|
|
vm.lcr = res.data.lcr; //license consent required flag
|
|
vm.pp = res.data.pp; //Perpetual build if true
|
|
}
|
|
|
|
//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 AyaNova 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: "ay-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;
|
|
vm.loggedInWithKnownPassword =
|
|
vm.input.username == "superuser" && vm.input.password == "l3tm3in";
|
|
|
|
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 AyaNova 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"
|
|
);
|
|
})();
|
|
}
|
|
|
|
//Show toast if out of date to non customer users, perpetual only ensured by server
|
|
if (
|
|
!vm.$store.getters.isCustomerUser &&
|
|
!vm.$store.state.globalSettings.showUpdateAvailable
|
|
) {
|
|
window.$gz.eventBus.$emit(
|
|
"notify-warning",
|
|
`AyaNova update available ${vm.$store.state.globalSettings.latestVersion}`,
|
|
vm.$store.state.globalSettings.changeLogUrl,
|
|
10000
|
|
);
|
|
// (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>
|