bugfixing
This commit is contained in:
@@ -3,3 +3,9 @@ apps:
|
|||||||
script: /app/index.js
|
script: /app/index.js
|
||||||
exec_mode: cluster
|
exec_mode: cluster
|
||||||
instances: 3
|
instances: 3
|
||||||
|
# Pojistka proti bodu 1 (a podobným leakům): pokud paměť workeru
|
||||||
|
# přeroste limit, PM2 ho sám restartuje dřív, než dojde k OOM na
|
||||||
|
# celém VPS. Hodnotu je potřeba doladit dle reálné spotřeby jednoho
|
||||||
|
# SSR workeru po opravě bodu 1 (pm2 monit / pm2 list) - viz
|
||||||
|
# handoffs/05-pm2-docker-memory-limits.md.
|
||||||
|
max_memory_restart: '400M'
|
||||||
|
|||||||
+6
-1
@@ -28,7 +28,12 @@ module.exports = configure(function (ctx) {
|
|||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
"sentry",
|
// client-only: Sentry browser SDK (browserTracingIntegration,
|
||||||
|
// replayIntegration) is DOM-only and has no business running per SSR
|
||||||
|
// request - server: false means Quasar excludes this boot file from
|
||||||
|
// the server bundle entirely (not just a runtime guard).
|
||||||
|
// See handoffs/04-sentry-ssr-guard.md.
|
||||||
|
{path: "sentry", server: false},
|
||||||
"cookies",
|
"cookies",
|
||||||
"axios",
|
"axios",
|
||||||
"apollo",
|
"apollo",
|
||||||
|
|||||||
+109
-64
@@ -1,5 +1,6 @@
|
|||||||
import {ApolloClient, ApolloLink, InMemoryCache} from '@apollo/client/core'
|
import {ApolloClient, ApolloLink, InMemoryCache} from '@apollo/client/core'
|
||||||
import {ErrorLink} from "@apollo/client/link/error";
|
import {ErrorLink} from "@apollo/client/link/error";
|
||||||
|
import {defineStore} from "pinia";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {Notify} from "quasar";
|
import {Notify} from "quasar";
|
||||||
import {errorObject, warningObject} from "src/composables/dialog";
|
import {errorObject, warningObject} from "src/composables/dialog";
|
||||||
@@ -29,76 +30,120 @@ const handleRefreshToken = (store, router, forward, operation) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apolloWrapper = {};
|
// SSR: boot() runs again for every request. The client/query/mutate below
|
||||||
export default (({store, router}) => {
|
// used to live on a shared module-level `apolloWrapper` object that every
|
||||||
const uploadLink = new UploadHttpLink({
|
// request's boot() reassigned in place - under concurrent SSR requests, one
|
||||||
uri: process.env.API_URL + '/graphql',
|
// request could end up calling into another request's (still-being-set-up
|
||||||
credentials: 'include'
|
// or already-overwritten) apolloClient/store, i.e. querying/mutating with
|
||||||
})
|
// the wrong user's auth context. See
|
||||||
|
// handoffs/02-apollo-wrapper-race-condition.md for the full analysis.
|
||||||
|
//
|
||||||
|
// Fix: make this a Pinia store instead of a plain shared object. `init()`
|
||||||
|
// is called from boot() with the request's own `store`/`router` explicitly
|
||||||
|
// (same proven pattern as useAuthStore(store) elsewhere in this file), so
|
||||||
|
// the closures below always close over the correct request. `query`/
|
||||||
|
// `mutate` are then read back through Pinia's own SSR-safe per-request
|
||||||
|
// resolution (useApolloStore() with no arg, called from preFetch/
|
||||||
|
// onServerPrefetch-triggered store actions - the same mechanism every
|
||||||
|
// other useXStore() call in this codebase already relies on), instead of
|
||||||
|
// through a shared mutable object.
|
||||||
|
export const useApolloStore = defineStore('Apollo', () => {
|
||||||
|
let client = null
|
||||||
|
let queryImpl = null
|
||||||
|
let mutateImpl = null
|
||||||
|
|
||||||
const authLink = new ApolloLink((operation, forward) => {
|
const init = ({store, router}) => {
|
||||||
const accessToken = useAuthStore(store).getAccessToken();
|
if (client) return // boot() already initialized this request's store
|
||||||
if (accessToken) {
|
|
||||||
operation.setContext({
|
|
||||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return forward(operation);
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorLink = new ErrorLink(({graphQLErrors, networkError, operation, forward}) => {
|
const uploadLink = new UploadHttpLink({
|
||||||
if (networkError?.statusCode === 401) {
|
uri: process.env.API_URL + '/graphql',
|
||||||
return handleRefreshToken(store, router, forward, operation);
|
credentials: 'include'
|
||||||
}
|
})
|
||||||
|
|
||||||
if (graphQLErrors) {
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
for (const error of graphQLErrors) {
|
const accessToken = useAuthStore(store).getAccessToken();
|
||||||
if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
if (accessToken) {
|
||||||
return handleRefreshToken(store, router, forward, operation);
|
operation.setContext({
|
||||||
} else if (error.message.includes("404") || error.message.includes("Not found")) {
|
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||||
useAppStore(store).appStatus = 404;
|
});
|
||||||
} else {
|
}
|
||||||
Notify.create(errorObject("API error: " + error.message));
|
return forward(operation);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorLink = new ErrorLink(({graphQLErrors, networkError, operation, forward}) => {
|
||||||
|
if (networkError?.statusCode === 401) {
|
||||||
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphQLErrors) {
|
||||||
|
for (const error of graphQLErrors) {
|
||||||
|
if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||||
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
|
} else if (error.message.includes("404") || error.message.includes("Not found")) {
|
||||||
|
useAppStore(store).appStatus = 404;
|
||||||
|
} else {
|
||||||
|
Notify.create(errorObject("API error: " + error.message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
|
||||||
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
connectToDevTools: true,
|
|
||||||
ssrMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
apolloWrapper.query = (query, variables = {}) => {
|
|
||||||
return apolloClient.query({
|
|
||||||
query: query,
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
variables: variables
|
|
||||||
})
|
})
|
||||||
.then((response) => response)
|
|
||||||
.catch(err => {
|
client = new ApolloClient({
|
||||||
const errStr = err.toString();
|
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||||
if (errStr.includes('Not found') || errStr.includes("No row was found")) {
|
cache: new InMemoryCache(),
|
||||||
useAppStore(store).appStatus = 404;
|
connectToDevTools: true,
|
||||||
return Promise.resolve();
|
ssrMode: true,
|
||||||
} else if (!errStr.includes("401") && !errStr.includes("Not authorized")) {
|
});
|
||||||
console.error("Error during query: ", err)
|
|
||||||
}
|
queryImpl = (query, variables = {}) => {
|
||||||
return Promise.reject(err);
|
return client.query({
|
||||||
})
|
query: query,
|
||||||
};
|
|
||||||
apolloWrapper.mutate = (mutation, variables) => {
|
|
||||||
return apolloClient
|
|
||||||
.mutate({
|
|
||||||
mutation: mutation,
|
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
variables: variables,
|
variables: variables
|
||||||
})
|
})
|
||||||
.then((response) => response)
|
.then((response) => response)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error during mutation: ", err);
|
const errStr = err.toString();
|
||||||
})
|
if (errStr.includes('Not found') || errStr.includes("No row was found")) {
|
||||||
};
|
useAppStore(store).appStatus = 404;
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if (!errStr.includes("401") && !errStr.includes("Not authorized")) {
|
||||||
|
console.error("Error during query: ", err)
|
||||||
|
}
|
||||||
|
return Promise.reject(err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
mutateImpl = (mutation, variables) => {
|
||||||
|
return client
|
||||||
|
.mutate({
|
||||||
|
mutation: mutation,
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
variables: variables,
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error during mutation: ", err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = (query, variables) => queryImpl(query, variables)
|
||||||
|
const mutate = (mutation, variables) => mutateImpl(mutation, variables)
|
||||||
|
|
||||||
|
return {init, query, mutate}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Backward-compatible shape so existing call sites
|
||||||
|
// (`apolloWrapper.query(...)`, `apolloWrapper.mutate(...)` in
|
||||||
|
// stores/helpers.js and stores/Organizations.js) keep working unchanged,
|
||||||
|
// now delegating to the request-scoped store above instead of a shared
|
||||||
|
// mutable singleton.
|
||||||
|
export const apolloWrapper = {
|
||||||
|
query: (...args) => useApolloStore().query(...args),
|
||||||
|
mutate: (...args) => useApolloStore().mutate(...args),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (({store, router}) => {
|
||||||
|
useApolloStore(store).init({store, router})
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-5
@@ -1,9 +1,32 @@
|
|||||||
import {Cookies} from 'quasar'
|
import {Cookies} from 'quasar'
|
||||||
import {boot} from "quasar/wrappers";
|
import {boot} from "quasar/wrappers";
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
|
||||||
let cookies = Cookies;
|
// SSR: boot() runs again for every request. `cookies` used to be a single
|
||||||
export default boot(({ssrContext}) => {
|
// shared module-level variable reassigned in place by every request's
|
||||||
cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
|
// boot() - under concurrent SSR requests, one request's
|
||||||
|
// Cookies.parseSSR(ssrContext) result (which carries that request's own
|
||||||
|
// accessToken) could overwrite another still-in-flight request's
|
||||||
|
// `cookies` before it finished reading it, leaking one user's access
|
||||||
|
// token to another. See handoffs/03-cookies-shared-state.md.
|
||||||
|
//
|
||||||
|
// Fix: make this a Pinia store, initialized explicitly per request from
|
||||||
|
// boot() with that request's own `store` (same proven pattern as
|
||||||
|
// boot/apollo.js), instead of a shared mutable variable.
|
||||||
|
export const useCookiesStore = defineStore('Cookies', () => {
|
||||||
|
let instance = null
|
||||||
|
|
||||||
|
const init = (ssrContext) => {
|
||||||
|
instance = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = (...args) => instance.get(...args)
|
||||||
|
const set = (...args) => instance.set(...args)
|
||||||
|
const remove = (...args) => instance.remove(...args)
|
||||||
|
|
||||||
|
return {init, get, set, remove}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default boot(({store, ssrContext}) => {
|
||||||
|
useCookiesStore(store).init(ssrContext)
|
||||||
});
|
});
|
||||||
|
|
||||||
export {cookies}
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import {boot} from "quasar/wrappers";
|
import {boot} from "quasar/wrappers";
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
|
|
||||||
|
// Client-only boot file (see quasar.config.js: {path: "sentry", server:
|
||||||
|
// false} - excludes this from the server bundle entirely). The guard below
|
||||||
|
// is a defensive second layer in case that config entry is ever changed
|
||||||
|
// back to a plain string. See handoffs/04-sentry-ssr-guard.md.
|
||||||
export default boot(({app, router}) => {
|
export default boot(({app, router}) => {
|
||||||
|
if (process.env.SERVER) return;
|
||||||
if (process.env.NODE_ENV !== 'production') return;
|
if (process.env.NODE_ENV !== 'production') return;
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
|||||||
+13
-5
@@ -1,9 +1,17 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore, getActivePinia} from 'pinia'
|
||||||
import {api} from "boot/axios";
|
import {api} from "boot/axios";
|
||||||
import {cookies} from "boot/cookies";
|
import {useCookiesStore} from "boot/cookies";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
export const useAuthStore = defineStore('Auth', () => {
|
export const useAuthStore = defineStore('Auth', () => {
|
||||||
|
// Captured synchronously at setup time (see handoffs/03-cookies-shared-state.md):
|
||||||
|
// getActivePinia() here always resolves to the same pinia instance this
|
||||||
|
// store was created for (Pinia sets it up right before running setup()),
|
||||||
|
// so closing over it now and passing it explicitly to useCookiesStore()
|
||||||
|
// later keeps cookie access correctly scoped to this request even if
|
||||||
|
// other concurrent SSR requests are being set up in between.
|
||||||
|
const pinia = getActivePinia();
|
||||||
|
|
||||||
const login = async (formData) => {
|
const login = async (formData) => {
|
||||||
return await api.post("/login", formData).then(({data}) => {
|
return await api.post("/login", formData).then(({data}) => {
|
||||||
setTokens(data);
|
setTokens(data);
|
||||||
@@ -18,18 +26,18 @@ export const useAuthStore = defineStore('Auth', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unsetTokens = () => {
|
const unsetTokens = () => {
|
||||||
cookies.remove("accessToken")
|
useCookiesStore(pinia).remove("accessToken")
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTokens = ({access_token, access_token_validity}) => {
|
const setTokens = ({access_token, access_token_validity}) => {
|
||||||
cookies.set("accessToken", access_token, {
|
useCookiesStore(pinia).set("accessToken", access_token, {
|
||||||
expires: `${access_token_validity}s`,
|
expires: `${access_token_validity}s`,
|
||||||
sameSite: 'Strict'
|
sameSite: 'Strict'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccessToken = () => {
|
const getAccessToken = () => {
|
||||||
return cookies.get("accessToken");
|
return useCookiesStore(pinia).get("accessToken");
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAccessToken = () => !!getAccessToken();
|
const hasAccessToken = () => !!getAccessToken();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {EDIT_USER} from "src/graphql/mutations/user";
|
|||||||
import {LOGGED_USER_DETAIL, PILOT_DETAIL} from "src/graphql/queries/user";
|
import {LOGGED_USER_DETAIL, PILOT_DETAIL} from "src/graphql/queries/user";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {mutate, query} from "stores/helpers";
|
import {mutate, query} from "stores/helpers";
|
||||||
import {cookies} from "boot/cookies";
|
|
||||||
|
|
||||||
export const useLoggedUserStore = defineStore('LoggedUser', () => {
|
export const useLoggedUserStore = defineStore('LoggedUser', () => {
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {EDIT_USER} from "src/graphql/mutations/user";
|
|||||||
import {LOGGED_USER_DETAIL, PILOT_DETAIL} from "src/graphql/queries/user";
|
import {LOGGED_USER_DETAIL, PILOT_DETAIL} from "src/graphql/queries/user";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {mutate, query} from "stores/helpers";
|
import {mutate, query} from "stores/helpers";
|
||||||
import {cookies} from "boot/cookies";
|
|
||||||
|
|
||||||
export const usePilotsStore = defineStore('Pilots', () => {
|
export const usePilotsStore = defineStore('Pilots', () => {
|
||||||
const pilot = ref(null);
|
const pilot = ref(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user