From 502b76c5d67faa0ef22975f82c9985f492ba1bcf Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 4 Feb 2026 16:32:28 +0100 Subject: [PATCH] fix(backend): solve 500 errors with auto-migrations and standardized filenames --- Dockerfile | 39 ++++++++-------- cmd/server/main.go | 15 ++++++ go.mod | 19 ++++---- internal/database/database.go | 46 ++++++++++++++++--- ...s.down.sql => 004_add_user_roles.down.sql} | 0 ...roles.up.sql => 004_add_user_roles.up.sql} | 0 6 files changed, 83 insertions(+), 36 deletions(-) rename migrations/{000003_add_user_roles.down.sql => 004_add_user_roles.down.sql} (100%) rename migrations/{000003_add_user_roles.up.sql => 004_add_user_roles.up.sql} (100%) diff --git a/Dockerfile b/Dockerfile index 22cdd7b..4059fa4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,33 @@ -# Build stage -FROM golang:1.22-alpine AS builder +# Multi-stage Dockerfile for FlowReader +# Step 1: Build the React Frontend +FROM node:20-alpine AS web-builder +WORKDIR /app/web +COPY web/package*.json ./ +RUN npm install +COPY web/ ./ +RUN npm run build + +# Step 2: Build the Go Backend +FROM golang:1.24-alpine AS builder WORKDIR /app - -# Install dependencies RUN apk add --no-cache git - -# Copy go mod files -COPY go.mod go.sum* ./ +COPY go.mod go.sum ./ RUN go mod download - -# Copy source code COPY . . - -# Build the application +# Copy the built frontend from Step 1 +COPY --from=web-builder /app/web/dist ./web/dist RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server ./cmd/server -# Final stage +# Step 3: Final Production Image FROM alpine:3.19 - WORKDIR /app - -# Install wget for healthcheck RUN apk add --no-cache wget ca-certificates - -# Copy binary from builder COPY --from=builder /server /app/server +# Copy the static files for the Go server to serve +COPY --from=builder /app/web/dist /app/web/dist +# Copy migration files for auto-migration +COPY --from=builder /app/migrations /app/migrations -# Expose port EXPOSE 8080 - -# Run the application CMD ["/app/server"] diff --git a/cmd/server/main.go b/cmd/server/main.go index a85e8da..89cca88 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -139,6 +139,21 @@ func main() { }) }) + // Serve Static Files (Frontend) + staticPath := "./web/dist" + if _, err := os.Stat(staticPath); err == nil { + fs := http.FileServer(http.Dir(staticPath)) + r.Handle("/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // If the file exists, serve it, otherwise serve index.html (for SPA routing) + path := staticPath + r.URL.Path + if _, err := os.Stat(path); os.IsNotExist(err) { + http.ServeFile(w, r, staticPath+"/index.html") + return + } + fs.ServeHTTP(w, r) + })) + } + // Create server srv := &http.Server{ Addr: ":" + cfg.Port, diff --git a/go.mod b/go.mod index 6c299af..17da617 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,31 @@ module github.com/michael/flowreader -go 1.22 +go 1.24.0 require ( github.com/go-chi/chi/v5 v5.0.11 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.5.2 + github.com/microcosm-cc/bluemonday v1.0.27 + github.com/mmcdole/gofeed v1.3.0 + golang.org/x/crypto v0.47.0 ) require ( github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/mmcdole/gofeed v1.3.0 // indirect github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect ) diff --git a/internal/database/database.go b/internal/database/database.go index 8cc5946..5c5b088 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -6,6 +6,11 @@ import ( "fmt" "log" + "os" + "path/filepath" + "sort" + "strings" + "github.com/jackc/pgx/v5/pgxpool" ) @@ -36,26 +41,53 @@ func Connect(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) { } // RunMigrations executes pending database migrations. -// Note: For production, use golang-migrate CLI. This is for development convenience. func RunMigrations(ctx context.Context, pool *pgxpool.Pool) error { - // Check if schema_migrations table exists + // Check if users table exists var exists bool err := pool.QueryRow(ctx, ` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' - AND table_name = 'schema_migrations' + AND table_name = 'users' ) `).Scan(&exists) if err != nil { - return fmt.Errorf("checking schema_migrations: %w", err) + return fmt.Errorf("checking users table: %w", err) } - if !exists { - log.Println("Migrations table not found. Run 'make migrate-up' to apply migrations.") + if exists { + log.Println("Database already initialized") return nil } - log.Println("Database migrations verified") + log.Println("Initializing database and applying migrations...") + + // Read migration files + entries, err := os.ReadDir("./migrations") + if err != nil { + return fmt.Errorf("reading migrations dir: %w", err) + } + + var upFiles []string + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".up.sql") { + upFiles = append(upFiles, entry.Name()) + } + } + sort.Strings(upFiles) + + for _, file := range upFiles { + log.Printf("Applying migration: %s", file) + content, err := os.ReadFile(filepath.Join("./migrations", file)) + if err != nil { + return fmt.Errorf("reading migration %s: %w", file, err) + } + + if _, err := pool.Exec(ctx, string(content)); err != nil { + return fmt.Errorf("executing migration %s: %w", file, err) + } + } + + log.Println("All migrations applied successfully") return nil } diff --git a/migrations/000003_add_user_roles.down.sql b/migrations/004_add_user_roles.down.sql similarity index 100% rename from migrations/000003_add_user_roles.down.sql rename to migrations/004_add_user_roles.down.sql diff --git a/migrations/000003_add_user_roles.up.sql b/migrations/004_add_user_roles.up.sql similarity index 100% rename from migrations/000003_add_user_roles.up.sql rename to migrations/004_add_user_roles.up.sql