Files
techxcar/backend/internal/tenant/repository.go
T
Luciano Milani 5de37bb512 Inicial
2026-07-02 12:47:55 +01:00

220 lines
6.8 KiB
Go

package tenant
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/techxcar/backend/pkg/database"
)
var uuidRe = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
type SuperAdmin struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
type Tenant struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type TenantUser struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
Role string `json:"role"`
Name string `json:"name"`
Active bool `json:"active"`
}
type Invite struct {
ID string `json:"id"`
TenantID *string `json:"tenant_id"`
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
UsedAt *time.Time `json:"used_at"`
CreatedAt time.Time `json:"created_at"`
}
type Repository struct {
db *database.DB
}
func NewRepository(db *database.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) GetSuperAdminByEmail(ctx context.Context, email string) (*SuperAdmin, error) {
row := r.db.Pool.QueryRow(ctx,
`SELECT id, email, password_hash, created_at FROM super_admins WHERE email = $1`, email)
var a SuperAdmin
err := row.Scan(&a.ID, &a.Email, &a.PasswordHash, &a.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("tenant: get super admin: %w", err)
}
return &a, nil
}
func (r *Repository) CreateSuperAdmin(ctx context.Context, email, passwordHash string) (*SuperAdmin, error) {
row := r.db.Pool.QueryRow(ctx,
`INSERT INTO super_admins (email, password_hash) VALUES ($1, $2)
RETURNING id, email, password_hash, created_at`,
email, passwordHash)
var a SuperAdmin
if err := row.Scan(&a.ID, &a.Email, &a.PasswordHash, &a.CreatedAt); err != nil {
return nil, fmt.Errorf("tenant: create super admin: %w", err)
}
return &a, nil
}
func (r *Repository) GetTenantBySlug(ctx context.Context, slug string) (*Tenant, error) {
row := r.db.Pool.QueryRow(ctx,
`SELECT id, slug, name, status, created_at FROM tenants WHERE slug = $1`, slug)
var t Tenant
err := row.Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("tenant: get by slug: %w", err)
}
return &t, nil
}
func (r *Repository) GetTenantByID(ctx context.Context, id string) (*Tenant, error) {
row := r.db.Pool.QueryRow(ctx,
`SELECT id, slug, name, status, created_at FROM tenants WHERE id = $1`, id)
var t Tenant
err := row.Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("tenant: get by id: %w", err)
}
return &t, nil
}
func (r *Repository) ListTenants(ctx context.Context) ([]*Tenant, error) {
rows, err := r.db.Pool.Query(ctx,
`SELECT id, slug, name, status, created_at FROM tenants ORDER BY created_at DESC`)
if err != nil {
return nil, fmt.Errorf("tenant: list: %w", err)
}
defer rows.Close()
var list []*Tenant
for rows.Next() {
var t Tenant
if err := rows.Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt); err != nil {
return nil, fmt.Errorf("tenant: list scan: %w", err)
}
list = append(list, &t)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("tenant: list rows: %w", err)
}
return list, nil
}
func (r *Repository) CreateTenant(ctx context.Context, slug, name string) (*Tenant, error) {
row := r.db.Pool.QueryRow(ctx,
`INSERT INTO tenants (slug, name) VALUES ($1, $2)
RETURNING id, slug, name, status, created_at`,
slug, name)
var t Tenant
if err := row.Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt); err != nil {
return nil, fmt.Errorf("tenant: create: %w", err)
}
return &t, nil
}
func (r *Repository) GetTenantUserByEmail(ctx context.Context, tenantID, email string) (*TenantUser, error) {
if !uuidRe.MatchString(tenantID) {
return nil, fmt.Errorf("tenant: invalid tenant ID format")
}
schema := `"tenant_` + strings.ReplaceAll(tenantID, "-", "_") + `"`
row := r.db.Pool.QueryRow(ctx,
fmt.Sprintf(`SELECT id, email, password_hash, role, name, active FROM %s.users WHERE email = $1`, schema),
email)
var u TenantUser
err := row.Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.Name, &u.Active)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("tenant: get user: %w", err)
}
return &u, nil
}
func (r *Repository) CreateTenantUser(ctx context.Context, tenantID, email, passwordHash, name, role string) (*TenantUser, error) {
if !uuidRe.MatchString(tenantID) {
return nil, fmt.Errorf("tenant: invalid tenant ID format")
}
schema := `"tenant_` + strings.ReplaceAll(tenantID, "-", "_") + `"`
row := r.db.Pool.QueryRow(ctx,
fmt.Sprintf(`INSERT INTO %s.users (email, password_hash, name, role)
VALUES ($1, $2, $3, $4) RETURNING id, email, password_hash, role, name, active`, schema),
email, passwordHash, name, role)
var u TenantUser
if err := row.Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.Name, &u.Active); err != nil {
return nil, fmt.Errorf("tenant: create user: %w", err)
}
return &u, nil
}
func (r *Repository) CreateInvite(ctx context.Context, tenantID *string, expiresIn time.Duration) (*Invite, error) {
token := uuid.New().String()
expiresAt := time.Now().Add(expiresIn)
row := r.db.Pool.QueryRow(ctx,
`INSERT INTO invites (tenant_id, token, expires_at) VALUES ($1, $2, $3)
RETURNING id, tenant_id, token, expires_at, used_at, created_at`,
tenantID, token, expiresAt)
var inv Invite
if err := row.Scan(&inv.ID, &inv.TenantID, &inv.Token, &inv.ExpiresAt, &inv.UsedAt, &inv.CreatedAt); err != nil {
return nil, fmt.Errorf("tenant: create invite: %w", err)
}
return &inv, nil
}
func (r *Repository) GetInviteByToken(ctx context.Context, token string) (*Invite, error) {
row := r.db.Pool.QueryRow(ctx,
`SELECT id, tenant_id, token, expires_at, used_at, created_at FROM invites WHERE token = $1`, token)
var inv Invite
err := row.Scan(&inv.ID, &inv.TenantID, &inv.Token, &inv.ExpiresAt, &inv.UsedAt, &inv.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("tenant: get invite: %w", err)
}
return &inv, nil
}
func (r *Repository) UseInvite(ctx context.Context, inviteID string) error {
tag, err := r.db.Pool.Exec(ctx,
`UPDATE invites SET used_at = NOW() WHERE id = $1`, inviteID)
if err != nil {
return fmt.Errorf("tenant: use invite: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("tenant: invite not found: %s", inviteID)
}
return nil
}