feat: site institucional completo com design system - Implementa\u00e7\u00e3o do site institucional da Aggios com design system completo incluindo gradientes, tipografia, componentes e se\u00e7\u00f5es de recursos, pre\u00e7os e CTA

This commit is contained in:
Erik Silva
2025-12-07 02:19:08 -03:00
parent 53f240a4da
commit bf6707e746
90 changed files with 25927 additions and 1 deletions

36
backend/.env.example Normal file
View File

@@ -0,0 +1,36 @@
# Server
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
ENV=development
JWT_SECRET=your-super-secret-jwt-key-change-in-production
# Database
DB_HOST=postgres
DB_PORT=5432
DB_USER=aggios
DB_PASSWORD=changeme
DB_NAME=aggios_db
DB_SSL_MODE=disable
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=changeme
# MinIO
MINIO_ENDPOINT=minio:9000
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=changeme
MINIO_USE_SSL=false
MINIO_BUCKET_NAME=aggios
# JWT
JWT_SECRET=your-super-secret-jwt-key-change-me-in-production
JWT_EXPIRATION=24h
REFRESH_TOKEN_EXPIRATION=7d
# Cors
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,https://aggios.app,https://dash.aggios.app
# Sentry (optional)
SENTRY_DSN=

42
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.so.*
*.dylib
# Test binary
*.test
# Output of the go coverage tool
*.out
# Go workspace file
go.work
# Dependency directories
vendor/
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Build output
bin/
dist/
# Logs
*.log

28
backend/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Build stage
FROM golang:1.23-alpine AS builder
WORKDIR /build
# Copy go.mod and go.sum from cmd/server
COPY cmd/server/go.mod cmd/server/go.sum ./
RUN go mod download
# Copy source code
COPY cmd/server/main.go ./
# Build
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
# Runtime image
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /build/server .
EXPOSE 8080
CMD ["./server"]

332
backend/README.md Normal file
View File

@@ -0,0 +1,332 @@
# Backend Go - Aggios
Backend robusto em Go com suporte a multi-tenant, autenticação segura (JWT), PostgreSQL, Redis e MinIO.
## 🚀 Quick Start
### Pré-requisitos
- Docker & Docker Compose
- Go 1.23+ (para desenvolvimento local)
### Setup inicial
```bash
# 1. Clone o repositório
cd aggios-app
# 2. Copiar variáveis de ambiente
cp .env.example .env
# 3. Iniciar stack (Traefik + Backend + BD + Cache + Storage)
docker-compose up -d
# 4. Verificar status
docker-compose ps
docker-compose logs -f backend
# 5. Testar API
curl -X GET http://localhost:8080/api/health
```
## 📚 Endpoints Disponíveis
### Autenticação (Público)
```bash
# Login
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "senha123"
}
# Response
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "aB_c123xYz...",
"token_type": "Bearer",
"expires_in": 86400
}
```
```bash
# Registrar novo usuário
POST /api/auth/register
Content-Type: application/json
{
"email": "newuser@example.com",
"password": "senha123",
"confirm_password": "senha123",
"first_name": "João",
"last_name": "Silva"
}
```
```bash
# Refresh token
POST /api/auth/refresh
Content-Type: application/json
{
"refresh_token": "aB_c123xYz..."
}
```
### Usuário (Autenticado)
```bash
# Obter dados do usuário
GET /api/users/me
Authorization: Bearer {access_token}
```
```bash
# Logout
POST /api/logout
Authorization: Bearer {access_token}
```
### Health Check
```bash
# Status da API e serviços
GET /api/health
# Response
{
"status": "up",
"timestamp": 1733376000,
"database": true,
"redis": true,
"minio": true
}
```
## 🔐 Autenticação
### JWT Structure
```json
{
"user_id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"tenant_id": "acme-tenant-id",
"exp": 1733462400,
"iat": 1733376000
}
```
### Headers esperados
```bash
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## 🏢 Multi-Tenant
Cada tenant tem seu próprio subdomain:
- `api.aggios.app` - API geral
- `acme.aggios.app` - Tenant "acme"
- `empresa1.aggios.app` - Tenant "empresa1"
O JWT contém o `tenant_id`, garantindo isolamento de dados.
## 📦 Serviços
### PostgreSQL
- **Host**: postgres (docker) / localhost (local)
- **Porta**: 5432
- **Usuário**: aggios
- **Database**: aggios_db
### Redis
- **Host**: redis (docker) / localhost (local)
- **Porta**: 6379
### MinIO (S3)
- **Endpoint**: minio:9000
- **Console**: http://minio-console.localhost
- **API**: http://minio.localhost
### Traefik
- **Dashboard**: http://traefik.localhost
- **Usuário**: admin / admin
## 🛠️ Desenvolvimento Local
### Build local
```bash
cd backend
go mod download
go mod tidy
# Rodar com hot reload (recomenda-se usar Air)
go run ./cmd/server/main.go
```
### Ambiente local
```bash
# Criar .env local
cp .env.example .env.local
# Ajustar hosts para localhost
DB_HOST=localhost
REDIS_HOST=localhost
MINIO_ENDPOINT=localhost:9000
```
### Testes
```bash
cd backend
go test ./...
go test -v -cover ./...
```
## 📝 Estrutura do Projeto
```
backend/
├── cmd/server/ # Entry point
├── internal/
│ ├── api/ # Handlers e middleware
│ ├── auth/ # JWT e autenticação
│ ├── config/ # Configuração
│ ├── database/ # PostgreSQL
│ ├── models/ # Estruturas de dados
│ ├── services/ # Lógica de negócio
│ └── storage/ # Redis e MinIO
└── migrations/ # SQL scripts
```
## 🔄 Docker Compose
Inicia stack completa:
```yaml
- Traefik: Reverse proxy + SSL
- PostgreSQL: Banco de dados
- Redis: Cache e sessões
- MinIO: Storage S3-compatible
- Backend: API Go
- Frontend: Next.js (institucional + dashboard)
```
### Comandos úteis
```bash
# Iniciar
docker-compose up -d
# Ver logs
docker-compose logs -f backend
# Parar
docker-compose down
# Resetar volumes (CUIDADO!)
docker-compose down -v
```
## 🚀 Deploy em Produção
### Variáveis críticas
```env
JWT_SECRET= # 32+ caracteres aleatórios
DB_PASSWORD= # Senha forte
REDIS_PASSWORD= # Senha forte
MINIO_ROOT_PASSWORD= # Senha forte
ENV=production # Ativar hardening
```
### HTTPS/SSL
- Let's Encrypt automático via Traefik
- Certificados salvos em `traefik/letsencrypt/acme.json`
- Renovação automática
### Backups
```bash
# PostgreSQL
docker exec aggios-postgres pg_dump -U aggios aggios_db > backup.sql
# MinIO
docker exec aggios-minio mc mirror minio/aggios ./backup-minio
```
## 📱 Integração Mobile
A API é pronta para iOS e Android:
```bash
# Não requer cookies (stateless JWT)
# Suporta CORS
# Content-Type: application/json
# Versionamento de API: /api/v1/*
```
Exemplo React Native:
```javascript
const login = async (email, password) => {
const response = await fetch('https://api.aggios.app/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
// Salvar data.access_token em AsyncStorage
// Usar em Authorization header
};
```
## 🐛 Troubleshooting
### PostgreSQL não conecta
```bash
docker-compose logs postgres
docker-compose exec postgres pg_isready -U aggios
```
### Redis não conecta
```bash
docker-compose logs redis
docker-compose exec redis redis-cli ping
```
### MinIO issues
```bash
docker-compose logs minio
docker-compose exec minio mc admin info minio
```
### Backend crashes
```bash
docker-compose logs backend
docker-compose exec backend /root/server # Testar manualmente
```
## 📚 Documentação Adicional
- [ARCHITECTURE.md](../ARCHITECTURE.md) - Design detalhado
- [Go Gin Documentation](https://gin-gonic.com/)
- [PostgreSQL Docs](https://www.postgresql.org/docs/)
- [Traefik Docs](https://doc.traefik.io/)
- [MinIO Docs](https://docs.min.io/)
## 📞 Suporte
Para issues ou perguntas sobre a API, consulte a documentação ou abra uma issue no repositório.
---
**Última atualização**: Dezembro 2025

10
backend/cmd/server/go.mod Normal file
View File

@@ -0,0 +1,10 @@
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
)

View File

@@ -0,0 +1,8 @@
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=

577
backend/cmd/server/main.go Normal file
View File

@@ -0,0 +1,577 @@
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"
)
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 {
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"),
)
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
return 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)
}
log.Println("✅ Conectado ao PostgreSQL")
return nil
}
func main() {
// Inicializar banco de dados
if err := initDB(); 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"}`)
})
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"}`)
})
// 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)))
port := os.Getenv("SERVER_PORT")
if port == "" {
port = "8080"
}
addr := fmt.Sprintf(":%s", 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)
if err := http.ListenAndServe(addr, nil); 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)
}

20
backend/go.mod Normal file
View File

@@ -0,0 +1,20 @@
module backend
go 1.23
require (
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/minio/minio-go/v7 v7.0.70
github.com/redis/go-redis/v9 v9.5.1
golang.org/x/crypto v0.27.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
github.com/klauspost/compress v1.17.9
github.com/klauspost/cpuid/v2 v2.2.8
)

12
backend/go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=