Files
raven-client/ayanova/src/views/login.vue
2023-04-25 00:03:57 +00:00

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>