Ir para o conteúdo

Backend - Integrações Externas

Este documento descreve todas as integrações externas do backend, incluindo configuração, uso e exemplos.

Índice

  1. Cloudflare R2 (Storage)
  2. SendGrid (Email)
  3. SMTP (Email Alternativo)
  4. OAuth (Google e Apple)

Cloudflare R2 (Storage)

Visão Geral

Cloudflare R2 é usado para armazenamento de arquivos (fotos de perfil, documentos, símbolos). É compatível com API S3, permitindo uso do SDK AWS.

Localização: internal/storage/r2.go

Configuração

Variáveis de Ambiente:

R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key
R2_SECRET_ACCESS_KEY=your-secret-key
R2_BUCKET_NAME=billings-ease-symbols
R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
R2_PUBLIC_URL=https://your-public-url.com

Inicialização:

r2Storage, err := storage.NewR2Storage(storage.R2Config{
    AccountID:       os.Getenv("R2_ACCOUNT_ID"),
    AccessKeyID:     os.Getenv("R2_ACCESS_KEY_ID"),
    SecretAccessKey: os.Getenv("R2_SECRET_ACCESS_KEY"),
    BucketName:      os.Getenv("R2_BUCKET_NAME"),
    Endpoint:        os.Getenv("R2_ENDPOINT"),
    PublicURL:       os.Getenv("R2_PUBLIC_URL"),
})

Estrutura de Pastas

Arquivos são organizados em pastas:

professionals/{userID}/profile/{uuid}.{ext}
professionals/{userID}/documents/{type}/{uuid}.{ext}
clients/{userID}/profile/{uuid}.{ext}
symbols/{uuid}.{ext}

Métodos Principais

UploadProfilePhoto

Upload de foto de perfil (profissional ou cliente).

result, err := r2Storage.UploadProfilePhotoForUserType(ctx, userID, userType, filename, data)

Parâmetros: - userID - ID do usuário - userType - "professional" ou "client" - filename - Nome original do arquivo - data - Bytes do arquivo

Validações: - Apenas imagens (JPEG, PNG, GIF, WebP) - Detecta tipo MIME automaticamente - Gera UUID para nome único

Retorna:

type UploadResult struct {
    Key       string // Chave no bucket
    URL       string // URL pública
    Size      int64  // Tamanho em bytes
    MimeType  string // Tipo MIME
    CreatedAt string // Data de criação
}

Exemplo:

file, err := c.FormFile("photo")
if err != nil {
    return err
}

src, _ := file.Open()
defer src.Close()

data, _ := io.ReadAll(src)
result, err := r2Storage.UploadProfilePhotoForUserType(ctx, userID, "professional", file.Filename, data)

UploadDocument

Upload de documento (certificado profissional, documento de identidade).

result, err := r2Storage.UploadDocument(ctx, userID, documentType, filename, data)

Parâmetros: - userID - ID do profissional - documentType - Tipo do documento (ex: "certificate", "cnh", "rg") - filename - Nome original - data - Bytes do arquivo

Validações: - Tipos permitidos: JPEG, PNG, PDF - Tamanho máximo: 5MB

Estrutura: professionals/{userID}/documents/{type}/{uuid}.{ext}

UploadSymbol

Upload de símbolo do Método Billings.

result, err := r2Storage.UploadSymbol(ctx, filename, data)

Validações: - Tipos permitidos: JPEG, PNG, GIF, WebP, SVG - Tamanho máximo: 2MB - Cache headers: 1 ano (immutable)

Estrutura: symbols/{uuid}.{ext}

Delete

Remove arquivo do R2.

err := r2Storage.Delete(ctx, key)

Ou por URL:

err := r2Storage.DeleteByURL(ctx, url)

PopulateURLFromKey

Gera URL pública a partir da chave.

url := r2Storage.PopulateURLFromKey(key)

Uso: Quando retornando modelos que têm apenas ProfilePhotoKey, popular ProfilePhotoURL:

if profile.ProfilePhotoKey != "" {
    profile.ProfilePhotoURL = r2Storage.PopulateURLFromKey(profile.ProfilePhotoKey)
}

Uso em Handlers

// Configurar storage globalmente
handlers.SetR2Storage(r2Storage)

// Usar em handler
r2Storage := handlers.GetR2Storage()
if r2Storage != nil && r2Storage.IsConfigured() {
    result, err := r2Storage.UploadProfilePhotoForUserType(ctx, userID, "professional", filename, data)
    if err != nil {
        return err
    }
    profile.ProfilePhotoKey = result.Key
    profile.ProfilePhotoURL = result.URL
}

Fallback

Se R2 não estiver configurado, o sistema funciona sem upload de arquivos. Handlers devem verificar:

if r2Storage != nil && r2Storage.IsConfigured() {
    // Fazer upload
} else {
    // Usar fallback ou retornar erro
}

SendGrid (Email)

Visão Geral

SendGrid é usado para envio de emails em produção (verificação de email, reset de senha, notificações).

Localização: internal/services/email/sendgrid_provider.go

Configuração

Variáveis de Ambiente:

SENDGRID_API_KEY=your-sendgrid-api-key
SENDGRID_FROM_EMAIL=noreply@billings-ease.com
FRONTEND_URL=https://app.billings-ease.com

Inicialização:

sendGridProvider, err := email.NewSendGridProvider(
    os.Getenv("SENDGRID_API_KEY"),
    os.Getenv("SENDGRID_FROM_EMAIL"),
    "Billings Ease",
)

Funcionalidades

SendEmail

Envia email genérico.

err := provider.SendEmail(to, subject, body)

Retry: Implementa retry automático (2 tentativas) em caso de erro de rede.

Templates

O EmailService fornece templates HTML:

  • SendVerificationEmail - Email de verificação de conta
  • SendPasswordResetEmail - Email de reset de senha

Exemplo:

emailService := email.GetEmailService()
err := emailService.SendVerificationEmail(user.Email, token)

Factory Pattern

O serviço de email usa factory para detectar automaticamente qual provider usar:

Localização: internal/services/email/factory.go

Lógica: 1. Se SENDGRID_API_KEY estiver configurado → usa SendGrid 2. Caso contrário, se SMTP_HOST estiver configurado → usa SMTP 3. Caso contrário, retorna erro

Uso:

emailService, err := email.NewEmailServiceFromEnv()
if err != nil {
    log.Fatal("Failed to initialize email service:", err)
}


SMTP (Email Alternativo)

Visão Geral

SMTP genérico é usado como alternativa ao SendGrid, útil para desenvolvimento ou servidores SMTP próprios.

Localização: internal/services/email/smtp_provider.go

Configuração

Variáveis de Ambiente:

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-password
SMTP_FROM_EMAIL=noreply@billings-ease.com
SMTP_FROM_NAME=Billings Ease
FRONTEND_URL=https://app.billings-ease.com

Inicialização:

smtpProvider, err := email.NewSMTPProvider(
    os.Getenv("SMTP_HOST"),
    os.Getenv("SMTP_PORT"),
    os.Getenv("SMTP_USERNAME"),
    os.Getenv("SMTP_PASSWORD"),
    os.Getenv("SMTP_FROM_EMAIL"),
    "Billings Ease",
)

Funcionalidades

SendEmail

Envia email via SMTP.

err := provider.SendEmail(to, subject, body)

Características: - Suporta autenticação (PLAIN) - Envia HTML formatado - Headers MIME corretos

Uso

O factory detecta automaticamente e usa SMTP se SendGrid não estiver configurado:

// Factory escolhe automaticamente
emailService, err := email.NewEmailServiceFromEnv()

OAuth (Google e Apple)

Visão Geral

OAuth permite autenticação via Google e Apple Sign In, além de autenticação tradicional (email/senha).

Localização: internal/services/oauth/

Arquitetura

OAuthService (orquestrador)
    ├── GoogleProvider
    └── AppleProvider

Fluxo: 1. Cliente solicita URL de autorização 2. Usuário autoriza no provedor 3. Provedor redireciona com código 4. Backend troca código por tokens 5. Backend valida ID Token 6. Backend cria/atualiza usuário e UserAuthProvider 7. Backend retorna tokens JWT

Google OAuth

Localização: internal/services/oauth/google_provider.go

Configuração:

Variáveis de Ambiente:

GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URL=https://api.billings-ease.com/api/auth/oauth/callback/google

URLs Padrão: - Auth URL: https://accounts.google.com/o/oauth2/v2/auth - Token URL: https://oauth2.googleapis.com/token - Issuer URL: https://accounts.google.com

Características: - Usa OIDC (OpenID Connect) - Valida ID Token - Suporta PKCE (code challenge/verifier) - Retry automático em caso de erro

Métodos:

GetAuthURL

Gera URL de autorização com PKCE.

authURL := provider.GetAuthURL(state, codeChallenge)

PKCE: - codeChallenge - SHA256 hash do codeVerifier - codeVerifier - String aleatória (43-128 caracteres)

ExchangeCode

Troca código por tokens.

token, err := provider.ExchangeCode(ctx, code, codeVerifier)

ValidateIDToken

Valida ID Token OIDC.

idToken, err := provider.ValidateIDToken(ctx, token)

Claims do ID Token: - sub - Provider User ID (usado como ProviderUserID) - email - Email do usuário - name - Nome do usuário - picture - Foto de perfil


Apple Sign In

Localização: internal/services/oauth/apple_provider.go

Configuração:

Variáveis de Ambiente:

APPLE_CLIENT_ID=your-apple-client-id
APPLE_TEAM_ID=your-apple-team-id
APPLE_KEY_ID=your-apple-key-id
APPLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----
APPLE_REDIRECT_URL=https://api.billings-ease.com/api/auth/oauth/callback/apple

URLs Padrão: - Auth URL: https://appleid.apple.com/auth/authorize - Token URL: https://appleid.apple.com/auth/token - Issuer URL: https://appleid.apple.com

Características Especiais:

Client Secret JWT

Apple requer que client_secret seja um JWT assinado com chave privada ECDSA:

clientSecret, err := provider.generateClientSecret()

Claims do JWT: - iss - Team ID - iat - Emitido em - exp - Expira em (6 horas) - aud - "https://appleid.apple.com" - sub - Client ID

Validade: - JWT expira em 6 horas - Chave privada Apple expira em ~6 meses (requer rotação)

Validação de ID Token

Apple retorna ID Token que deve ser validado:

idToken, err := provider.ValidateIDToken(ctx, token)

Claims do ID Token: - sub - Provider User ID (usado como ProviderUserID) - email - Email (pode não estar presente se já foi fornecido antes)


OAuth Service

Localização: internal/services/oauth/provider.go

Responsabilidades: - Orquestrar fluxo OAuth - Gerenciar providers (Google, Apple) - Criar/atualizar usuários - Criar/atualizar UserAuthProvider - Gerar tokens JWT

Métodos Principais:

Authenticate

Autentica usuário via OAuth.

result, err := oauthService.Authenticate(provider, token)

Processo: 1. Valida token com provedor 2. Extrai informações do ID Token 3. Busca ou cria usuário 4. Cria/atualiza UserAuthProvider 5. Gera tokens JWT 6. Retorna resultado

OAuthResult:

type OAuthResult struct {
    Token        string
    RefreshToken string
    User         *models.User
    IsNewUser    bool
}

GetAuthURL

Gera URL de autorização.

authURL, state, codeVerifier, err := oauthService.GetAuthURL(provider)

Retorna: - authURL - URL para redirecionar usuário - state - State para validação CSRF - codeVerifier - Para PKCE (armazenar temporariamente) - error - Erro se provider não configurado


Endpoints OAuth

Rotas Públicas: - GET /api/auth/oauth/:provider - Obter URL de autorização - GET /api/auth/oauth/callback/:provider - Callback do provedor

Rotas Protegidas: - POST /api/auth/oauth/associate/:provider - Associar provedor à conta - DELETE /api/auth/oauth/disassociate/:provider - Desassociar provedor - GET /api/auth/oauth/providers - Listar provedores associados

Fluxo Completo

1. Solicitar Autorização

GET /api/auth/oauth/google

Resposta:

{
  "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
  "state": "random-state-string",
  "code_verifier": "random-code-verifier"
}

2. Usuário Autoriza

Cliente redireciona para auth_url. Usuário autoriza no Google/Apple.

3. Callback

Provedor redireciona para:

/api/auth/oauth/callback/google?code=...&state=...

Handler:

func OAuthCallback(c echo.Context) error {
    provider := c.Param("provider")
    code := c.QueryParam("code")
    state := c.QueryParam("state")

    // Validar state (CSRF protection)
    // Buscar codeVerifier (armazenado temporariamente)

    result, err := oauthService.Authenticate(provider, code, codeVerifier)
    if err != nil {
        return err
    }

    // Redirecionar para frontend com tokens
    redirectURL := fmt.Sprintf("%s/auth/callback?token=%s&refresh_token=%s",
        frontendURL, result.Token, result.RefreshToken)
    return c.Redirect(http.StatusFound, redirectURL)
}

Associar Provedor a Conta Existente

Usuário autenticado pode associar provedor OAuth à sua conta:

POST /api/auth/oauth/associate/google
{
  "code": "...",
  "code_verifier": "..."
}

Processo: 1. Valida token OAuth 2. Busca UserAuthProvider existente 3. Se não existe, cria novo 4. Se existe, atualiza 5. Retorna sucesso

Desassociar Provedor

DELETE /api/auth/oauth/disassociate/google

Processo: 1. Busca UserAuthProvider do usuário 2. Faz soft delete 3. Retorna sucesso


Configuração e Inicialização

Exemplo Completo (cmd/api/main.go)

// R2 Storage
r2Storage, err := storage.NewR2Storage(storage.R2Config{
    AccountID:       os.Getenv("R2_ACCOUNT_ID"),
    AccessKeyID:     os.Getenv("R2_ACCESS_KEY_ID"),
    SecretAccessKey: os.Getenv("R2_SECRET_ACCESS_KEY"),
    BucketName:      os.Getenv("R2_BUCKET_NAME"),
    Endpoint:        os.Getenv("R2_ENDPOINT"),
    PublicURL:       os.Getenv("R2_PUBLIC_URL"),
})
if err != nil {
    log.Printf("R2 Storage não configurado: %v", err)
}
handlers.SetR2Storage(r2Storage)

// Email Service
emailService, err := email.NewEmailServiceFromEnv()
if err != nil {
    log.Fatal("Failed to initialize email service:", err)
}

// OAuth Service
authRepo := authRepo.NewGormAuthRepository(database.DB)
providerRepo := authRepo.NewGormUserAuthProviderRepository(database.DB)
oauthService := oauthService.NewOAuthService(authRepo, providerRepo, baseURL)

Troubleshooting

R2 Storage

Problema: Upload falha - Verificar credenciais (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY) - Verificar bucket existe - Verificar permissões do bucket - Verificar endpoint está correto

Problema: URLs não funcionam - Verificar R2_PUBLIC_URL está configurado - Verificar bucket tem domínio público configurado - Verificar CORS no bucket

SendGrid

Problema: Emails não chegam - Verificar SENDGRID_API_KEY está correto - Verificar domínio verificado no SendGrid - Verificar logs do SendGrid - Verificar spam folder

Problema: Rate limit - SendGrid tem limites de envio - Verificar plano do SendGrid - Implementar queue de emails (futuro)

OAuth

Problema: Google OAuth falha - Verificar GOOGLE_CLIENT_ID e GOOGLE_CLIENT_SECRET - Verificar redirect URL está registrado no Google Console - Verificar scopes estão corretos

Problema: Apple Sign In falha - Verificar APPLE_PRIVATE_KEY está correta (formato PEM) - Verificar chave não expirou (~6 meses) - Verificar APPLE_KEY_ID e APPLE_TEAM_ID estão corretos - Verificar redirect URL está registrado no Apple Developer


Referências