Skip to content

Commit

Permalink
Refactor application
Browse files Browse the repository at this point in the history
* Migrate from `express` to `hono`
* Restructure backend files similar to Nest.js
* Use TypeScript `strict` mode and fix related issues
* Use Zod and OpenAPI integration of Hono to validate requests and responses
* Use typed Hono Client in frontend
* Validate environment variables with Zod
* Allow configuration of number of trusted proxies
* Add security-related headers to all HTTP responses
* Replace `supertest` instance with Hono and `app.request`

Signed-off-by: Marvin A. Ruder <[email protected]>
  • Loading branch information
marvinruder committed Aug 28, 2024
1 parent 414aff7 commit b61b061
Show file tree
Hide file tree
Showing 277 changed files with 15,077 additions and 13,702 deletions.
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ globalFolder: ${HOME}/.cache/yarn

networkConcurrency: 1024

packageExtensions: {}
packageExtensions: {}
7 changes: 1 addition & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,7 @@ RUN \
cp -r packages/backend/dist/* /app && \
cp -r packages/backend/prisma/migrations /app/prisma && \
cp packages/backend/prisma/client/schema.prisma /app/prisma/client && \
ln -s ./client/schema.prisma /app/prisma/schema.prisma && \
cp \
.yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui.css \
.yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui-bundle.js \
.yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js \
/app/public/api-docs/
ln -s ./client/schema.prisma /app/prisma/schema.prisma

FROM --platform=$BUILDPLATFORM node:22.7.0-alpine AS build-frontend
ENV NODE_ENV=production
Expand Down
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
:-:|:-:
| **Quality** | [![GitHub checks](https://img.shields.io/github/checks-status/marvinruder/rating-tracker/main?logo=github&label=Checks&style=flat-square)](https://github.com/marvinruder/rating-tracker/actions) [![Codacy grade](https://img.shields.io/codacy/grade/6a7a7b68631a42ef88fc478a709141ea?label=Code%20Quality&logo=codacy&style=flat-square)](https://app.codacy.com/gh/marvinruder/rating-tracker/dashboard) [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/marvinruder/rating-tracker/github-actions.yml?label=GitHub%20Actions&logo=github-actions&style=flat-square)](https://github.com/marvinruder/rating-tracker/actions/workflows/github-actions.yml) [![CodeQL](https://img.shields.io/github/actions/workflow/status/marvinruder/rating-tracker/codeql.yml?label=CodeQL&logo=github-actions&style=flat-square)](https://github.com/marvinruder/rating-tracker/actions/workflows/codeql.yml) [![Codacy coverage](https://img.shields.io/codacy/coverage/6a7a7b68631a42ef88fc478a709141ea?logo=codacy&label=Coverage&style=flat-square)](https://app.codacy.com/gh/marvinruder/rating-tracker/coverage/dashboard) [![Jenkins build](https://jenkins.mruder.dev/buildStatus/icon?job=rating-tracker-multibranch%2Fmain&subject=Build&style=flat-square)](https://jenkins.internal.mruder.dev/job/rating-tracker-multibranch) <!-- ![Snyk Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/marvinruder/rating-tracker?label=Vulnerabilities&style=flat-square) --> |
| **Repository** | [![GitHub Contributors](https://img.shields.io/github/contributors/marvinruder/rating-tracker?label=Contributors&logo=github&style=flat-square)](https://github.com/marvinruder/rating-tracker/graphs/contributors) [![Commit Activity](https://img.shields.io/github/commit-activity/m/marvinruder/rating-tracker?label=Commit%20Activity&logo=github&style=flat-square)](https://github.com/marvinruder/rating-tracker/graphs/commit-activity) [![Last commit](https://img.shields.io/github/last-commit/marvinruder/rating-tracker?label=Last%20Commit&logo=github&style=flat-square)](https://github.com/marvinruder/rating-tracker/commits/main) [![Issues](https://img.shields.io/github/issues/marvinruder/rating-tracker?label=Issues&logo=github&style=flat-square)](https://github.com/marvinruder/rating-tracker/issues) [![Bugs](https://img.shields.io/github/issues/marvinruder/rating-tracker/bug?label=Bug%20Issues&logo=openbugbounty&logoColor=red&style=flat-square)](https://github.com/marvinruder/rating-tracker/issues?q=is%3Aopen+is%3Aissue+label%3Abug) [![Pull Requests](https://img.shields.io/github/issues-pr/marvinruder/rating-tracker?label=Pull%20Requests&logo=github&style=flat-square)](https://github.com/marvinruder/rating-tracker/pulls) |
| **Dependencies** | [![Typescript](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/dev/typescript?label=Typescript&logo=typescript&color=3178C6&style=flat-square)](https://www.typescriptlang.org) [![esbuild](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/esbuild?filename=packages%2Fbackend%2Fpackage.json&label=esbuild&logo=esbuild&color=FFCF00&style=flat-square)](https://esbuild.github.io) [![Prisma](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/@prisma/client?filename=packages%2Fbackend%2Fpackage.json&label=Prisma&logo=prisma&color=2D3748&style=flat-square)](https://www.prisma.io) [![React](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/react?filename=packages%2Ffrontend%2Fpackage.json&label=React&logo=react&color=61DAFB&style=flat-square)](https://react.dev) [![Material UI](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/@mui/material?filename=packages%2Ffrontend%2Fpackage.json&label=Material%20UI&logo=mui&color=007FFF&style=flat-square)](https://mui.com) [![Vite](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/vite?filename=packages%2Ffrontend%2Fpackage.json&label=Vite&logo=vite&color=646CFF&style=flat-square)](https://vitejs.dev) [![Vitest](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/vitest?filename=packages%2Fbackend%2Fpackage.json&label=Vitest&logo=vitest&color=6E9F18&style=flat-square)](https://vitest.dev) [![Package Manager](https://img.shields.io/badge/dynamic/json?label=Package%20Manager&query=%24.packageManager&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmarvinruder%2Frating-tracker%2Fmain%2Fpackage.json&logo=yarn&color=2C8EBB&style=flat-square)](https://yarnpkg.com) |
| **Dependencies** | [![Typescript](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/dev/typescript?label=Typescript&logo=typescript&color=3178C6&style=flat-square)](https://www.typescriptlang.org) [![esbuild](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/esbuild?filename=packages%2Fbackend%2Fpackage.json&label=esbuild&logo=esbuild&color=FFCF00&style=flat-square)](https://esbuild.github.io) [![Hono](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/hono?filename=packages%2Fbackend%2Fpackage.json&label=Hono&logo=hono&color=E36002&style=flat-square)](https://hono.dev) [![Prisma](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/@prisma/client?filename=packages%2Fbackend%2Fpackage.json&label=Prisma&logo=prisma&color=2D3748&style=flat-square)](https://www.prisma.io) [![React](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/react?filename=packages%2Ffrontend%2Fpackage.json&label=React&logo=react&color=61DAFB&style=flat-square)](https://react.dev) [![Material UI](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/@mui/material?filename=packages%2Ffrontend%2Fpackage.json&label=Material%20UI&logo=mui&color=007FFF&style=flat-square)](https://mui.com) [![Vite](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/vite?filename=packages%2Ffrontend%2Fpackage.json&label=Vite&logo=vite&color=646CFF&style=flat-square)](https://vitejs.dev) [![Vitest](https://img.shields.io/github/package-json/dependency-version/marvinruder/rating-tracker/vitest?filename=packages%2Fbackend%2Fpackage.json&label=Vitest&logo=vitest&color=6E9F18&style=flat-square)](https://vitest.dev) [![Package Manager](https://img.shields.io/badge/dynamic/json?label=Package%20Manager&query=%24.packageManager&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmarvinruder%2Frating-tracker%2Fmain%2Fpackage.json&logo=yarn&color=2C8EBB&style=flat-square)](https://yarnpkg.com) |

---

Expand Down Expand Up @@ -222,6 +222,7 @@ services:
PORT: 21076
DOMAIN: "example.com"
SUBDOMAIN: "ratingtracker"
TRUSTWORTHY_PROXY_COUNT: 1
LOG_FILE: "/app/logs/rating-tracker-log-(DATE).log" # (DATE) is replaced by the current date to support log rotation
DATABASE_URL: "postgresql://rating-tracker:********@postgres:5432/rating-tracker?schema=rating-tracker"
MAX_FETCH_CONCURRENCY: 4
Expand Down Expand Up @@ -257,21 +258,12 @@ Run a shell in the Signal REST API container and proceed with [this excellent do
#### Configure webserver as reverse proxy
After setting up NGINX as a webserver with SSL, the following virtual host configuration can be used to run a reverse proxy which also adds security- and privacy-related HTTP headers compatible with Rating Tracker.
After setting up NGINX as a webserver with SSL, the following virtual host configuration can be used to run a reverse proxy:
<details>
<summary>View NGINX configuration</summary>
```nginx
add_header "Strict-Transport-Security" "max-age=31536000; includeSubDomains" always;
add_header "X-Frame-Options" "DENY";
add_header "X-Content-Type-Options" "nosniff";
add_header "Referrer-Policy" "same-origin";
add_header "Cross-Origin-Opener-Policy" "same-origin";
add_header "Cross-Origin-Resource-Policy" "same-site";
add_header "Cross-Origin-Embedder-Policy" "credentialless";
add_header "Permissions-Policy" "interest-cohort=();";

resolver 127.0.0.11 valid=15s; # DNS resolver from Docker to resolve Docker Compose container names

location / {
Expand Down Expand Up @@ -301,6 +293,7 @@ Variable | Example Value | Explanation
**`PORT`** | `21076` | The TCP port Rating Tracker is served on.
**`DOMAIN`** | `example.com` | The domain Rating Tracker will be available at. This is especially important for WebAuthn, since credentials will only be offered to the user by their client when the domain provided as part of the registration or authentication challence matches the domain of the URL the user navigated to.
`SUBDOMAIN` | `ratingtracker` | An optional subdomain. Credentials created for one domain can be used to authenticate to different Rating Tracker instances served on all subdomains of that domain, making it easy to use multiple deployment stages, development servers etc.
`TRUSTWORTHY_PROXY_COUNT` | `1` | A number of trusted proxies that are allowed to set the `X-Forwarded-For` header to identifier the real IP of a client. If unset, the header is not trusted.
**`DATABASE_URL`** | `postgresql://rating-tracker:********@127.0.0.1:5432/rating-tracker?schema=rating-tracker` | The connection URL of the PostgreSQL instance, specifying username, password, host, port, database and schema. Can also use the PostgreSQL service name (e.g. `postgres` in [this configuration](#minimal-example-setup-using-docker-compose)) as hostname if set up within the same Docker Compose file.
`LOG_FILE` | `/var/log/rating-tracker-(DATE).log` | A file path for storing Rating Tracker log files. The string `(DATE)` will be replaced by the current date. If unset, logs are stored in the `/tmp` directory.
`LOG_LEVEL` | `debug` | The level for the log output to `stdout`. Can be one of `fatal`, `error`, `warn`, `info`, `debug`, `trace`. If unset, `info` will be used.
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@
},
"prisma": {
"built": true
},
"swagger-ui-dist": {
"unplugged": true
}
}
}
1 change: 1 addition & 0 deletions packages/backend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"rules": {
"require-await": ["warn"],
"no-unused-vars": "off",
"prefer-template": "warn",
"@typescript-eslint/await-thenable": ["warn"],
"@typescript-eslint/consistent-type-imports": ["warn", {}],
"@typescript-eslint/no-import-type-side-effects": ["warn"],
Expand Down
73 changes: 54 additions & 19 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,50 @@
},
"license": "MIT",
"type": "module",
"exports": {
"./api/account": {
"types": "./src/account/account.controller.ts"
},
"./api/auth": {
"types": "./src/auth/auth.controller.ts"
},
"./api/favorites": {
"types": "./src/favorite/favorite.controller.ts"
},
"./api/fetch": {
"types": "./src/fetch/fetch.controller.ts"
},
"./api/logobackground": {
"types": "./src/stock/logobackground.controller.ts"
},
"./api/portfolios": {
"types": "./src/portfolio/portfolio.controller.ts"
},
"./api/proxy": {
"types": "./src/proxy/proxy.controller.ts"
},
"./api/resources": {
"types": "./src/resource/resource.controller.ts"
},
"./api/session": {
"types": "./src/session/session.controller.ts"
},
"./api/status": {
"types": "./src/status/status.controller.ts"
},
"./api/stocks": {
"types": "./src/stock/stock.controller.ts"
},
"./api/users": {
"types": "./src/user/user.controller.ts"
},
"./api/watchlists": {
"types": "./src/watchlist/watchlist.controller.ts"
},
"./types": {
"types": "./src/types/index.d.ts"
}
},
"scripts": {
"dev:run": "node --env-file=.env --watch --enable-source-maps dist/server.mjs",
"dev:watch": "yarn build --watch",
Expand All @@ -18,51 +62,42 @@
"prisma:generate": "pnpify prisma generate",
"prisma:migrate:dev": "pnpify prisma migrate dev",
"prisma:migrate:diff": "pnpify prisma migrate diff --from-schema-datasource ./prisma/schema.prisma --to-schema-datamodel ./prisma/schema.prisma --script",
"fix:bundle": "mkdir -p dist/public/api-docs && cp ../../.yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui{.css,-bundle.js,-standalone-preset.js} dist/public/api-docs/",
"build": "esbuild --color=true src/server.ts --bundle --platform=node --packages=bundle --format=esm --inject:./src/utils/cjs-shim.ts --inject:./src/utils/h2-shim.ts --target=node$(node -v | sed 's/^v//') --sourcemap=linked --sources-content=false --log-override:commonjs-variable-in-esm=silent --outfile=dist/server.mjs",
"build": "esbuild --color=true src/server.ts --bundle --platform=node --packages=bundle --format=esm --inject:./src/utils/cjs-shim.ts --target=node$(node -v | sed 's/^v//') --sourcemap=linked --sources-content=false --log-override:commonjs-variable-in-esm=silent --outfile=dist/server.mjs",
"build:logFormatterConfig": "esbuild src/utils/logFormatterConfig.ts --bundle --minify --platform=node --packages=bundle --format=cjs --log-override:commonjs-variable-in-esm=silent --outfile=dist/pino-pretty-config.cjs",
"typecheck": "tsc --noEmit --tsBuildInfoFile $HOME/.cache/rating-tracker/backend.tsbuildinfo",
"lint": "eslint --color --cache --cache-location $HOME/.cache/rating-tracker/backend.eslintcache --cache-strategy content --max-warnings 0 --ext .ts src/ test/",
"lint:fix": "yarn lint --fix",
"postinstall": "yarn prisma:generate && yarn fix:bundle"
"postinstall": "yarn prisma:generate"
},
"dependencies": {
"@hono/node-server": "1.12.2",
"@hono/swagger-ui": "0.4.0",
"@hono/zod-openapi": "0.15.3",
"@prisma/client": "5.19.0",
"@rating-tracker/commons": "workspace:*",
"@simplewebauthn/server": "10.0.1",
"@vitest/coverage-v8": "2.0.5",
"@xmldom/xmldom": "0.8.10",
"@yarnpkg/pnpify": "4.1.1",
"chalk": "5.3.0",
"concurrently": "8.2.2",
"cookie-parser": "1.4.6",
"esbuild": "0.23.1",
"express": "4.19.2",
"express-async-errors": "3.1.1",
"express-openapi-validator": "5.3.3",
"express-rate-limit": "7.4.0",
"hono": "4.5.9",
"hono-rate-limiter": "0.4.0",
"node-cron": "3.0.3",
"pino": "9.3.2",
"pino-pretty": "11.2.2",
"prisma": "5.19.0",
"prisma-json-types-generator": "3.0.4",
"response-time": "2.3.2",
"supertest": "7.0.0",
"swagger-ui-dist": "5.17.14",
"swagger-ui-express": "5.0.1",
"typescript": "5.5.4",
"vite": "5.4.2",
"vitest": "2.0.5",
"xpath-ts2": "1.4.2"
"xpath-ts2": "1.4.2",
"zod": "3.23.8",
"zod-validation-error": "3.3.1"
},
"devDependencies": {
"@types/cookie-parser": "1.4.7",
"@types/express": "4.17.21",
"@types/node": "22.5.1",
"@types/node-cron": "3.0.11",
"@types/response-time": "2.3.8",
"@types/supertest": "6.0.2",
"@types/swagger-ui-express": "4.1.6",
"@typescript-eslint/eslint-plugin": "8.3.0",
"@typescript-eslint/parser": "8.3.0",
"@vitest/ui": "2.0.5",
Expand Down
Loading

0 comments on commit b61b061

Please sign in to comment.