Added new authentication methods using authKey

and cookies.  Also now handles changes to base url for
use with reverse proxies
This commit is contained in:
Peter Morton 2023-07-26 17:01:36 -05:00
parent 8f3e7578e7
commit 588f90feb6
16 changed files with 1356 additions and 364 deletions

View File

@ -1 +1,2 @@
VITE_EO_SERVICES_URL=http://localhost:3000
VITE_ROUTER_BASE=/

View File

@ -1 +1,2 @@
VITE_EO_SERVICES_URL=https://eo-services.mortons.site/api
VITE_EO_SERVICES_URL=https://eo-services.mortons.site
VITE_ROUTER_BASE=/eo-services

View File

@ -9,7 +9,8 @@ RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/dist /app/eo-services
COPY nginx.conf /etc/nginx/nginx.conf
COPY headers.js /etc/nginx/headers.js
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

6
headers.js Normal file
View File

@ -0,0 +1,6 @@
// /etc/nginx/headers.js
function headers_json(r) {
return JSON.stringify(r.headersIn)
}
export default {headers_json};

View File

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- baseHref %>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

@ -1,3 +1,5 @@
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
@ -8,20 +10,41 @@ events {
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
js_import headers.js;
js_set $headers_json headers.headers_json;
# Using custom log format here
log_format main '$remote_addr'
'\t$remote_user'
'\t$time_local'
'\t$request'
'\t$status'
'\t$headers_json';
# log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
location /eo-services {
mirror /mirror;
root /app;
index index.html;
try_files $uri $uri/ /index.html;
try_files $uri $uri/ /eo-services/index.html;
}
location /mirror {
internal;
proxy_pass https://eo-services.mortons.site/api/auth;
proxy_set_header X-Original-URI $request_uri;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;

1154
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,12 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
"format": "prettier . --write",
"push": "docker buildx bake --push"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
@ -17,9 +18,11 @@
"d3": "^7.8.5",
"d3-sankey": "^0.12.3",
"uuid": "^9.0.0",
"vite-plugin-html": "^3.2.0",
"vue": "^3.2.37",
"vue-router": "^4.1.5",
"vue-sidebar-menu": "^5.2.8"
"vue-sidebar-menu": "^5.2.8",
"vue3-cookies": "^1.0.6"
},
"devDependencies": {
"@types/d3-sankey": "^0.12.1",

View File

@ -1,102 +1,18 @@
<script>
import { h, markRaw } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { SidebarMenu } from "vue-sidebar-menu";
import "vue-sidebar-menu/dist/vue-sidebar-menu.css";
const separator = {
template: '<hr style="border-color: rgba(0, 0, 0, 0.1); margin: 20px;">',
}
const faIcon = (props) => {
return {
element: markRaw({
render: () => h("div", [h(FontAwesomeIcon, { size: "lg", ...props })]),
}),
};
};
export default {
name: "App",
components: {
SidebarMenu,
},
data() {
return {
menu: [
{
header: "Services",
hiddenOnCollapse: true,
},
{
href: "/",
title: "Home",
icon: faIcon({ icon: "fa-solid fa-house" }),
},
{
href: "/about",
title: "About",
icon: faIcon({ icon: "fa-solid fa-question" }),
},
{
component: markRaw(separator),
},
{
href: "/referenceId",
title: "Contacts Lookup",
icon: faIcon({ icon: "fa-solid fa-rectangle-list" }),
},
{
href: "/interactionsFlow",
title: "Interactions Flow",
icon: faIcon({ icon: "fa-solid fa-route" }),
},
{
href: "/customerAccount/12345",
title: "Customer Account Example",
icon: faIcon({ icon: "fa-solid fa-id-card" }),
},
],
collapsed: false,
};
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize);
},
methods: {
onResize() {
if (window.innerWidth <= 767) {
this.isOnMobile = true;
this.collapsed = true;
} else {
this.isOnMobile = false;
this.collapsed = false;
}
},
},
};
</script>
<template>
<sidebar-menu
v-model:collapsed="collapsed"
:menu="menu"
:show-one-child="true"
/>
<div id="services" :class="[{ collapsed: collapsed }]">
<div class="services">
<div class="container">
<router-view></router-view>
</div>
<div class="flex-container">
<router-view class="view sidebar" name="SideBarView"></router-view>
<div class="flex-child">
<router-view class="view main-content"></router-view>
</div>
</div>
<!-- <div>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/referenceId">Reference ID Tracker</router-link>
<router-link to="/interactionsFlow">Interactions Flow</router-link>
<router-link to="/customerAccount">Customer Account</router-link>
</div> -->
</template>
<style lang="scss">
@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600");
@ -114,35 +30,17 @@ body {
color: #262626;
}
#services {
padding-left: 290px;
transition: 0.3s ease;
}
#services.collapsed {
padding-left: 65px;
}
#services.onmobile {
padding-left: 65px;
}
.sidebar-overlay {
position: fixed;
width: 100%;
.flex-container {
display: flex;
height: 100%;
top: 0;
left: 0;
background-color: #000;
opacity: 0.5;
z-index: 900;
}
.services {
padding: 50px;
.flex-child {
flex: 1;
height: 100%;
border: 2px solid yellow;
}
.container {
max-width: 900px;
}
</style>
// .flex-child:first-child {
// margin-right: 20px;
// } </style>

View File

@ -1,44 +1,57 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import ReferenSearchByReferenceViewceID from "../views/SearchByReferenceView.vue";
import AboutView from "../views/AboutView.vue";
import DebugFrameView from "../views/DebugFrameView.vue";
import SideBarView from "../views/SideBarView.vue";
import SearchByReferenceView from "../views/SearchByReferenceView.vue";
import InteractionsFlowView from "../views/InteractionsFlowView.vue";
import CustomerAccountView from "../views/CustomerAccountView.vue";
const routes = [
{
path: "/",
name: "home",
component: HomeView,
components: { default: HomeView, SideBarView: SideBarView },
props: { default: false, SideBarView: true }
},
{
path: "/about",
name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
components: { default: AboutView, SideBarView: SideBarView },
props: { default: false, SideBarView: true }
},
{
path: "/referenceId",
name: "referenceId",
component: ReferenSearchByReferenceViewceID,
components: { default: SearchByReferenceView, SideBarView: SideBarView },
props: { default: route => ({ sessionIdentifier: route.query._sessionIdentifier }) }
},
{
path: "/interactionsFlow",
name: "interactionsFlow",
component: InteractionsFlowView,
components: { default: InteractionsFlowView, SideBarView: SideBarView },
props: { default: route => ({ sessionIdentifier: route.query._sessionIdentifier }) }
},
{
path: "/customerAccount/:accountNumber",
name: "customerAccount",
component: CustomerAccountView,
props: true,
components: { default: CustomerAccountView, SideBarView: SideBarView },
props: { default: true, SideBarView: true }
},
{
path: "/debug",
name: "debug",
component: DebugFrameView,
components: { default: DebugFrameView },
props: { default: true }
},
];
const router = createRouter({
history: createWebHistory(),
base: `${import.meta.env.VITE_ROUTER_BASE}`,
routes,
});

View File

@ -0,0 +1,23 @@
<script>
import { useCookies } from "vue3-cookies";
const { cookies } = useCookies();
export default {
name: "DebugFrameView",
data() {
return {
route : JSON.stringify(this.$route, null, 2),
cookies : JSON.stringify(cookies.keys(), null, 2),
}
}
}
</script>
<template>
<h1>Adaptive Frame Debug</h1>
<h2>Route information</h2>
<div>{{ route }}</div>
<h2>Cookie information</h2>
<div>{{ cookies }}</div>
</template>

View File

@ -11,8 +11,5 @@
>
Integration.
</p>
<h2>Services Available</h2>
<h3><a href="/referenceId">Reference ID</a></h3>
<p>/referenceId</p>
</div>
</template>

View File

@ -5,6 +5,12 @@ import * as d3 from "d3";
import * as align from "d3-sankey";
import { sankey, sankeyLinkHorizontal } from "d3-sankey";
import { v4 as uuidv4 } from "uuid";
import ErrorMessage from "../components/ErrorMessage.vue";
const props = defineProps({
username: { type: String, default: "" },
sessionIdentifier: { type: String, default: "" }
});
const filterOptions = ["Channel", "SubChannel", "Queue", "Outcome"];
const multiSelected = ref(filterOptions);
@ -22,29 +28,39 @@ function fetchData() {
//clear errors
errorMessage.value = null;
fetch(
`${import.meta.env.VITE_EO_SERVICES_URL}/api/interactions-flow?filter=${
multiSelected.value
}`,
{
credentials: "include", // fetch won't send cookies unless you set credentials
}
)
.then((response) => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = response.data || response.statusText;
return Promise.reject(error);
var authKey;
if (props.sessionIdentifier.length > 0) {
authKey = props.sessionIdentifier;
} else if (props.username.length > 0) {
authKey = props.username;
} else {
errorMessage.value = "_sessionIdentifier or username must be passed as query params."
}
if (authKey) {
fetch(
`${import.meta.env.VITE_EO_SERVICES_URL}/api/interactions-flow?filter=${multiSelected.value
}&authKey=${authKey}`,
{
credentials: "include", // fetch won't send cookies unless you set credentials
}
response.json().then((data) => {
generateSankey(data);
)
.then((response) => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = response.data || response.statusText;
return Promise.reject(error);
}
response.json().then((data) => {
generateSankey(data);
});
})
.catch((error) => {
console.log(error);
errorMessage.value = error;
});
})
.catch((error) => {
console.log(error);
errorMessage.value = error;
});
}
}
function generateSankey(data) {
@ -80,9 +96,9 @@ function generateSankey(data) {
.nodeId((d) => d.name)
.nodeAlign(
align[
`sankey${alignSelected.value[0].toUpperCase()}${alignSelected.value.slice(
1
)}`
`sankey${alignSelected.value[0].toUpperCase()}${alignSelected.value.slice(
1
)}`
]
) // d3.sankeyLeft, etc.
.nodeWidth(15)
@ -154,10 +170,10 @@ function generateSankey(data) {
linkColor === "source-target"
? (d) => `url(#${d.uid})`
: linkColor === "source"
? (d) => color(d.source.category)
: linkColor === "target"
? (d) => color(d.target.category)
: linkColor
? (d) => color(d.source.category)
: linkColor === "target"
? (d) => color(d.target.category)
: linkColor
)
.attr("stroke-width", (d) => Math.max(1, d.width));
@ -194,6 +210,7 @@ function generateSankey(data) {
</div>
<div id="chart"></div>
<ErrorMessage v-if="errorMessage" :message="errorMessage" />
</template>
<style scoped>
svg {

View File

@ -5,6 +5,11 @@ import ContactTable from "../components/ContactTable.vue";
import ContactsSummary from "../components/ContactsSummary.vue";
import ErrorMessage from "../components/ErrorMessage.vue";
const props = defineProps({
username: { type: String, default: "" },
sessionIdentifier: { type: String, default: "" }
});
const referenceId = ref("");
const contactData = ref(null);
const errorMessage = ref(null);
@ -17,29 +22,40 @@ function fetchData() {
//clear errors
errorMessage.value = null;
contactData.value = null;
fetch(
`${
import.meta.env.VITE_EO_SERVICES_URL
}/api/unified-data-gateway?referenceId=${referenceId.value}`,
{
credentials: "include", // fetch won't send cookies unless you set credentials
}
)
.then((response) => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = response.data || response.statusText;
return Promise.reject(error);
var authKey;
if (props.sessionIdentifier.length > 0) {
authKey = props.sessionIdentifier;
} else if (props.username.length > 0) {
authKey = props.username;
} else {
errorMessage.value = "_sessionIdentifier or username must be passed as query params."
}
if (authKey) {
fetch(
`${import.meta.env.VITE_EO_SERVICES_URL
}/api/unified-data-gateway?referenceId=${referenceId.value}&authKey=${authKey}`,
{
credentials: "include", // fetch won't send cookies unless you set credentials
}
response.json().then((data) => {
contactData.value = data;
)
.then((response) => {
// check for error response
if (!response.ok) {
// get error message from body or default to response statusText
const error = response.data || response.statusText;
return Promise.reject(error);
}
response.json().then((data) => {
contactData.value = data;
});
})
.catch((error) => {
console.log(error);
errorMessage.value = error;
});
})
.catch((error) => {
console.log(error);
errorMessage.value = error;
});
}
}
</script>
<template>
@ -47,22 +63,14 @@ function fetchData() {
<h2>Search Criteria</h2>
<div>
<label for="referenceId">Reference ID: </label>
<input
id="referenceId"
:value="referenceId"
placeholder="enter Reference ID here"
@input="onInput"
/>
<input id="referenceId" :value="referenceId" placeholder="enter Reference ID here" @input="onInput" />
<button @click="fetchData">Search Contacts</button>
</div>
<h2>Results</h2>
<ContactsSummary v-if="contactData" :summary="contactData.data.summary" />
<div v-else>No Contacts Found</div>
<ContactTable
v-if="contactData"
:table-data="contactData.data.findContactsCompletedBetween.edges"
/>
<ContactTable v-if="contactData" :table-data="contactData.data.findContactsCompletedBetween.edges" />
<ErrorMessage v-if="errorMessage" :message="errorMessage" />
</div>
</template>

95
src/views/SideBarView.vue Normal file
View File

@ -0,0 +1,95 @@
<script>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { SidebarMenu } from "vue-sidebar-menu";
import "vue-sidebar-menu/dist/vue-sidebar-menu.css";
import { h, markRaw } from "vue";
const separator = {
template: '<hr style="border-color: rgba(0, 0, 0, 0.1); margin: 20px;">',
}
const faIcon = (props) => {
return {
element: markRaw({
render: () => h("div", [h(FontAwesomeIcon, { size: "lg", ...props })]),
}),
};
};
export default {
name: "SideBarView",
components: {
SidebarMenu,
},
// props: {
// sidebar: {
// type: Boolean,
// required: false
// }
// },
data() {
return {
menu: [
{
header: "Services",
hiddenOnCollapse: true,
},
{
href: "/",
title: "Home",
icon: faIcon({ icon: "fa-solid fa-house" }),
},
{
href: "/about",
title: "About",
icon: faIcon({ icon: "fa-solid fa-question" }),
},
{
component: markRaw(separator),
},
{
href: "/referenceId",
title: "Contacts Lookup",
icon: faIcon({ icon: "fa-solid fa-rectangle-list" }),
},
{
href: "/interactionsFlow",
title: "Interactions Flow",
icon: faIcon({ icon: "fa-solid fa-route" }),
},
{
href: "/customerAccount/12345",
title: "Customer Account Example",
icon: faIcon({ icon: "fa-solid fa-id-card" }),
},
],
collapsed: false,
};
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize);
console.log()
},
methods: {
onResize() {
if (window.innerWidth <= 767) {
this.isOnMobile = true;
this.collapsed = true;
} else {
this.isOnMobile = false;
this.collapsed = false;
}
},
},
}
</script>
<template>
<div v-if="this.$route.query.sidebar" id="sidebar">
<sidebar-menu v-model:collapsed="collapsed" :menu="menu" :show-one-child="true" :relative="true"/>
</div>
</template>
<style>
</style>

View File

@ -1,12 +1,79 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { createHtmlPlugin } from "vite-plugin-html";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler',
},
export default defineConfig(({ command }) => {
if (command === "serve") {
return {
// dev specific config
plugins: [
vue(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: "src/main.js",
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: "index.html",
/**
* Data that needs to be injected into the index.html ejs template
*/
inject: {
data: {
baseHref: '<base href="/"/>',
},
},
}),
],
base: "/",
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler",
},
},
};
} else {
// command === 'build'
return {
// build specific config
plugins: [
vue(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: "src/main.js",
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: "index.html",
/**
* Data that needs to be injected into the index.html ejs template
*/
inject: {
data: {
baseHref: '<base href="/eo-services/"/>',
},
},
}),
],
base: "/eo-services/",
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler",
},
},
};
}
});