From 331ebbc4e4ad461717ff8f18b5e9173a696ca55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kv=C3=A1=C4=8Dek?= Date: Tue, 14 Nov 2023 16:20:01 +0100 Subject: [PATCH] Zaklad ssr --- Dockerfile | 15 +- docker-compose.yml | 3 +- quasar.config.js | 6 +- src-ssr/middlewares/render.js | 55 +++++++ src-ssr/server.js | 136 ++++++++++++++++++ src-ssr/ssr-flag.d.ts | 10 ++ src/boot/axios.js | 52 +++---- .../aircrafts/AircraftDetailSkeleton.vue | 2 +- .../basicSkeletons/BasicListSkeleton.vue | 8 +- .../flights/FlightDetailSkeleton.vue | 2 +- .../flights/FlightsListSkeleton.vue | 3 +- src/components/photos/PhotoGallery.vue | 11 +- src/components/poi/POIDetailSkeleton.vue | 4 +- .../profile/PublicProfileSkeleton.vue | 2 +- src/layouts/PlainLayout.vue | 34 ++--- src/pages/profile/flight/PhotoEditorPage.vue | 2 +- src/pages/public/TestPage.vue | 15 ++ src/router/index.js | 13 +- src/router/routes.js | 9 +- 19 files changed, 298 insertions(+), 84 deletions(-) create mode 100644 src-ssr/middlewares/render.js create mode 100644 src-ssr/server.js create mode 100644 src-ssr/ssr-flag.d.ts create mode 100644 src/pages/public/TestPage.vue diff --git a/Dockerfile b/Dockerfile index 7113f05..d854ec1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,12 +13,15 @@ FROM develop-stage as build-stage #ENV VUE_API_URL $API_URL RUN yarn --production=false -RUN quasar build +RUN quasar build -m ssr # production stage -FROM nginx:1.25-alpine as production-stage -COPY conf/nginx.conf /etc/nginx/nginx.conf -COPY --from=build-stage /app/dist/spa /app/html -EXPOSE 80 +#FROM nginx:1.25-alpine as production-stage +#COPY conf/nginx.conf /etc/nginx/nginx.conf +#COPY --from=build-stage /app/dist/spa /app/html +#EXPOSE 80 +# +#CMD ["nginx", "-g", "daemon off;"] + +FROM keymetrics/pm2:latest-alpine as production-stage -CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml index 8da81aa..da7a96a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,11 +29,12 @@ services: target: "develop-stage" ports: - "9000:9000" + - "9001:9100" volumes: - ./src:/app/src - ./public:/app/public - ./node_modules:/app/node_modules:delegated - command: quasar dev + command: quasar dev -m ssr environment: API_URL: "http://localhost:8000" diff --git a/quasar.config.js b/quasar.config.js index b657d33..c1c034f 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,7 +30,7 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli/boot-files boot: [ "axios", - "chartJs", + "chartJs" ], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css @@ -135,10 +135,10 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr ssr: { - // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! + ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! // will mess up SSR - // extendSSRWebserverConf (esbuildConf) {}, + extendSSRWebserverConf (esbuildConf) {}, // extendPackageJson (json) {}, pwa: false, diff --git a/src-ssr/middlewares/render.js b/src-ssr/middlewares/render.js new file mode 100644 index 0000000..2397f83 --- /dev/null +++ b/src-ssr/middlewares/render.js @@ -0,0 +1,55 @@ +import { ssrMiddleware } from 'quasar/wrappers' + +// This middleware should execute as last one +// since it captures everything and tries to +// render the page with Vue + +export default ssrMiddleware(({ app, resolve, render, serve }) => { + // we capture any other Express route and hand it + // over to Vue and Vue Router to render our page + app.get(resolve.urlPath('*'), (req, res) => { + res.setHeader('Content-Type', 'text/html') + + render(/* the ssrContext: */ { req, res }) + .then(html => { + // now let's send the rendered html to the client + res.send(html) + }) + .catch(err => { + // oops, we had an error while rendering the page + + // we were told to redirect to another URL + if (err.url) { + if (err.code) { + res.redirect(err.code, err.url) + } else { + res.redirect(err.url) + } + } else if (err.code === 404) { + // hmm, Vue Router could not find the requested route + + // Should reach here only if no "catch-all" route + // is defined in /src/routes + res.status(404).send('404 | Page Not Found') + } else if (process.env.DEV) { + // well, we treat any other code as error; + // if we're in dev mode, then we can use Quasar CLI + // to display a nice error page that contains the stack + // and other useful information + + // serve.error is available on dev only + serve.error({ err, req, res }) + } else { + // we're in production, so we should have another method + // to display something to the client when we encounter an error + // (for security reasons, it's not ok to display the same wealth + // of information as we do in development) + + // Render Error Page on production or + // create a route (/src/routes) for an error page and redirect to it + res.status(500).send('500 | Internal Server Error') + // console.error(err.stack) + } + }) + }) +}) diff --git a/src-ssr/server.js b/src-ssr/server.js new file mode 100644 index 0000000..b3594d9 --- /dev/null +++ b/src-ssr/server.js @@ -0,0 +1,136 @@ +/** + * More info about this file: + * https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver + * + * Runs in Node context. + */ + +/** + * Make sure to yarn add / npm install (in your project root) + * anything you import here (except for express and compression). + */ +import express from 'express' +import compression from 'compression' +import { + ssrClose, + ssrCreate, + ssrListen, + ssrRenderPreloadTag, + ssrServeStaticContent +} from 'quasar/wrappers' + +/** + * Create your webserver and return its instance. + * If needed, prepare your webserver to receive + * connect-like middlewares. + * + * Should NOT be async! + */ +export const create = ssrCreate((/* { ... } */) => { + const app = express() + + // attackers can use this header to detect apps running Express + // and then launch specifically-targeted attacks + app.disable('x-powered-by') + + // place here any middlewares that + // absolutely need to run before anything else + if (process.env.PROD) { + app.use(compression()) + } + + return app +}) + +/** + * You need to make the server listen to the indicated port + * and return the listening instance or whatever you need to + * close the server with. + * + * The "listenResult" param for the "close()" definition below + * is what you return here. + * + * For production, you can instead export your + * handler for serverless use or whatever else fits your needs. + */ +export const listen = ssrListen(async ({ app, port, isReady }) => { + await isReady() + return app.listen(port, () => { + if (process.env.PROD) { + console.log('Server listening at port ' + port) + } + }) +}) + +/** + * Should close the server and free up any resources. + * Will be used on development only when the server needs + * to be rebooted. + * + * Should you need the result of the "listen()" call above, + * you can use the "listenResult" param. + * + * Can be async. + */ +export const close = ssrClose(({ listenResult }) => { + return listenResult.close() +}) + +const maxAge = process.env.DEV + ? 0 + : 1000 * 60 * 60 * 24 * 30 + +/** + * Should return middleware that serves the indicated path + * with static content. + */ +export const serveStaticContent = ssrServeStaticContent((path, opts) => { + return express.static(path, { + maxAge, + ...opts + }) +}) + +const jsRE = /\.js$/ +const cssRE = /\.css$/ +const woffRE = /\.woff$/ +const woff2RE = /\.woff2$/ +const gifRE = /\.gif$/ +const jpgRE = /\.jpe?g$/ +const pngRE = /\.png$/ + +/** + * Should return a String with HTML output + * (if any) for preloading indicated file + */ +export const renderPreloadTag = ssrRenderPreloadTag((file) => { + if (jsRE.test(file) === true) { + return `` + } + + if (cssRE.test(file) === true) { + return `` + } + + if (woffRE.test(file) === true) { + return `` + } + + if (woff2RE.test(file) === true) { + return `` + } + + if (gifRE.test(file) === true) { + return `` + } + + if (jpgRE.test(file) === true) { + return `` + } + + if (pngRE.test(file) === true) { + return `` + } + + return '' +}) diff --git a/src-ssr/ssr-flag.d.ts b/src-ssr/ssr-flag.d.ts new file mode 100644 index 0000000..81a098b --- /dev/null +++ b/src-ssr/ssr-flag.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// THIS FEATURE-FLAG FILE IS AUTOGENERATED, +// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING +import "quasar/dist/types/feature-flag"; + +declare module "quasar/dist/types/feature-flag" { + interface QuasarFeatureFlags { + ssr: true; + } +} diff --git a/src/boot/axios.js b/src/boot/axios.js index 07ffed3..4a2cfee 100644 --- a/src/boot/axios.js +++ b/src/boot/axios.js @@ -2,6 +2,7 @@ import {boot} from 'quasar/wrappers' import axios from 'axios' import {useUserStore} from "stores/User"; + const api = axios.create({ baseURL: process.env.API_URL, withCredentials: true, @@ -12,37 +13,38 @@ const api = axios.create({ }, }) -api.interceptors.request.use((config) => { - const token = useUserStore().getAccessToken(); - if (token) { - config.headers['Authorization'] = `Bearer ${token}` - } +export default boot(({app, store}) => { - if (['/refresh', '/login'].includes(config.url)) { - delete config.headers['Authorization']; - } + api.interceptors.request.use((config) => { + const token = useUserStore(store).getAccessToken(); + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } - return config; -}) + if (['/refresh', '/login'].includes(config.url)) { + delete config.headers['Authorization']; + } + + 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().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); -}); + 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); + }); -export default boot(({app}) => { // for use inside Vue files (Options API) through this.$axios and this.$api // app.config.globalProperties.$axios = axios diff --git a/src/components/aircrafts/AircraftDetailSkeleton.vue b/src/components/aircrafts/AircraftDetailSkeleton.vue index 98ee401..2368f8f 100644 --- a/src/components/aircrafts/AircraftDetailSkeleton.vue +++ b/src/components/aircrafts/AircraftDetailSkeleton.vue @@ -13,7 +13,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue"; - + diff --git a/src/components/basicSkeletons/BasicListSkeleton.vue b/src/components/basicSkeletons/BasicListSkeleton.vue index 995e781..9e9342b 100644 --- a/src/components/basicSkeletons/BasicListSkeleton.vue +++ b/src/components/basicSkeletons/BasicListSkeleton.vue @@ -1,5 +1,6 @@