627 lines
19 KiB
Vue
627 lines
19 KiB
Vue
<template>
|
|
<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"
|
|
@keyup.enter="tfaVerify"
|
|
autofocus
|
|
></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 cols="12" v-if="showSmallBrandLogo()">
|
|
<v-img
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
height="64"
|
|
></v-img>
|
|
</v-col>
|
|
<!-- Large AyaNova logo. -->
|
|
<v-col cols="7" class="ml-16" v-if="showLargeBrandLogo()">
|
|
<v-img
|
|
:src="require('../assets/logo.svg')"
|
|
contain
|
|
height="128"
|
|
></v-img>
|
|
</v-col>
|
|
<v-col cols="12" md="7" v-if="formState.errorBoxMessage">
|
|
<gz-error
|
|
:error-box-message="formState.errorBoxMessage"
|
|
></gz-error>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="7" v-if="showEvalUsers == true">
|
|
<v-select
|
|
v-model="selectedTrialUserId"
|
|
:items="selectLists.trialUsers"
|
|
item-text="name"
|
|
item-value="l"
|
|
label="Trial mode example users"
|
|
prepend-icon="$ayiQuestionCircle"
|
|
@click:prepend="trialHelpClick"
|
|
@input="trialUserSelected"
|
|
return-object
|
|
data-cy="selecttrialuser"
|
|
>
|
|
</v-select>
|
|
</v-col>
|
|
<v-col cols="12" md="7">
|
|
<v-text-field
|
|
name="username"
|
|
id="username"
|
|
v-model="input.username"
|
|
prepend-icon="$ayiUser"
|
|
autofocus
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
:error="errorBadCreds"
|
|
v-focus
|
|
@keyup.enter="onEnterUserName"
|
|
></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" md="7">
|
|
<v-text-field
|
|
name="password"
|
|
id="password"
|
|
v-model="input.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"
|
|
@click="login()"
|
|
value="LOGIN"
|
|
:data-cy="`loginbutton${errorBadCreds ? '_failedcreds' : ''}`"
|
|
>
|
|
<v-icon>$ayiSignIn</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
|
|
<template v-if="$ay.dev">
|
|
<v-col cols="12">
|
|
<span class="subtitle-2 secondary--text">DEVELOPMENT MODE</span>
|
|
</v-col>
|
|
</template>
|
|
</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 style="width: 100%;" class="mx-auto" v-if="showFooterLogo()">
|
|
<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>
|
|
</template>
|
|
<script>
|
|
import { processLogin, processLogout } from "../api/authutil";
|
|
import ayaNovaVersion from "../api/ayanova-version";
|
|
export default {
|
|
data() {
|
|
return {
|
|
input: {
|
|
username: "superuser",
|
|
password: "l3tm3in"
|
|
},
|
|
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 },
|
|
showEvalUsers: false,
|
|
selectedTrialUserId: 1,
|
|
selectLists: {
|
|
trialUsers: [
|
|
{
|
|
name: "AyaNova SuperUser - all",
|
|
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: "Service",
|
|
l: "Service",
|
|
p: "Service"
|
|
},
|
|
{
|
|
name: "Service - restricted",
|
|
l: "ServiceRestricted",
|
|
p: "ServiceRestricted"
|
|
},
|
|
{
|
|
name: "Head office",
|
|
l: "HeadOffice",
|
|
p: "HeadOffice"
|
|
},
|
|
{
|
|
name: "Inventory",
|
|
l: "Inventory",
|
|
p: "Inventory"
|
|
},
|
|
{
|
|
name: "Inventory - restricted",
|
|
l: "InventoryRestricted",
|
|
p: "InventoryRestricted"
|
|
},
|
|
{
|
|
name: "Operations",
|
|
l: "OpsAdmin",
|
|
p: "OpsAdmin"
|
|
},
|
|
{
|
|
name: "Operations - restricted",
|
|
l: "OpsAdminRestricted",
|
|
p: "OpsAdminRestricted"
|
|
},
|
|
{
|
|
name: "Sales",
|
|
l: "Sales",
|
|
p: "Sales"
|
|
},
|
|
{
|
|
name: "Sales - restricted",
|
|
l: "SalesRestricted",
|
|
p: "SalesRestricted"
|
|
},
|
|
{
|
|
name: "Subcontractor",
|
|
l: "SubContractor",
|
|
p: "SubContractor"
|
|
},
|
|
{
|
|
name: "Subcontractor - restricted",
|
|
l: "SubContractorRestricted",
|
|
p: "SubContractorRestricted"
|
|
},
|
|
{
|
|
name: "Technician",
|
|
l: "Tech",
|
|
p: "Tech"
|
|
},
|
|
{
|
|
name: "Technician - restricted",
|
|
l: "TechRestricted",
|
|
p: "TechRestricted"
|
|
},
|
|
{
|
|
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"
|
|
}
|
|
]
|
|
}
|
|
};
|
|
},
|
|
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.showEvalUsers = res.data.eval;
|
|
vm.hasSmallLogo = res.data.sl;
|
|
vm.hasMediumLogo = res.data.ml;
|
|
vm.hasLargeLogo = res.data.ll;
|
|
}
|
|
} catch (error) {
|
|
//squash it, this isn't critical
|
|
}
|
|
},
|
|
computed: {
|
|
copyright() {
|
|
return ayaNovaVersion.copyright;
|
|
},
|
|
version() {
|
|
return ayaNovaVersion.version;
|
|
}
|
|
},
|
|
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-start-trial-login"
|
|
});
|
|
},
|
|
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) {
|
|
//don't expect this to ever get called but just in case
|
|
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) {
|
|
//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
|
|
if (
|
|
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://www.ayanova.com/subscriptionexpired.htm"
|
|
);
|
|
})();
|
|
}
|
|
|
|
const toPath = vm.$route.params.topath; //set in app.vue::mounted
|
|
if (toPath != undefined) {
|
|
//check if it's an open report link and if so
|
|
//trigger that to open in a new window and continue on to normal home page
|
|
if (toPath.startsWith("/xxxxxxxxviewreport")) {
|
|
(async function() {
|
|
//open report links have a query string /viewreport?oid=[objectid]&rid=[reportid]
|
|
const searchParams = new URLSearchParams(vm.$route.params.search);
|
|
//const objectId = parseInt(searchParams.get("oid"));
|
|
const reportId = parseInt(searchParams.get("rid"));
|
|
//await window.$gz.api.renderReport(objectId, reportId); //objectid,reportid
|
|
|
|
const reportDataOptions = {
|
|
ReportId: reportId,
|
|
SelectedRowIds: [parseInt(searchParams.get("oid"))],
|
|
ClientMeta: window.$gz.api.reportClientMetaData()
|
|
};
|
|
try {
|
|
await vm.$refs.reportSelector.open(reportDataOptions, reportId);
|
|
} catch (ex) {
|
|
window.$gz.errorHandler.handleFormError(ex, vm);
|
|
}
|
|
})();
|
|
vm.$router.push(vm.$store.state.homePage);
|
|
} else {
|
|
//otherwise 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>
|