Prepara versao dev 1.0
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
module server
|
||||
|
||||
go 1.23.12
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.10.9
|
||||
golang.org/x/crypto v0.27.0
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
@@ -2,576 +2,127 @@ package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/lib/pq"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"aggios-app/backend/internal/api/handlers"
|
||||
"aggios-app/backend/internal/api/middleware"
|
||||
"aggios-app/backend/internal/config"
|
||||
"aggios-app/backend/internal/repository"
|
||||
"aggios-app/backend/internal/service"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
// jwtSecret carrega o secret do ambiente ou usa fallback (NUNCA use fallback em produção)
|
||||
var jwtSecret = []byte(getEnvOrDefault("JWT_SECRET", "INSECURE-fallback-secret-CHANGE-THIS"))
|
||||
|
||||
// Rate limiting simples (IP -> timestamp das últimas tentativas)
|
||||
var loginAttempts = make(map[string][]time.Time)
|
||||
var registerAttempts = make(map[string][]time.Time)
|
||||
|
||||
const maxAttemptsPerMinute = 5
|
||||
|
||||
// corsMiddleware adiciona headers CORS
|
||||
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS - apenas domínios permitidos
|
||||
allowedOrigins := map[string]bool{
|
||||
"http://localhost": true, // Dev local
|
||||
"http://dash.localhost": true, // Dashboard dev
|
||||
"http://aggios.local": true, // Institucional dev
|
||||
"http://dash.aggios.local": true, // Dashboard dev alternativo
|
||||
"https://aggios.app": true, // Institucional prod
|
||||
"https://dash.aggios.app": true, // Dashboard prod
|
||||
"https://www.aggios.app": true, // Institucional prod www
|
||||
}
|
||||
|
||||
origin := r.Header.Get("Origin")
|
||||
if allowedOrigins[origin] {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
// Headers de segurança
|
||||
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("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
|
||||
// Handle preflight
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Log da requisição (sem dados sensíveis)
|
||||
log.Printf("📥 %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRequest representa os dados completos de registro
|
||||
type RegisterRequest struct {
|
||||
// Step 1 - Dados Pessoais
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
|
||||
// Step 2 - Empresa
|
||||
CompanyName string `json:"companyName"`
|
||||
CNPJ string `json:"cnpj"`
|
||||
RazaoSocial string `json:"razaoSocial"`
|
||||
Description string `json:"description"`
|
||||
Website string `json:"website"`
|
||||
Industry string `json:"industry"`
|
||||
TeamSize string `json:"teamSize"`
|
||||
|
||||
// Step 3 - Localização
|
||||
CEP string `json:"cep"`
|
||||
State string `json:"state"`
|
||||
City string `json:"city"`
|
||||
Neighborhood string `json:"neighborhood"`
|
||||
Street string `json:"street"`
|
||||
Number string `json:"number"`
|
||||
Complement string `json:"complement"`
|
||||
Contacts []struct {
|
||||
ID int `json:"id"`
|
||||
WhatsApp string `json:"whatsapp"`
|
||||
} `json:"contacts"`
|
||||
|
||||
// Step 4 - Domínio
|
||||
Subdomain string `json:"subdomain"`
|
||||
|
||||
// Step 5 - Personalização
|
||||
PrimaryColor string `json:"primaryColor"`
|
||||
SecondaryColor string `json:"secondaryColor"`
|
||||
LogoURL string `json:"logoUrl"`
|
||||
}
|
||||
|
||||
// RegisterResponse representa a resposta do registro
|
||||
type RegisterResponse struct {
|
||||
Token string `json:"token"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Company string `json:"company"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ErrorResponse representa uma resposta de erro
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// LoginRequest representa os dados de login
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// LoginResponse representa a resposta do login
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User UserPayload `json:"user"`
|
||||
}
|
||||
|
||||
// UserPayload representa os dados do usuário no token
|
||||
type UserPayload struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Company string `json:"company"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
}
|
||||
|
||||
// Claims customizado para JWT
|
||||
type Claims struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
TenantID string `json:"tenantId"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// getEnvOrDefault retorna variável de ambiente ou valor padrão
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// checkRateLimit verifica se IP excedeu limite de tentativas
|
||||
func checkRateLimit(ip string, attempts map[string][]time.Time) bool {
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-1 * time.Minute)
|
||||
|
||||
// Limpar tentativas antigas
|
||||
if timestamps, exists := attempts[ip]; exists {
|
||||
var recent []time.Time
|
||||
for _, t := range timestamps {
|
||||
if t.After(cutoff) {
|
||||
recent = append(recent, t)
|
||||
}
|
||||
}
|
||||
attempts[ip] = recent
|
||||
|
||||
// Verificar se excedeu limite
|
||||
if len(recent) >= maxAttemptsPerMinute {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Adicionar nova tentativa
|
||||
attempts[ip] = append(attempts[ip], now)
|
||||
return true
|
||||
}
|
||||
|
||||
// validateEmail valida formato de email
|
||||
func validateEmail(email string) bool {
|
||||
if len(email) < 3 || len(email) > 254 {
|
||||
return false
|
||||
}
|
||||
// Regex simples para validação
|
||||
return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
}
|
||||
|
||||
func initDB() error {
|
||||
func initDB(cfg *config.Config) (*sql.DB, error) {
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
os.Getenv("DB_HOST"),
|
||||
os.Getenv("DB_PORT"),
|
||||
os.Getenv("DB_USER"),
|
||||
os.Getenv("DB_PASSWORD"),
|
||||
os.Getenv("DB_NAME"),
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.User,
|
||||
cfg.Database.Password,
|
||||
cfg.Database.Name,
|
||||
)
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("postgres", connStr)
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erro ao abrir conexão: %v", err)
|
||||
return nil, fmt.Errorf("erro ao abrir conexão: %v", err)
|
||||
}
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return fmt.Errorf("erro ao conectar ao banco: %v", err)
|
||||
return nil, fmt.Errorf("erro ao conectar ao banco: %v", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Conectado ao PostgreSQL")
|
||||
return nil
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Inicializar banco de dados
|
||||
if err := initDB(); err != nil {
|
||||
// Load configuration
|
||||
cfg := config.Load()
|
||||
|
||||
// Initialize database
|
||||
db, err := initDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Erro ao inicializar banco: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Health check handlers
|
||||
http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, `{"status":"healthy","version":"1.0.0","database":"pending","redis":"pending","minio":"pending"}`)
|
||||
})
|
||||
// Initialize repositories
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
tenantRepo := repository.NewTenantRepository(db)
|
||||
companyRepo := repository.NewCompanyRepository(db)
|
||||
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
})
|
||||
// Initialize services
|
||||
authService := service.NewAuthService(userRepo, tenantRepo, cfg)
|
||||
agencyService := service.NewAgencyService(userRepo, tenantRepo, cfg)
|
||||
tenantService := service.NewTenantService(tenantRepo)
|
||||
companyService := service.NewCompanyService(companyRepo)
|
||||
|
||||
// Auth routes (com CORS)
|
||||
http.HandleFunc("/api/auth/register", corsMiddleware(handleRegister))
|
||||
http.HandleFunc("/api/auth/login", corsMiddleware(handleLogin))
|
||||
http.HandleFunc("/api/me", corsMiddleware(authMiddleware(handleMe)))
|
||||
// Initialize handlers
|
||||
healthHandler := handlers.NewHealthHandler()
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
agencyProfileHandler := handlers.NewAgencyHandler(tenantRepo)
|
||||
agencyHandler := handlers.NewAgencyRegistrationHandler(agencyService, cfg)
|
||||
tenantHandler := handlers.NewTenantHandler(tenantService)
|
||||
companyHandler := handlers.NewCompanyHandler(companyService)
|
||||
|
||||
port := os.Getenv("SERVER_PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
// Create middleware chain
|
||||
tenantDetector := middleware.TenantDetector(tenantRepo)
|
||||
corsMiddleware := middleware.CORS(cfg)
|
||||
securityMiddleware := middleware.SecurityHeaders
|
||||
rateLimitMiddleware := middleware.RateLimit(cfg)
|
||||
authMiddleware := middleware.Auth(cfg)
|
||||
|
||||
addr := fmt.Sprintf(":%s", port)
|
||||
// Setup routes
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Health check (no auth)
|
||||
mux.HandleFunc("/health", healthHandler.Check)
|
||||
mux.HandleFunc("/api/health", healthHandler.Check)
|
||||
|
||||
// Auth routes (public with rate limiting)
|
||||
mux.HandleFunc("/api/auth/login", authHandler.Login)
|
||||
|
||||
// Protected auth routes
|
||||
mux.Handle("/api/auth/change-password", authMiddleware(http.HandlerFunc(authHandler.ChangePassword)))
|
||||
|
||||
// Agency management (SUPERADMIN only)
|
||||
mux.HandleFunc("/api/admin/agencies/register", agencyHandler.RegisterAgency)
|
||||
mux.HandleFunc("/api/admin/agencies", tenantHandler.ListAll)
|
||||
mux.HandleFunc("/api/admin/agencies/", agencyHandler.HandleAgency)
|
||||
|
||||
// Client registration (ADMIN_AGENCIA only - requires auth)
|
||||
mux.Handle("/api/agencies/clients/register", authMiddleware(http.HandlerFunc(agencyHandler.RegisterClient)))
|
||||
|
||||
// Agency profile routes (protected)
|
||||
mux.Handle("/api/agency/profile", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
agencyProfileHandler.GetProfile(w, r)
|
||||
} else if r.Method == http.MethodPut || r.Method == http.MethodPatch {
|
||||
agencyProfileHandler.UpdateProfile(w, r)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})))
|
||||
|
||||
// Protected routes (require authentication)
|
||||
mux.Handle("/api/companies", authMiddleware(http.HandlerFunc(companyHandler.List)))
|
||||
mux.Handle("/api/companies/create", authMiddleware(http.HandlerFunc(companyHandler.Create)))
|
||||
|
||||
// Apply global middlewares: tenant -> cors -> security -> rateLimit -> mux
|
||||
handler := tenantDetector(corsMiddleware(securityMiddleware(rateLimitMiddleware(mux))))
|
||||
|
||||
// Start server
|
||||
addr := fmt.Sprintf(":%s", cfg.Server.Port)
|
||||
log.Printf("🚀 Server starting on %s", addr)
|
||||
log.Printf("📍 Health check: http://localhost:%s/health", port)
|
||||
log.Printf("🔗 API: http://localhost:%s/api/health", port)
|
||||
log.Printf("👤 Register: http://localhost:%s/api/auth/register", port)
|
||||
log.Printf("🔐 Login: http://localhost:%s/api/auth/login", port)
|
||||
log.Printf("👤 Me: http://localhost:%s/api/me", port)
|
||||
log.Printf("📍 Health check: http://localhost:%s/health", cfg.Server.Port)
|
||||
log.Printf("🔗 API: http://localhost:%s/api/health", cfg.Server.Port)
|
||||
log.Printf("🏢 Register Agency (SUPERADMIN): http://localhost:%s/api/admin/agencies/register", cfg.Server.Port)
|
||||
log.Printf("🔐 Login: http://localhost:%s/api/auth/login", cfg.Server.Port)
|
||||
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||
log.Fatalf("❌ Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegister handler para criar novo usuário
|
||||
func handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
// Apenas POST
|
||||
if r.Method != http.MethodPost {
|
||||
sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
ip := strings.Split(r.RemoteAddr, ":")[0]
|
||||
if !checkRateLimit(ip, registerAttempts) {
|
||||
sendError(w, "Too many registration attempts. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var req RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validações básicas
|
||||
if !validateEmail(req.Email) {
|
||||
sendError(w, "Invalid email format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
sendError(w, "Password is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
sendError(w, "Password must be at least 8 characters", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.FullName == "" {
|
||||
sendError(w, "Full name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CompanyName == "" {
|
||||
sendError(w, "Company name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Subdomain == "" {
|
||||
sendError(w, "Subdomain is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
var exists bool
|
||||
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", req.Email).Scan(&exists)
|
||||
if err != nil {
|
||||
sendError(w, "Database error", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao verificar email: %v", err)
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
sendError(w, "Email already registered", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// Hash da senha com bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
sendError(w, "Error processing password", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao hash senha: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Criar Tenant (empresa)
|
||||
tenantID := uuid.New().String()
|
||||
domain := fmt.Sprintf("%s.aggios.app", req.Subdomain)
|
||||
createdAt := time.Now()
|
||||
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO tenants (id, name, domain, subdomain, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
tenantID, req.CompanyName, domain, req.Subdomain, true, createdAt, createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
sendError(w, "Error creating company", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao criar tenant: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Tenant criado: %s (%s)", req.CompanyName, tenantID)
|
||||
|
||||
// Criar Usuário (administrador do tenant)
|
||||
userID := uuid.New().String()
|
||||
firstName := req.FullName
|
||||
lastName := ""
|
||||
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO users (id, tenant_id, email, password_hash, first_name, last_name, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
userID, tenantID, req.Email, string(hashedPassword), firstName, lastName, true, createdAt, createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
sendError(w, "Error creating user", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao inserir usuário: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Usuário criado: %s (%s)", req.Email, userID)
|
||||
|
||||
// Gerar token JWT para login automático
|
||||
token, err := generateToken(userID, req.Email, tenantID)
|
||||
if err != nil {
|
||||
sendError(w, "Error generating token", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao gerar token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := RegisterResponse{
|
||||
Token: token,
|
||||
ID: userID,
|
||||
Email: req.Email,
|
||||
Name: req.FullName,
|
||||
TenantID: tenantID,
|
||||
Company: req.CompanyName,
|
||||
Subdomain: req.Subdomain,
|
||||
CreatedAt: createdAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// sendError envia uma resposta de erro padronizada
|
||||
func sendError(w http.ResponseWriter, message string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{
|
||||
Error: http.StatusText(statusCode),
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// generateToken gera um JWT token para o usuário
|
||||
func generateToken(userID, email, tenantID string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
TenantID: tenantID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "aggios-api",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// authMiddleware verifica o token JWT
|
||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
sendError(w, "Authorization header required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == authHeader {
|
||||
sendError(w, "Invalid authorization format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
sendError(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Adicionar claims ao contexto (simplificado: usar headers)
|
||||
r.Header.Set("X-User-ID", claims.UserID)
|
||||
r.Header.Set("X-User-Email", claims.Email)
|
||||
r.Header.Set("X-Tenant-ID", claims.TenantID)
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogin handler para fazer login
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
ip := strings.Split(r.RemoteAddr, ":")[0]
|
||||
if !checkRateLimit(ip, loginAttempts) {
|
||||
sendError(w, "Too many login attempts. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !validateEmail(req.Email) || req.Password == "" {
|
||||
sendError(w, "Invalid credentials", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Buscar usuário no banco
|
||||
var userID, email, passwordHash, firstName, tenantID string
|
||||
var tenantName, subdomain string
|
||||
|
||||
err := db.QueryRow(`
|
||||
SELECT u.id, u.email, u.password_hash, u.first_name, u.tenant_id, t.name, t.subdomain
|
||||
FROM users u
|
||||
INNER JOIN tenants t ON u.tenant_id = t.id
|
||||
WHERE u.email = $1 AND u.is_active = true
|
||||
`, req.Email).Scan(&userID, &email, &passwordHash, &firstName, &tenantID, &tenantName, &subdomain)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
sendError(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sendError(w, "Database error", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao buscar usuário: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verificar senha
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
|
||||
sendError(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Gerar token JWT
|
||||
token, err := generateToken(userID, email, tenantID)
|
||||
if err != nil {
|
||||
sendError(w, "Error generating token", http.StatusInternalServerError)
|
||||
log.Printf("Erro ao gerar token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Login bem-sucedido: %s", email)
|
||||
|
||||
response := LoginResponse{
|
||||
Token: token,
|
||||
User: UserPayload{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Name: firstName,
|
||||
TenantID: tenantID,
|
||||
Company: tenantName,
|
||||
Subdomain: subdomain,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleMe retorna dados do usuário autenticado
|
||||
func handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
tenantID := r.Header.Get("X-Tenant-ID")
|
||||
|
||||
var email, firstName, lastName string
|
||||
var tenantName, subdomain string
|
||||
|
||||
err := db.QueryRow(`
|
||||
SELECT u.email, u.first_name, u.last_name, t.name, t.subdomain
|
||||
FROM users u
|
||||
INNER JOIN tenants t ON u.tenant_id = t.id
|
||||
WHERE u.id = $1 AND u.tenant_id = $2
|
||||
`, userID, tenantID).Scan(&email, &firstName, &lastName, &tenantName, &subdomain)
|
||||
|
||||
if err != nil {
|
||||
sendError(w, "User not found", http.StatusNotFound)
|
||||
log.Printf("Erro ao buscar usuário: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fullName := firstName
|
||||
if lastName != "" {
|
||||
fullName += " " + lastName
|
||||
}
|
||||
|
||||
response := UserPayload{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Name: fullName,
|
||||
TenantID: tenantID,
|
||||
Company: tenantName,
|
||||
Subdomain: subdomain,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user