forked from baron/baron-sso
Merge pull request 'feature/org-chart-tab-separation' (#568) from feature/org-chart-tab-separation into dev
Reviewed-on: baron/baron-sso#568
This commit is contained in:
@@ -268,10 +268,12 @@ func main() {
|
||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
ketoOutboxRepo := repository.NewKetoOutboxRepository(db) // Reuse or re-init
|
||||
sharedLinkRepo := repository.NewSharedLinkRepository(db)
|
||||
kratosAdminService := service.NewKratosAdminService()
|
||||
oryAdminProvider := service.NewOryProvider()
|
||||
|
||||
tenantService := service.NewTenantService(tenantRepo, userRepo, userGroupRepo, ketoOutboxRepo)
|
||||
sharedLinkService := service.NewSharedLinkService(sharedLinkRepo)
|
||||
userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, ketoOutboxRepo, kratosAdminService)
|
||||
tenantService.SetKetoService(ketoService) // Keto 주입
|
||||
|
||||
@@ -291,7 +293,7 @@ func main() {
|
||||
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService, ketoService, tenantService, authHandler)
|
||||
devHandler.HeadlessJWKS = headlessJWKSCache
|
||||
devHandler.AuditRepo = auditRepo
|
||||
tenantHandler := handler.NewTenantHandler(db, tenantService, userRepo, ketoService, ketoOutboxRepo, kratosAdminService)
|
||||
tenantHandler := handler.NewTenantHandler(db, tenantService, userRepo, ketoService, ketoOutboxRepo, kratosAdminService, sharedLinkService)
|
||||
userGroupHandler := handler.NewUserGroupHandler(userGroupService)
|
||||
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
|
||||
userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, ketoOutboxRepo, userRepo, userGroupRepo, auditRepo)
|
||||
@@ -522,6 +524,9 @@ func main() {
|
||||
api.Get("/audit", auditHandler.ListLogs)
|
||||
api.Get("/audit/auth/timeline", authHandler.GetAuthTimeline)
|
||||
|
||||
// [New] Shared Link Public API (No Auth required)
|
||||
api.Get("/public/orgchart", tenantHandler.GetPublicOrgChart)
|
||||
|
||||
// Public Tenant Registration
|
||||
api.Post("/tenants/registration", tenantHandler.RegisterTenantPublic)
|
||||
|
||||
@@ -615,6 +620,12 @@ func main() {
|
||||
// Tenant Management (Mixed roles, handler filters results)
|
||||
admin.Get("/tenants", requireAnyUser, tenantHandler.ListTenants)
|
||||
admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant)
|
||||
|
||||
// [New] Shared Link Management
|
||||
admin.Post("/tenants/:id/share-links", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.CreateShareLink)
|
||||
admin.Get("/tenants/:id/share-links", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.ListShareLinks)
|
||||
admin.Delete("/share-links/:id", requireAdmin, tenantHandler.DeleteShareLink)
|
||||
|
||||
admin.Delete("/tenants/bulk", requireSuperAdmin, tenantHandler.DeleteTenantsBulk)
|
||||
admin.Post("/tenants/:id/approve", requireSuperAdmin, tenantHandler.ApproveTenant)
|
||||
admin.Get("/tenants/:id", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.GetTenant)
|
||||
|
||||
@@ -41,6 +41,7 @@ func migrateSchemas(db *gorm.DB) error {
|
||||
&domain.ClientSecret{},
|
||||
&domain.ClientConsent{},
|
||||
&domain.KetoOutbox{},
|
||||
&domain.SharedLink{},
|
||||
// &domain.RelyingParty{}, // Removed: SSOT is Hydra + Keto
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
53
backend/internal/domain/shared_link.go
Normal file
53
backend/internal/domain/shared_link.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SharedLink struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
||||
TenantID string `gorm:"type:uuid;not null;index" json:"tenantId"`
|
||||
Token string `gorm:"uniqueIndex;not null" json:"token"`
|
||||
Name string `gorm:"not null" json:"name"` // 링크 식별을 위한 이름 (예: "24년 상반기 채용공고용")
|
||||
Description string `json:"description"`
|
||||
AccessLevel string `gorm:"default:'READ_ONLY'" json:"accessLevel"`
|
||||
IsActive bool `gorm:"default:true" json:"isActive"`
|
||||
ExpiresAt *time.Time `json:"expiresAt"`
|
||||
Password string `json:"-"` // 필요 시 비밀번호 (선택 사항)
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Relation
|
||||
Tenant Tenant `gorm:"foreignKey:TenantID" json:"-"`
|
||||
}
|
||||
|
||||
func (s *SharedLink) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.NewString()
|
||||
}
|
||||
if s.Token == "" {
|
||||
// 32바이트(64자)의 강력한 난수 토큰 생성
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Token = hex.EncodeToString(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SharedLink) IsValid() bool {
|
||||
if !s.IsActive {
|
||||
return false
|
||||
}
|
||||
if s.ExpiresAt != nil && s.ExpiresAt.Before(time.Now()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -3784,6 +3784,13 @@ func extractFirstString(data map[string]interface{}, keys ...string) string {
|
||||
if str, ok := val.(string); ok && str != "" {
|
||||
return str
|
||||
}
|
||||
// Handle numeric types by converting to string
|
||||
if num, ok := val.(float64); ok {
|
||||
return fmt.Sprint(num)
|
||||
}
|
||||
if num, ok := val.(int); ok {
|
||||
return fmt.Sprint(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
@@ -114,10 +114,20 @@ func (m *AsyncMockUserRepo) CountByTenant(ctx context.Context, tenantID string)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, tenantIDs)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ type TenantHandler struct {
|
||||
Keto service.KetoService
|
||||
KetoOutbox repository.KetoOutboxRepository
|
||||
KratosAdmin service.KratosAdminService
|
||||
SharedLink service.SharedLinkService
|
||||
}
|
||||
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repository.UserRepository, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService) *TenantHandler {
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repository.UserRepository, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService, sharedLink service.SharedLinkService) *TenantHandler {
|
||||
return &TenantHandler{
|
||||
DB: db,
|
||||
Service: svc,
|
||||
@@ -30,6 +31,7 @@ func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repositor
|
||||
Keto: keto,
|
||||
KetoOutbox: outbox,
|
||||
KratosAdmin: kratos,
|
||||
SharedLink: sharedLink,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,3 +867,136 @@ func normalizeTenantType(value string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (h *TenantHandler) CreateShareLink(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ExpiresAt *time.Time `json:"expiresAt"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
link, err := h.SharedLink.CreateLink(c.Context(), tenantID, req.Name, req.Description, req.ExpiresAt)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(link)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ListShareLinks(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
links, err := h.SharedLink.GetLinksByTenant(c.Context(), tenantID)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.JSON(links)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) DeleteShareLink(c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := h.SharedLink.DeactivateLink(c.Context(), id); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.JSON(fiber.Map{"message": "Share link deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "share token is required")
|
||||
}
|
||||
|
||||
link, err := h.SharedLink.ValidateToken(c.Context(), token)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
parentMap := make(map[string]string)
|
||||
for _, t := range allTenants {
|
||||
if t.ParentID != nil {
|
||||
parentMap[t.ID] = *t.ParentID
|
||||
}
|
||||
}
|
||||
|
||||
findRoot := func(id string) string {
|
||||
curr := id
|
||||
for {
|
||||
p, exists := parentMap[curr]
|
||||
if !exists || p == "" { break }
|
||||
curr = p
|
||||
}
|
||||
return curr
|
||||
}
|
||||
|
||||
sharedRootID := findRoot(link.TenantID)
|
||||
var filteredTenants []domain.Tenant
|
||||
var tenantIDs []string
|
||||
var slugs []string
|
||||
|
||||
for _, t := range allTenants {
|
||||
if findRoot(t.ID) == sharedRootID {
|
||||
filteredTenants = append(filteredTenants, t)
|
||||
tenantIDs = append(tenantIDs, t.ID)
|
||||
slugs = append(slugs, t.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
type publicUserSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
CompanyCode string `json:"companyCode"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var publicUsers []publicUserSummary
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Fetch users by IDs
|
||||
var usersByID []domain.User
|
||||
h.DB.Where("tenant_id IN ?", tenantIDs).Preload("Tenant").Find(&usersByID)
|
||||
for _, u := range usersByID {
|
||||
if u.Status != "active" || seen[u.ID] { continue }
|
||||
seen[u.ID] = true
|
||||
cc := u.CompanyCode
|
||||
if cc == "" && u.Tenant != nil { cc = u.Tenant.Slug }
|
||||
publicUsers = append(publicUsers, publicUserSummary{
|
||||
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch users by Slugs
|
||||
var usersBySlug []domain.User
|
||||
h.DB.Where("company_code IN ?", slugs).Preload("Tenant").Find(&usersBySlug)
|
||||
for _, u := range usersBySlug {
|
||||
if u.Status != "active" || seen[u.ID] { continue }
|
||||
seen[u.ID] = true
|
||||
cc := u.CompanyCode
|
||||
if cc == "" && u.Tenant != nil { cc = u.Tenant.Slug }
|
||||
publicUsers = append(publicUsers, publicUserSummary{
|
||||
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
||||
})
|
||||
}
|
||||
|
||||
tenantSummaries := make([]tenantSummary, 0, len(filteredTenants))
|
||||
for _, t := range filteredTenants {
|
||||
tenantSummaries = append(tenantSummaries, mapTenantSummary(t))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"tenants": tenantSummaries,
|
||||
"users": publicUsers,
|
||||
"sharedWith": link.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,10 +127,20 @@ func (m *MockUserRepoForHandler) CountByTenant(ctx context.Context, tenantID str
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, tenantIDs)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
if args.Get(0) == nil {
|
||||
|
||||
@@ -1266,6 +1266,25 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if traits == nil {
|
||||
traits = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// [Preserve & Merge] Multi-Tenant Info
|
||||
var existingCodes []string
|
||||
if codes, ok := traits["companyCodes"].([]interface{}); ok {
|
||||
for _, v := range codes {
|
||||
if str, ok := v.(string); ok && str != "" {
|
||||
existingCodes = append(existingCodes, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keto에서 "실제" 소속 정보를 먼저 확인 (엑셀 임포트 사용자 대응)
|
||||
if len(existingCodes) <= 1 && h.TenantService != nil {
|
||||
if joined, err := h.TenantService.ListJoinedTenants(c.Context(), userID); err == nil {
|
||||
for _, t := range joined {
|
||||
existingCodes = append(existingCodes, t.Slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
traits["name"] = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
@@ -1286,7 +1305,33 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
traits["tenant_id"] = tenant.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Add to existingCodes if not present
|
||||
found := false
|
||||
for _, existing := range existingCodes {
|
||||
if existing == code {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && code != "" {
|
||||
existingCodes = append(existingCodes, code)
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and save back companyCodes
|
||||
var uniqueCodes []string
|
||||
seenCodes := map[string]bool{}
|
||||
for _, c := range existingCodes {
|
||||
if !seenCodes[c] && c != "" {
|
||||
seenCodes[c] = true
|
||||
uniqueCodes = append(uniqueCodes, c)
|
||||
}
|
||||
}
|
||||
if len(uniqueCodes) > 0 {
|
||||
traits["companyCodes"] = uniqueCodes
|
||||
}
|
||||
|
||||
if req.Department != nil {
|
||||
traits["department"] = strings.TrimSpace(*req.Department)
|
||||
}
|
||||
@@ -1420,16 +1465,32 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// [Self-Healing] If the UI explicitly assigned the tenant, force a Keto relation sync.
|
||||
// This fixes issues where local DB had the tenant, but Keto failed to create the relation previously.
|
||||
if req.CompanyCode != nil && h.KetoOutboxRepo != nil && updatedLocalUser.TenantID != nil {
|
||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *updatedLocalUser.TenantID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + updatedLocalUser.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
// [Self-Healing] Sync all companyCodes to Keto
|
||||
if h.KetoOutboxRepo != nil && h.TenantService != nil {
|
||||
if codes, ok := updated.Traits["companyCodes"].([]interface{}); ok {
|
||||
for _, cVal := range codes {
|
||||
if cStr, ok := cVal.(string); ok && cStr != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(bgCtx, cStr); err == nil && tenant != nil {
|
||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + updatedLocalUser.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if updatedLocalUser.TenantID != nil {
|
||||
// Fallback if companyCodes doesn't exist
|
||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *updatedLocalUser.TenantID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + updatedLocalUser.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
51
backend/internal/repository/shared_link_repository.go
Normal file
51
backend/internal/repository/shared_link_repository.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SharedLinkRepository interface {
|
||||
Create(ctx context.Context, link *domain.SharedLink) error
|
||||
FindByToken(ctx context.Context, token string) (*domain.SharedLink, error)
|
||||
FindByTenantID(ctx context.Context, tenantID string) ([]domain.SharedLink, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
Update(ctx context.Context, link *domain.SharedLink) error
|
||||
}
|
||||
|
||||
type sharedLinkRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSharedLinkRepository(db *gorm.DB) SharedLinkRepository {
|
||||
return &sharedLinkRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *sharedLinkRepository) Create(ctx context.Context, link *domain.SharedLink) error {
|
||||
return r.db.WithContext(ctx).Create(link).Error
|
||||
}
|
||||
|
||||
func (r *sharedLinkRepository) FindByToken(ctx context.Context, token string) (*domain.SharedLink, error) {
|
||||
var link domain.SharedLink
|
||||
err := r.db.WithContext(ctx).Where("token = ? AND is_active = ?", token, true).First(&link).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (r *sharedLinkRepository) FindByTenantID(ctx context.Context, tenantID string) ([]domain.SharedLink, error) {
|
||||
var links []domain.SharedLink
|
||||
err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Find(&links).Error
|
||||
return links, err
|
||||
}
|
||||
|
||||
func (r *sharedLinkRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Delete(&domain.SharedLink{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *sharedLinkRepository) Update(ctx context.Context, link *domain.SharedLink) error {
|
||||
return r.db.WithContext(ctx).Save(link).Error
|
||||
}
|
||||
@@ -20,6 +20,8 @@ type UserRepository interface {
|
||||
CountByTenant(ctx context.Context, tenantID string) (int64, error)
|
||||
CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error)
|
||||
CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error)
|
||||
FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error)
|
||||
FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Multiple identifiers support
|
||||
@@ -261,3 +263,15 @@ func (r *userRepository) FindTenantIDByLoginID(ctx context.Context, loginID stri
|
||||
}
|
||||
return record.TenantID, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
err := r.db.WithContext(ctx).Where("tenant_id IN ?", tenantIDs).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
err := r.db.WithContext(ctx).Where("company_code IN ?", codes).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
63
backend/internal/service/shared_link_service.go
Normal file
63
backend/internal/service/shared_link_service.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SharedLinkService interface {
|
||||
CreateLink(ctx context.Context, tenantID, name, description string, expiresAt *time.Time) (*domain.SharedLink, error)
|
||||
ValidateToken(ctx context.Context, token string) (*domain.SharedLink, error)
|
||||
GetLinksByTenant(ctx context.Context, tenantID string) ([]domain.SharedLink, error)
|
||||
DeactivateLink(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type sharedLinkService struct {
|
||||
repo repository.SharedLinkRepository
|
||||
}
|
||||
|
||||
func NewSharedLinkService(repo repository.SharedLinkRepository) SharedLinkService {
|
||||
return &sharedLinkService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *sharedLinkService) CreateLink(ctx context.Context, tenantID, name, description string, expiresAt *time.Time) (*domain.SharedLink, error) {
|
||||
link := &domain.SharedLink{
|
||||
TenantID: tenantID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
ExpiresAt: expiresAt,
|
||||
IsActive: true,
|
||||
AccessLevel: "READ_ONLY",
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, link); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (s *sharedLinkService) ValidateToken(ctx context.Context, token string) (*domain.SharedLink, error) {
|
||||
link, err := s.repo.FindByToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid or expired share link")
|
||||
}
|
||||
|
||||
if !link.IsValid() {
|
||||
return nil, errors.New("share link has expired or is inactive")
|
||||
}
|
||||
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func (s *sharedLinkService) GetLinksByTenant(ctx context.Context, tenantID string) ([]domain.SharedLink, error) {
|
||||
return s.repo.FindByTenantID(ctx, tenantID)
|
||||
}
|
||||
|
||||
func (s *sharedLinkService) DeactivateLink(ctx context.Context, id string) error {
|
||||
// 실제 삭제 대신 비활성화 처리 (soft-delete와 유사)
|
||||
// 하지만 여기서는 간단히 활성 플래그만 끔
|
||||
return s.repo.Delete(ctx, id) // 리포지토리의 Delete는 GORM의 DeletedAt을 사용하여 soft-delete함
|
||||
}
|
||||
@@ -143,6 +143,11 @@ func (m *MockUserRepoForTenant) CountByTenant(ctx context.Context, tenantID stri
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, tenantIDs)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
|
||||
args := m.Called(tenantIDs)
|
||||
if args.Get(0) == nil {
|
||||
@@ -151,6 +156,11 @@ func (m *MockUserRepoForTenant) CountByTenantIDs(ctx context.Context, tenantIDs
|
||||
return args.Get(0).(map[string]int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
if args.Get(0) == nil {
|
||||
|
||||
@@ -86,6 +86,11 @@ func (m *MockUserRepository) CountByTenant(ctx context.Context, tenantID string)
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, tenantIDs)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
|
||||
args := m.Called(tenantIDs)
|
||||
if args.Get(0) == nil {
|
||||
@@ -94,6 +99,11 @@ func (m *MockUserRepository) CountByTenantIDs(ctx context.Context, tenantIDs []s
|
||||
return args.Get(0).(map[string]int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
|
||||
args := m.Called(ctx, codes)
|
||||
if args.Get(0) == nil {
|
||||
|
||||
Reference in New Issue
Block a user