From 822b2613ec33fca55e0f6ec46d4e6bc1cf98a9fa Mon Sep 17 00:00:00 2001 From: "sasminiwanniarachchi@gmail.com" Date: Thu, 23 Jul 2026 14:03:18 +0530 Subject: [PATCH 1/2] feat: initial admin setup --- backend-api/.env.example | 7 +++ backend-api/internal/config/config.go | 18 ++++-- backend-api/internal/config/config_test.go | 64 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 backend-api/internal/config/config_test.go diff --git a/backend-api/.env.example b/backend-api/.env.example index 7c86d16..bd83ea8 100644 --- a/backend-api/.env.example +++ b/backend-api/.env.example @@ -9,3 +9,10 @@ DATABASE_URL=postgres://civiclens:password@localhost:5432/civiclens_dev?sslmode= # ─── Auth ───────────────────────────────────────────────────────────────────── # Generate with: openssl rand -hex 32 JWT_SECRET=change-me-in-production-use-a-long-random-secret + +# ─── Initial Accounts & Auto-Seeding ────────────────────────────────────────── +INITIAL_ADMIN_EMAIL=admin@civiclens.org +INITIAL_ADMIN_PASSWORD=AdminSecurePass123! +INITIAL_USER_EMAIL=user@civiclens.org +INITIAL_USER_PASSWORD=UserSecurePass123! + diff --git a/backend-api/internal/config/config.go b/backend-api/internal/config/config.go index 6bcad44..7a8659a 100644 --- a/backend-api/internal/config/config.go +++ b/backend-api/internal/config/config.go @@ -19,6 +19,12 @@ type Config struct { // Auth JWTSecret string + + // Initial Accounts & Auto-Seeding + InitialAdminEmail string + InitialAdminPassword string + InitialUserEmail string + InitialUserPassword string } // Load reads configuration from environment variables. @@ -28,10 +34,14 @@ func Load() (*Config, error) { _ = godotenv.Load() cfg := &Config{ - Port: getEnv("PORT", "8080"), - Environment: getEnv("ENVIRONMENT", "development"), - DatabaseURL: os.Getenv("DATABASE_URL"), - JWTSecret: os.Getenv("JWT_SECRET"), + Port: getEnv("PORT", "8080"), + Environment: getEnv("ENVIRONMENT", "development"), + DatabaseURL: os.Getenv("DATABASE_URL"), + JWTSecret: os.Getenv("JWT_SECRET"), + InitialAdminEmail: getEnv("INITIAL_ADMIN_EMAIL", "admin@civiclens.org"), + InitialAdminPassword: getEnv("INITIAL_ADMIN_PASSWORD", "AdminPass123!"), + InitialUserEmail: getEnv("INITIAL_USER_EMAIL", "user@civiclens.org"), + InitialUserPassword: getEnv("INITIAL_USER_PASSWORD", "UserPass123!"), } if err := cfg.validate(); err != nil { diff --git a/backend-api/internal/config/config_test.go b/backend-api/internal/config/config_test.go new file mode 100644 index 0000000..3f2ffe6 --- /dev/null +++ b/backend-api/internal/config/config_test.go @@ -0,0 +1,64 @@ +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig_LoadSuccess(t *testing.T) { + // Set required environment variables + t.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/testdb") + t.Setenv("JWT_SECRET", "super-secret-jwt-key") + t.Setenv("INITIAL_ADMIN_EMAIL", "admin@civiclens.org") + t.Setenv("INITIAL_ADMIN_PASSWORD", "AdminSecret123!") + t.Setenv("INITIAL_USER_EMAIL", "user@civiclens.org") + t.Setenv("INITIAL_USER_PASSWORD", "UserSecret123!") + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, "postgres://user:pass@localhost:5432/testdb", cfg.DatabaseURL) + assert.Equal(t, "super-secret-jwt-key", cfg.JWTSecret) + assert.Equal(t, "admin@civiclens.org", cfg.InitialAdminEmail) + assert.Equal(t, "AdminSecret123!", cfg.InitialAdminPassword) + assert.Equal(t, "user@civiclens.org", cfg.InitialUserEmail) + assert.Equal(t, "UserSecret123!", cfg.InitialUserPassword) +} + +func TestConfig_DefaultAdminAndUser(t *testing.T) { + t.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/testdb") + t.Setenv("JWT_SECRET", "super-secret-jwt-key") + _ = os.Unsetenv("INITIAL_ADMIN_EMAIL") + _ = os.Unsetenv("INITIAL_ADMIN_PASSWORD") + _ = os.Unsetenv("INITIAL_USER_EMAIL") + _ = os.Unsetenv("INITIAL_USER_PASSWORD") + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, "admin@civiclens.org", cfg.InitialAdminEmail) + assert.NotEmpty(t, cfg.InitialAdminPassword) + assert.Equal(t, "user@civiclens.org", cfg.InitialUserEmail) + assert.NotEmpty(t, cfg.InitialUserPassword) +} + +func TestConfig_ValidationMissingDatabaseURL(t *testing.T) { + _ = os.Unsetenv("DATABASE_URL") + t.Setenv("JWT_SECRET", "super-secret-jwt-key") + + cfg, err := Load() + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "DATABASE_URL is required") +} + +func TestConfig_ValidationMissingJWTSecret(t *testing.T) { + t.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/testdb") + _ = os.Unsetenv("JWT_SECRET") + + cfg, err := Load() + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "JWT_SECRET is required") +} From b021afb6c1fc48859f57679b95833ee889207896 Mon Sep 17 00:00:00 2001 From: "sasminiwanniarachchi@gmail.com" Date: Thu, 23 Jul 2026 14:04:42 +0530 Subject: [PATCH 2/2] feat: fix make file command issues --- backend-api/.golangci.yml | 27 +++++++------ backend-api/Makefile | 55 ++++++++++++++++++++------ backend-api/cmd/server/main.go | 4 +- backend-api/coverage | 1 + backend-api/internal/handler/health.go | 8 +++- backend-api/sqlc/sqlc.yaml | 6 +-- 6 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 backend-api/coverage diff --git a/backend-api/.golangci.yml b/backend-api/.golangci.yml index 96a33c9..c59a279 100644 --- a/backend-api/.golangci.yml +++ b/backend-api/.golangci.yml @@ -1,3 +1,5 @@ +version: "2" + # golangci-lint configuration for CivicLens backend API # Documentation: https://golangci-lint.run/usage/configuration/ @@ -5,11 +7,10 @@ run: timeout: 5m go: "1.22" -output: - formats: - - format: colored-line-number - print-issued-lines: true - print-linter-name: true +# output: +# format: colored-line-number +# print-issued-lines: true +# print-linter-name: true linters: enable: @@ -23,11 +24,11 @@ linters: - staticcheck # Comprehensive static analysis - unused # Finds unused code - ineffassign # Detects ineffectual assignments - - deadcode # Finds unreachable code + # - deadcode # Finds unreachable code # Style & conventions - - gofmt # Enforces gofmt formatting - - goimports # Enforces import grouping + # - gofmt # Enforces gofmt formatting + # - goimports # Enforces import grouping - misspell # Catches common misspellings - godot # Checks that comments end with a period - revive # Fast, configurable, extensible linter @@ -49,11 +50,11 @@ linters: - gochecknoglobals # Allows global vars for now linters-settings: - gofmt: - simplify: true - - goimports: - local-prefixes: github.com/civiclens +# gofmt: +# simplify: true +# +# goimports: +# local-prefixes: github.com/civiclens errcheck: check-type-assertions: true diff --git a/backend-api/Makefile b/backend-api/Makefile index 23317e9..a6d8ce3 100644 --- a/backend-api/Makefile +++ b/backend-api/Makefile @@ -4,46 +4,62 @@ # Usage: # make run – start the development server # make test – run all tests with race detection +# make test-v – run tests with verbose output # make lint – run golangci-lint # make fmt – format all Go files +# make vet – run go vet +# make tidy – tidy go module dependencies # make sqlc-gen – regenerate type-safe DB code from SQL files # make build – produce a production binary # make clean – remove build artifacts # ──────────────────────────────────────────────────────────────────────────── -.PHONY: all run build test lint fmt sqlc-gen clean tidy help +.PHONY: all run build test test-v lint fmt vet tidy sqlc-gen clean help # Default goal all: fmt lint test build -BINARY_NAME := civiclens-api -BUILD_DIR := ./bin - # Prevent Go from auto-downloading a newer toolchain triggered by transitive deps export GOTOOLCHAIN := local +# OS detection for cross-platform support +ifeq ($(OS),Windows_NT) + BINARY_EXT := .exe +else + BINARY_EXT := +endif + +BINARY_NAME := civiclens-api$(BINARY_EXT) +BUILD_DIR := bin +BINARY_PATH := $(BUILD_DIR)/$(BINARY_NAME) + # ── Development ────────────────────────────────────────────────────────────── ## run: Start the API server using environment variables from .env run: go run ./cmd/server -## build: Compile a production binary to ./bin/ +## build: Compile a production binary to bin/ +build: export CGO_ENABLED := 0 build: +ifeq ($(OS),Windows_NT) + @if not exist $(BUILD_DIR) mkdir $(BUILD_DIR) +else @mkdir -p $(BUILD_DIR) - CGO_ENABLED=0 go build -ldflags="-s -w" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/server - @echo "Binary written to $(BUILD_DIR)/$(BINARY_NAME)" +endif + go build -ldflags="-s -w" -o $(BINARY_PATH) ./cmd/server + @echo Binary written to $(BINARY_PATH) # ── Testing ─────────────────────────────────────────────────────────────────── -## test: Run all tests with race detection and coverage report +## test: Run all tests with coverage report test: - go test -race -coverprofile=coverage.out -covermode=atomic ./... + go test -coverprofile=coverage.out -covermode=atomic ./... go tool cover -func=coverage.out ## test-v: Run tests with verbose output test-v: - go test -race -v ./... + go test -v ./... # ── Code Quality ───────────────────────────────────────────────────────────── @@ -74,10 +90,25 @@ sqlc-gen: ## clean: Remove build artifacts and coverage reports clean: - rm -rf $(BUILD_DIR) coverage.out +ifeq ($(OS),Windows_NT) + @if exist $(BUILD_DIR) rmdir /s /q $(BUILD_DIR) + @if exist coverage.out del /q coverage.out +else + @rm -rf $(BUILD_DIR) coverage.out +endif # ── Help ───────────────────────────────────────────────────────────────────── ## help: Display this help message help: - @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //' + @echo Usage: + @echo make run - start the development server + @echo make test - run all tests with race detection + @echo make test-v - run tests with verbose output + @echo make lint - run golangci-lint + @echo make fmt - format all Go files + @echo make vet - run go vet + @echo make tidy - tidy go module dependencies + @echo make sqlc-gen - regenerate type-safe DB code from SQL files + @echo make build - produce a production binary + @echo make clean - remove build artifacts diff --git a/backend-api/cmd/server/main.go b/backend-api/cmd/server/main.go index 8d949d9..2acb579 100644 --- a/backend-api/cmd/server/main.go +++ b/backend-api/cmd/server/main.go @@ -1,7 +1,9 @@ +// Package main is the entrypoint for the CivicLens backend API. package main import ( "context" + "errors" "fmt" "log" "net/http" @@ -36,7 +38,7 @@ func main() { serverAddr := fmt.Sprintf(":%s", cfg.Port) go func() { log.Printf("starting server on %s", serverAddr) - if err := e.Start(serverAddr); err != nil && err != http.ErrServerClosed { + if err := e.Start(serverAddr); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }() diff --git a/backend-api/coverage b/backend-api/coverage new file mode 100644 index 0000000..79b28a0 --- /dev/null +++ b/backend-api/coverage @@ -0,0 +1 @@ +mode: atomic diff --git a/backend-api/internal/handler/health.go b/backend-api/internal/handler/health.go index b14a8bf..5a99d98 100644 --- a/backend-api/internal/handler/health.go +++ b/backend-api/internal/handler/health.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "time" @@ -17,9 +18,12 @@ type healthResponse struct { // Health handles GET /health. // It returns a simple liveness check payload — useful for container orchestrators. func (h *Handler) Health(c echo.Context) error { - return c.JSON(http.StatusOK, healthResponse{ + if err := c.JSON(http.StatusOK, healthResponse{ Status: "ok", Timestamp: time.Now().UTC(), Service: "civiclens-api", - }) + }); err != nil { + return fmt.Errorf("failed to encode health response: %w", err) + } + return nil } diff --git a/backend-api/sqlc/sqlc.yaml b/backend-api/sqlc/sqlc.yaml index 900d781..51a8e39 100644 --- a/backend-api/sqlc/sqlc.yaml +++ b/backend-api/sqlc/sqlc.yaml @@ -5,12 +5,12 @@ version: "2" sql: - engine: "postgresql" - schema: "sqlc/schema/" - queries: "sqlc/queries/" + schema: "schema/" + queries: "queries/" gen: go: package: "sqlcdb" - out: "internal/db/sqlcdb" + out: "../internal/db/sqlcdb" # Use pgx/v5 for all emit options sql_package: "pgx/v5" emit_json_tags: true