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 }