diff --git a/quasar.config.js b/quasar.config.js index c1c034f..fb36068 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,6 +30,7 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli/boot-files boot: [ "axios", + "apollo", "chartJs" ], diff --git a/src/boot/apollo.js b/src/boot/apollo.js new file mode 100644 index 0000000..add596f --- /dev/null +++ b/src/boot/apollo.js @@ -0,0 +1,96 @@ +// import {boot} from "quasar/wrappers"; +import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core' +import {createUploadLink} from 'apollo-upload-client'; +import {onError} from "@apollo/client/link/error"; +import {useUserStore} from "stores/User"; +import {useAppStore} from "stores/App"; +import {Notify} from "quasar"; +import {errorObject, warningObject} from "src/composables/dialog"; + +const handleRefreshToken = (store, forward, operation) => { + return fromPromise(useUserStore(store).refreshAccessToken() + .catch((error) => { + Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu.")); + window.location = "/login"; + })) + .filter((value) => Boolean(value)) + .flatMap((accessToken) => { + const oldHeaders = operation.getContext().headers; + operation.setContext({ + headers: { + ...oldHeaders, authorization: `Bearer ${accessToken}`, + }, + }); + + // retry the request, returning the new observable + return forward(operation); + }); +} + + + +export const apolloWrapper = {}; +export default (({store}) => { + const uploadLink = createUploadLink({ + uri: process.env.API_URL + '/graphql', + }) + + const authLink = new ApolloLink((operation, forward) => { + const accessToken = useUserStore(store).getAccessToken(); + + if (accessToken) { + operation.setContext({ + headers: {"Authorization": `Bearer ${accessToken}`}, + }); + } + return forward(operation); + }); + + const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => { + if (networkError?.statusCode === 401) { + return handleRefreshToken(store, forward, operation); + } + + if (graphQLErrors) { + for (const error of graphQLErrors) { + if (error.message === "Not found") { + useAppStore(store).appStatus = 404; + } else if (error.message.includes("401") || error.message.includes("Not authorized")) { + return handleRefreshToken(store, forward, operation); + } else { + Notify.create(errorObject("API error: " + error.message)); + } + } + } + }); + + + const apolloClient = new ApolloClient({ + link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true, + }); + // const apolloProvider = createApolloProvider({ + // defaultClient: apolloClient, + // }); + + + apolloWrapper.query = (query, variables = {}) => { + return apolloClient.query({ + query: query, fetchPolicy: 'no-cache', variables: variables + }) + .then((response) => response) + .catch(err => { + console.error("Error during query: ", err) + return err; + }) + }; + apolloWrapper.mutate = (mutation, variables) => { + return apolloClient + .mutate({ + mutation: mutation, fetchPolicy: 'no-cache', variables: variables, + }) + .then((response) => response) + .catch(err => { + console.error("Error during mutation: ", err); + }) + }; +}); diff --git a/src/boot/axios.js b/src/boot/axios.js index 4a2cfee..bc75909 100644 --- a/src/boot/axios.js +++ b/src/boot/axios.js @@ -4,22 +4,21 @@ import {useUserStore} from "stores/User"; const api = axios.create({ - baseURL: process.env.API_URL, - withCredentials: true, - xsrfHeaderName: "X-CSRF-TOKEN", - headers: { - Accept: "application/json", - "Content-Type": "application/json;charset=UTF-8", - }, + baseURL: process.env.API_URL, + withCredentials: true, + xsrfHeaderName: "X-CSRF-TOKEN", + headers: { + Accept: "application/json", + "Content-Type": "application/json;charset=UTF-8", + }, }) -export default boot(({app, store}) => { - - api.interceptors.request.use((config) => { - const token = useUserStore(store).getAccessToken(); - if (token) { - config.headers['Authorization'] = `Bearer ${token}` - } +export default boot(({store}) => { + api.interceptors.request.use((config) => { + const token = useUserStore(store).getAccessToken(); + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } if (['/refresh', '/login'].includes(config.url)) { delete config.headers['Authorization']; @@ -28,32 +27,21 @@ export default boot(({app, store}) => { return config; }) -// Response interceptor for API calls - api.interceptors.response.use((response) => { - return response - }, async function (error) { - const originalRequest = error.config; - if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) { - const newToken = await useUserStore(store).refreshAccessToken().catch(err => { - console.error(err); - return Promise.reject("Logged out"); - }) - api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken; - return api(originalRequest); - } - return Promise.reject(error); - }); - - - // for use inside Vue files (Options API) through this.$axios and this.$api - - // app.config.globalProperties.$axios = axios - // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) - // so you won't necessarily have to import axios in each vue file - - app.config.globalProperties.$api = api - // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) - // so you can easily perform requests against your app's API + // Response interceptor for API calls + api.interceptors.response.use( + (response) => response, + async function (error) { + const originalRequest = error.config; + if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) { + const newToken = await useUserStore(store).refreshAccessToken().catch(err => { + console.error(err); + return Promise.reject("Logged out"); + }) + api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken; + return api(originalRequest); + } + return Promise.reject(error); + }); }) export {axios, api} diff --git a/src/boot/chartJs.js b/src/boot/chartJs.js index 41daa61..84a3431 100644 --- a/src/boot/chartJs.js +++ b/src/boot/chartJs.js @@ -2,5 +2,5 @@ import {boot} from "quasar/wrappers"; import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js"; export default boot(({app}) => { - ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale) -}) + ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale); +}); diff --git a/src/components/FormSubmitButton.vue b/src/components/FormSubmitButton.vue new file mode 100644 index 0000000..22ad3aa --- /dev/null +++ b/src/components/FormSubmitButton.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/components/FormWrapper.vue b/src/components/FormWrapper.vue index f2c34d8..ce8d8f4 100644 --- a/src/components/FormWrapper.vue +++ b/src/components/FormWrapper.vue @@ -2,6 +2,7 @@ import {isRequestPending} from "src/composables/form/props"; import {computed, useSlots} from "vue"; import {useQuasar} from "quasar"; +import FormSubmitButton from "components/FormSubmitButton.vue"; const props = defineProps({ modelValue: { @@ -23,38 +24,11 @@ const emits = defineEmits(["save"]); const saveForm = () => { emits("save", props.modelValue); } - -const $q = useQuasar(); -const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md); - - - diff --git a/src/components/aircrafts/AircraftDetailCard.vue b/src/components/aircrafts/AircraftDetailCard.vue index dd0b9bd..02e0d1f 100644 --- a/src/components/aircrafts/AircraftDetailCard.vue +++ b/src/components/aircrafts/AircraftDetailCard.vue @@ -1,5 +1,4 @@