220 lines
6.8 KiB
Go
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
|
|
}
|