diff --git a/components.d.ts b/components.d.ts index 3aa6149..1813313 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,6 +7,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AutoLogout: typeof import('./src/components/AutoLogout.vue')['default'] ChatMessage: typeof import('./src/components/ChatMessage.vue')['default'] ChatWidget: typeof import('./src/components/ChatWidget.vue')['default'] ChatWidgetOld: typeof import('./src/components/ChatWidgetOld.vue')['default'] diff --git a/public/js/inactivity.js b/public/js/inactivity.js deleted file mode 100644 index 75a52a6..0000000 --- a/public/js/inactivity.js +++ /dev/null @@ -1,110 +0,0 @@ -import * as countdownTimer from '../../src/assets/inactivity-timer.js' - -// holds the idle duration in ms (current value = 301 seconds) -var timeoutIntervalInMillis = 5 * 60 * 1000 + 1000; -// holds the timeout variables for easy destruction and reconstruction of the setTimeout hooks -var timeHook = null; - -function initializeTimeHook() { - // this method has the purpose of creating our timehooks and scheduling the call to our logout function when the idle time has been reached - if (timeHook == null) { - timeHook = setTimeout( function () { destroyTimeHook(); logout(); }.bind(this), timeoutIntervalInMillis); - } -} - -function destroyTimeHook() { - // this method has the sole purpose of destroying any time hooks we might have created - clearTimeout(timeHook); - timeHook = null; -} - -function resetTimeHook(event) { - // this method replaces the current time hook with a new time hook - destroyTimeHook(); - initializeTimeHook(); - countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000); - // show event type, element and coordinates of the click - // console.log(event.type + " at " + event.currentTarget); - // console.log("Coordinates: " + event.clientX + ":" + event.clientY); - console.log("Reset inactivity of a user"); -} - -function setupListeners() { - // here we setup the event listener for the mouse click operation - document.addEventListener("click", resetTimeHook); - document.addEventListener("mousemove", resetTimeHook); - document.addEventListener("mousedown", resetTimeHook); - document.addEventListener("keypress", resetTimeHook); - document.addEventListener("touchmove", resetTimeHook); - console.log("Listeners for user inactivity activated"); -} - -function destroyListeners() { - // here we destroy event listeners for the mouse click operation - document.removeEventListener("click", resetTimeHook); - document.removeEventListener("mousemove", resetTimeHook); - document.removeEventListener("mousedown", resetTimeHook); - document.removeEventListener("keypress", resetTimeHook); - document.removeEventListener("touchmove", resetTimeHook); - console.log("Listeners for user inactivity deactivated"); -} - -function logout() { - destroyListeners(); - countdownTimer.destroyCountdownTimer(); - console.log("Logging you out due to inactivity.."); - const logoffButton = document.getElementById("logout"); - logoffButton.click(); -} - -async function makeObpApiCall() { - //debug - console.log("calling API"); - let timeoutInSeconds; - try { - let obpApiHost = document.getElementById("nav"); - console.log(obpApiHost); - if(obpApiHost) { - obpApiHost = obpApiHost.href.split("?")[0]; - } - - const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`); - const json = await response.json(); - if(json.timeout_in_seconds) { - timeoutInSeconds = json.timeout_in_seconds; - console.log(`Suggested value ${timeoutInSeconds} is used`); - } else { - timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds - console.log(`Default value ${timeoutInSeconds} is used`); - } - } catch (e) { - console.error(e); - timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds, even if the session timeout endpoint is not reachable for whatever reason - console.log(`Default value ${timeoutInSeconds} is used`); - } - return timeoutInSeconds; -} - -async function getSuggestedSessionTimeout() { - if(!sessionStorage.getItem("suggested-session-timeout-in-seconds")) { - let timeoutInSeconds = await makeObpApiCall(); - sessionStorage.setItem("suggested-session-timeout-in-seconds", timeoutInSeconds); - } - return sessionStorage.getItem("suggested-session-timeout-in-seconds") * 1000 + 1000; // We need timeout in millis -} - -// self executing function to trigger the operation on page load -(async function () { - timeoutIntervalInMillis = await getSuggestedSessionTimeout(); // Try to get suggested value - const logoffButton = document.getElementById("countdown-timer-span"); - if(logoffButton) { - // to prevent any lingering timeout handlers preventing memory leaks - destroyTimeHook(); - // setup a fresh time hook - initializeTimeHook(); - // setup initial event listeners - setupListeners(); - // Reset countdown timer - countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000); - } -})(); \ No newline at end of file diff --git a/server/controllers/UserController.ts b/server/controllers/UserController.ts index 98b6db3..4664b61 100644 --- a/server/controllers/UserController.ts +++ b/server/controllers/UserController.ts @@ -49,10 +49,17 @@ export class UserController { this.oauthInjectedService.requestTokenKey = undefined this.oauthInjectedService.requestTokenSecret = undefined session['clientConfig'] = undefined - if(!this.obpExplorerHome) { - console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`) + + if (request.query.redirect) { + response.redirect(request.query.redirect as string) + } else { + if(!this.obpExplorerHome) { + console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`) + } + response.redirect(this.obpExplorerHome) } - response.redirect(this.obpExplorerHome) + + return response } diff --git a/server/middlewares/OauthAccessTokenMiddleware.ts b/server/middlewares/OauthAccessTokenMiddleware.ts index 880d130..dbf2255 100644 --- a/server/middlewares/OauthAccessTokenMiddleware.ts +++ b/server/middlewares/OauthAccessTokenMiddleware.ts @@ -64,15 +64,31 @@ export default class OauthAccessTokenMiddleware implements ExpressMiddlewareInte console.log(`OauthAccessTokenMiddleware.ts use says: clientConfig: ${JSON.stringify(clientConfig)}`) session['clientConfig'] = clientConfig console.log('OauthAccessTokenMiddleware.ts use says: Seems OK, redirecting..') + + let redirectPage: String + const obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST if(!obpExplorerHome) { console.error(`VITE_OBP_API_EXPLORER_HOST: ${obpExplorerHome}`) } - console.log(`OauthAccessTokenMiddleware.ts use says: Will redirect to: ${obpExplorerHome}`) + + if (session['redirectPage']) { + try { + redirectPage = session['redirectPage'] + + } catch (e) { + console.log('OauthAccessTokenMiddleware.ts use says: Error decoding redirect URI') + redirectPage = obpExplorerHome + } + } else { + redirectPage = obpExplorerHome + } + + console.log(`OauthAccessTokenMiddleware.ts use says: Will redirect to: ${redirectPage}`) console.log('OauthAccessTokenMiddleware.ts use says: Here comes the session:') console.log(session) - response.redirect(`${obpExplorerHome}`) + response.redirect(redirectPage) } } ) diff --git a/server/middlewares/OauthRequestTokenMiddleware.ts b/server/middlewares/OauthRequestTokenMiddleware.ts index 19011fe..c9cfedb 100644 --- a/server/middlewares/OauthRequestTokenMiddleware.ts +++ b/server/middlewares/OauthRequestTokenMiddleware.ts @@ -39,6 +39,13 @@ export default class OauthRequestTokenMiddleware implements ExpressMiddlewareInt console.debug('process.env.VITE_OBP_API_PORTAL_HOST:', process.env.VITE_OBP_API_PORTAL_HOST) const oauthService = this.oauthInjectedService const consumer = oauthService.getConsumer() + const redirectPage = request.query.redirect + const session = request.session + + if (redirectPage) { + session['redirectPage'] = redirectPage + } + consumer.getOAuthRequestToken((error: any, oauthTokenKey: string, oauthTokenSecret: string) => { if (error) { const errorStr = JSON.stringify(error) diff --git a/src/assets/inactivity-timer.js b/src/assets/inactivity-timer.js deleted file mode 100644 index 93b4882..0000000 --- a/src/assets/inactivity-timer.js +++ /dev/null @@ -1,36 +0,0 @@ -function addSeconds(date, seconds) { - date.setSeconds(date.getSeconds() + seconds); - return date; -} - -export function showCountdownTimer() { - - // Get current date and time - var now = new Date().getTime(); - let distance = countDownDate - now; - - // Output the result in an element with id="countdown-timer-span" - let elementId = ("countdown-timer-span"); - document.getElementById(elementId).innerHTML = "in " + Math.floor(distance / 1000) + "s"; - - // If the count down is over release resources - if (distance < 0) { - destroyCountdownTimer(); - } -} - - -// Set the date we're counting down to -let countDownDate = addSeconds(new Date(), 5); - -let showTimerInterval = null; - -export function destroyCountdownTimer() { - clearInterval(showTimerInterval); -} - -export function resetCountdownTimer(seconds) { - destroyCountdownTimer(); // Destroy previous timer if any - countDownDate = addSeconds(new Date(), seconds); // Set the date we're counting down to - showTimerInterval = setInterval(showCountdownTimer, 1000); // Update the count down every 1 second -} \ No newline at end of file diff --git a/src/components/AutoLogout.vue b/src/components/AutoLogout.vue new file mode 100644 index 0000000..75a0e4f --- /dev/null +++ b/src/components/AutoLogout.vue @@ -0,0 +1,157 @@ +<script setup lang="ts"> +import { ElNotification, NotificationHandle } from 'element-plus'; +import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue'; + +// Props can be defined with defineProps +const props = defineProps({ + // Define your props here +}); + +// Types of events that will reset the timeout +const events = ['click', 'mousemove', 'keydown', 'keypress', 'mousedown', 'scroll', 'load']; + +// Set timers +let warningTimeout: NodeJS.Timeout; +let logoutTimeout: NodeJS.Timeout; + +let logoutTime: number; +let countdownInterval: NodeJS.Timeout; + +// Add these variables at the top of your script +let defaultWarningDelay = 1000 * 270; // 4.5 minutes by default +let defaultLogoutDelay = 1000 * 300; // 5 minutes by default + + +// Methods +function setTimers(warningDelay = defaultWarningDelay, logoutDelay = defaultLogoutDelay) { + logoutTime = Date.now() + logoutDelay; + + warningTimeout = setTimeout(warningMessage, warningDelay); // 4 seconds for development, change later + logoutTimeout = setTimeout(logout, logoutDelay); // 15 seconds for development, change later +} + +let warningNotification: NotificationHandle; + +async function getOBPSuggestedTimeout() { + const obpApiHost = import.meta.env.VITE_OBP_API_HOST; + let timeoutInSeconds: number; + // Fetch the suggested timeout from the OBP API + + const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`); + const json = await response.json(); + if(json.timeout_in_seconds) { + timeoutInSeconds = json.timeout_in_seconds; + console.log(`Suggested value ${timeoutInSeconds} is used`); + } else { + timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds + console.log(`Default value ${timeoutInSeconds} is used`); + } + + return timeoutInSeconds; +} + +function resetTimeout() { + // Logic to reset the timeout + clearTimeout(warningTimeout); + clearTimeout(logoutTimeout); + clearInterval(countdownInterval); + + + if (warningNotification) { + warningNotification.close(); + } + + setTimers(); +} + +function warningMessage() { + // Logic to show warning message + console.log('Warning: You will be logged out soon'); + + let secondsLeft = ref(Math.ceil((logoutTime - Date.now()) / 1000)); + // Update the countdown every second + countdownInterval = setInterval(() => { + secondsLeft.value = Math.ceil((logoutTime - Date.now()) / 1000); + + // If time's up or almost up, clear the interval + if (secondsLeft.value <= 0) { + clearInterval(countdownInterval); + return; + } + + + }, 1000); + + warningNotification = ElNotification({ + title: 'Inactivity Warning', + message: () => h('p', null, [ + h('span', null, 'You will be logged out in'), + h('strong', { style: 'color: red' }, ` ${secondsLeft.value} `), + h('span', null, 'seconds.'), + ]) + , + type: 'warning', + duration: 0, + position: 'top-left', + showClose: false, + }) +} + +function logout() { + // Logic to log out the user + console.log('Logging out...'); + document.getElementById("logoff")?.click(); // If the ID of the logout button changes, this will not work +} + +// Lifecycle hooks +onMounted(() => { + events.forEach(event => { + window.addEventListener(event, resetTimeout); + }) + + setTimers(); + + // Update with API suggested values when available + getOBPSuggestedTimeout().then(timeoutInSeconds => { + // Convert to milliseconds + const logoutDelay = timeoutInSeconds * 1000; + // Set warning to appear 30 seconds before logout + const warningDelay = Math.max(logoutDelay - 30000, 0); + + // Update the defaults + defaultWarningDelay = warningDelay; + defaultLogoutDelay = logoutDelay; + + // Reset timers with new values + resetTimeout(); + }).catch(error => { + console.error("Failed to get suggested timeout:", error); + // Continue with defaults + }); +}); + +onBeforeUnmount(() => { + // Cleanup code before component is unmounted + clearTimeout(warningTimeout); + clearTimeout(logoutTimeout); + clearInterval(countdownInterval); + events.forEach(event => { + window.removeEventListener(event, resetTimeout); + }); +}); + + + + +</script> + +<style scoped> +/* Your component styles here */ +</style> + + +<template> + <div> + <!-- Your component content here --> + </div> +</template> \ No newline at end of file diff --git a/src/components/ChatWidget.vue b/src/components/ChatWidget.vue index 96cf513..2bb6f21 100644 --- a/src/components/ChatWidget.vue +++ b/src/components/ChatWidget.vue @@ -4,7 +4,7 @@ placeholder for Opey II Chat widget <script lang="ts"> import { ref, reactive } from 'vue' -import { Close, Top as ElTop } from '@element-plus/icons-vue' +import { Close, Top as ElTop, WarnTriangleFilled } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import ChatMessage from './ChatMessage.vue'; import { v4 as uuidv4 } from 'uuid'; @@ -17,6 +17,7 @@ export default { return { Close, ElTop, + WarnTriangleFilled, } }, data() { @@ -24,6 +25,7 @@ export default { chatOpen: false, input: '', lastUserMessasgeFailed: false, + errorState: <{ type?: "authenticationError", message?: string, icon?: any }> {}, // add types of error as needed chat: useChat(), } }, @@ -34,15 +36,7 @@ export default { this.chat = useChat() const isLoggedIn = await this.checkLoginStatus() console.log('Is logged in: ', isLoggedIn) - if (isLoggedIn) { - try { - await this.chat.handleAuthentication() - } catch (error) { - console.error('Error in chat:', error); - ElMessage.error('Failed to authenticate.') - } - - } + }, methods: { async toggleChat() { @@ -53,7 +47,15 @@ export default { const currentResponseKeys = Object.keys(currentUser) if (currentResponseKeys.includes('username')) { if (!this.chat.userIsAuthenticated) { - await this.chat.handleAuthentication() + try { + await this.chat.handleAuthentication() + } catch (error) { + console.error('Error in chat:', error); + this.errorState.type = "authenticationError" + this.errorState.message = "Woops! Looks like we are having trouble connecting to Opey..." + this.errorState.icon = WarnTriangleFilled + } + } return true } else { @@ -114,7 +116,14 @@ export default { <el-button type="danger" :icon="Close" @click="toggleChat" size="small" circle></el-button> </el-header> <el-main> - <div v-if="!chat.userIsAuthenticated" class="login-container"> + + <div v-if="errorState.type === 'authenticationError'" class="login-container"> + <el-icon :size="40" color="#FF4D4F"> + <component :is="errorState.icon" /> + </el-icon> + <p class="login-message" size="large">{{ errorState.message }}</p> + </div> + <div v-else-if="!chat.userIsAuthenticated" class="login-container"> <p class="login-message" size="large">Opey is only available once logged on.</p> <a href="/api/connect" class="login-button router-link">Log on</a> </div> diff --git a/src/components/HeaderNav.vue b/src/components/HeaderNav.vue index 69429cd..49a86d3 100644 --- a/src/components/HeaderNav.vue +++ b/src/components/HeaderNav.vue @@ -60,7 +60,8 @@ const headerLinksBackgroundColor = ref(headerLinksBackgroundColorSetting) const clearActiveTab = () => { const activeLinks = document.querySelectorAll('.router-link') for (const active of activeLinks) { - if (active.id) { + // Skip login and logoff buttons + if (active.id && active.id !== 'login' && active.id !== 'logoff') { active.style.backgroundColor = 'transparent' active.style.color = '#39455f' } @@ -112,6 +113,12 @@ watchEffect(() => { } } }) + +const getCurrentPath = () => { + const currentPath = route.path + return currentPath +} + </script> <template> @@ -157,11 +164,11 @@ watchEffect(() => { <arrow-down /> </el-icon> </span>--> - <a v-bind:href="'/api/connect'" v-show="isShowLoginButton" class="login-button router-link"> + <a v-bind:href="'/api/connect?redirect='+ encodeURIComponent(getCurrentPath())" v-show="isShowLoginButton" class="login-button router-link" id="login"> {{ $t('header.login') }} </a> <span v-show="isShowLogOffButton" class="login-user">{{ loginUsername }}</span> - <a v-bind:href="'/api/user/logoff'" v-show="isShowLogOffButton" class="logoff-button router-link"> + <a v-bind:href="'/api/user/logoff?redirect=' + encodeURIComponent(getCurrentPath())" v-show="isShowLogOffButton" class="logoff-button router-link" id="logoff"> {{ $t('header.logoff') }} </a> </RouterView> @@ -227,8 +234,8 @@ nav { cursor: pointer; } -.login-button, -.logoff-button { +a.login-button, +a.logoff-button { margin: 5px; color: #ffffff; background-color: #32b9ce; diff --git a/src/components/Preview.vue b/src/components/Preview.vue index d28bea8..76fbb9d 100644 --- a/src/components/Preview.vue +++ b/src/components/Preview.vue @@ -264,10 +264,16 @@ const onError = (error) => { <template> <main> - <el-form ref="requestFormRef" :model="requestForm"> + <el-form ref="requestFormRef" :model="requestForm" @submit.prevent> <el-form-item prop="url"> <div class="flex-request-preview-panel"> - <input type="text" v-model="url" :set="(requestForm.url = url)" id="search-input" /> + <input + type="text" + v-model="url" + :set="(requestForm.url = url)" + id="search-input" + @keyup.enter="submit(requestFormRef, submitRequest)" + /> <el-button :type="type" id="search-button" diff --git a/src/obp/resource-docs.ts b/src/obp/resource-docs.ts index 6a20997..cdb0794 100644 --- a/src/obp/resource-docs.ts +++ b/src/obp/resource-docs.ts @@ -37,6 +37,14 @@ export async function getOBPResourceDocs(apiStandardAndVersion: string): Promise return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp`) } + +export async function getOBPDynamicResourceDocs(apiStandardAndVersion: string): Promise<any> { + const logMessage = `Loading Dynamic Docs for ${apiStandardAndVersion}` + console.log(logMessage) + updateLoadingInfoMessage(logMessage) + return await get(`/obp/${OBP_API_VERSION}/resource-docs/${apiStandardAndVersion}/obp?content=dynamic`) +} + export function getFilteredGroupedResourceDocs(apiStandardAndVersion: string, tags: any, docs: any): Promise<any> { console.log(docs); if (apiStandardAndVersion === undefined || docs === undefined || docs[apiStandardAndVersion] === undefined) return Promise.resolve<any>({}) @@ -70,6 +78,20 @@ export async function cacheDoc(cacheStorageOfResourceDocs: any): Promise<any> { const scannedAPIVersions = apiVersions.scanned_api_versions const resourceDocsMapping: any = {} for (const { apiStandard, API_VERSION } of scannedAPIVersions) { + + // we need this to cache the dynamic entities resource doc + if (API_VERSION === 'dynamic-entity') { + const logMessage = `Caching Dynamic API { standard: ${apiStandard}, version: ${API_VERSION} }` + console.log(logMessage) + if (apiStandard) { + const version = `${apiStandard.toUpperCase()}${API_VERSION}` + const resourceDocs = await getOBPDynamicResourceDocs(version) + if (version && Object.keys(resourceDocs).includes('resource_docs')) + resourceDocsMapping[version] = resourceDocs + } + updateLoadingInfoMessage(logMessage) + continue + } const logMessage = `Caching API { standard: ${apiStandard}, version: ${API_VERSION} }` console.log(logMessage) if (apiStandard) { diff --git a/src/stores/chat.ts b/src/stores/chat.ts index e25252c..44a62af 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -178,7 +178,7 @@ export const useChat = defineStore('chat', { } } catch (error) { - console.error('Error creating session:', error); + throw new Error(`Failed to create Opey session: ${error}`); } }, diff --git a/src/views/BodyView.vue b/src/views/BodyView.vue index c213ffe..da2ae49 100644 --- a/src/views/BodyView.vue +++ b/src/views/BodyView.vue @@ -28,14 +28,26 @@ <script setup lang="ts"> import SearchNav from '../components/SearchNav.vue' import Menu from '../components/Menu.vue' +import AutoLogout from '../components/AutoLogout.vue' import ChatWidget from '../components/ChatWidget.vue' -import Collections from '../components/Collections.vue' -import { inject } from 'vue' +import { onMounted, ref } from 'vue' +import { getCurrentUser } from '../obp' + +const isLoggedIn = ref(false); + +onMounted(async () => { + const currentUser = await getCurrentUser() + const currentResponseKeys = Object.keys(currentUser) + isLoggedIn.value = currentResponseKeys.includes('username') +}) + const isChatbotEnabled = import.meta.env.VITE_CHATBOT_ENABLED === 'true' </script> <template> + + <AutoLogout v-if=isLoggedIn /> <el-container class="root"> <el-aside class="search-nav" width="20%"> <!--Left-->