Backend - Integrações Externas¶
Este documento descreve todas as integrações externas do backend, incluindo configuração, uso e exemplos.
Índice¶
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 contaSendPasswordResetEmail- 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