Browse Source

INITIAL: Initial project GoApi with HMAC

gdias 3 months ago
commit
34832cc581

+ 4 - 0
.env.example

@@ -0,0 +1,4 @@
+SERVER_PORT=xxxx
+DB_DRIVER=xxxxx
+DB_PATH=./db/xxxx.db
+HMAC_SECRET=xxxxxx

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.env
+*.db
+*.db-*

+ 62 - 0
README.md

@@ -0,0 +1,62 @@
+# GoApi
+
+Este projeto é uma API simples para aprendizado em GoLang seguindo a estrutura utilizada por padrão em empresas que utilizam Go. 
+
+## 🚀 Como rodar o projeto
+
+### Instale as dependências e atualize o go.mod
+
+```bash
+ go get github.com/joho/godotenv
+ go get github.com/mattn/go-sqlite3
+ go mod tidy
+```
+
+### Inicie o servidor de desenvolvimento
+
+```bash
+go run ./cmd/GoApi/main.go
+```
+
+A aplicação estará disponível em `http://localhost:8080` (ou similar, conforme o terminal indicar).
+
+## 📝 HMAC
+
+Para funcionamento desse middleware, é necessário adicionar as seguintes headers na requisição:
+
+- X-Timestamp: unix seconds
+- X-Signature: assinatura em hex
+
+Alem de adicionar um pre-request script no Postman para gerar a assinatura.
+
+```// Lê dados do ambiente
+const secret = pm.environment.get("hmac_secret");
+if (!secret) {
+  throw new Error("Env var 'hmac_secret' não definida no Postman.");
+}
+
+// Método e path exatos
+const method = pm.request.method; // GET/POST
+// Garante que usamos somente o path (sem query)
+const url = new URL(pm.request.url.toString());
+const path = url.pathname;
+
+// Timestamp em segundos
+const ts = Math.floor(Date.now() / 1000).toString();
+
+// Corpo bruto (para GET deve ser string vazia)
+let body = "";
+if (method !== "GET" && pm.request.body && pm.request.body.raw) {
+  // Use exatamente o que vai no corpo da requisição
+  body = pm.request.body.raw;
+}
+
+// Monta a mensagem canônica
+const msg = method + "\n" + path + "\n" + ts + "\n" + body;
+
+// Calcula HMAC-SHA256 em hex usando CryptoJS (nativo do Postman)
+const signature = CryptoJS.HmacSHA256(msg, secret).toString(CryptoJS.enc.Hex);
+
+// Define headers
+pm.request.headers.upsert({ key: "X-Timestamp", value: ts });
+pm.request.headers.upsert({ key: "X-Signature", value: signature });```

+ 38 - 0
cmd/GoApi/main.go

@@ -0,0 +1,38 @@
+package main
+
+import (
+    "database/sql"
+	_ "github.com/mattn/go-sqlite3"
+
+    "log"
+    "net/http"
+
+    "GoApi/internal/api"
+    "GoApi/internal/config"
+    "GoApi/internal/middleware"
+    "GoApi/internal/repository"
+)
+
+func main() {
+    cfg := config.Load()
+
+    db, err := sql.Open(cfg.DBDriver, cfg.DBPath)
+    if err != nil {
+        log.Fatal("ERROR: Not connected on the database", err)
+    }
+    defer db.Close()
+
+    eventRepo := repository.NewEventRepository(db)
+    eventController := api.NewEventController(eventRepo)
+    createEventController := api.NewCreateEventController(eventRepo)
+
+    mux := http.NewServeMux()
+    mux.HandleFunc("/events", eventController.GetEventsHandler)
+    mux.HandleFunc("/events/create", createEventController.CreateEventHandler)
+
+    // Wrap com middleware HMAC
+    hmacProtected := middleware.HMACAuth(cfg.HMACSecret)(mux)
+
+    log.Println("INFO: Server running on port:", cfg.ServerPort)
+    log.Fatal(http.ListenAndServe(":"+cfg.ServerPort, hmacProtected))
+}

+ 39 - 0
db/database.sql

@@ -0,0 +1,39 @@
+BEGIN TRANSACTION;
+CREATE TABLE user (
+	user_id INTEGER PRIMARY KEY AUTOINCREMENT,
+	user_name TEXT NOT NULL,
+	user_email TEXT NOT NULL UNIQUE,
+    user_password TEXT NOT NULL,
+    user_flag TEXT NOT NULL DEFAULT 'default'
+);
+
+
+CREATE TABLE collaborator (
+    collaborator_id INTEGER PRIMARY KEY AUTOINCREMENT,
+    collaborator_name TEXT NOT NULL
+);
+
+CREATE TABLE events (
+    event_id INTEGER PRIMARY KEY AUTOINCREMENT,
+    event_name TEXT NOT NULL,
+    event_date TEXT NOT NULL,
+    event_passage_value TEXT NOT NULL,
+    event_hotel_value TEXT NOT NULL,
+    event_gift_value TEXT NOT NULL,
+    event_team_value TEXT NOT NULL,
+    event_sponsor_value TEXT NOT NULL,
+    event_total_value TEXT NOT NULL
+);
+
+CREATE TABLE event_collaborators (
+    event_id INTEGER NOT NULL,
+    collaborator_id INTEGER NOT NULL,
+    PRIMARY KEY (event_id, collaborator_id),
+    FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
+    FOREIGN KEY (collaborator_id) REFERENCES collaborator(collaborator_id) ON DELETE CASCADE
+);
+COMMIT;
+
+
+
+

+ 31 - 0
db/migrations/insert_default.sql

@@ -0,0 +1,31 @@
+BEGIN TRANSACTION;
+-- Insert Users
+INSERT INTO user (user_name, user_email, user_password, user_flag)
+VALUES 
+('Gabriel', 'gabriel@smartpay.com.vc', 'senha123', 'default'),
+('Angelica', 'angelica@smartpay.com.vc', 'senha123', 'default');
+
+-- Insert collaborator
+INSERT INTO collaborator (collaborator_name)
+VALUES
+('Rocelo'),
+('Angelica'),
+('Marcelo');
+
+-- Insert events
+INSERT INTO events (
+    event_name, event_date, event_passage_value, event_hotel_value, 
+    event_gift_value, event_team_value, event_sponsor_value, event_total_value
+)
+VALUES
+('Prod Tech', '2025-12-01', '100', '200', '50', '150', '300', '800'),
+('Summer Tech', '2025-11-15', '150', '250', '70', '200', '500', '1170');
+
+INSERT INTO event_collaborators (event_id, collaborator_id)
+VALUES
+(1, 1),
+(1, 2),
+(2, 2),
+(2, 3);
+
+COMMIT;

+ 8 - 0
go.mod

@@ -0,0 +1,8 @@
+module GoApi
+
+go 1.25.0
+
+require (
+	github.com/joho/godotenv v1.5.1
+	github.com/mattn/go-sqlite3 v1.14.32
+)

+ 4 - 0
go.sum

@@ -0,0 +1,4 @@
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

+ 77 - 0
internal/api/createEvent.go

@@ -0,0 +1,77 @@
+package api
+
+import (
+    "encoding/json"
+    "net/http"
+
+    "GoApi/internal/repository"
+)
+
+// Novo controller dedicado para criação de eventos
+type CreateEventController struct {
+    Repo repository.EventRepository
+}
+
+func NewCreateEventController(repo repository.EventRepository) CreateEventController {
+    return CreateEventController{Repo: repo}
+}
+
+type CreateEventRequest struct {
+    Name           string  `json:"name"`
+    Date           string  `json:"date"`
+    PassageValue   string  `json:"passage_value"`
+    HotelValue     string  `json:"hotel_value"`
+    GiftValue      string  `json:"gift_value"`
+    TeamValue      string  `json:"team_value"`
+    SponsorValue   string  `json:"sponsor_value"`
+    TotalValue     string  `json:"total_value"`
+    CollaboratorIDs []int64 `json:"collaborator_ids"`
+}
+
+func (c *CreateEventController) CreateEventHandler(w http.ResponseWriter, r *http.Request) {
+    if r.Method != http.MethodPost {
+        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+        return
+    }
+
+    // Decodifica o JSON do corpo da requisição
+    var req CreateEventRequest
+    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+        http.Error(w, "Invalid request body", http.StatusBadRequest)
+        return
+    }
+
+    // Valida os campos obrigatórios
+    if req.Name == "" || req.Date == "" || req.TotalValue == "" {
+        http.Error(w, "Name, date and total value are required fields", http.StatusBadRequest)
+        return
+    }
+
+    // Cria o objeto de evento
+    event := &repository.Event{
+        Name:           req.Name,
+        Date:           req.Date,
+        PassageValue:   req.PassageValue,
+        HotelValue:     req.HotelValue,
+        GiftValue:      req.GiftValue,
+        TeamValue:      req.TeamValue,
+        SponsorValue:   req.SponsorValue,
+        TotalValue:     req.TotalValue,
+        CollaboratorIDs: req.CollaboratorIDs,
+    }
+
+    // Chama o repositório para criar o evento
+    eventID, err := c.Repo.CreateEvent(event)
+    if err != nil {
+        http.Error(w, "Failed to create event: "+err.Error(), http.StatusInternalServerError)
+        return
+    }
+
+    // Retorna o ID do evento criado
+    w.Header().Set("Content-Type", "application/json")
+    w.WriteHeader(http.StatusCreated)
+    json.NewEncoder(w).Encode(map[string]interface{}{
+        "id":      eventID,
+        "message": "Event created successfully",
+    })
+}

+ 27 - 0
internal/api/getEvents.go

@@ -0,0 +1,27 @@
+package api
+
+import (
+    "encoding/json"
+    "net/http"
+
+    "GoApi/internal/repository"
+)
+
+type EventController struct {
+    Repo repository.EventRepository
+}
+
+func NewEventController(repo repository.EventRepository) EventController {
+    return EventController{Repo: repo}
+}
+
+func (c EventController) GetEventsHandler(w http.ResponseWriter, r *http.Request) {
+    events, err := c.Repo.GetEvents()
+    if err != nil {
+        http.Error(w, "ERROR: Not found events", http.StatusInternalServerError)
+        return
+    }
+
+    w.Header().Set("Content-Type", "application/json")
+    json.NewEncoder(w).Encode(events)
+}

+ 42 - 0
internal/config/config.go

@@ -0,0 +1,42 @@
+package config
+
+import (
+	"log"
+	"os"
+	"github.com/joho/godotenv"
+)
+
+type Config struct {
+	ServerPort string
+	DBDriver   string
+	DBPath     string
+	HMACSecret string
+}
+
+func Load() Config {
+	if err := godotenv.Load(); err != nil {
+		log.Println("ERRO: .env not found.")
+	}
+
+	cfg := Config{
+		ServerPort: os.Getenv("SERVER_PORT"),
+		DBDriver:   os.Getenv("DB_DRIVER"),
+		DBPath:     os.Getenv("DB_PATH"),
+		HMACSecret: os.Getenv("HMAC_SECRET"),
+	}
+
+	if cfg.ServerPort == "" {
+		log.Fatal("ERRO: SERVER_PORT not found.")
+	}
+	if cfg.DBDriver == "" {
+		log.Fatal("ERRO: DB_DRIVER not found.")
+	}
+	if cfg.DBPath == "" {
+		log.Fatal("ERRO: DB_PATH not found.")
+	}
+	if cfg.HMACSecret == "" {
+		log.Fatal("ERRO: HMAC_SECRET not found.")
+	}
+
+	return cfg
+}

+ 63 - 0
internal/middleware/hmac.go

@@ -0,0 +1,63 @@
+package middleware
+
+import (
+    "bytes"
+    "crypto/hmac"
+    "crypto/sha256"
+    "encoding/hex"
+    "io"
+    "net/http"
+    "strconv"
+    "time"
+)
+
+func HMACAuth(secret string) func(http.Handler) http.Handler {
+    return func(next http.Handler) http.Handler {
+        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+            ts := r.Header.Get("X-Timestamp")
+            sig := r.Header.Get("X-Signature")
+            if ts == "" || sig == "" {
+                http.Error(w, "Missing HMAC headers", http.StatusUnauthorized)
+                return
+            }
+
+            unix, err := strconv.ParseInt(ts, 10, 64)
+            if err != nil {
+                http.Error(w, "Invalid timestamp", http.StatusUnauthorized)
+                return
+            }
+            now := time.Now().Unix()
+            const skew = int64(300) 
+            if unix < now-skew || unix > now+skew {
+                http.Error(w, "Timestamp out of allowed range", http.StatusUnauthorized)
+                return
+            }
+
+            var bodyBytes []byte
+            if r.Body != nil {
+                bodyBytes, _ = io.ReadAll(r.Body)
+            }
+            r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
+
+            msg := r.Method + "\n" + r.URL.Path + "\n" + ts + "\n" + string(bodyBytes)
+
+            mac := hmac.New(sha256.New, []byte(secret))
+            mac.Write([]byte(msg))
+            expected := mac.Sum(nil)
+            expectedHex := hex.EncodeToString(expected)
+
+            provided, err := hex.DecodeString(sig)
+            if err != nil {
+                http.Error(w, "Invalid signature encoding", http.StatusUnauthorized)
+                return
+            }
+
+            if !hmac.Equal(expected, provided) && sig != expectedHex {
+                http.Error(w, "Invalid signature", http.StatusUnauthorized)
+                return
+            }
+
+            next.ServeHTTP(w, r)
+        })
+    }
+}

+ 171 - 0
internal/repository/eventsModel.go

@@ -0,0 +1,171 @@
+package repository
+
+import (
+    "database/sql"
+    "fmt"
+)
+
+type Collaborator struct {
+    ID   int64  `json:"id"`
+    Name string `json:"name"`
+}
+
+type Event struct {
+    ID              int
+    Name            string
+    Date            string
+    PassageValue    string
+    HotelValue      string
+    GiftValue       string
+    TeamValue       string
+    SponsorValue    string
+    TotalValue      string
+    CollaboratorIDs []int64 `json:"-"`
+    Collaborators   []Collaborator
+}
+
+type EventRepository struct {
+    DB *sql.DB
+}
+
+func NewEventRepository(db *sql.DB) EventRepository {
+    return EventRepository{DB: db}
+}
+
+func (r EventRepository) GetEvents() ([]Event, error) {
+    rows, err := r.DB.Query(`
+        SELECT 
+            e.event_id,
+            e.event_name,
+            e.event_date,
+            e.event_passage_value,
+            e.event_hotel_value,
+            e.event_gift_value,
+            e.event_team_value,
+            e.event_sponsor_value,
+            e.event_total_value,
+            c.collaborator_id,
+            c.collaborator_name
+        FROM events e
+        NATURAL JOIN event_collaborators
+        NATURAL JOIN collaborator c
+    `)
+    if err != nil {
+        return nil, err
+    }
+    defer rows.Close()
+
+    eventsMap := make(map[int]Event)
+
+    for rows.Next() {
+        var id int
+        var name, date, passage, hotel, gift, team, sponsor, total string
+        var collID int64
+        var collName string
+
+        if err := rows.Scan(&id, &name, &date, &passage, &hotel, &gift, &team, &sponsor, &total, &collID, &collName); err != nil {
+            return nil, err
+        }
+
+        e, exists := eventsMap[id]
+        if !exists {
+            e = Event{
+                ID:              id,
+                Name:            name,
+                Date:            date,
+                PassageValue:    passage,
+                HotelValue:      hotel,
+                GiftValue:       gift,
+                TeamValue:       team,
+                SponsorValue:    sponsor,
+                TotalValue:      total,
+                CollaboratorIDs: []int64{},
+                Collaborators:   []Collaborator{},
+            }
+        }
+
+        e.Collaborators = append(e.Collaborators, Collaborator{ID: collID, Name: collName})
+        eventsMap[id] = e
+    }
+
+    events := make([]Event, 0, len(eventsMap))
+    for _, e := range eventsMap {
+        events = append(events, e)
+    }
+
+    return events, rows.Err()
+}
+
+func (r EventRepository) CreateEvent(event *Event) (int64, error) {
+    tx, err := r.DB.Begin()
+    if err != nil {
+        return 0, fmt.Errorf("failed to begin transaction: %w", err)
+    }
+
+    var eventID int64
+    err = tx.QueryRow(`
+        INSERT INTO events (
+            event_name, 
+            event_date, 
+            event_passage_value, 
+            event_hotel_value, 
+            event_gift_value, 
+            event_team_value, 
+            event_sponsor_value, 
+            event_total_value
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+        RETURNING event_id
+    `, 
+        event.Name,
+        event.Date,
+        event.PassageValue,
+        event.HotelValue,
+        event.GiftValue,
+        event.TeamValue,
+        event.SponsorValue,
+        event.TotalValue,
+    ).Scan(&eventID)
+
+    if err != nil {
+        tx.Rollback()
+        return 0, fmt.Errorf("failed to insert event: %w", err)
+    }
+
+    for _, collabID := range event.CollaboratorIDs {
+        if collabID <= 0 {
+            continue
+        }
+
+        var exists bool
+        err = tx.QueryRow(
+            "SELECT EXISTS(SELECT 1 FROM collaborator WHERE collaborator_id = ?)",
+            collabID,
+        ).Scan(&exists)
+
+        if err != nil {
+            tx.Rollback()
+            return 0, fmt.Errorf("failed to verify collaborator: %w", err)
+        }
+
+        if !exists {
+            tx.Rollback()
+            return 0, fmt.Errorf("collaborator with ID %d does not exist", collabID)
+        }
+
+        _, err = tx.Exec(
+            "INSERT INTO event_collaborators (event_id, collaborator_id) VALUES (?, ?)",
+            eventID,
+            collabID,
+        )
+        if err != nil {
+            tx.Rollback()
+            return 0, fmt.Errorf("failed to associate collaborator %d with event: %w", collabID, err)
+        }
+    }
+
+    if err := tx.Commit(); err != nil {
+        return 0, fmt.Errorf("failed to commit transaction: %w", err)
+    }
+
+    return eventID, nil
+}