3126 lines
83 KiB
Markdown
3126 lines
83 KiB
Markdown
# Golang Backend Blueprint: Clean Architecture with Wire DI & Cassandra
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** November 2025
|
|
**Based on:** maplepress-backend architecture
|
|
|
|
This document provides a step-by-step guide to building a new Golang backend using the same architecture, authentication system, and reusable components from the MaplePress backend. Use this as a reference when creating new backend projects from scratch.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Architecture Overview](#architecture-overview)
|
|
2. [Project Initialization](#project-initialization)
|
|
3. [Directory Structure](#directory-structure)
|
|
4. [Core Dependencies](#core-dependencies)
|
|
5. [Configuration System](#configuration-system)
|
|
6. [Dependency Injection with Wire](#dependency-injection-with-wire)
|
|
7. [Reusable pkg/ Components](#reusable-pkg-components)
|
|
8. [Authentication System](#authentication-system)
|
|
9. [Clean Architecture Layers](#clean-architecture-layers)
|
|
10. [Database Setup (Cassandra)](#database-setup-cassandra)
|
|
11. [Middleware Implementation](#middleware-implementation)
|
|
12. [HTTP Server Setup](#http-server-setup)
|
|
13. [Docker & Infrastructure](#docker--infrastructure)
|
|
14. [CLI Commands (Cobra)](#cli-commands-cobra)
|
|
15. [Development Workflow](#development-workflow)
|
|
16. [Testing Strategy](#testing-strategy)
|
|
17. [Production Deployment](#production-deployment)
|
|
|
|
---
|
|
|
|
## 1. Architecture Overview
|
|
|
|
### Core Principles
|
|
|
|
Our backend architecture follows these fundamental principles:
|
|
|
|
1. **Clean Architecture** - Separation of concerns with clear dependency direction
|
|
2. **Dependency Injection** - Using Google Wire for compile-time DI
|
|
3. **Multi-tenancy** - Tenant isolation at the application layer
|
|
4. **Security-first** - CWE-compliant security measures throughout
|
|
5. **Code Reuse** - Extensive use of shared `pkg/` components
|
|
|
|
### Architecture Layers
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Interface Layer (HTTP) │
|
|
│ • Handlers (HTTP endpoints) │
|
|
│ • DTOs (Data Transfer Objects) │
|
|
│ • Middleware (JWT, API Key, Rate Limiting, CORS) │
|
|
└──────────────────────┬──────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Service Layer │
|
|
│ • Orchestration logic │
|
|
│ • Transaction management (SAGA pattern) │
|
|
│ • Cross-use-case coordination │
|
|
└──────────────────────┬──────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Use Case Layer │
|
|
│ • Focused, single-responsibility operations │
|
|
│ • Business logic encapsulation │
|
|
│ • Validation and domain rules │
|
|
└──────────────────────┬──────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Repository Layer │
|
|
│ • Data access implementations │
|
|
│ • Database queries (Cassandra CQL) │
|
|
│ • Cache operations (Redis) │
|
|
└──────────────────────┬──────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Domain Layer │
|
|
│ • Entities (domain models) │
|
|
│ • Repository interfaces │
|
|
│ • Domain errors │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
Shared Infrastructure (pkg/)
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ • Logger (Zap) • Security (JWT, Password, API Key)│
|
|
│ • Database (Cassandra) • Cache (Redis, Two-Tier) │
|
|
│ • Storage (S3) • Rate Limiting │
|
|
│ • Email (Mailgun) • Search (Meilisearch) │
|
|
│ • Distributed Mutex • Validation │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Dependency Direction
|
|
|
|
**Critical Rule:** Dependencies flow INWARD only (outer layers depend on inner layers).
|
|
|
|
```
|
|
Interface → Service → Use Case → Repository → Domain
|
|
↓ ↓ ↓ ↓
|
|
└───────────┴─────────┴───────────┴──→ pkg/ (Infrastructure)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Project Initialization
|
|
|
|
### Step 1: Create Project Structure
|
|
|
|
```bash
|
|
# Create new project directory
|
|
mkdir -p cloud/your-backend-name
|
|
cd cloud/your-backend-name
|
|
|
|
# Initialize Go module
|
|
go mod init codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
|
|
|
|
# Create initial directory structure
|
|
mkdir -p {app,cmd,config,internal,pkg,migrations,static,docs}
|
|
```
|
|
|
|
### Step 2: Initialize Go Workspace (if using monorepo)
|
|
|
|
```bash
|
|
# From monorepo root
|
|
cd /path/to/monorepo
|
|
go work use ./cloud/your-backend-name
|
|
```
|
|
|
|
### Step 3: Add Core Dependencies
|
|
|
|
```bash
|
|
# Core dependencies
|
|
go get github.com/google/wire@v0.7.0
|
|
go get github.com/spf13/cobra@v1.10.1
|
|
go get go.uber.org/zap@v1.27.0
|
|
|
|
# Database & Cache
|
|
go get github.com/gocql/gocql@v1.7.0
|
|
go get github.com/redis/go-redis/v9@v9.16.0
|
|
go get github.com/golang-migrate/migrate/v4@v4.19.0
|
|
|
|
# Security
|
|
go get github.com/golang-jwt/jwt/v5@v5.3.0
|
|
go get golang.org/x/crypto@v0.41.0
|
|
go get github.com/awnumar/memguard@v0.23.0
|
|
|
|
# HTTP & Utilities
|
|
go get github.com/google/uuid@v1.6.0
|
|
|
|
# Install Wire CLI
|
|
go install github.com/google/wire/cmd/wire@latest
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Directory Structure
|
|
|
|
### Complete Directory Layout
|
|
|
|
```
|
|
your-backend-name/
|
|
├── app/ # Dependency injection (Wire)
|
|
│ ├── wire.go # Wire dependency providers
|
|
│ ├── wire_gen.go # Generated by Wire (gitignore)
|
|
│ └── app.go # Application bootstrapper
|
|
│
|
|
├── cmd/ # CLI commands (Cobra)
|
|
│ ├── root.go # Root command
|
|
│ ├── daemon/
|
|
│ │ └── daemon.go # Main server command
|
|
│ ├── migrate/
|
|
│ │ └── migrate.go # Database migrations
|
|
│ └── version/
|
|
│ └── version.go # Version command
|
|
│
|
|
├── config/ # Configuration
|
|
│ ├── config.go # Config loader
|
|
│ └── constants/
|
|
│ ├── constants.go # Global constants
|
|
│ └── session.go # Session context keys
|
|
│
|
|
├── internal/ # Private application code
|
|
│ ├── domain/ # Domain entities & interfaces
|
|
│ │ ├── user/
|
|
│ │ │ ├── entity.go # User domain entity
|
|
│ │ │ └── repository.go # User repository interface
|
|
│ │ └── tenant/
|
|
│ │ ├── entity.go # Tenant domain entity
|
|
│ │ └── repository.go # Tenant repository interface
|
|
│ │
|
|
│ ├── repository/ # Repository implementations
|
|
│ │ ├── user/
|
|
│ │ │ ├── impl.go # Repository struct
|
|
│ │ │ ├── create.go # Create operations
|
|
│ │ │ ├── get.go # Read operations
|
|
│ │ │ ├── update.go # Update operations
|
|
│ │ │ ├── delete.go # Delete operations
|
|
│ │ │ └── models/
|
|
│ │ │ └── user.go # Database models
|
|
│ │ └── tenant/
|
|
│ │ └── ... # Similar structure
|
|
│ │
|
|
│ ├── usecase/ # Use cases (focused operations)
|
|
│ │ ├── user/
|
|
│ │ │ ├── create_user_entity.go
|
|
│ │ │ ├── save_user_to_repo.go
|
|
│ │ │ ├── validate_user_email_unique.go
|
|
│ │ │ └── get.go
|
|
│ │ └── gateway/
|
|
│ │ ├── login.go
|
|
│ │ ├── hash_password.go
|
|
│ │ └── verify_password.go
|
|
│ │
|
|
│ ├── service/ # Service layer (orchestration)
|
|
│ │ ├── provider.go # Service providers for Wire
|
|
│ │ ├── session.go # Session management service
|
|
│ │ ├── gateway/
|
|
│ │ │ ├── login.go # Login orchestration
|
|
│ │ │ ├── register.go # Registration orchestration
|
|
│ │ │ └── provider.go # Gateway service providers
|
|
│ │ └── user/
|
|
│ │ ├── create.go # User creation orchestration
|
|
│ │ └── provider.go # User service providers
|
|
│ │
|
|
│ ├── interface/http/ # HTTP interface layer
|
|
│ │ ├── server.go # HTTP server & routing
|
|
│ │ ├── handler/ # HTTP handlers
|
|
│ │ │ ├── healthcheck/
|
|
│ │ │ │ └── healthcheck_handler.go
|
|
│ │ │ ├── gateway/
|
|
│ │ │ │ ├── login_handler.go
|
|
│ │ │ │ ├── register_handler.go
|
|
│ │ │ │ └── refresh_handler.go
|
|
│ │ │ └── user/
|
|
│ │ │ ├── create_handler.go
|
|
│ │ │ └── get_handler.go
|
|
│ │ ├── dto/ # Data Transfer Objects
|
|
│ │ │ ├── gateway/
|
|
│ │ │ │ ├── login_dto.go
|
|
│ │ │ │ └── register_dto.go
|
|
│ │ │ └── user/
|
|
│ │ │ ├── create_dto.go
|
|
│ │ │ └── get_dto.go
|
|
│ │ └── middleware/
|
|
│ │ └── tenant.go # Tenant context middleware
|
|
│ │
|
|
│ ├── http/middleware/ # Shared HTTP middleware
|
|
│ │ ├── jwt.go # JWT authentication
|
|
│ │ ├── apikey.go # API key authentication
|
|
│ │ ├── ratelimit.go # Rate limiting
|
|
│ │ ├── security_headers.go # Security headers (CORS, CSP)
|
|
│ │ ├── request_size_limit.go # Request size limits
|
|
│ │ └── provider.go # Middleware providers
|
|
│ │
|
|
│ └── scheduler/ # Background schedulers (cron)
|
|
│ ├── quota_reset.go # Monthly quota reset
|
|
│ └── ip_cleanup.go # GDPR IP cleanup
|
|
│
|
|
├── pkg/ # Reusable packages (copy from maplepress-backend)
|
|
│ ├── logger/ # Structured logging (Zap)
|
|
│ │ ├── logger.go
|
|
│ │ └── sanitizer.go # PII sanitization
|
|
│ ├── security/ # Security utilities
|
|
│ │ ├── jwt/ # JWT provider
|
|
│ │ ├── password/ # Password hashing & validation
|
|
│ │ ├── apikey/ # API key generation & hashing
|
|
│ │ ├── clientip/ # IP extraction & validation
|
|
│ │ ├── ipcrypt/ # IP encryption (GDPR)
|
|
│ │ └── validator/ # Credential validation
|
|
│ ├── storage/
|
|
│ │ ├── database/ # Cassandra client
|
|
│ │ │ ├── cassandra.go
|
|
│ │ │ └── migrator.go
|
|
│ │ ├── cache/ # Redis client
|
|
│ │ │ └── redis.go
|
|
│ │ └── object/ # S3-compatible storage
|
|
│ │ └── s3.go
|
|
│ ├── cache/ # Two-tier caching
|
|
│ │ ├── redis.go
|
|
│ │ ├── cassandra.go
|
|
│ │ └── twotier.go
|
|
│ ├── ratelimit/ # Rate limiting
|
|
│ │ ├── ratelimiter.go
|
|
│ │ └── login_ratelimiter.go
|
|
│ ├── distributedmutex/ # Distributed locking
|
|
│ │ └── distributedmutex.go
|
|
│ ├── validation/ # Input validation
|
|
│ │ └── validator.go
|
|
│ ├── httperror/ # HTTP error handling
|
|
│ │ └── error.go
|
|
│ ├── httpresponse/ # HTTP response helpers
|
|
│ │ └── response.go
|
|
│ └── transaction/ # SAGA pattern
|
|
│ └── saga.go
|
|
│
|
|
├── migrations/ # Cassandra migrations (CQL)
|
|
│ ├── 000001_create_users.up.cql
|
|
│ ├── 000001_create_users.down.cql
|
|
│ ├── 000002_create_tenants.up.cql
|
|
│ └── 000002_create_tenants.down.cql
|
|
│
|
|
├── static/ # Static files (if needed)
|
|
├── docs/ # Documentation
|
|
├── .env.sample # Sample environment variables
|
|
├── .env # Local environment (gitignored)
|
|
├── .gitignore # Git ignore rules
|
|
├── docker-compose.dev.yml # Development docker compose
|
|
├── Dockerfile # Production dockerfile
|
|
├── dev.Dockerfile # Development dockerfile
|
|
├── Taskfile.yml # Task runner configuration
|
|
├── go.mod # Go module definition
|
|
├── go.sum # Go module checksums
|
|
├── main.go # Application entry point
|
|
└── README.md # Project documentation
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Core Dependencies
|
|
|
|
### Essential go.mod Dependencies
|
|
|
|
```go
|
|
module codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
|
|
|
|
go 1.24.4
|
|
|
|
require (
|
|
// Dependency Injection
|
|
github.com/google/wire v0.7.0
|
|
|
|
// CLI Framework
|
|
github.com/spf13/cobra v1.10.1
|
|
|
|
// Logging
|
|
go.uber.org/zap v1.27.0
|
|
|
|
// Database & Cache
|
|
github.com/gocql/gocql v1.7.0
|
|
github.com/redis/go-redis/v9 v9.16.0
|
|
github.com/golang-migrate/migrate/v4 v4.19.0
|
|
|
|
// Security
|
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
|
golang.org/x/crypto v0.41.0
|
|
github.com/awnumar/memguard v0.23.0 // Secure memory handling
|
|
|
|
// HTTP & Utilities
|
|
github.com/google/uuid v1.6.0
|
|
|
|
// Distributed Locking
|
|
github.com/bsm/redislock v0.9.4
|
|
|
|
// Background Jobs (optional)
|
|
github.com/robfig/cron/v3 v3.0.1
|
|
|
|
// Optional: Email (Mailgun)
|
|
github.com/mailgun/mailgun-go/v4 v4.23.0
|
|
|
|
// Optional: Search (Meilisearch)
|
|
github.com/meilisearch/meilisearch-go v0.34.1
|
|
|
|
// Optional: GeoIP (for IP country blocking)
|
|
github.com/oschwald/geoip2-golang v1.13.0
|
|
|
|
// Optional: AWS S3
|
|
github.com/aws/aws-sdk-go-v2 v1.36.3
|
|
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
|
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Configuration System
|
|
|
|
### config/config.go
|
|
|
|
Create a centralized configuration system that loads from environment variables:
|
|
|
|
```go
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config holds all application configuration
|
|
type Config struct {
|
|
App AppConfig
|
|
Server ServerConfig
|
|
HTTP HTTPConfig
|
|
Security SecurityConfig
|
|
Database DatabaseConfig
|
|
Cache CacheConfig
|
|
Logger LoggerConfig
|
|
// Add more config sections as needed
|
|
}
|
|
|
|
// AppConfig holds application-level configuration
|
|
type AppConfig struct {
|
|
Environment string // development, staging, production
|
|
Version string
|
|
JWTSecret string
|
|
}
|
|
|
|
// ServerConfig holds HTTP server configuration
|
|
type ServerConfig struct {
|
|
Host string
|
|
Port int
|
|
}
|
|
|
|
// HTTPConfig holds HTTP request handling configuration
|
|
type HTTPConfig struct {
|
|
MaxRequestBodySize int64 // Maximum request body size in bytes
|
|
ReadTimeout time.Duration // Maximum duration for reading the entire request
|
|
WriteTimeout time.Duration // Maximum duration before timing out writes
|
|
IdleTimeout time.Duration // Maximum amount of time to wait for the next request
|
|
}
|
|
|
|
// SecurityConfig holds security-related configuration
|
|
type SecurityConfig struct {
|
|
TrustedProxies []string // CIDR blocks of trusted reverse proxies
|
|
IPEncryptionKey string // 32-character hex key for IP encryption (GDPR)
|
|
AllowedOrigins []string // CORS allowed origins
|
|
}
|
|
|
|
// DatabaseConfig holds Cassandra database configuration
|
|
type DatabaseConfig struct {
|
|
Hosts []string
|
|
Keyspace string
|
|
Consistency string
|
|
Replication int
|
|
MigrationsPath string
|
|
}
|
|
|
|
// CacheConfig holds Redis cache configuration
|
|
type CacheConfig struct {
|
|
Host string
|
|
Port int
|
|
Password string
|
|
DB int
|
|
}
|
|
|
|
// LoggerConfig holds logging configuration
|
|
type LoggerConfig struct {
|
|
Level string // debug, info, warn, error
|
|
Format string // json, console
|
|
}
|
|
|
|
// Load loads configuration from environment variables
|
|
func Load() (*Config, error) {
|
|
cfg := &Config{
|
|
App: AppConfig{
|
|
Environment: getEnv("APP_ENVIRONMENT", "development"),
|
|
Version: getEnv("APP_VERSION", "0.1.0"),
|
|
JWTSecret: getEnv("APP_JWT_SECRET", "change-me-in-production"),
|
|
},
|
|
Server: ServerConfig{
|
|
Host: getEnv("SERVER_HOST", "0.0.0.0"),
|
|
Port: getEnvAsInt("SERVER_PORT", 8000),
|
|
},
|
|
HTTP: HTTPConfig{
|
|
MaxRequestBodySize: getEnvAsInt64("HTTP_MAX_REQUEST_BODY_SIZE", 10*1024*1024), // 10 MB
|
|
ReadTimeout: getEnvAsDuration("HTTP_READ_TIMEOUT", 30*time.Second),
|
|
WriteTimeout: getEnvAsDuration("HTTP_WRITE_TIMEOUT", 30*time.Second),
|
|
IdleTimeout: getEnvAsDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
|
|
},
|
|
Security: SecurityConfig{
|
|
TrustedProxies: getEnvAsSlice("SECURITY_TRUSTED_PROXIES", []string{}),
|
|
IPEncryptionKey: getEnv("SECURITY_IP_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff"),
|
|
AllowedOrigins: getEnvAsSlice("SECURITY_CORS_ALLOWED_ORIGINS", []string{}),
|
|
},
|
|
Database: DatabaseConfig{
|
|
Hosts: getEnvAsSlice("DATABASE_HOSTS", []string{"localhost"}),
|
|
Keyspace: getEnv("DATABASE_KEYSPACE", "your_keyspace"),
|
|
Consistency: getEnv("DATABASE_CONSISTENCY", "QUORUM"),
|
|
Replication: getEnvAsInt("DATABASE_REPLICATION", 3),
|
|
MigrationsPath: getEnv("DATABASE_MIGRATIONS_PATH", "file://migrations"),
|
|
},
|
|
Cache: CacheConfig{
|
|
Host: getEnv("CACHE_HOST", "localhost"),
|
|
Port: getEnvAsInt("CACHE_PORT", 6379),
|
|
Password: getEnv("CACHE_PASSWORD", ""),
|
|
DB: getEnvAsInt("CACHE_DB", 0),
|
|
},
|
|
Logger: LoggerConfig{
|
|
Level: getEnv("LOGGER_LEVEL", "info"),
|
|
Format: getEnv("LOGGER_FORMAT", "json"),
|
|
},
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := cfg.validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// validate checks if the configuration is valid
|
|
func (c *Config) validate() error {
|
|
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
|
return fmt.Errorf("invalid server port: %d", c.Server.Port)
|
|
}
|
|
|
|
if c.Database.Keyspace == "" {
|
|
return fmt.Errorf("database keyspace is required")
|
|
}
|
|
|
|
if len(c.Database.Hosts) == 0 {
|
|
return fmt.Errorf("at least one database host is required")
|
|
}
|
|
|
|
if c.App.JWTSecret == "" {
|
|
return fmt.Errorf("APP_JWT_SECRET is required")
|
|
}
|
|
|
|
// Production security checks
|
|
if c.App.Environment == "production" {
|
|
if strings.Contains(strings.ToLower(c.App.JWTSecret), "change-me") {
|
|
return fmt.Errorf("SECURITY ERROR: JWT secret is using default value in production")
|
|
}
|
|
|
|
if len(c.App.JWTSecret) < 32 {
|
|
return fmt.Errorf("SECURITY ERROR: JWT secret is too short for production (minimum 32 characters)")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
func getEnvAsInt(key string, defaultValue int) int {
|
|
valueStr := os.Getenv(key)
|
|
if valueStr == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
value, err := strconv.Atoi(valueStr)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func getEnvAsInt64(key string, defaultValue int64) int64 {
|
|
valueStr := os.Getenv(key)
|
|
if valueStr == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
value, err := strconv.ParseInt(valueStr, 10, 64)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func getEnvAsSlice(key string, defaultValue []string) []string {
|
|
valueStr := os.Getenv(key)
|
|
if valueStr == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
// Simple comma-separated parsing
|
|
var result []string
|
|
for _, item := range strings.Split(valueStr, ",") {
|
|
trimmed := strings.TrimSpace(item)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return defaultValue
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
|
|
valueStr := os.Getenv(key)
|
|
if valueStr == "" {
|
|
return defaultValue
|
|
}
|
|
|
|
value, err := time.ParseDuration(valueStr)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
|
|
return value
|
|
}
|
|
```
|
|
|
|
### config/constants/constants.go
|
|
|
|
Define global constants:
|
|
|
|
```go
|
|
package constants
|
|
|
|
// Context keys for request context
|
|
type ContextKey string
|
|
|
|
const (
|
|
// Session context keys
|
|
SessionIsAuthorized ContextKey = "session_is_authorized"
|
|
SessionID ContextKey = "session_id"
|
|
SessionUserID ContextKey = "session_user_id"
|
|
SessionUserUUID ContextKey = "session_user_uuid"
|
|
SessionUserEmail ContextKey = "session_user_email"
|
|
SessionUserName ContextKey = "session_user_name"
|
|
SessionUserRole ContextKey = "session_user_role"
|
|
SessionTenantID ContextKey = "session_tenant_id"
|
|
|
|
// API Key context keys
|
|
APIKeyIsAuthorized ContextKey = "apikey_is_authorized"
|
|
APIKeySiteID ContextKey = "apikey_site_id"
|
|
APIKeyTenantID ContextKey = "apikey_tenant_id"
|
|
)
|
|
|
|
// User roles
|
|
const (
|
|
RoleAdmin = "admin"
|
|
RoleUser = "user"
|
|
RoleGuest = "guest"
|
|
)
|
|
|
|
// Tenant status
|
|
const (
|
|
TenantStatusActive = "active"
|
|
TenantStatusInactive = "inactive"
|
|
TenantStatusSuspended = "suspended"
|
|
)
|
|
```
|
|
|
|
### .env.sample
|
|
|
|
Create a sample environment file:
|
|
|
|
```bash
|
|
# Application Configuration
|
|
APP_ENVIRONMENT=development
|
|
APP_VERSION=0.1.0
|
|
APP_JWT_SECRET=change-me-in-production-use-openssl-rand-base64-64
|
|
|
|
# HTTP Server Configuration
|
|
SERVER_HOST=0.0.0.0
|
|
SERVER_PORT=8000
|
|
|
|
# HTTP Timeouts & Limits
|
|
HTTP_MAX_REQUEST_BODY_SIZE=10485760 # 10 MB
|
|
HTTP_READ_TIMEOUT=30s
|
|
HTTP_WRITE_TIMEOUT=30s
|
|
HTTP_IDLE_TIMEOUT=60s
|
|
|
|
# Security Configuration
|
|
# Trusted proxies for X-Forwarded-For validation (comma-separated CIDR)
|
|
SECURITY_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
|
# IP encryption key for GDPR compliance (32 hex characters)
|
|
SECURITY_IP_ENCRYPTION_KEY=00112233445566778899aabbccddeeff
|
|
# CORS allowed origins (comma-separated)
|
|
SECURITY_CORS_ALLOWED_ORIGINS=https://yourdomain.com
|
|
|
|
# Cassandra Database Configuration
|
|
DATABASE_HOSTS=localhost:9042
|
|
DATABASE_KEYSPACE=your_keyspace
|
|
DATABASE_CONSISTENCY=QUORUM
|
|
DATABASE_REPLICATION=3
|
|
DATABASE_MIGRATIONS_PATH=file://migrations
|
|
|
|
# Redis Cache Configuration
|
|
CACHE_HOST=localhost
|
|
CACHE_PORT=6379
|
|
CACHE_PASSWORD=
|
|
CACHE_DB=0
|
|
|
|
# Logger Configuration
|
|
LOGGER_LEVEL=info
|
|
LOGGER_FORMAT=json
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Dependency Injection with Wire
|
|
|
|
### Overview
|
|
|
|
We use Google Wire for compile-time dependency injection. Wire generates code at build time, eliminating runtime reflection and providing type safety.
|
|
|
|
### app/wire.go
|
|
|
|
This file defines all providers for Wire:
|
|
|
|
```go
|
|
//go:build wireinject
|
|
// +build wireinject
|
|
|
|
package app
|
|
|
|
import (
|
|
"github.com/google/wire"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/http/middleware"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/gateway"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/healthcheck"
|
|
userrepo "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/repository/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service"
|
|
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/gateway"
|
|
gatewayuc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/gateway"
|
|
userusecase "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/logger"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/password"
|
|
rediscache "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/storage/cache"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/storage/database"
|
|
)
|
|
|
|
// InitializeApplication wires up all dependencies
|
|
func InitializeApplication(cfg *config.Config) (*Application, error) {
|
|
wire.Build(
|
|
// Infrastructure layer (pkg/)
|
|
logger.ProvideLogger,
|
|
database.ProvideCassandraSession,
|
|
|
|
// Cache layer
|
|
rediscache.ProvideRedisClient,
|
|
cache.ProvideRedisCache,
|
|
cache.ProvideCassandraCache,
|
|
cache.ProvideTwoTierCache,
|
|
|
|
// Security layer
|
|
security.ProvideJWTProvider,
|
|
password.NewPasswordProvider,
|
|
password.NewPasswordValidator,
|
|
password.NewBreachChecker,
|
|
security.ProvideClientIPExtractor,
|
|
|
|
// Repository layer
|
|
userrepo.ProvideRepository,
|
|
|
|
// Use case layer
|
|
userusecase.ProvideCreateUserEntityUseCase,
|
|
userusecase.ProvideSaveUserToRepoUseCase,
|
|
userusecase.ProvideGetUserUseCase,
|
|
gatewayuc.ProvideLoginUseCase,
|
|
gatewayuc.ProvideHashPasswordUseCase,
|
|
gatewayuc.ProvideVerifyPasswordUseCase,
|
|
|
|
// Service layer
|
|
service.ProvideSessionService,
|
|
gatewaysvc.ProvideLoginService,
|
|
|
|
// Middleware layer
|
|
middleware.ProvideJWTMiddleware,
|
|
middleware.ProvideSecurityHeadersMiddleware,
|
|
middleware.ProvideRequestSizeLimitMiddleware,
|
|
|
|
// Handler layer
|
|
healthcheck.ProvideHealthCheckHandler,
|
|
gateway.ProvideLoginHandler,
|
|
|
|
// HTTP server
|
|
http.ProvideServer,
|
|
|
|
// Application
|
|
ProvideApplication,
|
|
)
|
|
|
|
return nil, nil
|
|
}
|
|
```
|
|
|
|
### app/app.go
|
|
|
|
Application bootstrapper:
|
|
|
|
```go
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http"
|
|
)
|
|
|
|
// Application represents the main application
|
|
type Application struct {
|
|
config *config.Config
|
|
httpServer *http.Server
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideApplication creates the application instance
|
|
func ProvideApplication(
|
|
cfg *config.Config,
|
|
httpServer *http.Server,
|
|
logger *zap.Logger,
|
|
) *Application {
|
|
return &Application{
|
|
config: cfg,
|
|
httpServer: httpServer,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Start starts the application
|
|
func (app *Application) Start() error {
|
|
app.logger.Info("Starting application",
|
|
zap.String("environment", app.config.App.Environment),
|
|
zap.String("version", app.config.App.Version))
|
|
|
|
// Start HTTP server in goroutine
|
|
go func() {
|
|
if err := app.httpServer.Start(); err != nil {
|
|
app.logger.Fatal("HTTP server failed", zap.Error(err))
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
app.waitForShutdown()
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForShutdown waits for interrupt signal and performs graceful shutdown
|
|
func (app *Application) waitForShutdown() {
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
<-quit
|
|
app.logger.Info("Shutting down application...")
|
|
|
|
// Create context with timeout for graceful shutdown
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Shutdown HTTP server
|
|
if err := app.httpServer.Shutdown(ctx); err != nil {
|
|
app.logger.Error("HTTP server shutdown failed", zap.Error(err))
|
|
}
|
|
|
|
app.logger.Info("Application shutdown complete")
|
|
}
|
|
```
|
|
|
|
### Generate Wire Code
|
|
|
|
```bash
|
|
# Navigate to app directory
|
|
cd app
|
|
|
|
# Run wire to generate wire_gen.go
|
|
wire
|
|
|
|
# You should see: wire_gen.go created
|
|
```
|
|
|
|
### Add to .gitignore
|
|
|
|
```
|
|
app/wire_gen.go
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Reusable pkg/ Components
|
|
|
|
The `pkg/` directory contains infrastructure components that can be copied directly from `maplepress-backend` and reused across projects.
|
|
|
|
### Components to Copy
|
|
|
|
#### 7.1 Logger (pkg/logger/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/logger/`
|
|
|
|
**Purpose:** Structured logging with Zap, including PII sanitization
|
|
|
|
**Key files:**
|
|
- `logger.go` - Logger provider and configuration
|
|
- `sanitizer.go` - Email and sensitive data sanitization
|
|
|
|
**Usage:**
|
|
```go
|
|
logger := logger.ProvideLogger(cfg)
|
|
logger.Info("User logged in", zap.String("user_id", userID))
|
|
logger.Error("Operation failed", zap.Error(err))
|
|
```
|
|
|
|
#### 7.2 Security (pkg/security/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/security/`
|
|
|
|
**Purpose:** Comprehensive security utilities
|
|
|
|
**Subpackages:**
|
|
|
|
##### JWT (pkg/security/jwt/)
|
|
```go
|
|
// Generate JWT tokens
|
|
jwtProvider := security.ProvideJWTProvider(cfg)
|
|
accessToken, accessExpiry, err := jwtProvider.GenerateToken(sessionID, 15*time.Minute)
|
|
|
|
// Validate JWT
|
|
sessionID, err := jwtProvider.ValidateToken(tokenString)
|
|
```
|
|
|
|
##### Password (pkg/security/password/)
|
|
```go
|
|
// Hash password
|
|
passwordProvider := password.NewPasswordProvider()
|
|
hashedPassword, err := passwordProvider.HashPassword("user-password")
|
|
|
|
// Verify password
|
|
isValid, err := passwordProvider.VerifyPassword("user-password", hashedPassword)
|
|
|
|
// Check for breached passwords (CWE-521)
|
|
breachChecker := password.NewBreachChecker()
|
|
isBreached, err := breachChecker.CheckPassword("user-password")
|
|
```
|
|
|
|
##### API Key (pkg/security/apikey/)
|
|
```go
|
|
// Generate API key
|
|
generator := apikey.ProvideGenerator()
|
|
apiKey := generator.Generate() // Returns: "mp_live_abc123..."
|
|
|
|
// Hash API key for storage
|
|
hasher := apikey.ProvideHasher()
|
|
hashedKey, err := hasher.Hash(apiKey)
|
|
|
|
// Verify API key
|
|
isValid, err := hasher.Verify(apiKey, hashedKey)
|
|
```
|
|
|
|
##### Client IP (pkg/security/clientip/)
|
|
```go
|
|
// Extract client IP with X-Forwarded-For validation (CWE-348)
|
|
extractor := security.ProvideClientIPExtractor(cfg)
|
|
clientIP, err := extractor.ExtractIP(r)
|
|
```
|
|
|
|
##### IP Encryption (pkg/security/ipcrypt/)
|
|
```go
|
|
// Encrypt IP for GDPR compliance (CWE-359)
|
|
encryptor := ipcrypt.ProvideIPEncryptor(cfg)
|
|
encryptedIP, err := encryptor.Encrypt("192.168.1.1")
|
|
decryptedIP, err := encryptor.Decrypt(encryptedIP)
|
|
```
|
|
|
|
#### 7.3 Database (pkg/storage/database/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/storage/database/`
|
|
|
|
**Purpose:** Cassandra connection and migration management
|
|
|
|
```go
|
|
// Connect to Cassandra
|
|
session, err := database.ProvideCassandraSession(cfg, logger)
|
|
|
|
// Run migrations
|
|
migrator := database.NewMigrator(cfg, logger)
|
|
err := migrator.Up()
|
|
```
|
|
|
|
#### 7.4 Cache (pkg/cache/ and pkg/storage/cache/)
|
|
|
|
**Copy from:**
|
|
- `cloud/maplepress-backend/pkg/cache/`
|
|
- `cloud/maplepress-backend/pkg/storage/cache/`
|
|
|
|
**Purpose:** Redis client and two-tier caching (Redis + Cassandra)
|
|
|
|
```go
|
|
// Two-tier cache (fast Redis + persistent Cassandra)
|
|
cache := cache.ProvideTwoTierCache(redisCache, cassandraCache, logger)
|
|
|
|
// Set with TTL
|
|
err := cache.Set(ctx, "key", value, 1*time.Hour)
|
|
|
|
// Get
|
|
value, err := cache.Get(ctx, "key")
|
|
|
|
// Delete
|
|
err := cache.Delete(ctx, "key")
|
|
```
|
|
|
|
#### 7.5 Rate Limiting (pkg/ratelimit/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/ratelimit/`
|
|
|
|
**Purpose:** Redis-based rate limiting with configurable limits
|
|
|
|
```go
|
|
// Create rate limiter
|
|
limiter := ratelimit.NewRateLimiter(redisClient, logger)
|
|
|
|
// Check rate limit
|
|
allowed, err := limiter.Allow(ctx, "user:123", 100, time.Hour)
|
|
if !allowed {
|
|
// Rate limit exceeded
|
|
}
|
|
```
|
|
|
|
#### 7.6 Distributed Mutex (pkg/distributedmutex/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/distributedmutex/`
|
|
|
|
**Purpose:** Redis-based distributed locking to prevent race conditions
|
|
|
|
```go
|
|
// Acquire lock
|
|
mutex := distributedmutex.ProvideDistributedMutexAdapter(redisClient, logger)
|
|
lock, err := mutex.Lock(ctx, "resource:123", 30*time.Second)
|
|
if err != nil {
|
|
// Failed to acquire lock
|
|
}
|
|
defer lock.Unlock(ctx)
|
|
|
|
// Critical section protected by lock
|
|
// ...
|
|
```
|
|
|
|
#### 7.7 Validation (pkg/validation/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/validation/`
|
|
|
|
**Purpose:** Input validation utilities
|
|
|
|
```go
|
|
validator := validation.NewValidator()
|
|
|
|
// Validate email
|
|
if !validator.IsValidEmail("user@example.com") {
|
|
// Invalid email
|
|
}
|
|
|
|
// Validate UUID
|
|
if !validator.IsValidUUID("123e4567-e89b-12d3-a456-426614174000") {
|
|
// Invalid UUID
|
|
}
|
|
```
|
|
|
|
#### 7.8 HTTP Utilities (pkg/httperror/ and pkg/httpresponse/)
|
|
|
|
**Copy from:**
|
|
- `cloud/maplepress-backend/pkg/httperror/`
|
|
- `cloud/maplepress-backend/pkg/httpresponse/`
|
|
|
|
**Purpose:** Consistent HTTP error and response handling
|
|
|
|
```go
|
|
// Send error response
|
|
httperror.SendError(w, http.StatusBadRequest, "Invalid input", err)
|
|
|
|
// Send JSON response
|
|
httpresponse.SendJSON(w, http.StatusOK, map[string]interface{}{
|
|
"message": "Success",
|
|
"data": data,
|
|
})
|
|
```
|
|
|
|
#### 7.9 Transaction SAGA (pkg/transaction/)
|
|
|
|
**Copy from:** `cloud/maplepress-backend/pkg/transaction/`
|
|
|
|
**Purpose:** SAGA pattern for distributed transactions
|
|
|
|
```go
|
|
// Create SAGA
|
|
saga := transaction.NewSaga(logger)
|
|
|
|
// Add compensation steps
|
|
saga.AddStep(
|
|
func(ctx context.Context) error {
|
|
// Forward operation
|
|
return createUser(ctx, user)
|
|
},
|
|
func(ctx context.Context) error {
|
|
// Compensation (rollback)
|
|
return deleteUser(ctx, user.ID)
|
|
},
|
|
)
|
|
|
|
// Execute SAGA
|
|
err := saga.Execute(ctx)
|
|
if err != nil {
|
|
// SAGA failed and all compensations were executed
|
|
}
|
|
```
|
|
|
|
### Copy Script
|
|
|
|
Create a script to copy all reusable components:
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# copy-pkg.sh - Copy reusable pkg/ components from maplepress-backend
|
|
|
|
SOURCE_DIR="../maplepress-backend/pkg"
|
|
DEST_DIR="./pkg"
|
|
|
|
# Create destination directory
|
|
mkdir -p "$DEST_DIR"
|
|
|
|
# Copy all pkg components
|
|
cp -r "$SOURCE_DIR/logger" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/security" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/storage" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/cache" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/ratelimit" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/distributedmutex" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/validation" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/httperror" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/httpresponse" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/httpvalidation" "$DEST_DIR/"
|
|
cp -r "$SOURCE_DIR/transaction" "$DEST_DIR/"
|
|
|
|
# Optional components (copy if needed)
|
|
# cp -r "$SOURCE_DIR/emailer" "$DEST_DIR/"
|
|
# cp -r "$SOURCE_DIR/search" "$DEST_DIR/"
|
|
# cp -r "$SOURCE_DIR/dns" "$DEST_DIR/"
|
|
|
|
echo "✅ All pkg/ components copied successfully"
|
|
|
|
# Update import paths
|
|
echo "⚠️ Remember to update import paths in copied files from:"
|
|
echo " codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend"
|
|
echo " to:"
|
|
echo " codeberg.org/mapleopentech/monorepo/cloud/your-backend-name"
|
|
```
|
|
|
|
Run the script:
|
|
|
|
```bash
|
|
chmod +x copy-pkg.sh
|
|
./copy-pkg.sh
|
|
```
|
|
|
|
### Update Import Paths
|
|
|
|
After copying, you'll need to update import paths in all copied files:
|
|
|
|
```bash
|
|
# Find and replace import paths
|
|
find ./pkg -type f -name "*.go" -exec sed -i '' \
|
|
's|codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend|codeberg.org/mapleopentech/monorepo/cloud/your-backend-name|g' \
|
|
{} +
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Authentication System
|
|
|
|
Our authentication system uses JWT tokens with session management stored in a two-tier cache (Redis + Cassandra).
|
|
|
|
### 8.1 Authentication Flow
|
|
|
|
```
|
|
┌──────────┐
|
|
│ Client │
|
|
└─────┬────┘
|
|
│ 1. POST /api/v1/login
|
|
│ {email, password}
|
|
↓
|
|
┌─────────────────────────┐
|
|
│ Login Handler │
|
|
└────────┬────────────────┘
|
|
│ 2. Validate input
|
|
↓
|
|
┌─────────────────────────┐
|
|
│ Login Service │
|
|
└────────┬────────────────┘
|
|
│ 3. Verify credentials
|
|
│ 4. Create session
|
|
│ 5. Generate JWT tokens
|
|
↓
|
|
┌─────────────────────────┐
|
|
│ Session Service │
|
|
│ (Two-Tier Cache) │
|
|
└────────┬────────────────┘
|
|
│ 6. Store in Redis (fast)
|
|
│ 7. Store in Cassandra (persistent)
|
|
↓
|
|
┌─────────────────────────┐
|
|
│ Response │
|
|
│ - access_token │
|
|
│ - refresh_token │
|
|
│ - user_info │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
### 8.2 Session Service Implementation
|
|
|
|
**internal/service/session.go:**
|
|
|
|
```go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
|
|
)
|
|
|
|
const (
|
|
SessionTTL = 24 * time.Hour // Session expires after 24 hours
|
|
)
|
|
|
|
// SessionService handles session management
|
|
type SessionService interface {
|
|
CreateSession(ctx context.Context, userID uint64, userUUID uuid.UUID, email, name, role string, tenantID uuid.UUID) (*domain.Session, error)
|
|
GetSession(ctx context.Context, sessionID string) (*domain.Session, error)
|
|
DeleteSession(ctx context.Context, sessionID string) error
|
|
InvalidateUserSessions(ctx context.Context, userUUID uuid.UUID) error
|
|
}
|
|
|
|
type sessionService struct {
|
|
cache cache.TwoTierCacher
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideSessionService provides a session service instance
|
|
func ProvideSessionService(cache cache.TwoTierCacher, logger *zap.Logger) SessionService {
|
|
return &sessionService{
|
|
cache: cache,
|
|
logger: logger.Named("session-service"),
|
|
}
|
|
}
|
|
|
|
// CreateSession creates a new session and stores it in cache
|
|
func (s *sessionService) CreateSession(
|
|
ctx context.Context,
|
|
userID uint64,
|
|
userUUID uuid.UUID,
|
|
email, name, role string,
|
|
tenantID uuid.UUID,
|
|
) (*domain.Session, error) {
|
|
// Generate unique session ID
|
|
sessionID := uuid.New().String()
|
|
|
|
// Create session object
|
|
session := &domain.Session{
|
|
ID: sessionID,
|
|
UserID: userID,
|
|
UserUUID: userUUID,
|
|
UserEmail: email,
|
|
UserName: name,
|
|
UserRole: role,
|
|
TenantID: tenantID,
|
|
CreatedAt: time.Now().UTC(),
|
|
ExpiresAt: time.Now().UTC().Add(SessionTTL),
|
|
}
|
|
|
|
// Serialize to JSON
|
|
sessionJSON, err := json.Marshal(session)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal session: %w", err)
|
|
}
|
|
|
|
// Store in two-tier cache (Redis + Cassandra)
|
|
cacheKey := fmt.Sprintf("session:%s", sessionID)
|
|
if err := s.cache.Set(ctx, cacheKey, sessionJSON, SessionTTL); err != nil {
|
|
return nil, fmt.Errorf("failed to store session: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Session created",
|
|
zap.String("session_id", sessionID),
|
|
zap.String("user_uuid", userUUID.String()))
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// GetSession retrieves a session from cache
|
|
func (s *sessionService) GetSession(ctx context.Context, sessionID string) (*domain.Session, error) {
|
|
cacheKey := fmt.Sprintf("session:%s", sessionID)
|
|
|
|
// Get from two-tier cache (tries Redis first, falls back to Cassandra)
|
|
sessionJSON, err := s.cache.Get(ctx, cacheKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("session not found: %w", err)
|
|
}
|
|
|
|
// Deserialize from JSON
|
|
var session domain.Session
|
|
if err := json.Unmarshal(sessionJSON, &session); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
|
|
}
|
|
|
|
// Check if session is expired
|
|
if time.Now().UTC().After(session.ExpiresAt) {
|
|
// Delete expired session
|
|
_ = s.DeleteSession(ctx, sessionID)
|
|
return nil, fmt.Errorf("session expired")
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// DeleteSession removes a session from cache
|
|
func (s *sessionService) DeleteSession(ctx context.Context, sessionID string) error {
|
|
cacheKey := fmt.Sprintf("session:%s", sessionID)
|
|
return s.cache.Delete(ctx, cacheKey)
|
|
}
|
|
|
|
// InvalidateUserSessions invalidates all sessions for a user (CWE-384: Session Fixation Prevention)
|
|
func (s *sessionService) InvalidateUserSessions(ctx context.Context, userUUID uuid.UUID) error {
|
|
// Note: This is a simplified implementation
|
|
// In production, you should maintain a user->sessions mapping in cache
|
|
// For now, sessions will naturally expire after SessionTTL
|
|
s.logger.Info("Invalidating user sessions",
|
|
zap.String("user_uuid", userUUID.String()))
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### 8.3 Domain Session Entity
|
|
|
|
**internal/domain/session.go:**
|
|
|
|
```go
|
|
package domain
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Session represents a user session
|
|
type Session struct {
|
|
ID string `json:"id"`
|
|
UserID uint64 `json:"user_id"`
|
|
UserUUID uuid.UUID `json:"user_uuid"`
|
|
UserEmail string `json:"user_email"`
|
|
UserName string `json:"user_name"`
|
|
UserRole string `json:"user_role"`
|
|
TenantID uuid.UUID `json:"tenant_id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
```
|
|
|
|
### 8.4 JWT Middleware
|
|
|
|
**internal/http/middleware/jwt.go:**
|
|
|
|
```go
|
|
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/jwt"
|
|
)
|
|
|
|
// JWTMiddleware validates JWT tokens and populates session context
|
|
type JWTMiddleware struct {
|
|
jwtProvider jwt.Provider
|
|
sessionService service.SessionService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewJWTMiddleware creates a new JWT middleware
|
|
func NewJWTMiddleware(
|
|
jwtProvider jwt.Provider,
|
|
sessionService service.SessionService,
|
|
logger *zap.Logger,
|
|
) *JWTMiddleware {
|
|
return &JWTMiddleware{
|
|
jwtProvider: jwtProvider,
|
|
sessionService: sessionService,
|
|
logger: logger.Named("jwt-middleware"),
|
|
}
|
|
}
|
|
|
|
// ProvideJWTMiddleware provides JWT middleware for Wire
|
|
func ProvideJWTMiddleware(
|
|
jwtProvider jwt.Provider,
|
|
sessionService service.SessionService,
|
|
logger *zap.Logger,
|
|
) *JWTMiddleware {
|
|
return NewJWTMiddleware(jwtProvider, sessionService, logger)
|
|
}
|
|
|
|
// Handler validates JWT and populates context
|
|
func (m *JWTMiddleware) Handler(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Get Authorization header
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// Expected format: "JWT <token>"
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || parts[0] != "JWT" {
|
|
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
token := parts[1]
|
|
|
|
// Validate token
|
|
sessionID, err := m.jwtProvider.ValidateToken(token)
|
|
if err != nil {
|
|
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// Get session from cache
|
|
session, err := m.sessionService.GetSession(r.Context(), sessionID)
|
|
if err != nil {
|
|
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// Populate context with session data
|
|
ctx := r.Context()
|
|
ctx = context.WithValue(ctx, constants.SessionIsAuthorized, true)
|
|
ctx = context.WithValue(ctx, constants.SessionID, session.ID)
|
|
ctx = context.WithValue(ctx, constants.SessionUserUUID, session.UserUUID.String())
|
|
ctx = context.WithValue(ctx, constants.SessionUserEmail, session.UserEmail)
|
|
ctx = context.WithValue(ctx, constants.SessionUserName, session.UserName)
|
|
ctx = context.WithValue(ctx, constants.SessionUserRole, session.UserRole)
|
|
ctx = context.WithValue(ctx, constants.SessionTenantID, session.TenantID.String())
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// RequireAuth ensures the request is authenticated
|
|
func (m *JWTMiddleware) RequireAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
isAuthorized, ok := r.Context().Value(constants.SessionIsAuthorized).(bool)
|
|
if !ok || !isAuthorized {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
```
|
|
|
|
### 8.5 Login Handler Example
|
|
|
|
**internal/interface/http/handler/gateway/login_handler.go:**
|
|
|
|
```go
|
|
package gateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/dto/gateway"
|
|
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/gateway"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httperror"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httpresponse"
|
|
)
|
|
|
|
// LoginHandler handles user login
|
|
type LoginHandler struct {
|
|
loginService gatewaysvc.LoginService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideLoginHandler provides a login handler for Wire
|
|
func ProvideLoginHandler(
|
|
loginService gatewaysvc.LoginService,
|
|
logger *zap.Logger,
|
|
) *LoginHandler {
|
|
return &LoginHandler{
|
|
loginService: loginService,
|
|
logger: logger.Named("login-handler"),
|
|
}
|
|
}
|
|
|
|
// Handle processes login requests
|
|
func (h *LoginHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
|
// Parse request
|
|
var req gateway.LoginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httperror.SendError(w, http.StatusBadRequest, "Invalid request body", err)
|
|
return
|
|
}
|
|
|
|
// Validate input
|
|
if req.Email == "" || req.Password == "" {
|
|
httperror.SendError(w, http.StatusBadRequest, "Email and password are required", nil)
|
|
return
|
|
}
|
|
|
|
// Execute login
|
|
response, err := h.loginService.Login(r.Context(), &gatewaysvc.LoginInput{
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
})
|
|
if err != nil {
|
|
h.logger.Error("Login failed", zap.Error(err))
|
|
httperror.SendError(w, http.StatusUnauthorized, "Invalid credentials", err)
|
|
return
|
|
}
|
|
|
|
// Send response
|
|
httpresponse.SendJSON(w, http.StatusOK, response)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Clean Architecture Layers
|
|
|
|
### Layer Structure
|
|
|
|
#### 9.1 Domain Layer (internal/domain/)
|
|
|
|
**Purpose:** Core business entities and repository interfaces
|
|
|
|
**Example: User Entity**
|
|
|
|
```go
|
|
// internal/domain/user/entity.go
|
|
package user
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// User represents a user entity
|
|
type User struct {
|
|
ID uuid.UUID
|
|
TenantID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Role string
|
|
Password string // Hashed
|
|
Status string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
```
|
|
|
|
**Example: User Repository Interface**
|
|
|
|
```go
|
|
// internal/domain/user/repository.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Repository defines user data access interface
|
|
type Repository interface {
|
|
Create(ctx context.Context, user *User) error
|
|
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
|
GetByEmail(ctx context.Context, email string) (*User, error)
|
|
Update(ctx context.Context, user *User) error
|
|
Delete(ctx context.Context, id uuid.UUID) error
|
|
}
|
|
```
|
|
|
|
#### 9.2 Repository Layer (internal/repository/)
|
|
|
|
**Purpose:** Data access implementations
|
|
|
|
**Example: User Repository Implementation**
|
|
|
|
```go
|
|
// internal/repository/user/impl.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/gocql/gocql"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
|
|
)
|
|
|
|
type repository struct {
|
|
session *gocql.Session
|
|
cache cache.TwoTierCacher
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideRepository provides a user repository for Wire
|
|
func ProvideRepository(
|
|
session *gocql.Session,
|
|
cache cache.TwoTierCacher,
|
|
logger *zap.Logger,
|
|
) user.Repository {
|
|
return &repository{
|
|
session: session,
|
|
cache: cache,
|
|
logger: logger.Named("user-repository"),
|
|
}
|
|
}
|
|
|
|
// Create creates a new user
|
|
func (r *repository) Create(ctx context.Context, user *user.User) error {
|
|
query := `
|
|
INSERT INTO users (id, tenant_id, email, name, role, password, status, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
|
|
return r.session.Query(query,
|
|
user.ID,
|
|
user.TenantID,
|
|
user.Email,
|
|
user.Name,
|
|
user.Role,
|
|
user.Password,
|
|
user.Status,
|
|
user.CreatedAt,
|
|
user.UpdatedAt,
|
|
).WithContext(ctx).Exec()
|
|
}
|
|
|
|
// GetByID retrieves a user by ID
|
|
func (r *repository) GetByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
|
|
// Try cache first
|
|
cacheKey := fmt.Sprintf("user:id:%s", id.String())
|
|
if cachedData, err := r.cache.Get(ctx, cacheKey); err == nil {
|
|
var u user.User
|
|
if err := json.Unmarshal(cachedData, &u); err == nil {
|
|
return &u, nil
|
|
}
|
|
}
|
|
|
|
// Query database
|
|
query := `
|
|
SELECT id, tenant_id, email, name, role, password, status, created_at, updated_at
|
|
FROM users
|
|
WHERE id = ?
|
|
`
|
|
|
|
var u user.User
|
|
err := r.session.Query(query, id).
|
|
WithContext(ctx).
|
|
Scan(&u.ID, &u.TenantID, &u.Email, &u.Name, &u.Role, &u.Password, &u.Status, &u.CreatedAt, &u.UpdatedAt)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache result
|
|
if data, err := json.Marshal(u); err == nil {
|
|
_ = r.cache.Set(ctx, cacheKey, data, 15*time.Minute)
|
|
}
|
|
|
|
return &u, nil
|
|
}
|
|
```
|
|
|
|
#### 9.3 Use Case Layer (internal/usecase/)
|
|
|
|
**Purpose:** Focused, single-responsibility business operations
|
|
|
|
**Example: Create User Entity Use Case**
|
|
|
|
```go
|
|
// internal/usecase/user/create_user_entity.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
|
|
)
|
|
|
|
// CreateUserEntityUseCase creates a user entity from input
|
|
type CreateUserEntityUseCase struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideCreateUserEntityUseCase provides the use case for Wire
|
|
func ProvideCreateUserEntityUseCase(logger *zap.Logger) *CreateUserEntityUseCase {
|
|
return &CreateUserEntityUseCase{
|
|
logger: logger.Named("create-user-entity-uc"),
|
|
}
|
|
}
|
|
|
|
// CreateUserEntityInput represents the input
|
|
type CreateUserEntityInput struct {
|
|
TenantID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Role string
|
|
HashedPassword string
|
|
}
|
|
|
|
// Execute creates a user entity
|
|
func (uc *CreateUserEntityUseCase) Execute(
|
|
ctx context.Context,
|
|
input *CreateUserEntityInput,
|
|
) (*user.User, error) {
|
|
now := time.Now().UTC()
|
|
|
|
entity := &user.User{
|
|
ID: uuid.New(),
|
|
TenantID: input.TenantID,
|
|
Email: input.Email,
|
|
Name: input.Name,
|
|
Role: input.Role,
|
|
Password: input.HashedPassword,
|
|
Status: constants.TenantStatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
uc.logger.Info("User entity created",
|
|
zap.String("user_id", entity.ID.String()),
|
|
zap.String("email", entity.Email))
|
|
|
|
return entity, nil
|
|
}
|
|
```
|
|
|
|
**Example: Save User to Repo Use Case**
|
|
|
|
```go
|
|
// internal/usecase/user/save_user_to_repo.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
|
|
)
|
|
|
|
// SaveUserToRepoUseCase saves a user to repository
|
|
type SaveUserToRepoUseCase struct {
|
|
repo user.Repository
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideSaveUserToRepoUseCase provides the use case for Wire
|
|
func ProvideSaveUserToRepoUseCase(
|
|
repo user.Repository,
|
|
logger *zap.Logger,
|
|
) *SaveUserToRepoUseCase {
|
|
return &SaveUserToRepoUseCase{
|
|
repo: repo,
|
|
logger: logger.Named("save-user-to-repo-uc"),
|
|
}
|
|
}
|
|
|
|
// Execute saves the user
|
|
func (uc *SaveUserToRepoUseCase) Execute(
|
|
ctx context.Context,
|
|
user *user.User,
|
|
) error {
|
|
if err := uc.repo.Create(ctx, user); err != nil {
|
|
uc.logger.Error("Failed to save user",
|
|
zap.String("user_id", user.ID.String()),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
uc.logger.Info("User saved to repository",
|
|
zap.String("user_id", user.ID.String()))
|
|
|
|
return nil
|
|
}
|
|
```
|
|
|
|
#### 9.4 Service Layer (internal/service/)
|
|
|
|
**Purpose:** Orchestration logic, transaction management (SAGA)
|
|
|
|
**Example: Create User Service**
|
|
|
|
```go
|
|
// internal/service/user/create.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
userusecase "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/password"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/transaction"
|
|
)
|
|
|
|
// CreateUserService handles user creation orchestration
|
|
type CreateUserService interface {
|
|
Create(ctx context.Context, input *CreateUserInput) (*CreateUserResponse, error)
|
|
}
|
|
|
|
type createUserService struct {
|
|
validateEmailUC userusecase.ValidateUserEmailUniqueUseCase
|
|
createEntityUC *userusecase.CreateUserEntityUseCase
|
|
saveToRepoUC *userusecase.SaveUserToRepoUseCase
|
|
passwordProvider password.Provider
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideCreateUserService provides the service for Wire
|
|
func ProvideCreateUserService(
|
|
validateEmailUC *userusecase.ValidateUserEmailUniqueUseCase,
|
|
createEntityUC *userusecase.CreateUserEntityUseCase,
|
|
saveToRepoUC *userusecase.SaveUserToRepoUseCase,
|
|
passwordProvider password.Provider,
|
|
logger *zap.Logger,
|
|
) CreateUserService {
|
|
return &createUserService{
|
|
validateEmailUC: validateEmailUC,
|
|
createEntityUC: createEntityUC,
|
|
saveToRepoUC: saveToRepoUC,
|
|
passwordProvider: passwordProvider,
|
|
logger: logger.Named("create-user-service"),
|
|
}
|
|
}
|
|
|
|
type CreateUserInput struct {
|
|
TenantID uuid.UUID
|
|
Email string
|
|
Name string
|
|
Role string
|
|
Password string
|
|
}
|
|
|
|
type CreateUserResponse struct {
|
|
UserID string
|
|
Email string
|
|
Name string
|
|
Role string
|
|
}
|
|
|
|
// Create orchestrates user creation with SAGA pattern
|
|
func (s *createUserService) Create(
|
|
ctx context.Context,
|
|
input *CreateUserInput,
|
|
) (*CreateUserResponse, error) {
|
|
// Validate email uniqueness
|
|
if err := s.validateEmailUC.Execute(ctx, input.Email); err != nil {
|
|
return nil, fmt.Errorf("email validation failed: %w", err)
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := s.passwordProvider.HashPassword(input.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("password hashing failed: %w", err)
|
|
}
|
|
|
|
// Create user entity
|
|
userEntity, err := s.createEntityUC.Execute(ctx, &userusecase.CreateUserEntityInput{
|
|
TenantID: input.TenantID,
|
|
Email: input.Email,
|
|
Name: input.Name,
|
|
Role: input.Role,
|
|
HashedPassword: hashedPassword,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("entity creation failed: %w", err)
|
|
}
|
|
|
|
// Use SAGA pattern for transaction management
|
|
saga := transaction.NewSaga(s.logger)
|
|
|
|
// Step 1: Save user to repository
|
|
saga.AddStep(
|
|
func(ctx context.Context) error {
|
|
return s.saveToRepoUC.Execute(ctx, userEntity)
|
|
},
|
|
func(ctx context.Context) error {
|
|
// Compensation: Delete user if subsequent steps fail
|
|
// (implement delete use case)
|
|
s.logger.Warn("Compensating: deleting user", zap.String("user_id", userEntity.ID.String()))
|
|
return nil
|
|
},
|
|
)
|
|
|
|
// Execute SAGA
|
|
if err := saga.Execute(ctx); err != nil {
|
|
return nil, fmt.Errorf("user creation failed: %w", err)
|
|
}
|
|
|
|
return &CreateUserResponse{
|
|
UserID: userEntity.ID.String(),
|
|
Email: userEntity.Email,
|
|
Name: userEntity.Name,
|
|
Role: userEntity.Role,
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
#### 9.5 Interface Layer (internal/interface/http/)
|
|
|
|
**Purpose:** HTTP handlers and DTOs
|
|
|
|
**Example: Create User Handler**
|
|
|
|
```go
|
|
// internal/interface/http/handler/user/create_handler.go
|
|
package user
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/dto/user"
|
|
usersvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httperror"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httpresponse"
|
|
)
|
|
|
|
// CreateHandler handles user creation
|
|
type CreateHandler struct {
|
|
createService usersvc.CreateUserService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideCreateHandler provides the handler for Wire
|
|
func ProvideCreateHandler(
|
|
createService usersvc.CreateUserService,
|
|
logger *zap.Logger,
|
|
) *CreateHandler {
|
|
return &CreateHandler{
|
|
createService: createService,
|
|
logger: logger.Named("create-user-handler"),
|
|
}
|
|
}
|
|
|
|
// Handle processes user creation requests
|
|
func (h *CreateHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
|
// Get tenant ID from context (populated by JWT middleware)
|
|
tenantIDStr, ok := r.Context().Value(constants.SessionTenantID).(string)
|
|
if !ok {
|
|
httperror.SendError(w, http.StatusUnauthorized, "Tenant ID not found", nil)
|
|
return
|
|
}
|
|
|
|
tenantID, err := uuid.Parse(tenantIDStr)
|
|
if err != nil {
|
|
httperror.SendError(w, http.StatusBadRequest, "Invalid tenant ID", err)
|
|
return
|
|
}
|
|
|
|
// Parse request
|
|
var req user.CreateUserRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httperror.SendError(w, http.StatusBadRequest, "Invalid request body", err)
|
|
return
|
|
}
|
|
|
|
// Validate input
|
|
if req.Email == "" || req.Password == "" || req.Name == "" {
|
|
httperror.SendError(w, http.StatusBadRequest, "Missing required fields", nil)
|
|
return
|
|
}
|
|
|
|
// Create user
|
|
response, err := h.createService.Create(r.Context(), &usersvc.CreateUserInput{
|
|
TenantID: tenantID,
|
|
Email: req.Email,
|
|
Name: req.Name,
|
|
Role: req.Role,
|
|
Password: req.Password,
|
|
})
|
|
if err != nil {
|
|
h.logger.Error("User creation failed", zap.Error(err))
|
|
httperror.SendError(w, http.StatusInternalServerError, "Failed to create user", err)
|
|
return
|
|
}
|
|
|
|
// Send response
|
|
httpresponse.SendJSON(w, http.StatusCreated, response)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Database Setup (Cassandra)
|
|
|
|
### 10.1 Cassandra Schema Design
|
|
|
|
**Design Principles:**
|
|
1. **Query-driven modeling** - Design tables based on query patterns
|
|
2. **Denormalization** - Duplicate data to avoid joins
|
|
3. **Partition keys** - Choose keys that distribute data evenly
|
|
4. **Clustering keys** - Define sort order within partitions
|
|
|
|
**Example Migration:**
|
|
|
|
```cql
|
|
-- migrations/000001_create_users.up.cql
|
|
|
|
-- Users table (by ID)
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id uuid,
|
|
tenant_id uuid,
|
|
email text,
|
|
name text,
|
|
role text,
|
|
password text,
|
|
status text,
|
|
created_at timestamp,
|
|
updated_at timestamp,
|
|
PRIMARY KEY (id)
|
|
);
|
|
|
|
-- Users by email (for login lookups)
|
|
CREATE TABLE IF NOT EXISTS users_by_email (
|
|
email text,
|
|
tenant_id uuid,
|
|
user_id uuid,
|
|
PRIMARY KEY (email)
|
|
);
|
|
|
|
-- Users by tenant (for listing users in a tenant)
|
|
CREATE TABLE IF NOT EXISTS users_by_tenant (
|
|
tenant_id uuid,
|
|
user_id uuid,
|
|
email text,
|
|
name text,
|
|
role text,
|
|
status text,
|
|
created_at timestamp,
|
|
PRIMARY KEY (tenant_id, created_at, user_id)
|
|
) WITH CLUSTERING ORDER BY (created_at DESC, user_id ASC);
|
|
|
|
-- Create indexes
|
|
CREATE INDEX IF NOT EXISTS users_status_idx ON users (status);
|
|
CREATE INDEX IF NOT EXISTS users_tenant_idx ON users (tenant_id);
|
|
```
|
|
|
|
```cql
|
|
-- migrations/000001_create_users.down.cql
|
|
|
|
DROP TABLE IF EXISTS users_by_tenant;
|
|
DROP TABLE IF EXISTS users_by_email;
|
|
DROP INDEX IF EXISTS users_tenant_idx;
|
|
DROP INDEX IF EXISTS users_status_idx;
|
|
DROP TABLE IF EXISTS users;
|
|
```
|
|
|
|
### 10.2 Migration Management
|
|
|
|
**pkg/storage/database/migrator.go:**
|
|
|
|
```go
|
|
package database
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/golang-migrate/migrate/v4"
|
|
_ "github.com/golang-migrate/migrate/v4/database/cassandra"
|
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
)
|
|
|
|
// Migrator handles database migrations
|
|
type Migrator struct {
|
|
migrate *migrate.Migrate
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewMigrator creates a new migrator
|
|
func NewMigrator(cfg *config.Config, logger *zap.Logger) *Migrator {
|
|
// Build Cassandra connection string
|
|
dbURL := fmt.Sprintf("cassandra://%s/%s?consistency=%s",
|
|
cfg.Database.Hosts[0],
|
|
cfg.Database.Keyspace,
|
|
cfg.Database.Consistency,
|
|
)
|
|
|
|
// Create migrate instance
|
|
m, err := migrate.New(cfg.Database.MigrationsPath, dbURL)
|
|
if err != nil {
|
|
logger.Fatal("Failed to create migrator", zap.Error(err))
|
|
}
|
|
|
|
return &Migrator{
|
|
migrate: m,
|
|
logger: logger.Named("migrator"),
|
|
}
|
|
}
|
|
|
|
// Up runs all pending migrations
|
|
func (m *Migrator) Up() error {
|
|
m.logger.Info("Running migrations...")
|
|
if err := m.migrate.Up(); err != nil && err != migrate.ErrNoChange {
|
|
return fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
|
|
version, _, _ := m.migrate.Version()
|
|
m.logger.Info("Migrations completed",
|
|
zap.Uint("version", uint(version)))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Down rolls back the last migration
|
|
func (m *Migrator) Down() error {
|
|
m.logger.Info("Rolling back last migration...")
|
|
if err := m.migrate.Steps(-1); err != nil {
|
|
return fmt.Errorf("rollback failed: %w", err)
|
|
}
|
|
|
|
version, _, _ := m.migrate.Version()
|
|
m.logger.Info("Rollback completed",
|
|
zap.Uint("version", uint(version)))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Version returns current migration version
|
|
func (m *Migrator) Version() (uint, bool, error) {
|
|
return m.migrate.Version()
|
|
}
|
|
|
|
// ForceVersion forces migration to specific version
|
|
func (m *Migrator) ForceVersion(version int) error {
|
|
m.logger.Warn("Forcing migration version",
|
|
zap.Int("version", version))
|
|
return m.migrate.Force(version)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Middleware Implementation
|
|
|
|
### 11.1 Security Headers Middleware
|
|
|
|
**internal/http/middleware/security_headers.go:**
|
|
|
|
```go
|
|
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
)
|
|
|
|
// SecurityHeadersMiddleware adds security headers to responses
|
|
type SecurityHeadersMiddleware struct {
|
|
config *config.Config
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideSecurityHeadersMiddleware provides the middleware for Wire
|
|
func ProvideSecurityHeadersMiddleware(
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
) *SecurityHeadersMiddleware {
|
|
return &SecurityHeadersMiddleware{
|
|
config: cfg,
|
|
logger: logger.Named("security-headers-middleware"),
|
|
}
|
|
}
|
|
|
|
// Handler applies security headers
|
|
func (m *SecurityHeadersMiddleware) Handler(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// CWE-693: Apply security headers
|
|
|
|
// CORS headers
|
|
origin := r.Header.Get("Origin")
|
|
if m.isAllowedOrigin(origin) {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
}
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Tenant-ID")
|
|
w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
|
|
|
|
// Handle preflight requests
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
// Security headers (CWE-693: Protection Mechanism Failure)
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// isAllowedOrigin checks if origin is in allowed list
|
|
func (m *SecurityHeadersMiddleware) isAllowedOrigin(origin string) bool {
|
|
if origin == "" {
|
|
return false
|
|
}
|
|
|
|
// In development, allow localhost
|
|
if m.config.App.Environment == "development" {
|
|
if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check against configured allowed origins
|
|
for _, allowed := range m.config.Security.AllowedOrigins {
|
|
if origin == allowed {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
```
|
|
|
|
### 11.2 Request Size Limit Middleware
|
|
|
|
**internal/http/middleware/request_size_limit.go:**
|
|
|
|
```go
|
|
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
)
|
|
|
|
// RequestSizeLimitMiddleware limits request body size
|
|
type RequestSizeLimitMiddleware struct {
|
|
config *config.Config
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideRequestSizeLimitMiddleware provides the middleware for Wire
|
|
func ProvideRequestSizeLimitMiddleware(
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
) *RequestSizeLimitMiddleware {
|
|
return &RequestSizeLimitMiddleware{
|
|
config: cfg,
|
|
logger: logger.Named("request-size-limit-middleware"),
|
|
}
|
|
}
|
|
|
|
// LimitSmall applies 1MB limit (for auth endpoints)
|
|
func (m *RequestSizeLimitMiddleware) LimitSmall() func(http.Handler) http.Handler {
|
|
return m.limitWithSize(1 * 1024 * 1024) // 1 MB
|
|
}
|
|
|
|
// LimitMedium applies 5MB limit (for typical API endpoints)
|
|
func (m *RequestSizeLimitMiddleware) LimitMedium() func(http.Handler) http.Handler {
|
|
return m.limitWithSize(5 * 1024 * 1024) // 5 MB
|
|
}
|
|
|
|
// LimitLarge applies 50MB limit (for file uploads)
|
|
func (m *RequestSizeLimitMiddleware) LimitLarge() func(http.Handler) http.Handler {
|
|
return m.limitWithSize(50 * 1024 * 1024) // 50 MB
|
|
}
|
|
|
|
// limitWithSize creates a middleware with specific size limit
|
|
func (m *RequestSizeLimitMiddleware) limitWithSize(maxBytes int64) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Limit request body size (CWE-770: Resource Exhaustion)
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. HTTP Server Setup
|
|
|
|
### internal/interface/http/server.go
|
|
|
|
```go
|
|
package http
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
httpmw "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/http/middleware"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/gateway"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/healthcheck"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/middleware"
|
|
)
|
|
|
|
// Server represents the HTTP server
|
|
type Server struct {
|
|
server *http.Server
|
|
logger *zap.Logger
|
|
jwtMiddleware *httpmw.JWTMiddleware
|
|
securityHeadersMiddleware *httpmw.SecurityHeadersMiddleware
|
|
requestSizeLimitMw *httpmw.RequestSizeLimitMiddleware
|
|
config *config.Config
|
|
healthHandler *healthcheck.Handler
|
|
loginHandler *gateway.LoginHandler
|
|
}
|
|
|
|
// ProvideServer creates a new HTTP server
|
|
func ProvideServer(
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
jwtMiddleware *httpmw.JWTMiddleware,
|
|
securityHeadersMiddleware *httpmw.SecurityHeadersMiddleware,
|
|
requestSizeLimitMw *httpmw.RequestSizeLimitMiddleware,
|
|
healthHandler *healthcheck.Handler,
|
|
loginHandler *gateway.LoginHandler,
|
|
) *Server {
|
|
mux := http.NewServeMux()
|
|
|
|
s := &Server{
|
|
logger: logger,
|
|
jwtMiddleware: jwtMiddleware,
|
|
securityHeadersMiddleware: securityHeadersMiddleware,
|
|
requestSizeLimitMw: requestSizeLimitMw,
|
|
config: cfg,
|
|
healthHandler: healthHandler,
|
|
loginHandler: loginHandler,
|
|
}
|
|
|
|
// Register routes
|
|
s.registerRoutes(mux)
|
|
|
|
// Create HTTP server
|
|
s.server = &http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
|
Handler: s.applyMiddleware(mux),
|
|
ReadTimeout: cfg.HTTP.ReadTimeout,
|
|
WriteTimeout: cfg.HTTP.WriteTimeout,
|
|
IdleTimeout: cfg.HTTP.IdleTimeout,
|
|
}
|
|
|
|
logger.Info("✓ HTTP server configured",
|
|
zap.String("address", s.server.Addr),
|
|
zap.Duration("read_timeout", cfg.HTTP.ReadTimeout),
|
|
zap.Duration("write_timeout", cfg.HTTP.WriteTimeout))
|
|
|
|
return s
|
|
}
|
|
|
|
// registerRoutes registers all HTTP routes
|
|
func (s *Server) registerRoutes(mux *http.ServeMux) {
|
|
// ===== PUBLIC ROUTES =====
|
|
mux.HandleFunc("GET /health", s.healthHandler.Handle)
|
|
|
|
// Authentication routes
|
|
mux.HandleFunc("POST /api/v1/login",
|
|
s.requestSizeLimitMw.LimitSmall()(
|
|
http.HandlerFunc(s.loginHandler.Handle),
|
|
).ServeHTTP)
|
|
|
|
// ===== AUTHENTICATED ROUTES =====
|
|
// Add your authenticated routes here with JWT middleware
|
|
// Example:
|
|
// mux.HandleFunc("GET /api/v1/me", s.applyAuthOnly(s.meHandler.Handle))
|
|
}
|
|
|
|
// applyMiddleware applies global middleware to all routes
|
|
func (s *Server) applyMiddleware(handler http.Handler) http.Handler {
|
|
// Apply middleware in order
|
|
handler = middleware.LoggerMiddleware(s.logger)(handler)
|
|
handler = s.securityHeadersMiddleware.Handler(handler)
|
|
return handler
|
|
}
|
|
|
|
// applyAuthOnly applies only JWT authentication middleware
|
|
func (s *Server) applyAuthOnly(handler http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
s.jwtMiddleware.Handler(
|
|
s.jwtMiddleware.RequireAuth(
|
|
http.HandlerFunc(handler),
|
|
),
|
|
).ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// Start starts the HTTP server
|
|
func (s *Server) Start() error {
|
|
s.logger.Info("")
|
|
s.logger.Info("🚀 Backend is ready!")
|
|
s.logger.Info("",
|
|
zap.String("address", s.server.Addr),
|
|
zap.String("url", fmt.Sprintf("http://localhost:%d", s.config.Server.Port)))
|
|
s.logger.Info("")
|
|
|
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return fmt.Errorf("failed to start server: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the HTTP server
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
s.logger.Info("shutting down HTTP server")
|
|
|
|
if err := s.server.Shutdown(ctx); err != nil {
|
|
return fmt.Errorf("failed to shutdown server: %w", err)
|
|
}
|
|
|
|
s.logger.Info("HTTP server shut down successfully")
|
|
return nil
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Docker & Infrastructure
|
|
|
|
### 13.1 Development Dockerfile
|
|
|
|
**dev.Dockerfile:**
|
|
|
|
```dockerfile
|
|
FROM golang:1.24-alpine
|
|
|
|
# Install development tools
|
|
RUN apk add --no-cache git curl bash
|
|
|
|
# Set working directory
|
|
WORKDIR /go/src/codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
|
|
|
|
# Install Wire
|
|
RUN go install github.com/google/wire/cmd/wire@latest
|
|
|
|
# Copy go.mod and go.sum
|
|
COPY go.mod go.sum ./
|
|
|
|
# Download dependencies
|
|
RUN go mod download
|
|
|
|
# Copy source code
|
|
COPY . .
|
|
|
|
# Generate Wire code
|
|
RUN cd app && wire
|
|
|
|
# Build the application
|
|
RUN go build -o app-dev .
|
|
|
|
# Expose port
|
|
EXPOSE 8000
|
|
|
|
# Run the application
|
|
CMD ["./app-dev", "daemon"]
|
|
```
|
|
|
|
### 13.2 Production Dockerfile
|
|
|
|
**Dockerfile:**
|
|
|
|
```dockerfile
|
|
###
|
|
### Build Stage
|
|
###
|
|
|
|
FROM golang:1.24-alpine AS build-env
|
|
|
|
# Create app directory
|
|
RUN mkdir /app
|
|
WORKDIR /app
|
|
|
|
# Copy dependency list
|
|
COPY go.mod go.sum ./
|
|
|
|
# Install dependencies
|
|
RUN go mod download
|
|
|
|
# Copy all files
|
|
COPY . .
|
|
|
|
# Install Wire
|
|
RUN go install github.com/google/wire/cmd/wire@latest
|
|
|
|
# Generate Wire code
|
|
RUN cd app && wire
|
|
|
|
# Build for Linux AMD64
|
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app-backend .
|
|
|
|
###
|
|
### Run Stage
|
|
###
|
|
|
|
FROM alpine:latest
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy executable
|
|
COPY --from=build-env /app/app-backend .
|
|
|
|
# Copy migrations
|
|
COPY --from=build-env /app/migrations ./migrations
|
|
|
|
# Copy static files (if any)
|
|
COPY --from=build-env /app/static ./static
|
|
|
|
EXPOSE 8000
|
|
|
|
# Run the server
|
|
CMD ["/app/app-backend", "daemon"]
|
|
```
|
|
|
|
### 13.3 Docker Compose (Development)
|
|
|
|
**docker-compose.dev.yml:**
|
|
|
|
```yaml
|
|
# Use external network from shared infrastructure
|
|
networks:
|
|
maple-dev:
|
|
external: true
|
|
|
|
services:
|
|
app:
|
|
container_name: your-backend-dev
|
|
stdin_open: true
|
|
build:
|
|
context: .
|
|
dockerfile: ./dev.Dockerfile
|
|
ports:
|
|
- "${SERVER_PORT:-8000}:${SERVER_PORT:-8000}"
|
|
env_file:
|
|
- .env
|
|
environment:
|
|
# Application
|
|
APP_ENVIRONMENT: ${APP_ENVIRONMENT:-development}
|
|
APP_VERSION: ${APP_VERSION:-0.1.0-dev}
|
|
APP_JWT_SECRET: ${APP_JWT_SECRET:-dev-secret}
|
|
|
|
# Server
|
|
SERVER_HOST: ${SERVER_HOST:-0.0.0.0}
|
|
SERVER_PORT: ${SERVER_PORT:-8000}
|
|
|
|
# Cassandra (connect to shared infrastructure)
|
|
DATABASE_HOSTS: ${DATABASE_HOSTS:-cassandra-1:9042,cassandra-2:9042,cassandra-3:9042}
|
|
DATABASE_KEYSPACE: ${DATABASE_KEYSPACE:-your_keyspace}
|
|
DATABASE_CONSISTENCY: ${DATABASE_CONSISTENCY:-ONE}
|
|
DATABASE_REPLICATION: ${DATABASE_REPLICATION:-3}
|
|
DATABASE_MIGRATIONS_PATH: ${DATABASE_MIGRATIONS_PATH:-file://migrations}
|
|
|
|
# Redis (connect to shared infrastructure)
|
|
CACHE_HOST: ${CACHE_HOST:-redis}
|
|
CACHE_PORT: ${CACHE_PORT:-6379}
|
|
CACHE_PASSWORD: ${CACHE_PASSWORD:-}
|
|
CACHE_DB: ${CACHE_DB:-0}
|
|
|
|
# Logger
|
|
LOGGER_LEVEL: ${LOGGER_LEVEL:-debug}
|
|
LOGGER_FORMAT: ${LOGGER_FORMAT:-console}
|
|
|
|
volumes:
|
|
- ./:/go/src/codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
|
|
networks:
|
|
- maple-dev
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:${SERVER_PORT:-8000}/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
```
|
|
|
|
### 13.4 Task Runner (Taskfile.yml)
|
|
|
|
**Taskfile.yml:**
|
|
|
|
```yaml
|
|
version: "3"
|
|
|
|
env:
|
|
COMPOSE_PROJECT_NAME: your-backend
|
|
|
|
vars:
|
|
DOCKER_COMPOSE_CMD:
|
|
sh: |
|
|
if command -v docker-compose >/dev/null 2>&1; then
|
|
echo "docker-compose"
|
|
elif docker compose version >/dev/null 2>&1; then
|
|
echo "docker compose"
|
|
else
|
|
echo "docker-compose"
|
|
fi
|
|
|
|
tasks:
|
|
# Development workflow
|
|
dev:
|
|
desc: Start app in development mode
|
|
cmds:
|
|
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml up --build"
|
|
|
|
dev:down:
|
|
desc: Stop development app
|
|
cmds:
|
|
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml down"
|
|
|
|
dev:restart:
|
|
desc: Quick restart
|
|
cmds:
|
|
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml restart"
|
|
|
|
dev:logs:
|
|
desc: View app logs
|
|
cmds:
|
|
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml logs -f"
|
|
|
|
dev:shell:
|
|
desc: Open shell in running container
|
|
cmds:
|
|
- docker exec -it your-backend-dev sh
|
|
|
|
# Database operations
|
|
migrate:up:
|
|
desc: Run all migrations up
|
|
cmds:
|
|
- ./app-backend migrate up
|
|
|
|
migrate:down:
|
|
desc: Run all migrations down
|
|
cmds:
|
|
- ./app-backend migrate down
|
|
|
|
# Build and test
|
|
build:
|
|
desc: Build the Go binary
|
|
cmds:
|
|
- task: wire
|
|
- go build -o app-backend
|
|
|
|
wire:
|
|
desc: Generate Wire dependency injection
|
|
cmds:
|
|
- cd app && wire
|
|
|
|
test:
|
|
desc: Run tests
|
|
cmds:
|
|
- go test ./... -v
|
|
|
|
lint:
|
|
desc: Run linters
|
|
cmds:
|
|
- go vet ./...
|
|
|
|
format:
|
|
desc: Format code
|
|
cmds:
|
|
- go fmt ./...
|
|
|
|
tidy:
|
|
desc: Tidy Go modules
|
|
cmds:
|
|
- go mod tidy
|
|
|
|
# Production deployment
|
|
deploy:
|
|
desc: Build and push production container
|
|
cmds:
|
|
- docker build -f Dockerfile --rm -t registry.example.com/your-org/your_backend:prod --platform linux/amd64 .
|
|
- docker push registry.example.com/your-org/your_backend:prod
|
|
```
|
|
|
|
---
|
|
|
|
## 14. CLI Commands (Cobra)
|
|
|
|
### 14.1 Root Command
|
|
|
|
**cmd/root.go:**
|
|
|
|
```go
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/daemon"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/migrate"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/version"
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "your-backend",
|
|
Short: "Your Backend Service",
|
|
Long: `Your Backend - Clean Architecture with Wire DI and Cassandra`,
|
|
}
|
|
|
|
// Execute runs the root command
|
|
func Execute() {
|
|
rootCmd.AddCommand(daemon.DaemonCmd())
|
|
rootCmd.AddCommand(migrate.MigrateCmd())
|
|
rootCmd.AddCommand(version.VersionCmd())
|
|
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 14.2 Daemon Command
|
|
|
|
**cmd/daemon/daemon.go:**
|
|
|
|
```go
|
|
package daemon
|
|
|
|
import (
|
|
"log"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/app"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
)
|
|
|
|
// DaemonCmd returns the daemon command
|
|
func DaemonCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "daemon",
|
|
Short: "Start the HTTP server",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Load configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Initialize application with Wire
|
|
application, err := app.InitializeApplication(cfg)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize application: %v", err)
|
|
}
|
|
|
|
// Start application
|
|
if err := application.Start(); err != nil {
|
|
log.Fatalf("Application failed: %v", err)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
### 14.3 Main Entry Point
|
|
|
|
**main.go:**
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd"
|
|
)
|
|
|
|
func main() {
|
|
cmd.Execute()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 15. Development Workflow
|
|
|
|
### Daily Development Flow
|
|
|
|
```bash
|
|
# 1. Start shared infrastructure (first time)
|
|
cd cloud/infrastructure/development
|
|
task dev:start
|
|
|
|
# 2. Start your backend
|
|
cd cloud/your-backend-name
|
|
task dev
|
|
|
|
# 3. Run migrations (first time)
|
|
task migrate:up
|
|
|
|
# 4. Make code changes...
|
|
|
|
# 5. Quick restart (after code changes)
|
|
task dev:restart
|
|
|
|
# 6. View logs
|
|
task dev:logs
|
|
|
|
# 7. Run tests
|
|
task test
|
|
|
|
# 8. Format and lint
|
|
task format
|
|
task lint
|
|
|
|
# 9. Stop backend
|
|
task dev:down
|
|
```
|
|
|
|
### Common Development Tasks
|
|
|
|
```bash
|
|
# Generate Wire dependencies
|
|
task wire
|
|
|
|
# Build binary locally
|
|
task build
|
|
|
|
# Run locally (without Docker)
|
|
./app-backend daemon
|
|
|
|
# Create new migration
|
|
./app-backend migrate create create_new_table
|
|
|
|
# Check migration version
|
|
./app-backend migrate version
|
|
|
|
# Reset database
|
|
task db:reset
|
|
|
|
# Open shell in container
|
|
task dev:shell
|
|
|
|
# Check application version
|
|
./app-backend version
|
|
```
|
|
|
|
---
|
|
|
|
## 16. Testing Strategy
|
|
|
|
### 16.1 Unit Testing
|
|
|
|
**Example: Use Case Test**
|
|
|
|
```go
|
|
// internal/usecase/user/create_user_entity_test.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestCreateUserEntityUseCase_Execute(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
uc := ProvideCreateUserEntityUseCase(logger)
|
|
|
|
input := &CreateUserEntityInput{
|
|
TenantID: uuid.New(),
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Role: "user",
|
|
HashedPassword: "hashed-password",
|
|
}
|
|
|
|
entity, err := uc.Execute(context.Background(), input)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if entity.Email != input.Email {
|
|
t.Errorf("Expected email %s, got %s", input.Email, entity.Email)
|
|
}
|
|
|
|
if entity.ID == uuid.Nil {
|
|
t.Error("Expected non-nil ID")
|
|
}
|
|
}
|
|
```
|
|
|
|
### 16.2 Integration Testing
|
|
|
|
Use test containers for integration tests:
|
|
|
|
```go
|
|
// internal/repository/user/integration_test.go
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/gocql/gocql"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
)
|
|
|
|
func TestUserRepository_Integration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
// Start Cassandra container
|
|
ctx := context.Background()
|
|
cassandraContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: testcontainers.ContainerRequest{
|
|
Image: "cassandra:4.1",
|
|
ExposedPorts: []string{"9042/tcp"},
|
|
},
|
|
Started: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to start container: %v", err)
|
|
}
|
|
defer cassandraContainer.Terminate(ctx)
|
|
|
|
// Run tests...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 17. Production Deployment
|
|
|
|
### 17.1 Build Production Container
|
|
|
|
```bash
|
|
# Build for Linux AMD64
|
|
task deploy
|
|
```
|
|
|
|
### 17.2 Environment Variables (Production)
|
|
|
|
```bash
|
|
# .env (production)
|
|
APP_ENVIRONMENT=production
|
|
APP_VERSION=1.0.0
|
|
APP_JWT_SECRET=<use-openssl-rand-base64-64>
|
|
|
|
SERVER_HOST=0.0.0.0
|
|
SERVER_PORT=8000
|
|
|
|
DATABASE_HOSTS=cassandra-prod-1:9042,cassandra-prod-2:9042,cassandra-prod-3:9042
|
|
DATABASE_KEYSPACE=your_keyspace_prod
|
|
DATABASE_CONSISTENCY=QUORUM
|
|
DATABASE_REPLICATION=3
|
|
|
|
CACHE_HOST=redis-prod
|
|
CACHE_PORT=6379
|
|
CACHE_PASSWORD=<strong-password>
|
|
CACHE_DB=0
|
|
|
|
SECURITY_IP_ENCRYPTION_KEY=<use-openssl-rand-hex-16>
|
|
SECURITY_CORS_ALLOWED_ORIGINS=https://yourdomain.com
|
|
|
|
LOGGER_LEVEL=info
|
|
LOGGER_FORMAT=json
|
|
```
|
|
|
|
### 17.3 Health Checks
|
|
|
|
**internal/interface/http/handler/healthcheck/healthcheck_handler.go:**
|
|
|
|
```go
|
|
package healthcheck
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
|
|
)
|
|
|
|
// Handler handles health check requests
|
|
type Handler struct {
|
|
config *config.Config
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// ProvideHealthCheckHandler provides the handler for Wire
|
|
func ProvideHealthCheckHandler(
|
|
cfg *config.Config,
|
|
logger *zap.Logger,
|
|
) *Handler {
|
|
return &Handler{
|
|
config: cfg,
|
|
logger: logger.Named("healthcheck-handler"),
|
|
}
|
|
}
|
|
|
|
// Handle responds to health check requests
|
|
func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
|
|
response := map[string]interface{}{
|
|
"status": "ok",
|
|
"timestamp": time.Now().UTC(),
|
|
"environment": h.config.App.Environment,
|
|
"version": h.config.App.Version,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
You now have a complete blueprint for building Golang backends with:
|
|
|
|
✅ **Clean Architecture** - Proper layer separation and dependency flow
|
|
✅ **Wire Dependency Injection** - Compile-time, type-safe DI
|
|
✅ **JWT Authentication** - Secure session management with two-tier caching
|
|
✅ **Cassandra Database** - Query-driven schema design with migrations
|
|
✅ **Reusable pkg/ Components** - Copy-paste infrastructure utilities
|
|
✅ **Security-First** - CWE-compliant middleware and validation
|
|
✅ **Docker Infrastructure** - Development and production containers
|
|
✅ **CLI Commands** - Cobra-based command structure
|
|
|
|
### Next Steps
|
|
|
|
1. **Copy this document** to your new project's `docs/` directory
|
|
2. **Run the copy-pkg.sh script** to copy reusable components
|
|
3. **Update import paths** throughout the codebase
|
|
4. **Customize** domain entities, use cases, and services for your specific needs
|
|
5. **Add business logic** while maintaining the architectural patterns
|
|
|
|
### Reference Implementation
|
|
|
|
Always refer back to `cloud/maplepress-backend` for:
|
|
- Complete working examples
|
|
- Advanced patterns (SAGA, rate limiting, etc.)
|
|
- Production-tested code
|
|
- Security best practices
|
|
|
|
---
|
|
|
|
**Questions or Issues?**
|
|
Review the maplepress-backend codebase or create a new issue in the repository.
|