diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..918fb9e --- /dev/null +++ b/.env.example @@ -0,0 +1,180 @@ +# Environment Configuration Template for Bit Issues Backend +# Copy this file to .env and fill in your actual values +# This file contains all required environment variables with examples and documentation + +# ============================================================================= +# SERVER CONFIGURATION +# ============================================================================= + +# HTTP Server Address +# Purpose: Host and port the HTTP server will listen on +# Format: host:port (e.g., 0.0.0.0:3000, 127.0.0.1:3000) +# Default: 127.0.0.1:3000 +# Example: 0.0.0.0:3000 +HTTP__ADDRESS=127.0.0.1:3000 + +# Proxy Header Name +# Purpose: Header name used to identify the original client IP when behind a proxy +# Format: string (e.g., X-Forwarded-For, X-Real-IP) +# Default: X-Forwarded-For +HTTP__PROXY_HEADER=X-Forwarded-For + +# Trusted Proxy Addresses +# Purpose: List of trusted proxy addresses (CIDR notation supported) +# Format: comma-separated list of addresses +# Example: 10.0.0.0/8,172.16.0.0/12 +HTTP__PROXIES= + +# ============================================================================= +# OPENAPI CONFIGURATION +# ============================================================================= + +# OpenAPI Documentation Enabled +# Purpose: Enable/disable OpenAPI documentation endpoint +# Format: boolean (true/false) +# Default: true +HTTP__OPENAPI__ENABLED=true + +# OpenAPI Public Host +# Purpose: Public host URL for OpenAPI documentation (used in OpenAPI spec) +# Format: hostname (e.g., api.example.com) +# Example: api.bitissues.dev +HTTP__OPENAPI__PUBLIC_HOST= + +# OpenAPI Documentation Path +# Purpose: URL path for OpenAPI documentation (e.g., /docs, /api/docs) +# Default: (empty - uses default path) +# Example: /api/docs +HTTP__OPENAPI__PUBLIC_PATH= + +# ============================================================================= +# DATABASE CONFIGURATION +# ============================================================================= + +# Database Connection URL +# Purpose: Connection string for MariaDB/MySQL database +# Format: mariadb://[username:password@]host[:port]/database[?query_params] +# Example: mariadb://user:password@localhost:3306/dbname?charset=utf8mb4&parseTime=True&loc=UTC&clientFoundRows=true +# Example: mariadb://bit-issues:bit-issues@127.0.0.1:3306/bit-issues?charset=utf8mb4&parseTime=True&loc=UTC&clientFoundRows=true +DATABASE__URL=mariadb://bit-issues:bit-issues@127.0.0.1:3306/bit-issues?charset=utf8mb4&parseTime=True&loc=UTC&clientFoundRows=true + +# Database Connection Max Idle Time +# Purpose: Maximum amount of time a database connection may be idle +# Format: duration (e.g., 1m, 30s, 1h) +# Default: 0 (no limit) +# Example: 1m +DATABASE__CONN_MAX_IDLE_TIME=0 + +# Database Connection Max Lifetime +# Purpose: Maximum amount of time a database connection may be reused +# Format: duration (e.g., 1h, 30m) +# Default: 0 (no limit) +# Example: 1h +DATABASE__CONN_MAX_LIFETIME=0 + +# Database Max Open Connections +# Purpose: Maximum number of open database connections +# Format: integer +# Default: 0 (unlimited) +# Example: 25 +DATABASE__MAX_OPEN_CONNS=0 + +# Database Max Idle Connections +# Purpose: Maximum number of idle database connections +# Format: integer +# Default: 0 (unlimited) +# Example: 10 +DATABASE__MAX_IDLE_CONNS=0 + +# ============================================================================= +# JWT AUTHENTICATION CONFIGURATION +# ============================================================================= + +# JWT Secret Key +# Purpose: Secret key used to sign and verify JWT tokens (minimum 32 bytes) +# Format: string (at least 32 characters) +# IMPORTANT: Change this to a secure random string in production! +# Default: secret +# Example: your-secure-secret-key-at-least-32-bytes-long +JWT__SECRET=secret + +# JWT Access Token TTL +# Purpose: Time-to-live for access tokens +# Format: duration (e.g., 15m, 1h, 24h) +# Default: 15m +# Example: 15m +JWT__ACCESS_TTL=15m + +# JWT Issuer +# Purpose: Issuer claim for JWT tokens +# Format: string (e.g., domain name) +# Default: bitissues.dev +# Example: bitissues.dev +JWT__ISSUER=bitissues.dev + +# ============================================================================= +# STORAGE CONFIGURATION (S3 Compatible) +# ============================================================================= + +# S3-Compatible Storage URL +# Purpose: Connection URL for S3-compatible storage (MinIO, AWS S3, etc.) +# Format: s3://bucket_name?endpoint=host:port®ion=region[&insecure=true] +# Example: s3://attachments?endpoint=localhost:9000®ion=us-east-1&insecure=true +# Example: s3://bit-issues/uploads?endpoint=storage.example.com®ion=us-east-1 +STORAGE__URL=s3://bit-issues/uploads?endpoint=localhost:9000®ion=us-east-1&insecure=true + +# Presigned Link TTL +# Purpose: Time-to-live for presigned storage URLs +# Format: duration (e.g., 15m, 1h) +# Default: 15m +# Example: 15m +STORAGE__LINKS_TTL=15m + +# AWS Access Key ID +# Purpose: AWS/MinIO access key for storage operations +# Format: string +# Example: minioadmin +AWS_ACCESS_KEY_ID=minioadmin + +# AWS Secret Access Key +# Purpose: AWS/MinIO secret key for storage operations +# Format: string +# Example: minioadmin +AWS_SECRET_ACCESS_KEY=minioadmin + +# AWS Region +# Purpose: AWS region for S3 storage +# Format: string (e.g., us-east-1, eu-west-1) +# Default: us-east-1 +# Example: us-east-1 +AWS_REGION=us-east-1 + +# ============================================================================= +# ATTACHMENTS CONFIGURATION +# ============================================================================= + +# Maximum Attachment Size +# Purpose: Maximum size allowed for file attachments (in bytes) +# Format: integer (bytes) +# Default: 10485760 (10 MB) +# Example: 10485760 (10 MB) +# Example: 52428800 (50 MB) +ATTACHMENTS__MAX_SIZE=10485760 + +# ============================================================================= +# GENERAL CONFIGURATION +# ============================================================================= + +# Timezone +# Purpose: Server timezone for logging and timestamps +# Format: IANA timezone (e.g., UTC, America/New_York, Europe/London) +# Default: UTC +# Example: UTC +# Example: Asia/Krasnoyarsk +TIMEZONE=UTC + +# Config Path (Optional) +# Purpose: Path to an optional YAML configuration file that overrides env vars +# Format: filesystem path +# Example: ./config/local.yaml +CONFIG_PATH= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8e95f4a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./main.go + + +FROM alpine:latest + +# Install certificates and timezone data +RUN apk add --no-cache ca-certificates tzdata + +# Create non-root user +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser --home /app appuser + +# Set the Current Working Directory inside the container +WORKDIR /app + +# Copy the binary file from the previous stage +COPY --from=builder --chown=appuser:appuser /out/server /app/server + +USER appuser + +# Command to run the executable +ENTRYPOINT ["/app/server"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d36062c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,91 @@ +name: bit-issues_backend +services: + backend: + build: + context: . + dockerfile: Dockerfile + restart: on-failure + depends_on: + mariadb: + condition: service_healthy + minio-init: + condition: service_completed_successfully + environment: + TZ: ${TIMEZONE:-UTC} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin} + DATABASE__URL: mariadb://bit-issues:bit-issues@mariadb:3306/bit-issues?charset=utf8mb4&parseTime=True&loc=Local&clientFoundRows=true + HTTP__ADDRESS: 0.0.0.0:3000 + JWT__SECRET: ${JWT__SECRET:-secret} + STORAGE__URL: s3://bit-issues/uploads?endpoint=localhost:9000®ion=us-east-1&insecure=true + ports: + - "3000:3000" + extra_hosts: + - "localhost:host-gateway" + + minio: + image: minio/minio:latest + command: server /data + environment: + TZ: ${TIMEZONE:-UTC} + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-minioadmin} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-minioadmin} + MINIO_REGION_NAME: ${AWS_REGION:-us-east-1} + MINIO_API_CORS_ALLOW_ORIGIN: "*" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 30s + timeout: 5s + retries: 3 + ports: + - "9000:9000" + volumes: + - minio-data:/data + + minio-init: + image: minio/mc:latest + depends_on: + minio: + condition: service_healthy + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -eu + echo "waiting for minio..." + until (/usr/bin/mc alias set local http://minio:9000 "$${AWS_ACCESS_KEY_ID:-minioadmin}" "$${AWS_SECRET_ACCESS_KEY:-minioadmin}") >/dev/null 2>&1; do + sleep 1 + done + /usr/bin/mc mb --ignore-existing local/bit-issues + echo "minio ready" + environment: + TZ: ${TIMEZONE:-UTC} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin} + + mariadb: + image: mariadb:lts + environment: + TZ: ${TIMEZONE:-UTC} + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: bit-issues + MARIADB_USER: bit-issues + MARIADB_PASSWORD: bit-issues + MARIADB_AUTO_UPGRADE: ON + volumes: + - mariadb-data:/var/lib/mysql + healthcheck: + test: + [ + "CMD", + "healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized", + ] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + mariadb-data: + minio-data: diff --git a/internal/storage/module.go b/internal/storage/module.go index 4ee2983..0e2dc86 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -22,6 +22,7 @@ func Module() fx.Option { return miniofx.Config{ Endpoint: u.Query().Get("endpoint"), Region: u.Query().Get("region"), + Secure: u.Query().Get("insecure") != "true", }, nil }), fx.Provide(NewService), diff --git a/pkg/miniofx/client.go b/pkg/miniofx/client.go index 1d63381..e155e01 100644 --- a/pkg/miniofx/client.go +++ b/pkg/miniofx/client.go @@ -8,11 +8,9 @@ import ( ) func NewClient(config Config) (*minio.Client, error) { - endpoint := config.Endpoint - - client, err := minio.New(endpoint, &minio.Options{ + client, err := minio.New(config.Endpoint, &minio.Options{ Creds: credentials.NewEnvAWS(), - Secure: true, + Secure: config.Secure, Region: config.Region, }) if err != nil { diff --git a/pkg/miniofx/config.go b/pkg/miniofx/config.go index 8f32c99..defa505 100644 --- a/pkg/miniofx/config.go +++ b/pkg/miniofx/config.go @@ -3,4 +3,5 @@ package miniofx type Config struct { Endpoint string Region string + Secure bool }