bugfixing
This commit is contained in:
@@ -3,3 +3,9 @@ apps:
|
||||
script: /app/index.js
|
||||
exec_mode: cluster
|
||||
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"
|
||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||
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",
|
||||
"axios",
|
||||
"apollo",
|
||||
|
||||
+109
-64
@@ -1,5 +1,6 @@
|
||||
import {ApolloClient, ApolloLink, InMemoryCache} from '@apollo/client/core'
|
||||
import {ErrorLink} from "@apollo/client/link/error";
|
||||
import {defineStore} from "pinia";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {Notify} from "quasar";
|
||||
import {errorObject, warningObject} from "src/composables/dialog";
|
||||
@@ -29,76 +30,120 @@ const handleRefreshToken = (store, router, forward, operation) => {
|
||||
}))
|
||||
}
|
||||
|
||||
export const apolloWrapper = {};
|
||||
export default (({store, router}) => {
|
||||
const uploadLink = new UploadHttpLink({
|
||||
uri: process.env.API_URL + '/graphql',
|
||||
credentials: 'include'
|
||||
})
|
||||
// SSR: boot() runs again for every request. The client/query/mutate below
|
||||
// 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 authLink = new ApolloLink((operation, forward) => {
|
||||
const accessToken = useAuthStore(store).getAccessToken();
|
||||
if (accessToken) {
|
||||
operation.setContext({
|
||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||
});
|
||||
}
|
||||
return forward(operation);
|
||||
});
|
||||
const init = ({store, router}) => {
|
||||
if (client) return // boot() already initialized this request's store
|
||||
|
||||
const errorLink = new ErrorLink(({graphQLErrors, networkError, operation, forward}) => {
|
||||
if (networkError?.statusCode === 401) {
|
||||
return handleRefreshToken(store, router, forward, operation);
|
||||
}
|
||||
const uploadLink = new UploadHttpLink({
|
||||
uri: process.env.API_URL + '/graphql',
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
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 authLink = new ApolloLink((operation, forward) => {
|
||||
const accessToken = useAuthStore(store).getAccessToken();
|
||||
if (accessToken) {
|
||||
operation.setContext({
|
||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||
});
|
||||
}
|
||||
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 => {
|
||||
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);
|
||||
})
|
||||
};
|
||||
apolloWrapper.mutate = (mutation, variables) => {
|
||||
return apolloClient
|
||||
.mutate({
|
||||
mutation: mutation,
|
||||
|
||||
client = new ApolloClient({
|
||||
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: true,
|
||||
ssrMode: true,
|
||||
});
|
||||
|
||||
queryImpl = (query, variables = {}) => {
|
||||
return client.query({
|
||||
query: query,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables,
|
||||
variables: variables
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during mutation: ", err);
|
||||
})
|
||||
};
|
||||
.then((response) => response)
|
||||
.catch(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 {boot} from "quasar/wrappers";
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
let cookies = Cookies;
|
||||
export default boot(({ssrContext}) => {
|
||||
cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
|
||||
// SSR: boot() runs again for every request. `cookies` used to be a single
|
||||
// shared module-level variable reassigned in place by every request's
|
||||
// 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 * 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}) => {
|
||||
if (process.env.SERVER) return;
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
|
||||
Sentry.init({
|
||||
|
||||
+13
-5
@@ -1,9 +1,17 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {defineStore, getActivePinia} from 'pinia'
|
||||
import {api} from "boot/axios";
|
||||
import {cookies} from "boot/cookies";
|
||||
import {useCookiesStore} from "boot/cookies";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
|
||||
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) => {
|
||||
return await api.post("/login", formData).then(({data}) => {
|
||||
setTokens(data);
|
||||
@@ -18,18 +26,18 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
};
|
||||
|
||||
const unsetTokens = () => {
|
||||
cookies.remove("accessToken")
|
||||
useCookiesStore(pinia).remove("accessToken")
|
||||
};
|
||||
|
||||
const setTokens = ({access_token, access_token_validity}) => {
|
||||
cookies.set("accessToken", access_token, {
|
||||
useCookiesStore(pinia).set("accessToken", access_token, {
|
||||
expires: `${access_token_validity}s`,
|
||||
sameSite: 'Strict'
|
||||
});
|
||||
};
|
||||
|
||||
const getAccessToken = () => {
|
||||
return cookies.get("accessToken");
|
||||
return useCookiesStore(pinia).get("accessToken");
|
||||
};
|
||||
|
||||
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 {ref} from "vue";
|
||||
import {mutate, query} from "stores/helpers";
|
||||
import {cookies} from "boot/cookies";
|
||||
|
||||
export const useLoggedUserStore = defineStore('LoggedUser', () => {
|
||||
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 {ref} from "vue";
|
||||
import {mutate, query} from "stores/helpers";
|
||||
import {cookies} from "boot/cookies";
|
||||
|
||||
export const usePilotsStore = defineStore('Pilots', () => {
|
||||
const pilot = ref(null);
|
||||
|
||||
Reference in New Issue
Block a user