Prepara versao dev 1.0
This commit is contained in:
53
backend/internal/api/middleware/auth.go
Normal file
53
backend/internal/api/middleware/auth.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"aggios-app/backend/internal/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const UserIDKey contextKey = "userID"
|
||||
|
||||
// Auth validates JWT tokens
|
||||
func Auth(cfg *config.Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
bearerToken := strings.Split(authHeader, " ")
|
||||
if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
|
||||
http.Error(w, "Invalid token format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userID := claims["user_id"].(string)
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
34
backend/internal/api/middleware/cors.go
Normal file
34
backend/internal/api/middleware/cors.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"aggios-app/backend/internal/config"
|
||||
)
|
||||
|
||||
// CORS adds CORS headers to responses
|
||||
func CORS(cfg *config.Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Allow all localhost origins for development
|
||||
if 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, Host")
|
||||
w.Header().Set("Access-Control-Max-Age", "3600")
|
||||
|
||||
// Handle preflight request
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
96
backend/internal/api/middleware/ratelimit.go
Normal file
96
backend/internal/api/middleware/ratelimit.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"aggios-app/backend/internal/config"
|
||||
)
|
||||
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
attempts map[string][]time.Time
|
||||
maxAttempts int
|
||||
}
|
||||
|
||||
func newRateLimiter(maxAttempts int) *rateLimiter {
|
||||
rl := &rateLimiter{
|
||||
attempts: make(map[string][]time.Time),
|
||||
maxAttempts: maxAttempts,
|
||||
}
|
||||
|
||||
// Clean old entries every minute
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
rl.cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *rateLimiter) cleanup() {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for ip, attempts := range rl.attempts {
|
||||
var valid []time.Time
|
||||
for _, t := range attempts {
|
||||
if now.Sub(t) < time.Minute {
|
||||
valid = append(valid, t)
|
||||
}
|
||||
}
|
||||
if len(valid) == 0 {
|
||||
delete(rl.attempts, ip)
|
||||
} else {
|
||||
rl.attempts[ip] = valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimiter) isAllowed(ip string) bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
attempts := rl.attempts[ip]
|
||||
|
||||
// Filter attempts within the last minute
|
||||
var validAttempts []time.Time
|
||||
for _, t := range attempts {
|
||||
if now.Sub(t) < time.Minute {
|
||||
validAttempts = append(validAttempts, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validAttempts) >= rl.maxAttempts {
|
||||
return false
|
||||
}
|
||||
|
||||
validAttempts = append(validAttempts, now)
|
||||
rl.attempts[ip] = validAttempts
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RateLimit limits requests per IP address
|
||||
func RateLimit(cfg *config.Config) func(http.Handler) http.Handler {
|
||||
limiter := newRateLimiter(cfg.Security.MaxAttemptsPerMin)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := r.RemoteAddr
|
||||
|
||||
if !limiter.isAllowed(ip) {
|
||||
http.Error(w, "Too many requests", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
17
backend/internal/api/middleware/security.go
Normal file
17
backend/internal/api/middleware/security.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SecurityHeaders adds security headers to responses
|
||||
func SecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
56
backend/internal/api/middleware/tenant.go
Normal file
56
backend/internal/api/middleware/tenant.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"aggios-app/backend/internal/repository"
|
||||
)
|
||||
|
||||
type tenantContextKey string
|
||||
|
||||
const TenantIDKey tenantContextKey = "tenantID"
|
||||
const SubdomainKey tenantContextKey = "subdomain"
|
||||
|
||||
// TenantDetector detects tenant from subdomain
|
||||
func TenantDetector(tenantRepo *repository.TenantRepository) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
|
||||
// Extract subdomain
|
||||
// Examples:
|
||||
// - agencia-xyz.localhost -> agencia-xyz
|
||||
// - agencia-xyz.aggios.app -> agencia-xyz
|
||||
// - dash.localhost -> dash (master admin)
|
||||
// - localhost -> (institutional site)
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
var subdomain string
|
||||
|
||||
if len(parts) >= 2 {
|
||||
// Has subdomain
|
||||
subdomain = parts[0]
|
||||
|
||||
// Remove port if present
|
||||
if strings.Contains(subdomain, ":") {
|
||||
subdomain = strings.Split(subdomain, ":")[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Add subdomain to context
|
||||
ctx := context.WithValue(r.Context(), SubdomainKey, subdomain)
|
||||
|
||||
// If subdomain is not empty and not "dash" or "api", try to find tenant
|
||||
if subdomain != "" && subdomain != "dash" && subdomain != "api" && subdomain != "localhost" {
|
||||
tenant, err := tenantRepo.FindBySubdomain(subdomain)
|
||||
if err == nil && tenant != nil {
|
||||
ctx = context.WithValue(ctx, TenantIDKey, tenant.ID.String())
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user