From afdba679e676ebb5d8010faf70272a006e544fc4 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:16:51 +0300 Subject: [PATCH] temp: save progress --- .dockerignore | 8 ++ .env.template | 10 --- .github/workflows/manual-deploy.yml | 43 ++++++++++ COMMITS | 11 +++ bootstrap.sh | 62 +++++++++++++ docker/development/Dockerfile | 52 +++++++++++ docker/development/docker-compose.yml | 8 ++ docker/production/Dockerfile | 53 ++++++++++++ docker/production/docker-compose.yml | 105 +++++++++++++++++++++++ src/app/api/v1/checkout/route.ts | 21 +++++ src/app/api/v1/webhook/checkout/route.ts | 13 +++ src/lib/printify.ts | 19 ++++ 12 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/manual-deploy.yml create mode 100644 COMMITS create mode 100644 bootstrap.sh create mode 100644 docker/development/Dockerfile create mode 100644 docker/development/docker-compose.yml create mode 100644 docker/production/Dockerfile create mode 100644 docker/production/docker-compose.yml create mode 100644 src/app/api/v1/checkout/route.ts create mode 100644 src/app/api/v1/webhook/checkout/route.ts create mode 100644 src/lib/printify.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c7db2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +docker +.git \ No newline at end of file diff --git a/.env.template b/.env.template index c349cb1..e69de29 100644 --- a/.env.template +++ b/.env.template @@ -1,10 +0,0 @@ -MY_SECRET_VALUE="foobar" - -# Auth0 - https://auth0.com/docs/quickstart/webapp/nextjs/interactive -# Allowed callback URLs: https://my-app.vercel.app/api/auth/callback, http://localhost:3000/api/auth/callback -# Allowed logout URLs: https://my-app.vercel.app, http://localhost:3000 -AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value' -AUTH0_BASE_URL='http://localhost:3000' -AUTH0_ISSUER_BASE_URL='https://{yourDomain}' -AUTH0_CLIENT_ID='{yourClientId}' -AUTH0_CLIENT_SECRET='{yourClientSecret}' \ No newline at end of file diff --git a/.github/workflows/manual-deploy.yml b/.github/workflows/manual-deploy.yml new file mode 100644 index 0000000..66b4cc6 --- /dev/null +++ b/.github/workflows/manual-deploy.yml @@ -0,0 +1,43 @@ +# .github/workflows/manual-deploy.yml +# TODO_PRODUCTION + +name: Manually Deploy + +on: + push: + branches: + - main + +jobs: + #bootstrap: + # run bootstrap.ssh? + # PUBLIC_KEY="your_ssh_public_key_here" DOMAIN="your_domain_here" bash bootstrap.sh + + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Re-deploy, update secrets + env: + # secure: only stored in memory during the remote-ssh session + API_KEY: ${{ secrets.API_KEY }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: | + ssh user@your-vm-ip << 'EOF' + # Authenticate with GitHub CLI using the passed PAT + echo "$GITHUB_TOKEN" | gh auth login --with-token + + # Create the .env file with secrets + { + echo "API_KEY=$API_KEY" + echo "DB_PASSWORD=$DB_PASSWORD" + } > .env + + # Run docker-compose with the .env file + docker-compose --env-file .env up -d + + # Remove the .env file immediately + rm .env + EOF \ No newline at end of file diff --git a/COMMITS b/COMMITS new file mode 100644 index 0000000..095db09 --- /dev/null +++ b/COMMITS @@ -0,0 +1,11 @@ +(update the changelog each time) +commits: +yarn add sharp +git add yarn.lock +feat: stripe checkout with dummy data +feat: printify order integration +feat: enable e2e checkout functionality (stripe + printify) +feat: passwordless email login + +checklist: +https://nextjs.org/docs/app/building-your-application/deploying/production-checklist \ No newline at end of file diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..f709934 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# bootstrap.sh +# script to install/configure software on ubuntu LTS virtual machine +# TODO_PRODUCTION - test this ubuntu LTS + +# Check if PUBLIC_KEY is set +if [ -z "$PUBLIC_KEY" ]; then + echo "Error: PUBLIC_KEY environment variable is not set." + exit 1 +fi + +# Check if DOMAIN is set +if [ -z "$DOMAIN" ]; then + echo "Error: DOMAIN environment variable is not set." + exit 1 +fi + +# Check if EMAIL is set +if [ -z "$EMAIL" ]; then + echo "Error: EMAIL environment variable is not set." + exit 1 +fi + +# Update and install necessary packages +apt update && apt upgrade -y +apt install -y software-properties-common + +# Install Docker +apt install -y docker.io +systemctl enable docker +systemctl start docker + +# Install Docker Compose +DOCKER_COMPOSE_VERSION="v2.16.0" # Change to the desired version +curl -SL "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose + +# Configure UFW +ufw allow OpenSSH +ufw enable + +# Install Certbot and Nginx +apt install -y certbot + +# Obtain the SSL certificate using standalone mode +certbot certonly --standalone -d "$DOMAIN" --non-interactive --agree-tos --email "$EMAIL" + + +# Disable password authentication +sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +systemctl restart ssh + +# Add SSH public key +mkdir -p ~/.ssh +echo "$PUBLIC_KEY" >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys + +# Restart UFW to apply changes +systemctl restart ufw + +echo "Bootstrap script completed successfully." \ No newline at end of file diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile new file mode 100644 index 0000000..079d6b9 --- /dev/null +++ b/docker/development/Dockerfile @@ -0,0 +1,52 @@ +FROM node:20.16-alpine3.19 AS base + +# 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Use the corresponding env file for each environment. +COPY .env.development .env.production +RUN npm run build + +# 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + +COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file diff --git a/docker/development/docker-compose.yml b/docker/development/docker-compose.yml new file mode 100644 index 0000000..917e078 --- /dev/null +++ b/docker/development/docker-compose.yml @@ -0,0 +1,8 @@ +services: + nextjs-app-local: + build: + context: ../../ + dockerfile: docker/development/Dockerfile + image: with-docker-multi-env-development + ports: + - "3001:3000" \ No newline at end of file diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile new file mode 100644 index 0000000..891a61d --- /dev/null +++ b/docker/production/Dockerfile @@ -0,0 +1,53 @@ +FROM node:20.16-alpine3.19 AS base + +# 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +# 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# This will do the trick, use the corresponding env file for each environment. +# TODO_DEPLOYMENT - does docker build need this? +# COPY .env.production.sample .env.production +RUN npm run build + +# 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + +COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml new file mode 100644 index 0000000..1f6f84f --- /dev/null +++ b/docker/production/docker-compose.yml @@ -0,0 +1,105 @@ +# TODO_PRODUCTION + +services: + watchtower: + image: containrrr/watchtower + command: + - "--label-enable" + - "--interval" + - "30" + - "--rolling-restart" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + reverse-proxy: + image: traefik:v3.1 + command: + - "--log.level=ERROR" + - "--accesslog=true" + - "--providers.docker" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.websecure.address=:443" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" + - "--certificatesresolvers.myresolver.acme.email=elliott@zenful.cloud" + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" + - "--entrypoints.web.address=:80" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entryPoints.web.forwardedHeaders.insecure" + - "--entryPoints.websecure.forwardedHeaders.insecure" + ports: + - "80:80" + - "443:443" + volumes: + - letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + nextjs-app: + image: ghcr.io/spencerlepine/nextjs-app:prod + labels: + - "traefik.enable=true" + - "traefik.http.middlewares.nextjs-app-ratelimit.ratelimit.average=20" + - "traefik.http.routers.nextjs-app.rule=Host(`zenful.cloud`) && !Method(`POST`)" + - "traefik.http.routers.nextjs-app.entrypoints=websecure" + - "traefik.http.routers.nextjs-app.tls.certresolver=myresolver" + - "traefik.http.routers.nextjs-app.middlewares=nextjs-app-ratelimit" + # Define separate router for POST methods + - "traefik.http.middlewares.nextjs-app-ratelimit-post.ratelimit.average=1" + - "traefik.http.middlewares.nextjs-app-ratelimit-post.ratelimit.period=1m" + - "traefik.http.routers.nextjs-app-post.rule=Host(`zenful.cloud`) && Method(`POST`)" + - "traefik.http.routers.nextjs-app-post.middlewares=nextjs-app-ratelimit-post" + - "traefik.http.routers.nextjs-app-post.entrypoints=websecure" + - "traefik.http.routers.nextjs-app-post.tls.certresolver=myresolver" + # Proxy + - "traefik.http.routers.proxy.rule=Host(`proxy.dreamsofcode.io`)" + - "traefik.http.routers.proxy.entrypoints=websecure" + - "traefik.http.routers.proxy.tls.certresolver=myresolver" + # Enable watchtower + - "com.centurylinklabs.watchtower.enable=true" + environment: + - POSTGRES_HOST=db + - POSTGRES_USER=postgres + - POSTGRES_DB=nextjs-app + - POSTGRES_PORT=5432 + - POSTGRES_SSLMODE=disable + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + deploy: + mode: replicated + replicas: 3 + restart: always + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 40s + db: + image: postgres + restart: always + user: postgres + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=nextjs-app + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + expose: + - 5432 + healthcheck: + test: [ "CMD", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + dragonfly: + image: 'docker.dragonflydb.io/dragonflydb/dragonfly' + ulimits: + memlock: -1 + network_mode: "host" + volumes: + - dragonflydata:/data + +volumes: + db-data: + letsencrypt: + dragonflydata: diff --git a/src/app/api/v1/checkout/route.ts b/src/app/api/v1/checkout/route.ts new file mode 100644 index 0000000..15499b9 --- /dev/null +++ b/src/app/api/v1/checkout/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; + +// export const POST = async (request: NextRequest) => { +export const POST = async () => { + try { + // const cartProducts = await request.json(); + // parse cartProducts by ID and SIZE. + // generate prices server size + // + make sure to pass metadata to stripe, (slug, size) + + // // source: https://github.com/dayhaysoos/use-shopping-cart/blob/master/use-shopping-cart/utilities/serverless.js + // const line_items = validateCartItems(PRODUCT_INVENTORY, cartProducts) + + // TODO_STRIPE - endpoint + + return NextResponse.json({ message: 'work in progress' }); + } catch (error) { + console.error('Error processing request:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/app/api/v1/webhook/checkout/route.ts b/src/app/api/v1/webhook/checkout/route.ts new file mode 100644 index 0000000..50fc797 --- /dev/null +++ b/src/app/api/v1/webhook/checkout/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; + +// export const POST = async (request: NextRequest) => { +export const POST = async () => { + try { + // TODO_STRIPE - endpoint + + return NextResponse.json({ message: 'work in progress' }); + } catch (error) { + console.error('Error processing request:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/lib/printify.ts b/src/lib/printify.ts new file mode 100644 index 0000000..df75a18 --- /dev/null +++ b/src/lib/printify.ts @@ -0,0 +1,19 @@ +// // TODO_PRINTIFY +// export const formatCartItemForPrintify = () => {}; + +// function formatCartItemsForPrintify(clientCartItems) { +// // TODO_PRINTIFY +// const items = []; +// return items; +// } + +// // TODO_PRINTIFY +// async function sendOrderToProduction() { +// // return printifyOrderId +// } + +// // TODO_PRINTIFY +// async function createDraftOrder() { +// // draft an order, pass items, customer email +// // return printifyOrderId +// }