Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/components/explorer/Pagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const prevRow = (): void => {
watch(() => props.cpage, (newValue, oldValue) => {
if ((newValue !== oldValue) && pageExists(newValue)) {
pageInput.value = newValue;
verifyRow();
}
});

Expand Down
63 changes: 63 additions & 0 deletions app/src/composables/useXmlViewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ref, computed, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import { useQuery } from '@vue/apollo-composable';
import { XML_VIEWER } from '@/modules/gql/xml-gql';

export function useXmlViewer() {
const route = useRoute();
const store = useStore();

const xmlViewer = ref<any>({});

const { result, loading, refetch } = useQuery(
XML_VIEWER,
computed(() => ({
input: {
id: route.params.id,
isNewCuration: route.query?.isNewCuration
? JSON.parse(route.query.isNewCuration as string)
: false,
},
})),
{ fetchPolicy: 'cache-and-network' }
);

watch(
result,
(data) => {
if (data) {
xmlViewer.value = data.xmlViewer;
}
},
{ immediate: true }
);

const isAuth = computed(() => store.getters['auth/isAuthenticated']);
const isAdmin = computed(() => store.getters['auth/isAdmin']);
const dialogBoxActive = computed(() => store.getters.dialogBox);

const closeDialogBox = () => {
store.commit('setDialogBox');
};

const approveCuration = () => {
store.commit('setDialogBox');
};

const approval = async () => {
await store.dispatch('explorer/curation/approveCuration', { xml: xmlViewer.value });
};

return {
xmlViewer,
loading,
refetch,
isAuth,
isAdmin,
dialogBoxActive,
closeDialogBox,
approveCuration,
approval,
};
}
40 changes: 40 additions & 0 deletions app/src/modules/auth-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* JWT auth utilities for frontend token validation and cleanup.
* Decodes JWT payload via base64 (no secret needed) to check expiry.
*/

export const AUTH_STORAGE_KEYS = [
'token',
'userId',
'displayName',
'surName',
'givenName',
'isAdmin',
'tokenExpiration',
];

export function clearAuthStorage (): void {
AUTH_STORAGE_KEYS.forEach((key) => localStorage.removeItem(key));
}

export function getTokenExp (token: string): number | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(atob(parts[1]));
return typeof payload.exp === 'number' ? payload.exp : null;
} catch {
return null;
}
}

export function isTokenExpired (token: string): boolean {
const exp = getTokenExp(token);
if (exp === null) return true;
return Date.now() >= exp * 1000;
}

export function forceLogout (): void {
clearAuthStorage();
window.location.href = '/nm';
}
44 changes: 41 additions & 3 deletions app/src/modules/gql/apolloClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from '@apollo/client/core';
import {
ApolloClient,
ApolloLink,
InMemoryCache,
HttpLink,
Observable,
from,
} from '@apollo/client/core';
import { onError } from '@apollo/client/link/error';
import { isTokenExpired, forceLogout } from '../auth-utils';

const BASE = window.location.origin;
const uri = `${BASE}/api/graphql`;
const httpLink = new HttpLink({ uri });

const authLink = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem('token') || null;

if (token && isTokenExpired(token)) {
forceLogout();
return new Observable((observer) => {
observer.error(new Error('Token expired'));
observer.complete();
});
}

operation.setContext({
headers: {
authorization: token ? `${token}` : '',
Expand All @@ -15,8 +32,29 @@ const authLink = new ApolloLink((operation, forward) => {
return forward(operation);
});

const errorLink = onError(({ graphQLErrors, networkError }) => {
const authMessages = ['not authenticated', 'jwt expired', 'invalid token', 'jwt malformed'];

if (graphQLErrors) {
for (const err of graphQLErrors) {
const msg = (err.message || '').toLowerCase();
if (authMessages.some((keyword) => msg.includes(keyword))) {
forceLogout();
return;
}
}
}

if (networkError && 'statusCode' in networkError) {
const status = (networkError as any).statusCode;
if (status === 401 || status === 403) {
forceLogout();
}
}
});

export default new ApolloClient({
uri: uri,
link: authLink.concat(httpLink), // Chain auth token with the HttpLink
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
});
14 changes: 3 additions & 11 deletions app/src/pages/explorer/xml/Xml.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@
<!-- @ts-ignore -->
<template v-slot:pagination>
<Pagination
v-if="xmlFinder && xmlFinder.xmlData"
v-if="xmlFinder && xmlFinder.xmlData && xmlFinder.totalPages > 0"
:cpage="pageNumber"
:tpages="xmlFinder.totalPages || 1"
:tpages="xmlFinder.totalPages"
@go-to-page="loadPrevNextImage($event)"
/>
</template>
Expand All @@ -194,7 +194,7 @@
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onActivated, watch, watchEffect } from 'vue';
import { ref, computed, onMounted, watch, watchEffect } from 'vue';
import { useStore } from 'vuex';
import { useRouter, useRoute } from 'vue-router';
import { useQuery } from '@vue/apollo-composable';
Expand Down Expand Up @@ -507,12 +507,4 @@ onMounted(() => {
paramsReady.value = true;
});
});

// Refetch data when component is activated (e.g., when navigating back from detail view)
onActivated(() => {
paramsReady.value = false;
initializeGallery().finally(() => {
paramsReady.value = true;
});
});
</script>
50 changes: 48 additions & 2 deletions app/src/pages/explorer/xml/XmlHistory.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
<template>
<div class="xmlLoader">
<Dialog :minWidth="40" :active="dialogBoxActive">
<template #title>{{ approvalInProgress ? 'Confirmation' : 'Success' }}</template>
<template #content>
<div class="u_display-flex u_centralize_items u--margin-posmd">
<md-icon class="u--font-emph-smm u--margin-pos" style="color: green"
>check_circle</md-icon
>
<span>{{
approvalInProgress
? 'Please confirm your submission'
: 'XML has been approved and successfully ingested into the knowledge graph'
}}</span>
</div>
</template>
<template #actions>
<md-button v-if="approvalInProgress" @click="submitApproval">Submit</md-button>
<md-button @click.prevent="closeDialogBox">Close</md-button>
</template>
</Dialog>
<section class="u_width--max viz-u-postion__rel utility-roverflow" v-if="!yamlLoading">
<md-drawer
class="md-right"
Expand Down Expand Up @@ -71,6 +90,17 @@
<md-tooltip md-direction="top">Comment</md-tooltip>
<md-icon>comment</md-icon>
</md-button>

<md-button
@click="handleApproveCuration"
v-if="
isAuth && xmlViewer.status === 'Approved' && xmlViewer.curationState === 'Completed'
"
class="md-fab md-dense md-primary btn--primary"
>
<md-tooltip md-direction="top">Resubmit XML</md-tooltip>
<md-icon>check</md-icon>
</md-button>
</div>
</section>

Expand All @@ -84,7 +114,9 @@
import { ref, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useRouter, useRoute } from 'vue-router';
import { useXmlViewer } from '@/composables/useXmlViewer';
import Comment from '@/components/explorer/Comment.vue';
import Dialog from '@/components/Dialog.vue';
import Spinner from '@/components/Spinner.vue';
import TableComponent from '@/components/explorer/TableComponent.vue';

Expand All @@ -98,13 +130,15 @@ const store = useStore();
const router = useRouter();
const route = useRoute();

// Template refs
const codeBlock = ref<HTMLElement>();
// Composable
const { xmlViewer, isAuth, dialogBoxActive, approveCuration, approval, closeDialogBox } =
useXmlViewer();

// Reactive data
const showSidepanel = ref(false);
const type = ref('xml');
const yamlLoading = ref(false);
const approvalInProgress = ref(false);
const isSmallTabView = computed(() => {
return screen.width < 760;
});
Expand All @@ -128,6 +162,18 @@ const controlID = computed(() => {
const emptyState = computed(() => `No History Data Found for ${controlID.value}`);

// Methods
const handleApproveCuration = () => {
approvalInProgress.value = true;
approveCuration();
};

const submitApproval = async () => {
closeDialogBox();
approvalInProgress.value = false;
await approval();
store.dispatch('explorer/curation/fetchChangeLogs', xmlId.value);
};

const navBack = () => {
router.back();
};
Expand Down
Loading
Loading