package service import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "context" "fmt" "log/slog" ) type RelyingPartyService interface { Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) ListAll(ctx context.Context) ([]domain.RelyingParty, error) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error) Delete(ctx context.Context, clientID string) error } func (s *relyingPartyService) ListAll(ctx context.Context) ([]domain.RelyingParty, error) { return s.repo.ListAll(ctx) } func (s *relyingPartyService) ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error) { // Simple implementation for now, repository could be optimized with IN clause var allRps []domain.RelyingParty for _, tid := range tenantIDs { rps, _ := s.repo.ListByTenantID(ctx, tid) allRps = append(allRps, rps...) } return allRps, nil } type relyingPartyService struct { repo repository.RelyingPartyRepository hydraService *HydraAdminService ketoService KetoService } func NewRelyingPartyService( repo repository.RelyingPartyRepository, hydraService *HydraAdminService, ketoService KetoService, ) RelyingPartyService { return &relyingPartyService{ repo: repo, hydraService: hydraService, ketoService: ketoService, } } func (s *relyingPartyService) Create(ctx context.Context, tenantID string, client domain.HydraClient) (*domain.RelyingParty, error) { // 1. Create Client in Hydra // Ensure metadata contains tenant_id for reference if client.Metadata == nil { client.Metadata = make(map[string]interface{}) } client.Metadata["tenant_id"] = tenantID createdClient, err := s.hydraService.CreateClient(ctx, client) if err != nil { return nil, fmt.Errorf("failed to create hydra client: %w", err) } // 2. Create Record in DB rp := &domain.RelyingParty{ ClientID: createdClient.ClientID, TenantID: tenantID, Name: createdClient.ClientName, Description: "", // Hydra doesn't have description field standard, maybe in metadata? } if err := s.repo.Create(ctx, rp); err != nil { // Rollback: Delete Hydra Client _ = s.hydraService.DeleteClient(ctx, createdClient.ClientID) return nil, fmt.Errorf("failed to create relying party in db: %w", err) } // 3. Create Relation in Keto // RelyingParty:#parent_tenant@Tenant: err = s.ketoService.CreateRelation(ctx, "RelyingParty", createdClient.ClientID, "parent_tenant", "Tenant:"+tenantID) if err != nil { slog.Error("Failed to create keto relation for relying party", "error", err, "client_id", createdClient.ClientID) // We don't rollback here, but we should probably have a background job to fix this. // Or return error and let caller decide? For MVP, logging error is acceptable as per issue discussion (Eventual Consistency preferred). } return rp, nil } func (s *relyingPartyService) Get(ctx context.Context, clientID string) (*domain.RelyingParty, *domain.HydraClient, error) { // Get from DB rp, err := s.repo.FindByID(ctx, clientID) if err != nil { return nil, nil, err } // Get from Hydra hydraClient, err := s.hydraService.GetClient(ctx, clientID) if err != nil { return nil, nil, err } return rp, hydraClient, nil } func (s *relyingPartyService) List(ctx context.Context, tenantID string) ([]domain.RelyingParty, error) { return s.repo.ListByTenantID(ctx, tenantID) } func (s *relyingPartyService) Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error) { // Update Hydra updatedClient, err := s.hydraService.UpdateClient(ctx, clientID, client) if err != nil { return nil, err } // Update DB rp, err := s.repo.FindByID(ctx, clientID) if err != nil { return nil, err } rp.Name = updatedClient.ClientName // Update other fields if necessary if err := s.repo.Update(ctx, rp); err != nil { return nil, err } return rp, nil } func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error { // Delete from DB if err := s.repo.Delete(ctx, clientID); err != nil { return err } // Delete from Hydra if err := s.hydraService.DeleteClient(ctx, clientID); err != nil { slog.Error("Failed to delete hydra client", "error", err, "client_id", clientID) // Proceeding... } // Delete from Keto (Optional, but good practice to clean up) // We might not know the tenant ID here without querying DB first, but if DB is deleted, we might miss it. //Ideally, we should query DB first. // But `DeleteRelation` requires specific object/relation/subject. // If we want to delete ALL relations for this object, Keto API supports that? // `DeleteRelation` in our service wrapper is specific. // We can skip explicit Keto deletion for now as orphaned tuples are less critical than orphaned resources. return nil }