1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/tenant_service.go
2026-02-06 16:28:00 +09:00

156 lines
4.7 KiB
Go

package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/utils"
"context"
"errors"
"log/slog"
"strings"
"gorm.io/gorm"
)
type TenantService interface {
RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error)
RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error)
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
ApproveTenant(ctx context.Context, id string) error
SetKetoService(keto KetoService) // 추가
}
type tenantService struct {
repo repository.TenantRepository
keto KetoService
}
func NewTenantService(repo repository.TenantRepository) TenantService {
return &tenantService{repo: repo}
}
func (s *tenantService) SetKetoService(keto KetoService) {
s.keto = keto
}
func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
return s.repo.FindByID(ctx, id)
}
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
// Validate Slug
if ok, msg := utils.ValidateSlug(slug); !ok {
return nil, errors.New(msg)
}
// 1. Check if slug exists
existing, err := s.repo.FindBySlug(ctx, slug)
if err == nil && existing != nil {
return nil, errors.New("tenant slug already exists")
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 2. Create Tenant
tenant := &domain.Tenant{
Name: name,
Slug: slug,
Description: description,
Status: domain.TenantStatusActive,
}
if err := s.repo.Create(ctx, tenant); err != nil {
return nil, err
}
// 3. Add Domains (Auto-verify for manual admin registration)
for _, d := range domains {
if err := s.repo.AddDomain(ctx, tenant.ID, d); err != nil {
slog.Error("Failed to add domain to tenant", "tenant", slug, "domain", d, "error", err)
}
}
return s.repo.FindBySlug(ctx, slug)
}
func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
// Validate Slug
if ok, msg := utils.ValidateSlug(slug); !ok {
return nil, errors.New(msg)
}
// Verify that adminEmail domain matches the requested domainName
parts := strings.Split(adminEmail, "@")
if len(parts) != 2 || parts[1] != domainName {
return nil, errors.New("admin email domain must match the tenant domain")
}
tenant := &domain.Tenant{
Name: name,
Slug: slug,
Description: description,
Status: domain.TenantStatusPending,
Config: domain.JSONMap{"adminEmail": adminEmail},
}
if err := s.repo.Create(ctx, tenant); err != nil {
return nil, err
}
// Add Domain as unverified
// TODO: Create a more nuanced AddDomain that takes 'verified' param
// For now, Repo.AddDomain sets verified=true. I should fix Repo or just manually do it here if needed.
// Let's fix Repo later.
if err := s.repo.AddDomain(ctx, tenant.ID, domainName); err != nil {
return nil, err
}
return tenant, nil
}
func (s *tenantService) ApproveTenant(ctx context.Context, id string) error {
tenant, err := s.repo.FindByID(ctx, id)
if err != nil {
return err
}
tenant.Status = domain.TenantStatusActive
if err := s.repo.Update(ctx, tenant); err != nil {
return err
}
// [Keto] Sync relation
if s.keto != nil {
// 테넌트 자체를 정의 (Zanzibar style)
// 만약 신청 시 관리자 이메일이 있었다면 해당 사용자를 찾아 admin 권한 부여 시도
if adminEmail, ok := tenant.Config["adminEmail"].(string); ok && adminEmail != "" {
slog.Info("Syncing tenant admin to Keto", "tenant", tenant.Slug, "adminEmail", adminEmail)
// 여기서는 나중에 사용자가 가입할 때 처리하거나, 이미 가입된 사용자인지 확인 필요
// 우선 테넌트 관리자 관계 생성 로직은 사용자 가입/역할 변경 시점에 주로 발생하도록 설계
}
}
return nil
}
func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
tenant, err := s.repo.FindByDomain(ctx, emailDomain)
if err != nil {
return nil, err
}
// Only return ACTIVE tenants for auto-assignment
if tenant.Status != domain.TenantStatusActive {
return nil, errors.New("tenant is not active")
}
return tenant, nil
}
func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
return s.repo.FindBySlug(ctx, slug)
}