HttpOnly cookies are not visible to browser side javascript. Work around using oidc-token-service and username/password config :(
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.env
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Example Vue Applications
|
# Example Vue Applications
|
||||||
|
|
||||||
This project provides some example Vue applications that are designed to run within the [Channel Automation: Adaptive Framework](https://em-docs.verint.com/15_3/em-integration/Content/AdaptiveFramework/Adaptive_Framework_Components.htm?Highlight=Adaptive%20Framework).
|
This project provides some example Vue applications that are designed to run within the [Channel Automation: Adaptive Framework](https://em-docs.verint.com/15_3/em-integration/Content/AdaptiveFramework/Adaptive_Framework_Components.htm?Highlight=Adaptive%20Framework).
|
||||||
@@ -11,23 +10,56 @@ Provides debug infomation on cookies, [authentication](https://em-docs.verint.co
|
|||||||
|
|
||||||
### /telephonyContext
|
### /telephonyContext
|
||||||
|
|
||||||
Displays Telephony Context Information. This route has been designed to be used withing a Call Interaction.
|
Displays Telephony Context Information. This route has been designed to be used withing a Call Interaction.
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
Parameters prefixed with 'tps:' will fetch values from the Tenant Properties Service.
|
Parameters prefixed with 'tps:' will fetch values from the Tenant Properties Service.
|
||||||
|
|
||||||
| name | description |
|
| name | description |
|
||||||
| --- | --- |
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
| ani | Automatic Number Identification |
|
| ani | Automatic Number Identification |
|
||||||
| dnis | Dialed Number Identification Service |
|
| dnis | Dialed Number Identification Service |
|
||||||
| queue | Queue the Call was in before it was delivered to the agent |
|
| queue | Queue the Call was in before it was delivered to the agent |
|
||||||
|direction | INBOUND, OUTBOUND etc. |
|
| direction | INBOUND, OUTBOUND etc. |
|
||||||
| channel | AmazonConnect etc. |
|
| channel | AmazonConnect etc. |
|
||||||
| type | Voice, Voicemail etc. |
|
| type | Voice, Voicemail etc. |
|
||||||
| transferSummary | Mock of Transfer Summary |
|
| transferSummary | Mock of Transfer Summary |
|
||||||
| integrationCardTitle | Title over Integration card |
|
| integrationCardTitle | Title over Integration card |
|
||||||
| integrationCardDoc | HTML document for contents of integration card |
|
| integrationCardDoc | HTML document for contents of integration card |
|
||||||
|
|
||||||
|
# Installing into an Innovation Lab
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Make sure npm is installed and node us up to date
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install npm
|
||||||
|
sudo npm install -g n
|
||||||
|
sudo n stable
|
||||||
|
hash -r
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build the project
|
||||||
|
|
||||||
|
Clone this project, install and build.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /opt/em/projects/current/demo/static
|
||||||
|
git clone https://git.mortons.site/verint.com/ca_vue_apps.git
|
||||||
|
cd ca_vue_apps
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proxy Apache to the distribution
|
||||||
|
|
||||||
|
Add the following directive to the apache site configuration:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
@@ -45,11 +77,19 @@ List options using:
|
|||||||
npm run
|
npm run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Overrided API Values using a .env file. For example:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_ORIGIN=https://em20.verint.live
|
||||||
|
VITE_API_USERNAME=apiclient
|
||||||
|
VITE_API_PASSWORD=apiclient
|
||||||
|
```
|
||||||
|
|
||||||
## CORS Work Around
|
## CORS Work Around
|
||||||
|
|
||||||
When developing this on localhost against Channel Automation APIs hosted elsewhere, you will need to apply a work around for CORS because the preflight check will fail with a 401 Not Authorized error.
|
When developing this on localhost against Channel Automation APIs hosted elsewhere, you will need to apply a work around for CORS because the preflight check will fail with a 401 Not Authorized error.
|
||||||
|
|
||||||
The fix is to use Apache Rewrite on the API host server to respond OK to the preflight check. For example, with 'tenant-properties-service' API:
|
The fix is to use Apache Rewrite on the API host server to respond OK to the preflight check. For example, with 'tenant-properties-service' API:
|
||||||
|
|
||||||
```apache
|
```apache
|
||||||
<Location /tenant-properties-service>
|
<Location /tenant-properties-service>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||||
export const routerBase = import.meta.env.VITE_ROUTER_BASE;
|
export const routerBase = import.meta.env.VITE_ROUTER_BASE;
|
||||||
|
export const apiUsername = import.meta.env.VITE_API_USERNAME;
|
||||||
|
export const apiPassword = import.meta.env.VITE_API_PASSWORD;
|
||||||
|
export const apiOrigin = import.meta.env.VITE_API_ORIGIN;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
routerBase,
|
routerBase,
|
||||||
|
apiUsername,
|
||||||
|
apiPassword,
|
||||||
|
apiOrigin,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,30 +4,76 @@ import type { HydraCollection } from "../@types/HydraCollection";
|
|||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import type { VueCookies } from "vue-cookies";
|
import type { VueCookies } from "vue-cookies";
|
||||||
import { inject } from "vue";
|
import { inject } from "vue";
|
||||||
|
import { apiOrigin, apiUsername, apiPassword } from "../app.config.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const COOKIE_PREFIX = "__Host-VRNTOTCT";
|
export const COOKIE_PREFIX = "__Host-CA_API";
|
||||||
|
|
||||||
export function getChannnelAutomationAPI():
|
export async function getChannnelAutomationAPI(): Promise<
|
||||||
| Readonly<ChannelAutomationAPI>
|
Readonly<ChannelAutomationAPI> | undefined
|
||||||
| undefined {
|
> {
|
||||||
const $cookies = inject<VueCookies>("$cookies");
|
const $cookies = inject<VueCookies>("$cookies");
|
||||||
if ($cookies) {
|
if ($cookies) {
|
||||||
for (const cookieKey of $cookies.keys()) {
|
for (const cookieKey of $cookies.keys()) {
|
||||||
if (cookieKey.startsWith(COOKIE_PREFIX)) {
|
if (cookieKey.startsWith(COOKIE_PREFIX)) {
|
||||||
const authCookie = $cookies.get(cookieKey) as string;
|
return $cookies.get(cookieKey) as ChannelAutomationAPI;
|
||||||
const jwtDecoded = jwtDecode(authCookie);
|
|
||||||
if (jwtDecoded && jwtDecoded.iss) {
|
|
||||||
const issSplit = jwtDecoded.iss.split("/oidc-token-service/");
|
|
||||||
return {
|
|
||||||
host: issSplit[0],
|
|
||||||
tenant: issSplit[1],
|
|
||||||
authentication: authCookie,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We did not find a cookie fall back to oidc-token-service
|
||||||
|
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
body.append("grant_type", "password");
|
||||||
|
body.append("username", apiUsername);
|
||||||
|
body.append("password", apiPassword);
|
||||||
|
body.append("client_id", "default");
|
||||||
|
body.append(
|
||||||
|
"scope",
|
||||||
|
"oidc tags context_entitlements content_entitlements em_api_access",
|
||||||
|
);
|
||||||
|
|
||||||
|
const origin = apiOrigin ? apiOrigin : location.origin;
|
||||||
|
|
||||||
|
return await axios
|
||||||
|
.post(`${origin}/oidc-token-service/default/token`, body, { headers })
|
||||||
|
.then((response) => {
|
||||||
|
console.debug(response);
|
||||||
|
const data = response.data as {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwtDecoded = jwtDecode(data.access_token);
|
||||||
|
if (jwtDecoded && jwtDecoded.iss) {
|
||||||
|
const issSplit = jwtDecoded.iss.split("/oidc-token-service/");
|
||||||
|
|
||||||
|
const caAPI = {
|
||||||
|
host: issSplit[0],
|
||||||
|
tenant: issSplit[1],
|
||||||
|
authentication: data.access_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($cookies) {
|
||||||
|
$cookies.set(
|
||||||
|
COOKIE_PREFIX,
|
||||||
|
caAPI,
|
||||||
|
"1h",
|
||||||
|
"/",
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
"Strict",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return caAPI;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`oidc-token-service ${error}`);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
`getChannelAutomationAPI no valid cookie found. Please check that a cookie with ${COOKIE_PREFIX} prefix exists.`,
|
`getChannelAutomationAPI no valid cookie found. Please check that a cookie with ${COOKIE_PREFIX} prefix exists.`,
|
||||||
);
|
);
|
||||||
@@ -37,7 +83,7 @@ export function getChannnelAutomationAPI():
|
|||||||
export async function getTenantProperty(
|
export async function getTenantProperty(
|
||||||
key?: string,
|
key?: string,
|
||||||
): Promise<Array<TenantProperty> | undefined> {
|
): Promise<Array<TenantProperty> | undefined> {
|
||||||
const channelAutomationAPI = getChannnelAutomationAPI();
|
const channelAutomationAPI = await getChannnelAutomationAPI();
|
||||||
if (!channelAutomationAPI) {
|
if (!channelAutomationAPI) {
|
||||||
throw new Error("no channel automation api details");
|
throw new Error("no channel automation api details");
|
||||||
}
|
}
|
||||||
@@ -72,6 +118,4 @@ export async function getTenantProperty(
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
// console.error('getTenantProperty: rejecting');
|
|
||||||
// return Promise.reject();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ import "@jobinsjp/vue3-datatable/dist/style.css";
|
|||||||
|
|
||||||
import ErrorMessage from "../components/ErrorMessage.vue";
|
import ErrorMessage from "../components/ErrorMessage.vue";
|
||||||
|
|
||||||
import {
|
import { getChannnelAutomationAPI, getTenantProperty } from "../helpers";
|
||||||
COOKIE_PREFIX,
|
|
||||||
getChannnelAutomationAPI,
|
|
||||||
getTenantProperty,
|
|
||||||
} from "../helpers";
|
|
||||||
import { jwtDecode } from "jwt-decode";
|
|
||||||
import { TenantProperty } from "../@types/TenantProperty";
|
import { TenantProperty } from "../@types/TenantProperty";
|
||||||
|
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
|
const host = ref(location.host);
|
||||||
|
|
||||||
const $cookies = inject<VueCookies>("$cookies");
|
const $cookies = inject<VueCookies>("$cookies");
|
||||||
const data = ref([{ name: "", value: "" }]);
|
const data = ref([{ name: "", value: "" }]);
|
||||||
@@ -24,35 +21,24 @@ const tps = ref([] as Array<TenantProperty> | undefined);
|
|||||||
const channelAutomationAPI = ref({} as ChannelAutomationAPI | undefined);
|
const channelAutomationAPI = ref({} as ChannelAutomationAPI | undefined);
|
||||||
const jwtDecoded = ref({} as JSONWebToken);
|
const jwtDecoded = ref({} as JSONWebToken);
|
||||||
|
|
||||||
if ($cookies) {
|
|
||||||
const key = "__Host-VRNTOTCT404fdb6f";
|
|
||||||
$cookies.set(
|
|
||||||
key,
|
|
||||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4MThlZDMxMmVkOGRhNTVkZWZkM2EzZmI0OGY1NjQzMWFjMWMwMmEzZjZkMmFkMjVjNDA5ZmEwOTA1NDU3ZTkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZGVmYXVsdCJdLCJhenAiOiJkZWZhdWx0IiwiY29tLnZlcmludC5pc0V4dGVybmFsbHlBdXRoZW50aWNhdGVkIjpmYWxzZSwiY29tLnZlcmludC5sb2dpblJlc3VsdCI6MSwiY29udGVudF9lbnRpdGxlbWVudHMiOlsiUmVnaXN0ZXJlZFVzZXJDb250ZW50Il0sImNvbnRleHRfZW50aXRsZW1lbnRzIjpbIkNvbnRleHRVcGRhdGVyQVBJQWNjZXNzIl0sImVtX2FwaV9hY2Nlc3MiOlsic2VjdXJlbXNnX21lc3NhZ2Vfc2VhcmNoIiwiZW1haWxfb3V0Z29pbmdfZm9yd2FyZCIsImR5bmFtaWNlbnRpdHlfY3JlYXRlIiwiZHluYW1pY2VudGl0eV91cGRhdGUiLCJhY2Nlc3NfYXR0YWNobWVudF91cGxvYWQiLCJhY2Nlc3NfYXR0YWNobWVudF9kb3dubG9hZCIsImVtYWlsbWFpbGJveF9zZWFyY2giLCJlbWFpbF9vdXRnb2luZ19yZXBseSIsImFjY2Vzc19jYXNlX2NyZWF0ZSIsIkNhc2VBdHRhY2htZW50VmlldyIsImVudGl0eV9maWVsZF9hdWRpdF9yZWFkIiwiYWNjZXNzX2FnZW50X3VwZGF0ZSIsIkNhc2VBdHRhY2htZW50VXBsb2FkIiwiY3VzdG9tZXJfcmVhZCIsImVtYWlsbWFpbGJveF9jb25maWd1cmUiLCJ3aWFzX3JlYWQiLCJhY2Nlc3NfdWRnX3JlYWQiLCJhY2Nlc3NfY3VzdG9tZXJfY3JlYXRlIiwic2VjdXJlbXNnX2NvbnZlcnNhdGlvbl9jcmVhdGUiLCJzZWN1cmVtc2dfbWVzc2FnZV9jcmVhdGUiLCJzZWN1cmVtc2dfY29udmVyc2F0aW9uX3JlYWQiLCJzZWN1cmVtc2dfY29udmVyc2F0aW9uX3VwZGF0ZSIsInNlY3VyZW1zZ19tZXNzYWdlX2RlbGV0ZSIsImFjY2Vzc19jdXN0b21lcl9kZWxldGUiLCJhY2Nlc3NfYWdlbnRfY3JlYXRlIiwiYWNjZXNzX2FnZW50X3JlYWQiLCJhY2Nlc3NfdGVuYW50X3Byb3BlcnRpZXNfdXBkYXQiLCJhY2Nlc3NfdGVuYW50X3Byb3BlcnRpZXNfYmF0Y2giLCJhY2Nlc3NfYXR0YWNobWVudF9kZWxldGUiLCJlbWFpbF9vdXRnb2luZ19jcmVhdGUiLCJwcmVmZXJlbmNlc19kZXNrdG9wbG9jYWxlIiwiY2FzZV91cGRhdGUiLCJwcmVmZXJlbmNlc19jb250ZW50bG9jYWxlIl0sImV4cCI6MTcwMzIxMTg3MCwiaWF0IjoxNzAzMTc1ODcwLCJpc3MiOiJodHRwczovL2VtMjAudmVyaW50LmxpdmU6NDQzL29pZGMtdG9rZW4tc2VydmljZS9kZWZhdWx0IiwicmVhbG0iOiIvZGVmYXVsdCIsInN1YiI6ImFwaWNsaWVudCJ9.E7i3kL2kzZzYvpe-sxUxdBxALUe2hFzrf66DuSqubAj45ATwRIC7iKA6S-q0qd7opz6PCV4jw169u-V1VUsS4H31Oi7tSkBXETWAyLlw0BTctoPiKegb18QAobzAwGhdPF7zggeM2W6pvqsMbO1XrpfAop52gqF0Ww1ZePFLP9RI9OzS0sTWkjnDrGGDExSrJ_tYRs4jUgOsH5H-QNvg58QJ8KxFJzloUVhCHZR2LU1uckAvq19kxPt_iBUwxNoB-6C68qjHpzO_q9-EcnoPYaGj6igMp5u8qIoemuoWMPLN1R8o6bJ6glDmdSB4PB42irQHi_qMoFp0Lu_FamujnQ",
|
|
||||||
"1d",
|
|
||||||
"/",
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
"Strict",
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = $cookies.get(key) as string;
|
|
||||||
console.log(`Cookie with value [${value}]`);
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if ($cookies) {
|
if ($cookies) {
|
||||||
data.value = $cookies?.keys().map(function (value) {
|
data.value = $cookies?.keys().map(function (value) {
|
||||||
if (value.startsWith(COOKIE_PREFIX)) {
|
|
||||||
jwtDecoded.value = jwtDecode($cookies.get(value) as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: value,
|
name: value,
|
||||||
value: $cookies.get(value) as string,
|
value: $cookies.get(value) as string,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
channelAutomationAPI.value = getChannnelAutomationAPI();
|
getChannnelAutomationAPI()
|
||||||
|
.then((result) => {
|
||||||
|
channelAutomationAPI.value = result;
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
errorMessage.value = error.message;
|
||||||
|
console.log(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
getTenantProperty()
|
getTenantProperty()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
tps.value = result;
|
tps.value = result;
|
||||||
@@ -67,7 +53,8 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<h1>Channel Automation Vua Applications - Debugging</h1>
|
<h1>Channel Automation Vua Applications - Debugging</h1>
|
||||||
|
<h2>Location Information</h2>
|
||||||
|
<p>Host {{ host }}</p>
|
||||||
<h2>Cookie information</h2>
|
<h2>Cookie information</h2>
|
||||||
<DataTable :rows="data"></DataTable>
|
<DataTable :rows="data"></DataTable>
|
||||||
|
|
||||||
|
|||||||
3
src/vite-env.d.ts
vendored
3
src/vite-env.d.ts
vendored
@@ -3,6 +3,9 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string;
|
readonly VITE_API_BASE_URL: string;
|
||||||
readonly VITE_ROUTER_BASE: string;
|
readonly VITE_ROUTER_BASE: string;
|
||||||
|
readonly VITE_API_USERNAME: string;
|
||||||
|
readonly VITE_API_PASSWORD: string;
|
||||||
|
readonly VITE_API_ORIGIN: string;
|
||||||
// more env variables...
|
// more env variables...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user