diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..09319672 --- /dev/null +++ b/.env.example @@ -0,0 +1,63 @@ +# Environment +NODE_ENV=development + +# Ports +PORT=3000 + +# Client Port & URL (for development) +__DEV__CLIENT_PORT=5173 # Only used in development +__DEV__CLIENT_URL=http://localhost:5173 # Only used in development + +# URLs +PUBLIC_URL=http://localhost:3000 # This must reference a publicly accessible domain or IP address, not a docker container ID +STORAGE_URL=http://localhost:9000 # This must reference a publicly accessible domain or IP address, not a docker container ID +CHROME_URL=ws://localhost:8080 + +# Database (Prisma/PostgreSQL) +# This can be swapped out to use any other database, like MySQL +# Note: This is used only in the compose.yml file +POSTGRES_PORT=5432 +POSTGRES_DB=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres + +# Database (Prisma/PostgreSQL) +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public + +# Authentication Secrets +# generated with `openssl rand -base64 64` +ACCESS_TOKEN_SECRET=access_token_secret +REFRESH_TOKEN_SECRET=refresh_token_secret + +# Chrome Browser (for printing) +# generated with `openssl rand -hex 32` +CHROME_PORT=8080 +CHROME_TOKEN=chrome_token + +# Mail Server (for e-mails) +# For testing, you can use https://ethereal.email/create +# SMTP_URL=smtp://username:password@smtp.ethereal.email:587 + +# Storage +STORAGE_ENDPOINT=localhost +STORAGE_PORT=9000 +STORAGE_REGION=us-east-1 +STORAGE_BUCKET=default +STORAGE_ACCESS_KEY=minioadmin +STORAGE_SECRET_KEY=minioadmin + +# Redis (for cache & server session management) +REDIS_URL=redis://default:password@localhost:6379 + +# Sentry (for error reporting, Optional) +# SENTRY_DSN= + +# GitHub (OAuth, Optional) +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= + +# Google (OAuth, Optional) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL= diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index 88c0bcf4..7e2c4c10 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -33,8 +33,7 @@ jobs: - name: Extract version from package.json id: version - run: | - echo "version=$(jq -r '.version' package.json)" >> "$GITHUB_OUTPUT" + run: echo "version=$(jq -r '.version' package.json)" >> "$GITHUB_OUTPUT" - name: Set up QEMU uses: docker/setup-qemu-action@v3.0.0 @@ -48,6 +47,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to Quay.io + uses: docker/login-action@v3.0.0 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + - name: Login to GitHub Container Registery uses: docker/login-action@v3.0.0 with: @@ -59,8 +65,10 @@ jobs: id: meta uses: docker/metadata-action@v5.0.0 with: - images: ${{ env.IMAGE }} tags: type=semver,pattern={{version}},prefix=v,value=${{ steps.version.outputs.version }} + images: | + ${{ env.IMAGE }} + ghcr.io/${{ env.IMAGE }} - name: Build and Push by Digest uses: docker/build-push-action@v5.0.0 @@ -112,8 +120,10 @@ jobs: id: meta uses: docker/metadata-action@v5.0.0 with: - images: ${{ env.IMAGE }} tags: type=semver,pattern={{version}},prefix=v,value=${{ needs.build.outputs.version }} + images: | + ${{ env.IMAGE }} + ghcr.io/${{ env.IMAGE }} - name: Login to Docker Hub uses: docker/login-action@v3.0.0 @@ -121,6 +131,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to Quay.io + uses: docker/login-action@v3.0.0 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + - name: Login to GitHub Container Registery uses: docker/login-action@v3.0.0 with: diff --git a/.gitignore b/.gitignore index 9318bd04..89cc9e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ stats.html libs/prisma # Environment Variables -*.env* \ No newline at end of file +*.env* +!.env.example \ No newline at end of file diff --git a/apps/server/src/health/health.controller.ts b/apps/server/src/health/health.controller.ts index a83dfe89..48ca1b51 100644 --- a/apps/server/src/health/health.controller.ts +++ b/apps/server/src/health/health.controller.ts @@ -1,5 +1,6 @@ import { CacheInterceptor, CacheKey, CacheTTL } from "@nestjs/cache-manager"; import { Controller, Get, NotFoundException, UseInterceptors } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { HealthCheck, HealthCheckService } from "@nestjs/terminus"; import { RedisService } from "@songkeys/nestjs-redis"; import { RedisHealthIndicator } from "@songkeys/nestjs-redis-health"; @@ -9,6 +10,7 @@ import { BrowserHealthIndicator } from "./browser.health"; import { DatabaseHealthIndicator } from "./database.health"; import { StorageHealthIndicator } from "./storage.health"; +@ApiTags("Health") @Controller("health") export class HealthController { constructor( diff --git a/apps/server/src/storage/storage.controller.ts b/apps/server/src/storage/storage.controller.ts index d55779a9..8fa781a2 100644 --- a/apps/server/src/storage/storage.controller.ts +++ b/apps/server/src/storage/storage.controller.ts @@ -7,12 +7,14 @@ import { UseInterceptors, } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; +import { ApiTags } from "@nestjs/swagger"; import { TwoFactorGuard } from "@/server/auth/guards/two-factor.guard"; import { User } from "@/server/user/decorators/user.decorator"; import { StorageService } from "./storage.service"; +@ApiTags("Storage") @Controller("storage") export class StorageController { constructor(private readonly storageService: StorageService) {} diff --git a/package.json b/package.json index 48485137..633b5ac9 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "@songkeys/nestjs-redis": "^10.0.0", "@songkeys/nestjs-redis-health": "^10.0.0", "@swc/helpers": "~0.5.3", - "@tanstack/react-query": "^5.7.1", + "@tanstack/react-query": "^5.7.2", "@tiptap/extension-highlight": "^2.1.12", "@tiptap/extension-image": "^2.1.12", "@tiptap/extension-link": "^2.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7463a381..d201121f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,8 +153,8 @@ dependencies: specifier: ~0.5.3 version: 0.5.3 '@tanstack/react-query': - specifier: ^5.7.1 - version: 5.7.1(react-dom@18.2.0)(react@18.2.0) + specifier: ^5.7.2 + version: 5.7.2(react-dom@18.2.0)(react@18.2.0) '@tiptap/extension-highlight': specifier: ^2.1.12 version: 2.1.12(@tiptap/core@2.1.12) @@ -6381,12 +6381,12 @@ packages: - typescript dev: true - /@tanstack/query-core@5.4.3: - resolution: {integrity: sha512-fnI9ORjcuLGm1sNrKatKIosRQUpuqcD4SV7RqRSVmj8JSicX2aoMyKryHEBpVQvf6N4PaBVgBxQomjsbsGPssQ==} + /@tanstack/query-core@5.7.2: + resolution: {integrity: sha512-vPYoNCOI8W+jFLnyEAYQL7/qURE7XzVE/TvkmNSko8LU55jFshee342p4GNFOTsYFgJty7Da5bw4m2P3vaKWMw==} dev: false - /@tanstack/react-query@5.7.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-7onwx519jOKh7/nspf70dOSejJSYfhFRnVsSicuPrmO8L6Iw05G1wwDpp1PjqFLsm7suixlHDXhcNfK0sy0kWg==} + /@tanstack/react-query@5.7.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NMsfz0Wd3VPDvSpX9sw2x07r0kf9HwuAlNNHQvwMUHM7EcuVN/LYmlmTe40dWER1c/92Jhc5LODdtW0PyvHsFg==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -6397,7 +6397,7 @@ packages: react-native: optional: true dependencies: - '@tanstack/query-core': 5.4.3 + '@tanstack/query-core': 5.7.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false diff --git a/tools/compose/simple.yml b/tools/compose/simple.yml index d4c8962f..839b483a 100644 --- a/tools/compose/simple.yml +++ b/tools/compose/simple.yml @@ -50,7 +50,7 @@ services: command: redis-server --requirepass password app: - image: amruthpillai/reactive-resume + image: amruthpillai/reactive-resume:v4.0.0-alpha.0 restart: unless-stopped ports: - 3000:3000 @@ -63,19 +63,25 @@ services: # -- Environment Variables -- PORT: 3000 NODE_ENV: production + # -- URLs -- PUBLIC_URL: http://localhost:3000 STORAGE_URL: http://localhost:9000 + # -- Printer (Chrome) -- CHROME_URL: ws://chrome:3000 CHROME_TOKEN: chrome_token + # -- Database (Postgres) -- DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres + # -- Auth -- ACCESS_TOKEN_SECRET: access_token_secret REFRESH_TOKEN_SECRET: refresh_token_secret + # -- Emails -- # SMTP_URL: smtp://user:pass@smtp:587 # Optional + # -- Storage (Minio) -- STORAGE_ENDPOINT: minio STORAGE_PORT: 9000 @@ -83,14 +89,18 @@ services: STORAGE_BUCKET: default STORAGE_ACCESS_KEY: minioadmin STORAGE_SECRET_KEY: minioadmin + # -- Cache (Redis) -- REDIS_URL: redis://default:password@redis:6379 + # -- Sentry -- # SENTRY_DSN: https://id.sentry.io # Optional + # -- GitHub -- GITHUB_CLIENT_ID: github_client_id GITHUB_CLIENT_SECRET: github_client_secret GITHUB_CALLBACK_URL: http://localhost:3000/api/auth/github/callback + # -- Google -- GOOGLE_CLIENT_ID: google_client_id GOOGLE_CLIENT_SECRET: google_client_secret diff --git a/tools/compose/traefik-secure.yml b/tools/compose/traefik-secure.yml new file mode 100644 index 00000000..eb4047e6 --- /dev/null +++ b/tools/compose/traefik-secure.yml @@ -0,0 +1,150 @@ +version: "3" + +# In this Docker Compose example, we use Traefik to route requests to the app and storage containers in a secure manner (HTTPS). +# This example assumes you have a domain name (example.com) and a wildcard DNS record pointing to your server. +# The only exposed ports here are from Traefik (80 and 443). All non-secure requests are redirected to HTTPS. +# Note: Please change `example.com` to your domain name where necessary. + +services: + # Database (Postgres) + postgres: + image: postgres + restart: unless-stopped + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + healthcheck: + test: ["CMD", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + + # Storage (for image uploads) + minio: + image: minio/minio + restart: unless-stopped + command: server /data + volumes: + - minio_data:/data + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + labels: + - traefik.enable + - traefik.http.routers.app.rule=Host(`storage.example.com`) + - traefik.http.routers.app.entrypoints=websecure + - traefik.http.routers.app.tls.certresolver=letsencrypt + - traefik.http.services.app.loadbalancer.server.port=9000 + + # Chrome Browser (for printing and previews) + chrome: + image: browserless/chrome + restart: unless-stopped + environment: + TOKEN: chrome_token + EXIT_ON_HEALTH_FAILURE: true + PRE_REQUEST_HEALTH_CHECK: true + + # Redis (for cache & server session management) + redis: + image: redis + restart: unless-stopped + command: redis-server --save 60 1 --loglevel warning --requirepass password + volumes: + - redis_data:/data + + app: + image: amruthpillai/reactive-resume:v4.0.0-alpha.0 + restart: unless-stopped + depends_on: + - postgres + - minio + - redis + - chrome + environment: + # -- Environment Variables -- + PORT: 3000 + NODE_ENV: production + + # -- URLs -- + PUBLIC_URL: http://example.com + STORAGE_URL: http://storage.example.com + + # -- Printer (Chrome) -- + CHROME_URL: ws://chrome:3000 + CHROME_TOKEN: chrome_token + + # -- Database (Postgres) -- + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres + + # -- Auth -- + ACCESS_TOKEN_SECRET: access_token_secret + REFRESH_TOKEN_SECRET: refresh_token_secret + + # -- Emails -- + # SMTP_URL: smtp://user:pass@smtp:587 # Optional + + # -- Storage (Minio) -- + STORAGE_ENDPOINT: minio + STORAGE_PORT: 9000 + STORAGE_REGION: us-east-1 # Optional + STORAGE_BUCKET: default + STORAGE_ACCESS_KEY: minioadmin + STORAGE_SECRET_KEY: minioadmin + + # -- Cache (Redis) -- + REDIS_URL: redis://default:password@redis:6379 + + # -- Sentry -- + # SENTRY_DSN: https://id.sentry.io # Optional + + # -- GitHub -- + GITHUB_CLIENT_ID: github_client_id + GITHUB_CLIENT_SECRET: github_client_secret + GITHUB_CALLBACK_URL: http://localhost:3000/api/auth/github/callback + + # -- Google -- + GOOGLE_CLIENT_ID: google_client_id + GOOGLE_CLIENT_SECRET: google_client_secret + GOOGLE_CALLBACK_URL: http://localhost:3000/api/auth/google/callback + labels: + - traefik.enable + - traefik.http.routers.app.rule=Host(`example.com`) + - traefik.http.routers.app.entrypoints=websecure + - traefik.http.routers.app.tls + - traefik.http.routers.app.tls.certresolver=letsencrypt + - traefik.http.services.app.loadbalancer.server.port=3000 + + traefik: + image: traefik + command: + - --api + - --providers.docker + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.letsencrypt.acme.tlschallenge + - --certificatesresolvers.letsencrypt.acme.email=noreply@example.com + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + + # Let's Encrypt Staging Server (for testing) + - --certificatesResolvers.letsencrypt.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory + + # Redirect all HTTP requests to HTTPS + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + ports: + - 80:80 + - 443:443 + volumes: + - letsencrypt_data:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + minio_data: + redis_data: + postgres_data: + letsencrypt_data: diff --git a/tools/compose/traefik.yml b/tools/compose/traefik.yml index 4df7e3fd..e3b58247 100644 --- a/tools/compose/traefik.yml +++ b/tools/compose/traefik.yml @@ -53,7 +53,7 @@ services: command: redis-server --requirepass password app: - image: amruthpillai/reactive-resume + image: amruthpillai/reactive-resume:v4.0.0-alpha.0 restart: unless-stopped depends_on: - postgres @@ -64,19 +64,25 @@ services: # -- Environment Variables -- PORT: 3000 NODE_ENV: production + # -- URLs -- PUBLIC_URL: http://example.com STORAGE_URL: http://storage.example.com + # -- Printer (Chrome) -- CHROME_URL: ws://chrome:3000 CHROME_TOKEN: chrome_token + # -- Database (Postgres) -- DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres + # -- Auth -- ACCESS_TOKEN_SECRET: access_token_secret REFRESH_TOKEN_SECRET: refresh_token_secret + # -- Emails -- # SMTP_URL: smtp://user:pass@smtp:587 # Optional + # -- Storage (Minio) -- STORAGE_ENDPOINT: minio STORAGE_PORT: 9000 @@ -84,14 +90,18 @@ services: STORAGE_BUCKET: default STORAGE_ACCESS_KEY: minioadmin STORAGE_SECRET_KEY: minioadmin + # -- Cache (Redis) -- REDIS_URL: redis://default:password@redis:6379 + # -- Sentry -- # SENTRY_DSN: https://id.sentry.io # Optional + # -- GitHub -- GITHUB_CLIENT_ID: github_client_id GITHUB_CLIENT_SECRET: github_client_secret GITHUB_CALLBACK_URL: http://localhost:3000/api/auth/github/callback + # -- Google -- GOOGLE_CLIENT_ID: google_client_id GOOGLE_CLIENT_SECRET: google_client_secret