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",
|
||||||
|
|||||||
+52
-7
@@ -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,8 +30,31 @@ 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
|
||||||
|
// request's boot() reassigned in place - under concurrent SSR requests, one
|
||||||
|
// request could end up calling into another request's (still-being-set-up
|
||||||
|
// 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 init = ({store, router}) => {
|
||||||
|
if (client) return // boot() already initialized this request's store
|
||||||
|
|
||||||
const uploadLink = new UploadHttpLink({
|
const uploadLink = new UploadHttpLink({
|
||||||
uri: process.env.API_URL + '/graphql',
|
uri: process.env.API_URL + '/graphql',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -64,15 +88,15 @@ export default (({store, router}) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
client = new ApolloClient({
|
||||||
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
connectToDevTools: true,
|
connectToDevTools: true,
|
||||||
ssrMode: true,
|
ssrMode: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
apolloWrapper.query = (query, variables = {}) => {
|
queryImpl = (query, variables = {}) => {
|
||||||
return apolloClient.query({
|
return client.query({
|
||||||
query: query,
|
query: query,
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
variables: variables
|
variables: variables
|
||||||
@@ -89,8 +113,9 @@ export default (({store, router}) => {
|
|||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
apolloWrapper.mutate = (mutation, variables) => {
|
|
||||||
return apolloClient
|
mutateImpl = (mutation, variables) => {
|
||||||
|
return client
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: mutation,
|
mutation: mutation,
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
@@ -101,4 +126,24 @@ export default (({store, router}) => {
|
|||||||
console.error("Error during mutation: ", 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