diff --git a/.github/workflows/build-and-push-docker.yaml b/.github/workflows/build-and-push-docker.yaml index ebce2a2..2371709 100644 --- a/.github/workflows/build-and-push-docker.yaml +++ b/.github/workflows/build-and-push-docker.yaml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main workflow_dispatch: permissions: @@ -12,7 +15,7 @@ permissions: jobs: build-backend: - name: 🛠️ Build and Push Backend + name: 🛠️ Build and Push runs-on: ubuntu-latest steps: - name: 🚀 Checkout Code @@ -22,6 +25,13 @@ jobs: run: | echo "GIT_COMMIT_SHA=${{ github.sha }}" >> $GITHUB_ENV echo "GIT_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "IMAGE_TAG=latest" >> $GITHUB_ENV + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "IMAGE_TAG=rc-${{ github.event.pull_request.number }}" >> $GITHUB_ENV + else + echo "IMAGE_TAG=unknown" >> $GITHUB_ENV + fi - name: 🛠️ Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -33,63 +43,21 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.TOKEN }} - - name: 📦 Build and Push Backend Image + - name: 📦 Build and Push Image uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile push: true - tags: ghcr.io/kek-sec/gopherdrop:backend-latest - annotations: | - org.opencontainers.image.description=Backend API for GopherDrop, a secure one-time secret sharing service - build-args: | - GIT_COMMIT_SHA=${{ env.GIT_COMMIT_SHA }} - GIT_VERSION=${{ env.GIT_VERSION }} - labels: | - org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop - org.opencontainers.image.revision=${{ env.GIT_COMMIT_SHA }} - org.opencontainers.image.version=${{ env.GIT_VERSION }} - cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:backend-latest - cache-to: type=inline - - build-ui: - name: 🖥️ Build and Push UI - runs-on: ubuntu-latest - steps: - - name: 🚀 Checkout Code - uses: actions/checkout@v4 - - - name: 🏗️ Set Build Variables - run: | - echo "GIT_COMMIT_SHA=${{ github.sha }}" >> $GITHUB_ENV - echo "GIT_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - - - name: 🛠️ Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: 🔐 Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.TOKEN }} - - - name: 📦 Build and Push UI Image - uses: docker/build-push-action@v4 - with: - context: ./ui - file: ./ui/Dockerfile - push: true - tags: ghcr.io/kek-sec/gopherdrop:ui-latest + tags: ghcr.io/kek-sec/gopherdrop:${{ env.IMAGE_TAG }} annotations: | - org.opencontainers.image.description=UI for GopherDrop, a secure one-time secret sharing service + org.opencontainers.image.description=GopherDrop, a secure one-time secret sharing service build-args: | GIT_COMMIT_SHA=${{ env.GIT_COMMIT_SHA }} GIT_VERSION=${{ env.GIT_VERSION }} - VITE_API_URL=http://app:8080 labels: | org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop org.opencontainers.image.revision=${{ env.GIT_COMMIT_SHA }} org.opencontainers.image.version=${{ env.GIT_VERSION }} - cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:ui-latest + cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest cache-to: type=inline diff --git a/Dockerfile b/Dockerfile index 7056b4a..b7ab8d9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Stage 1: Build the Go binary -FROM golang:1.22-alpine AS builder +# Stage 1: Build the Go Backend +FROM golang:1.22-alpine AS backend-builder WORKDIR /app COPY . . @@ -16,15 +16,28 @@ RUN if [ "$DEBUG" = "true" ]; then \ go mod download && go build -o server ./cmd/server/main.go; \ fi -# Stage 2: Create the production image -FROM alpine:3.18 +# Stage 2: Build the Vue.js Frontend +FROM node:18-alpine AS frontend-builder WORKDIR /app -COPY --from=builder /app/server . +COPY ui/package.json ui/package-lock.json ./ +RUN npm install --legacy-peer-deps + +ARG VITE_API_URL="/api" +ENV VITE_API_URL=${VITE_API_URL} + +COPY ui ./ +RUN npm run build + +# Stage 3: Combine Backend and Frontend into a Single Image +FROM nginx:alpine # Add OCI Image Spec labels -LABEL org.opencontainers.image.title="GopherDrop Backend" \ - org.opencontainers.image.description="Backend for GopherDrop, a secure one-time secret sharing service" \ +ARG GIT_COMMIT_SHA +ARG GIT_VERSION + +LABEL org.opencontainers.image.title="GopherDrop" \ + org.opencontainers.image.description="GopherDrop - Secure one-time secret sharing service" \ org.opencontainers.image.source="https://github.com/kek-Sec/gopherdrop" \ org.opencontainers.image.revision="${GIT_COMMIT_SHA}" \ org.opencontainers.image.version="${GIT_VERSION}" \ @@ -32,14 +45,20 @@ LABEL org.opencontainers.image.title="GopherDrop Backend" \ org.opencontainers.image.documentation="https://github.com/kek-Sec/gopherdrop" \ org.opencontainers.image.licenses="MIT" -# Environment variables -ENV LISTEN_ADDR=:8080 -ENV STORAGE_PATH=/app/storage +# Copy the Go server binary +COPY --from=backend-builder /app/server /app/server + +# Copy the frontend static files +COPY --from=frontend-builder /app/dist /usr/share/nginx/html + +# Copy Nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf -# Create the storage directory +# Create the storage directory for the backend RUN mkdir -p /app/storage -EXPOSE 8080 +# Expose the ports for Nginx and the Go server +EXPOSE 80 8080 -# Command to run the server -CMD ["/app/server"] +# Run both the Go server and Nginx using a simple script +CMD ["/bin/sh", "-c", "/app/server & nginx -g 'daemon off;'"] diff --git a/Readme.md b/Readme.md index 334c0fe..c769a01 100755 --- a/Readme.md +++ b/Readme.md @@ -13,15 +13,13 @@ GopherDrop is a secure, self-hostable REST API and UI for sharing encrypted one- 1. [Features](#-features) 2. [Installation](#-installation) -3. [Build and Run](#️-build-and-run) -4. [Configuration](#️-configuration) -5. [Endpoints](#️-endpoints) +3. [Build and Run](#-build-and-run) +4. [Configuration](#-configuration) +5. [Endpoints](#-endpoints) 6. [Docker Deployment](#-docker-deployment) -7. [Persistence](#-persistence) -8. [Reverse Proxy Support](#-reverse-proxy-support) -9. [Contributing](#-contributing) -10. [License](#-license) -11. [Community and Support](#-community-and-support) +7. [Contributing](#-contributing) +8. [License](#-license) +9. [Community and Support](#-community-and-support) --- @@ -35,6 +33,15 @@ GopherDrop is a secure, self-hostable REST API and UI for sharing encrypted one- - **Dockerized Deployment**: Simple setup with Docker and Docker Compose. - **Production and Debug Modes**: Easily switch between production and debug builds. + +--- + +## 🐳 **Docker Deployment** + +### **Production `docker-compose.yml`** + +> docker-compose.prod.sample.yaml + --- ## 📥 **Installation** @@ -50,12 +57,11 @@ GopherDrop is a secure, self-hostable REST API and UI for sharing encrypted one- git clone https://github.com/kek-Sec/gopherdrop.git cd gopherdrop ``` - --- ## 🛠️ **Build and Run** -### **Production Setup** +### **Local Setup** To build and run GopherDrop in production mode: @@ -85,13 +91,6 @@ make down make test ``` -### **Access the Application** - -- **UI**: `http://localhost:8081` -- **API**: `http://localhost:8080` - ---- - ## ⚙️ **Configuration** ### **Using `.env` File** @@ -135,57 +134,6 @@ MAX_FILE_SIZE=10485760 | `GET` | `/send/:id` | Retrieve a send by its hash | | `GET` | `/send/:id/check` | Check if a send requires a password | -### **Example: Create a Send** - -```bash -curl -X POST http://localhost:8080/send \ - -F "type=text" \ - -F "data=This is a secret message" \ - -F "password=mysecurepassword" -``` - ---- - -## 🐳 **Docker Deployment** - -### **Production `docker-compose.yml`** - -> clone repository - -> docker-compose.prod.sample.yaml - ---- - -## 🌐 **Reverse Proxy Support** - -### **Nginx Configuration** - -Create a `nginx.conf`: - -```nginx -server { - listen 80; - - location / { - proxy_pass http://frontend:80; - } - - location /api/ { - proxy_pass http://backend:8080/; - } -} -``` - -### **Traefik Configuration** - -Add this to `docker-compose.yml`: - -```yaml -labels: - - "traefik.enable=true" - - "traefik.http.routers.gopherdrop.rule=Host(`example.com`)" - - "traefik.http.services.gopherdrop.loadbalancer.server.port=8080" -``` --- diff --git a/docker-compose.prod.sample.yaml b/docker-compose.prod.sample.yaml index 2caaedc..2916e5c 100755 --- a/docker-compose.prod.sample.yaml +++ b/docker-compose.prod.sample.yaml @@ -14,10 +14,10 @@ services: timeout: 5s retries: 5 networks: - - goepher_net + - gophernet app: - image: ghcr.io/kek-sec/gopherdrop:backend-latest + image: ghcr.io/kek-sec/gopherdrop:latest container_name: gopherdrop_app environment: DB_HOST: db @@ -29,30 +29,14 @@ services: LISTEN_ADDR: :8080 STORAGE_PATH: /app/storage MAX_FILE_SIZE: 10485760 - volumes: - - ./app_storage:/app/storage depends_on: db: condition: service_healthy + volumes: + - ./app_storage:/app/storage networks: - - goepher_net - ports: - - "8080:8080" - - ui: - build: - context: ./ui - dockerfile: Dockerfile - args: - VITE_API_URL: "http://app:8080" - container_name: gopherdrop_ui - depends_on: - - app - networks: - - goepher_net - ports: - - "8081:80" + - gophernet networks: - goepher_net: - external: true \ No newline at end of file + gophernet: + external: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 483c4ab..627d75d 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,20 +1,28 @@ services: db: image: postgres:17-alpine + container_name: gopherdrop_db environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: gopherdropdb - ports: - - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U user -d gopherdropdb"] interval: 10s timeout: 5s retries: 5 + networks: + - gopherdrop app: - build: . + build: + context: . + dockerfile: Dockerfile + args: + - VITE_API_URL=/api + container_name: gopherdrop_app environment: DB_HOST: db DB_USER: user @@ -28,16 +36,14 @@ services: depends_on: db: condition: service_healthy - ports: - - "8080:8080" - - ui: - build: - context: ./ui - dockerfile: Dockerfile - depends_on: - - app - environment: - VITE_API_URL: "http://app:8080" ports: - "8081:80" + networks: + - gopherdrop + +networks: + gopherdrop: + driver: bridge + +volumes: + db_data: {} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d03bdd4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Serve the frontend + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the backend service + location /api/ { + proxy_pass http://app:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/ui/Dockerfile b/ui/Dockerfile deleted file mode 100755 index 58ebb5c..0000000 --- a/ui/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Build stage -FROM node:18-alpine AS build - -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm install --legacy-peer-deps - -ARG VITE_API_URL -ENV VITE_API_URL=${VITE_API_URL} - -COPY . . -RUN npm run build - -# Production stage -FROM nginx:alpine - -# Add OCI Image Spec labels -ARG GIT_COMMIT_SHA -ARG GIT_VERSION - -LABEL org.opencontainers.image.title="GopherDrop UI" \ - org.opencontainers.image.description="UI for GopherDrop, a secure one-time secret sharing service" \ - org.opencontainers.image.source="https://github.com/kek-Sec/gopherdrop" \ - org.opencontainers.image.revision="${GIT_COMMIT_SHA}" \ - org.opencontainers.image.version="${GIT_VERSION}" \ - org.opencontainers.image.url="https://github.com/kek-Sec/gopherdrop" \ - org.opencontainers.image.documentation="https://github.com/kek-Sec/gopherdrop" \ - org.opencontainers.image.licenses="MIT" - -COPY --from=build /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/ui/nginx.conf b/ui/nginx.conf deleted file mode 100644 index 8621607..0000000 --- a/ui/nginx.conf +++ /dev/null @@ -1,12 +0,0 @@ -server { - listen 80; - server_name localhost; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } - -} diff --git a/ui/src/api.js b/ui/src/api.js index 93e7187..489da26 100755 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -3,6 +3,7 @@ * Uses fetch for HTTP requests. */ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080' +console.log('API_URL:', API_URL); export async function createSend(formData) { const res = await fetch(`${API_URL}/send`, { @@ -24,15 +25,13 @@ export async function createSend(formData) { } export async function getSend(hash, password = '') { - const url = new URL(`${API_URL}/send/${hash}`) - if (password) url.searchParams.set('password', password) + const API_BASE_URL = `${window.location.origin}/api` + const url = new URL(`${API_BASE_URL}/send/${hash}`) - console.log('Fetching secret from:', url.href) + if (password) url.searchParams.set('password', password) const res = await fetch(url) - console.log('Response status:', res.status) - console.log('Response headers:', [...res.headers.entries()]) if (res.status === 404) { console.log('Secret not found.') diff --git a/ui/src/pages/Create.vue b/ui/src/pages/Create.vue index 0dad96d..cc1a145 100755 --- a/ui/src/pages/Create.vue +++ b/ui/src/pages/Create.vue @@ -158,7 +158,7 @@ async function handleSubmit() { } function copyLink() { - const link = `${baseUrl}/view/${resultHash.value}` + const link = `${window.location.origin}/view/${resultHash.value}` navigator.clipboard.writeText(link) snackbar.value = true } diff --git a/ui/src/pages/View.vue b/ui/src/pages/View.vue index 220c767..a9f135d 100755 --- a/ui/src/pages/View.vue +++ b/ui/src/pages/View.vue @@ -69,31 +69,37 @@ const secretLoaded = ref(false) const snackbar = ref(false) async function loadSecret() { - errorMessage.value = '' - notFound.value = false - secretContent.value = '' - fileBlob.value = null + errorMessage.value = ''; + notFound.value = false; + secretContent.value = ''; + fileBlob.value = null; try { - const result = await getSend(hash, password.value) + const result = await getSend(hash, password.value); + if (result.notFound) { - notFound.value = true - return + console.log('Secret not found.'); + notFound.value = true; + return; } if (result.file) { - fileBlob.value = result.file - filename.value = result.filename + console.log('File secret loaded.'); + fileBlob.value = result.file; + filename.value = result.filename; } else { - secretContent.value = result.text + console.log('Text secret loaded:', result.text); + secretContent.value = result.text; } - secretLoaded.value = true + secretLoaded.value = true; } catch (err) { - errorMessage.value = 'Failed to load secret. Incorrect password or secret has expired.' + console.error('Error in loadSecret:', err); + errorMessage.value = 'Failed to load secret. Incorrect password or secret has expired.'; } } + function copyContent() { navigator.clipboard.writeText(secretContent.value) snackbar.value = true