From 5a2594eb88a485bbb4196cb61ad70d910b3eefba Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 8 Mar 2022 10:36:04 +0100 Subject: [PATCH] refactor(server): use proxy mechanisms to remove server_url config --- .env.example | 3 +- client/next.config.js | 13 ++++++ client/pages/r/[shortId].tsx | 30 ++++--------- client/services/axios.ts | 9 +--- client/services/resume.ts | 5 ++- client/utils/getResumeUrl.ts | 3 +- docker-compose.prod.yml | 5 --- docker-compose.yml | 44 +++++++++---------- pnpm-lock.yaml | 8 ++++ server/package.json | 1 + server/src/app.module.ts | 1 + .../src/assets/{resumes => exports}/.gitkeep | 0 server/src/config/app.config.ts | 3 +- server/src/config/config.module.ts | 6 ++- server/src/main.ts | 9 +--- server/src/printer/printer.service.ts | 7 ++- server/src/resume/resume.module.ts | 4 +- server/src/resume/resume.service.ts | 7 ++- 18 files changed, 76 insertions(+), 82 deletions(-) rename server/src/assets/{resumes => exports}/.gitkeep (100%) diff --git a/.env.example b/.env.example index c5f8f302..322881a8 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,7 @@ TZ=UTC SECRET_KEY=change-me # URLs -PUBLIC_APP_URL=http://localhost:3000 -PUBLIC_SERVER_URL=http://localhost:3100 +PUBLIC_URL=http://localhost:3000 # Database POSTGRES_HOST=localhost diff --git a/client/next.config.js b/client/next.config.js index 8d83cda3..fa73440b 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -15,6 +15,19 @@ const nextConfig = { domains: ['www.gravatar.com'], }, + async rewrites() { + if (process.env.NODE_ENV === 'development') { + return [ + { + source: '/api/:path*', + destination: 'http://localhost:3100/api/:path*', + }, + ]; + } + + return []; + }, + // Hack to make Tailwind darkMode 'class' strategy with CSS Modules // Ref: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomment-968368156 webpack: (config) => { diff --git a/client/pages/r/[shortId].tsx b/client/pages/r/[shortId].tsx index fb684cab..de012403 100644 --- a/client/pages/r/[shortId].tsx +++ b/client/pages/r/[shortId].tsx @@ -16,7 +16,7 @@ import Page from '@/components/build/Center/Page'; import { ServerError } from '@/services/axios'; import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer'; import { fetchResumeByShortId } from '@/services/resume'; -import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { useAppDispatch } from '@/store/hooks'; import { setResume } from '@/store/resume/resumeSlice'; import styles from '@/styles/pages/Preview.module.scss'; @@ -26,34 +26,18 @@ type QueryParams = { type Props = { shortId: string; - resume?: Resume; }; export const getServerSideProps: GetServerSideProps = async ({ query, locale = 'en' }) => { const { shortId } = query as QueryParams; - try { - const resume = await fetchResumeByShortId({ shortId }); - - return { props: { shortId, resume, ...(await serverSideTranslations(locale, ['common'])) } }; - } catch { - return { props: { shortId, ...(await serverSideTranslations(locale, ['common'])) } }; - } + return { props: { shortId, ...(await serverSideTranslations(locale, ['common'])) } }; }; -const Preview: NextPage = ({ shortId, resume: initialData }) => { +const Preview: NextPage = ({ shortId }) => { const dispatch = useAppDispatch(); - const resume = useAppSelector((state) => state.resume); - - useEffect(() => { - if (initialData && !isEmpty(initialData)) { - dispatch(setResume(initialData)); - } - }, [dispatch, initialData]); - - useQuery(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), { - initialData, + const { data: resume } = useQuery(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), { refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, @@ -64,7 +48,11 @@ const Preview: NextPage = ({ shortId, resume: initialData }) => { const { mutateAsync, isLoading } = useMutation(printResumeAsPdf); - if (isEmpty(resume)) return null; + useEffect(() => { + if (resume) dispatch(setResume(resume)); + }, [resume, dispatch]); + + if (!resume || isEmpty(resume)) return null; const layout: string[][][] = get(resume, 'metadata.layout', []); diff --git a/client/services/axios.ts b/client/services/axios.ts index 6e890a3d..402e0462 100644 --- a/client/services/axios.ts +++ b/client/services/axios.ts @@ -1,4 +1,3 @@ -import env from '@beam-australia/react-env'; import _axios, { AxiosError } from 'axios'; import Router from 'next/router'; @@ -13,13 +12,7 @@ export type ServerError = { path: string; }; -const axios = _axios.create({ - baseURL: `${env('SERVER_URL')}/api`, -}); - -export const uninterceptedAxios = _axios.create({ - baseURL: `${env('SERVER_URL')}/api`, -}); +const axios = _axios.create({ baseURL: '/api' }); axios.interceptors.request.use((config) => { const { accessToken } = store.getState().auth; diff --git a/client/services/resume.ts b/client/services/resume.ts index 55adf712..9be37e43 100644 --- a/client/services/resume.ts +++ b/client/services/resume.ts @@ -2,6 +2,8 @@ import { Resume } from '@reactive-resume/schema'; import { AxiosResponse } from 'axios'; import isEmpty from 'lodash/isEmpty'; +import isBrowser from '@/utils/isBrowser'; + import axios from './axios'; export type CreateResumeParams = { @@ -69,9 +71,10 @@ export const fetchResumeByIdentifier = async ({ slug, options = { secretKey: '' }, }: FetchResumeByIdentifierParams) => { + const prefix = !isBrowser && process.env.NODE_ENV === 'development' ? 'http://localhost:3100/api' : ''; const requestOptions = isEmpty(options.secretKey) ? {} : { params: { secretKey: options.secretKey } }; - return axios.get(`/resume/${username}/${slug}`, requestOptions).then((res) => res.data); + return axios.get(`${prefix}/resume/${username}/${slug}`, requestOptions).then((res) => res.data); }; export const createResume = (createResumeParams: CreateResumeParams) => diff --git a/client/utils/getResumeUrl.ts b/client/utils/getResumeUrl.ts index e24266a4..a133d20a 100644 --- a/client/utils/getResumeUrl.ts +++ b/client/utils/getResumeUrl.ts @@ -1,3 +1,4 @@ +import env from '@beam-australia/react-env'; import { Resume } from '@reactive-resume/schema'; import get from 'lodash/get'; @@ -19,7 +20,7 @@ const getResumeUrl = (resume: Resume, options: Options = defaultOptions): string const slug: string = get(resume, 'slug'); let url = ''; - let hostname = ''; + let hostname = env('URL'); if (typeof window !== 'undefined') { hostname = window.location.origin; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 78b41222..66b07cf0 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -7,8 +7,6 @@ services: env_file: .env ports: - 3100:3100 - depends_on: - - postgres client: image: amruthpillai/reactive-resume:client-latest @@ -18,6 +16,3 @@ services: - 3000:3000 depends_on: - server - -volumes: - pgdata: diff --git a/docker-compose.yml b/docker-compose.yml index 0cf66734..0e2c8e0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,29 +11,29 @@ services: - ./scripts/database/initialize.sql:/docker-entrypoint-initdb.d/initialize.sql - pgdata:/var/lib/postgresql/data - server: - build: - context: . - dockerfile: server/Dockerfile - container_name: server - env_file: .env - environment: - - POSTGRES_HOST=postgres - ports: - - 3100:3100 - depends_on: - - postgres + # server: + # build: + # context: . + # dockerfile: server/Dockerfile + # container_name: server + # env_file: .env + # environment: + # - POSTGRES_HOST=postgres + # ports: + # - 3100:3100 + # depends_on: + # - postgres - client: - build: - context: . - dockerfile: client/Dockerfile - container_name: client - env_file: .env - ports: - - 3000:3000 - depends_on: - - server + # client: + # build: + # context: . + # dockerfile: client/Dockerfile + # container_name: client + # env_file: .env + # ports: + # - 3000:3000 + # depends_on: + # - server volumes: pgdata: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ef3b22f..570caa80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,7 @@ importers: '@reactive-resume/schema': workspace:* '@sendgrid/mail': ^7.6.1 '@types/bcrypt': ^5.0.0 + '@types/cookie-parser': ^1.4.2 '@types/express': ^4.17.13 '@types/multer': ^1.4.7 '@types/node': ^17.0.21 @@ -271,6 +272,7 @@ importers: '@nestjs/schematics': 8.0.7_typescript@4.5.5 '@reactive-resume/schema': link:../schema '@types/bcrypt': 5.0.0 + '@types/cookie-parser': 1.4.2 '@types/express': 4.17.13 '@types/multer': 1.4.7 '@types/node': 17.0.21 @@ -1965,6 +1967,12 @@ packages: dependencies: '@types/node': 17.0.21 + /@types/cookie-parser/1.4.2: + resolution: {integrity: sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==} + dependencies: + '@types/express': 4.17.13 + dev: true + /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: diff --git a/server/package.json b/server/package.json index 21c5c3c2..c83deca0 100644 --- a/server/package.json +++ b/server/package.json @@ -53,6 +53,7 @@ "@nestjs/schematics": "^8.0.7", "@reactive-resume/schema": "workspace:*", "@types/bcrypt": "^5.0.0", + "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/multer": "^1.4.7", "@types/node": "^17.0.21", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index ec27bebe..8db68168 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -18,6 +18,7 @@ import { UsersModule } from './users/users.module'; @Module({ imports: [ ServeStaticModule.forRoot({ + serveRoot: '/api', rootPath: join(__dirname, 'assets'), }), ConfigModule, diff --git a/server/src/assets/resumes/.gitkeep b/server/src/assets/exports/.gitkeep similarity index 100% rename from server/src/assets/resumes/.gitkeep rename to server/src/assets/exports/.gitkeep diff --git a/server/src/config/app.config.ts b/server/src/config/app.config.ts index d7f2bda4..76cdd6f8 100644 --- a/server/src/config/app.config.ts +++ b/server/src/config/app.config.ts @@ -5,6 +5,5 @@ export default registerAs('app', () => ({ environment: process.env.NODE_ENV, secretKey: process.env.SECRET_KEY, port: parseInt(process.env.PORT, 10) || 3100, - url: process.env.PUBLIC_APP_URL || 'http://localhost:3000', - serverUrl: process.env.PUBLIC_SERVER_URL || 'http://localhost:3100', + url: process.env.PUBLIC_URL || 'http://localhost:3000', })); diff --git a/server/src/config/config.module.ts b/server/src/config/config.module.ts index ce2190d9..57c11843 100644 --- a/server/src/config/config.module.ts +++ b/server/src/config/config.module.ts @@ -16,8 +16,7 @@ const validationSchema = Joi.object({ NODE_ENV: Joi.string().valid('development', 'production').default('development'), // URLs - PUBLIC_APP_URL: Joi.string().default('http://localhost:3000'), - PUBLIC_SERVER_URL: Joi.string().default('http://localhost:3100'), + PUBLIC_URL: Joi.string().default('http://localhost:3000'), // Database POSTGRES_HOST: Joi.string().required(), @@ -38,6 +37,9 @@ const validationSchema = Joi.object({ // SendGrid SENDGRID_API_KEY: Joi.string().allow(''), + SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''), + SENDGRID_FROM_NAME: Joi.string().allow(''), + SENDGRID_FROM_EMAIL: Joi.string().allow(''), }); @Module({ diff --git a/server/src/main.ts b/server/src/main.ts index 12723278..9928eff6 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import cookieParser from 'cookie-parser'; -import { join } from 'path'; import { AppModule } from './app.module'; @@ -15,24 +14,18 @@ const bootstrap = async () => { app.setGlobalPrefix(globalPrefix); // Middleware - app.enableCors({ credentials: true }); app.enableShutdownHooks(); app.use(cookieParser()); // Pipes app.useGlobalPipes(new ValidationPipe({ transform: true })); - // Email Templates - app.setBaseViewsDir(join(__dirname, 'mail/templates')); - app.setViewEngine('hbs'); - const configService = app.get(ConfigService); - const serverUrl = configService.get('app.serverUrl'); const port = configService.get('app.port'); await app.listen(port); - Logger.log(`🚀 Server is running on: ${serverUrl}/${globalPrefix}`); + Logger.log(`🚀 Server is up and running!`); }; bootstrap(); diff --git a/server/src/printer/printer.service.ts b/server/src/printer/printer.service.ts index 3842f13e..ba926dad 100644 --- a/server/src/printer/printer.service.ts +++ b/server/src/printer/printer.service.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { SchedulerRegistry } from '@nestjs/schedule'; import { mkdir, unlink, writeFile } from 'fs/promises'; import { nanoid } from 'nanoid'; -import { join, resolve } from 'path'; +import { join } from 'path'; import { PDFDocument } from 'pdf-lib'; import { Browser, chromium } from 'playwright-chromium'; @@ -28,7 +28,6 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy { async printAsPdf(username: string, slug: string): Promise { const url = this.configService.get('app.url'); const secretKey = this.configService.get('app.secretKey'); - const serverUrl = this.configService.get('app.serverUrl'); const page = await this.browser.newPage(); @@ -44,9 +43,9 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy { }); const pdf = await PDFDocument.create(); - const directory = resolve('dist/assets/resumes'); + const directory = join(__dirname, '..', 'assets/exports'); const filename = `RxResume_PDFExport_${nanoid()}.pdf`; - const publicUrl = `${serverUrl}/resumes/${filename}`; + const publicUrl = `/api/exports/${filename}`; for (let index = 0; index < resumePages.length; index++) { await page.evaluate((page) => (document.body.innerHTML = page.innerHTML), resumePages[index]); diff --git a/server/src/resume/resume.module.ts b/server/src/resume/resume.module.ts index 05bd42b6..b4c50baf 100644 --- a/server/src/resume/resume.module.ts +++ b/server/src/resume/resume.module.ts @@ -22,8 +22,8 @@ import { ResumeService } from './resume.service'; storage: diskStorage({ destination: async (req, _, cb) => { const userId = (req.user as User).id; - const resumeId = req.params.id; - const destination = join(__dirname, `assets/uploads/${userId}/${resumeId}`); + const resumeId = +req.params.id; + const destination = join(__dirname, '..', `assets/uploads/${userId}/${resumeId}`); await mkdir(destination, { recursive: true }); diff --git a/server/src/resume/resume.service.ts b/server/src/resume/resume.service.ts index 0a3ce517..4ee237f2 100644 --- a/server/src/resume/resume.service.ts +++ b/server/src/resume/resume.service.ts @@ -218,9 +218,8 @@ export class ResumeService { async uploadPhoto(id: number, userId: number, filename: string) { const resume = await this.findOne(id, userId); - const serverUrl = this.configService.get('app.serverUrl'); - const url = `${serverUrl}/uploads/${userId}/${id}/${filename}`; + const url = `/api/uploads/${userId}/${id}/${filename}`; const updatedResume = set(resume, 'basics.photo.url', url); return this.resumeRepository.save(updatedResume); @@ -228,8 +227,8 @@ export class ResumeService { async deletePhoto(id: number, userId: number) { const resume = await this.findOne(id, userId); - const key = new URL(resume.basics.photo.url).pathname; - const photoPath = join(__dirname, 'assets', key); + const filepath = new URL(resume.basics.photo.url).pathname; + const photoPath = join(__dirname, '..', `assets/${filepath}`); const updatedResume = set(resume, 'basics.photo.url', ''); await unlink(photoPath);