- Validação cross-tenant no login e rotas protegidas
- File serving via /api/files/{bucket}/{path} (eliminação DNS)
- Mensagens de erro humanizadas inline (sem pop-ups)
- Middleware tenant detection via headers customizados
- Upload de logos retorna URLs via API
- README atualizado com changelog v1.4 completo
400 lines
8.8 KiB
Go
400 lines
8.8 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
|
|
"aggios-app/backend/internal/domain"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TenantRepository handles database operations for tenants
|
|
type TenantRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewTenantRepository creates a new tenant repository
|
|
func NewTenantRepository(db *sql.DB) *TenantRepository {
|
|
return &TenantRepository{db: db}
|
|
}
|
|
|
|
// DB returns the underlying database connection
|
|
func (r *TenantRepository) DB() *sql.DB {
|
|
return r.db
|
|
}
|
|
|
|
// Create creates a new tenant
|
|
func (r *TenantRepository) Create(tenant *domain.Tenant) error {
|
|
query := `
|
|
INSERT INTO tenants (
|
|
id, name, domain, subdomain, cnpj, razao_social, email, phone, website,
|
|
address, neighborhood, number, complement, city, state, zip,
|
|
description, industry, team_size, primary_color, secondary_color,
|
|
logo_url, logo_horizontal_url, created_at, updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
now := time.Now()
|
|
tenant.ID = uuid.New()
|
|
tenant.CreatedAt = now
|
|
tenant.UpdatedAt = now
|
|
|
|
return r.db.QueryRow(
|
|
query,
|
|
tenant.ID,
|
|
tenant.Name,
|
|
tenant.Domain,
|
|
tenant.Subdomain,
|
|
tenant.CNPJ,
|
|
tenant.RazaoSocial,
|
|
tenant.Email,
|
|
tenant.Phone,
|
|
tenant.Website,
|
|
tenant.Address,
|
|
tenant.Neighborhood,
|
|
tenant.Number,
|
|
tenant.Complement,
|
|
tenant.City,
|
|
tenant.State,
|
|
tenant.Zip,
|
|
tenant.Description,
|
|
tenant.Industry,
|
|
tenant.TeamSize,
|
|
tenant.PrimaryColor,
|
|
tenant.SecondaryColor,
|
|
tenant.LogoURL,
|
|
tenant.LogoHorizontalURL,
|
|
tenant.CreatedAt,
|
|
tenant.UpdatedAt,
|
|
).Scan(&tenant.ID, &tenant.CreatedAt, &tenant.UpdatedAt)
|
|
}
|
|
|
|
// FindByID finds a tenant by ID
|
|
func (r *TenantRepository) FindByID(id uuid.UUID) (*domain.Tenant, error) {
|
|
query := `
|
|
SELECT id, name, domain, subdomain, cnpj, razao_social, email, phone, website,
|
|
address, neighborhood, number, complement, city, state, zip, description, industry, team_size,
|
|
primary_color, secondary_color, logo_url, logo_horizontal_url,
|
|
is_active, created_at, updated_at
|
|
FROM tenants
|
|
WHERE id = $1
|
|
`
|
|
|
|
tenant := &domain.Tenant{}
|
|
var cnpj, razaoSocial, email, phone, website, address, neighborhood, number, complement, city, state, zip, description, industry, teamSize, primaryColor, secondaryColor, logoURL, logoHorizontalURL sql.NullString
|
|
|
|
err := r.db.QueryRow(query, id).Scan(
|
|
&tenant.ID,
|
|
&tenant.Name,
|
|
&tenant.Domain,
|
|
&tenant.Subdomain,
|
|
&cnpj,
|
|
&razaoSocial,
|
|
&email,
|
|
&phone,
|
|
&website,
|
|
&address,
|
|
&neighborhood,
|
|
&number,
|
|
&complement,
|
|
&city,
|
|
&state,
|
|
&zip,
|
|
&description,
|
|
&industry,
|
|
&teamSize,
|
|
&primaryColor,
|
|
&secondaryColor,
|
|
&logoURL,
|
|
&logoHorizontalURL,
|
|
&tenant.IsActive,
|
|
&tenant.CreatedAt,
|
|
&tenant.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if cnpj.Valid {
|
|
tenant.CNPJ = cnpj.String
|
|
}
|
|
if razaoSocial.Valid {
|
|
tenant.RazaoSocial = razaoSocial.String
|
|
}
|
|
if email.Valid {
|
|
tenant.Email = email.String
|
|
}
|
|
if phone.Valid {
|
|
tenant.Phone = phone.String
|
|
}
|
|
if website.Valid {
|
|
tenant.Website = website.String
|
|
}
|
|
if address.Valid {
|
|
tenant.Address = address.String
|
|
}
|
|
if neighborhood.Valid {
|
|
tenant.Neighborhood = neighborhood.String
|
|
}
|
|
if number.Valid {
|
|
tenant.Number = number.String
|
|
}
|
|
if complement.Valid {
|
|
tenant.Complement = complement.String
|
|
}
|
|
if city.Valid {
|
|
tenant.City = city.String
|
|
}
|
|
if state.Valid {
|
|
tenant.State = state.String
|
|
}
|
|
if zip.Valid {
|
|
tenant.Zip = zip.String
|
|
}
|
|
if description.Valid {
|
|
tenant.Description = description.String
|
|
}
|
|
if industry.Valid {
|
|
tenant.Industry = industry.String
|
|
}
|
|
if teamSize.Valid {
|
|
tenant.TeamSize = teamSize.String
|
|
}
|
|
if primaryColor.Valid {
|
|
tenant.PrimaryColor = primaryColor.String
|
|
}
|
|
if secondaryColor.Valid {
|
|
tenant.SecondaryColor = secondaryColor.String
|
|
}
|
|
if logoURL.Valid {
|
|
tenant.LogoURL = logoURL.String
|
|
}
|
|
if logoHorizontalURL.Valid {
|
|
tenant.LogoHorizontalURL = logoHorizontalURL.String
|
|
}
|
|
|
|
return tenant, nil
|
|
}
|
|
|
|
// FindBySubdomain finds a tenant by subdomain
|
|
func (r *TenantRepository) FindBySubdomain(subdomain string) (*domain.Tenant, error) {
|
|
query := `
|
|
SELECT id, name, domain, subdomain, primary_color, secondary_color, logo_url, logo_horizontal_url, created_at, updated_at
|
|
FROM tenants
|
|
WHERE subdomain = $1
|
|
`
|
|
|
|
tenant := &domain.Tenant{}
|
|
var primaryColor, secondaryColor, logoURL, logoHorizontalURL sql.NullString
|
|
|
|
err := r.db.QueryRow(query, subdomain).Scan(
|
|
&tenant.ID,
|
|
&tenant.Name,
|
|
&tenant.Domain,
|
|
&tenant.Subdomain,
|
|
&primaryColor,
|
|
&secondaryColor,
|
|
&logoURL,
|
|
&logoHorizontalURL,
|
|
&tenant.CreatedAt,
|
|
&tenant.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if primaryColor.Valid {
|
|
tenant.PrimaryColor = primaryColor.String
|
|
}
|
|
if secondaryColor.Valid {
|
|
tenant.SecondaryColor = secondaryColor.String
|
|
}
|
|
if logoURL.Valid {
|
|
tenant.LogoURL = logoURL.String
|
|
}
|
|
if logoHorizontalURL.Valid {
|
|
tenant.LogoHorizontalURL = logoHorizontalURL.String
|
|
}
|
|
|
|
return tenant, nil
|
|
}
|
|
|
|
// SubdomainExists checks if a subdomain is already taken
|
|
func (r *TenantRepository) SubdomainExists(subdomain string) (bool, error) {
|
|
var exists bool
|
|
query := `SELECT EXISTS(SELECT 1 FROM tenants WHERE subdomain = $1)`
|
|
err := r.db.QueryRow(query, subdomain).Scan(&exists)
|
|
return exists, err
|
|
}
|
|
|
|
// FindAll returns all tenants
|
|
func (r *TenantRepository) FindAll() ([]*domain.Tenant, error) {
|
|
query := `
|
|
SELECT id, name, domain, subdomain, email, phone, cnpj, logo_url, is_active, created_at, updated_at
|
|
FROM tenants
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
rows, err := r.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var tenants []*domain.Tenant
|
|
for rows.Next() {
|
|
tenant := &domain.Tenant{}
|
|
var email, phone, cnpj, logoURL sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&tenant.ID,
|
|
&tenant.Name,
|
|
&tenant.Domain,
|
|
&tenant.Subdomain,
|
|
&email,
|
|
&phone,
|
|
&cnpj,
|
|
&logoURL,
|
|
&tenant.IsActive,
|
|
&tenant.CreatedAt,
|
|
&tenant.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if email.Valid {
|
|
tenant.Email = email.String
|
|
}
|
|
if phone.Valid {
|
|
tenant.Phone = phone.String
|
|
}
|
|
if cnpj.Valid {
|
|
tenant.CNPJ = cnpj.String
|
|
}
|
|
if logoURL.Valid {
|
|
tenant.LogoURL = logoURL.String
|
|
}
|
|
|
|
tenants = append(tenants, tenant)
|
|
}
|
|
|
|
if tenants == nil {
|
|
return []*domain.Tenant{}, nil
|
|
}
|
|
|
|
return tenants, nil
|
|
}
|
|
|
|
// Delete removes a tenant (and cascades to related data)
|
|
func (r *TenantRepository) Delete(id uuid.UUID) error {
|
|
// Start transaction
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete all users associated with this tenant first
|
|
_, err = tx.Exec(`DELETE FROM users WHERE tenant_id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete the tenant
|
|
result, err := tx.Exec(`DELETE FROM tenants WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rows == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
|
|
// Commit transaction
|
|
return tx.Commit()
|
|
}
|
|
|
|
// UpdateProfile updates tenant profile information
|
|
func (r *TenantRepository) UpdateProfile(id uuid.UUID, updates map[string]interface{}) error {
|
|
query := `
|
|
UPDATE tenants SET
|
|
name = COALESCE($1, name),
|
|
cnpj = COALESCE($2, cnpj),
|
|
razao_social = COALESCE($3, razao_social),
|
|
email = COALESCE($4, email),
|
|
phone = COALESCE($5, phone),
|
|
website = COALESCE($6, website),
|
|
address = COALESCE($7, address),
|
|
neighborhood = COALESCE($8, neighborhood),
|
|
number = COALESCE($9, number),
|
|
complement = COALESCE($10, complement),
|
|
city = COALESCE($11, city),
|
|
state = COALESCE($12, state),
|
|
zip = COALESCE($13, zip),
|
|
description = COALESCE($14, description),
|
|
industry = COALESCE($15, industry),
|
|
team_size = COALESCE($16, team_size),
|
|
primary_color = COALESCE($17, primary_color),
|
|
secondary_color = COALESCE($18, secondary_color),
|
|
logo_url = COALESCE($19, logo_url),
|
|
logo_horizontal_url = COALESCE($20, logo_horizontal_url),
|
|
updated_at = $21
|
|
WHERE id = $22
|
|
`
|
|
|
|
_, err := r.db.Exec(
|
|
query,
|
|
updates["name"],
|
|
updates["cnpj"],
|
|
updates["razao_social"],
|
|
updates["email"],
|
|
updates["phone"],
|
|
updates["website"],
|
|
updates["address"],
|
|
updates["neighborhood"],
|
|
updates["number"],
|
|
updates["complement"],
|
|
updates["city"],
|
|
updates["state"],
|
|
updates["zip"],
|
|
updates["description"],
|
|
updates["industry"],
|
|
updates["team_size"],
|
|
updates["primary_color"],
|
|
updates["secondary_color"],
|
|
updates["logo_url"],
|
|
updates["logo_horizontal_url"],
|
|
time.Now(),
|
|
id,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// UpdateStatus updates the is_active status of a tenant
|
|
func (r *TenantRepository) UpdateStatus(id uuid.UUID, isActive bool) error {
|
|
query := `UPDATE tenants SET is_active = $1, updated_at = $2 WHERE id = $3`
|
|
_, err := r.db.Exec(query, isActive, time.Now(), id)
|
|
return err
|
|
}
|