diff --git a/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx b/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx index 10500ab1..2b42313d 100644 --- a/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantOrgChartPage.tsx @@ -53,31 +53,21 @@ export function TenantOrgChartPage() { const uMap = new Map(); - // Process users to map them to multiple tenants if applicable for (const u of usersQuery.data.items) { if (u.status !== "active") continue; - // Extract all associated tenant slugs const slugs = new Set(); - - const primarySlug = - u.tenantSlug?.toLowerCase() || u.companyCode?.toLowerCase() || ""; - if (primarySlug) { - slugs.add(primarySlug); - } + const primarySlug = u.tenantSlug?.toLowerCase() || u.companyCode?.toLowerCase() || ""; + if (primarySlug) slugs.add(primarySlug); if (u.joinedTenants && Array.isArray(u.joinedTenants)) { for (const jt of u.joinedTenants) { - if (jt.slug) { - slugs.add(jt.slug.toLowerCase()); - } + if (jt.slug) slugs.add(jt.slug.toLowerCase()); } } - // Add user to all matching slugs in the map for (const slug of slugs) { const list = uMap.get(slug) || []; - // Prevent duplicate user references in the same list if (!list.some((existing) => existing.id === u.id)) { list.push(u); } @@ -106,10 +96,8 @@ export function TenantOrgChartPage() { const buildHierarchy = (tNode: TenantNode, depth: number): OrgNode => { const slug = tNode.slug.toLowerCase(); const members = usersMap.get(slug) || []; - const children = tNode.children.map((c) => buildHierarchy(c, depth + 1)); - // Calculate recursive total users instead of simple tenant count to account for actual mapped members let recursiveTotal = members.length; for (const child of children) { recursiveTotal += child.totalCount || 0; @@ -154,13 +142,9 @@ export function TenantOrgChartPage() { if (pRect.width === 0 || cRect.width === 0) continue; - const parentLevel = Number.parseInt( - parent.getAttribute("data-level") || "0", - 10, - ); + const parentLevel = Number.parseInt(parent.getAttribute("data-level") || "0", 10); if (parentLevel === 0) { - // Horizontal fork for Level 0 -> Level 1 const px = pRect.left + pRect.width / 2 - rect.left + scrollLeft; const py = pRect.bottom - rect.top + scrollTop; const cx = cRect.left + cRect.width / 2 - rect.left + scrollLeft; @@ -176,11 +160,10 @@ export function TenantOrgChartPage() { path: `M ${px} ${py} L ${px} ${midY} L ${cx} ${midY} L ${cx} ${cy}`, }); } else { - // Vertical spine for Level >= 1 -> Level >= 2 - const spineX = pRect.left + 32 - rect.left + scrollLeft; // 32px indent from parent's left edge + const spineX = pRect.left + 24 - rect.left + scrollLeft; const py = pRect.bottom - rect.top + scrollTop; - const cx = cRect.left - rect.left + scrollLeft; // Child's left edge - const cy = cRect.top + 24 - rect.top + scrollTop; // Middle of child's header + const cx = cRect.left - rect.left + scrollLeft; + const cy = cRect.top + 20 - rect.top + scrollTop; newLines.push({ key: `${parentId}->${box.id}`, @@ -201,7 +184,6 @@ export function TenantOrgChartPage() { }, []); React.useLayoutEffect(() => { - const _forceTrigger = rootNodes.length + usersMap.size; const timeout = setTimeout(drawLines, 150); window.addEventListener("resize", drawLines); return () => { @@ -216,52 +198,45 @@ export function TenantOrgChartPage() { ); } - // Count unique users across the fetched payload - const totalUniqueUsers = - usersQuery.data?.items?.filter((u) => u.status === "active").length || 0; - - const targetNodes = - selectedDept === "전체" - ? rootNodes - : rootNodes.filter((n) => n.name === selectedDept); + const totalUniqueUsers = usersQuery.data?.items?.filter((u) => u.status === "active").length || 0; + const targetNodes = selectedDept === "전체" ? rootNodes : rootNodes.filter((n) => n.name === selectedDept); return ( -
-
-
-
-

조직도

-

- 조직(테넌트) 계층 구조를 기반으로 사용자들의 소속을 시각화합니다. -

-
+
+
+
+

MH Dashboard

+

조직 현황

-
+
{["전체", ...depts].map((d) => ( ))} -
+
총 {totalUniqueUsers}명
))} -
+
{targetNodes.map((tNode) => { const orgNode = buildHierarchy(tNode, 0); return ( -
- +
+
); })} @@ -346,89 +314,63 @@ function OrgNodeView({ setTimeout(onToggle, 100); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - toggle(); - } - }; - - const membersToShow = [...node.members].sort( - (a, b) => getRankWeight(a) - getRankWeight(b), - ); - + const membersToShow = [...node.members].sort((a, b) => getRankWeight(a) - getRankWeight(b)); const isVerticalChildren = node.level >= 1; const isVerticallyStacked = node.level >= 1; const embedChildren = - node.children.length > 0 && - node.children.every((c) => c.children.length === 0); + node.children.length > 0 && node.children.every((c) => c.children.length === 0); + + // Determine header color based on level + const headerBgClass = node.level === 0 ? "bg-[#0a2a22]" : "bg-[#2f5547]"; return ( -
+
{!collapsed && membersToShow.length > 0 && ( -
+
{membersToShow.map((m) => ( - + ))}
)} {!collapsed && embedChildren && ( -
+
{node.children.map((child) => { - const childMembers = [...child.members].sort( - (a, b) => getRankWeight(a) - getRankWeight(b), - ); + const childMembers = [...child.members].sort((a, b) => getRankWeight(a) - getRankWeight(b)); return (
-
+
{child.name} - - ({child.totalCount || child.members.length}) - + {child.totalCount || child.members.length}
{childMembers.length > 0 && ( -
+
{childMembers.map((m) => ( - + ))}
)} @@ -440,20 +382,9 @@ function OrgNodeView({
{!collapsed && !embedChildren && node.children.length > 0 && ( -
+
{node.children.map((c) => ( - + ))}
)} @@ -467,41 +398,39 @@ function MemberCard({ }: { member: UserSummary; companyCode?: string }) { const coColor = (() => { const c = (companyCode || member.companyCode || "").toLowerCase(); - if (c.includes("hanmac")) return "bg-[#1E3A8A] text-white border-[#1E3A8A]"; - if (c.includes("saman")) return "bg-[#047857] text-white border-[#047857]"; - if (c.includes("ptc")) return "bg-[#C2410C] text-white border-[#C2410C]"; - if (c.includes("baron")) return "bg-[#4338CA] text-white border-[#4338CA]"; - return "bg-slate-600 text-white border-slate-700"; + if (c.includes("hanmac")) return "border-l-[#ef4444]"; + if (c.includes("saman")) return "border-l-[#ffb366]"; + if (c.includes("ptc")) return "border-l-[#a855f7]"; + if (c.includes("baron")) return "border-l-[#3b82f6]"; + return "border-l-slate-400"; })(); - const roleBadge = - member.jobTitle && member.jobTitle !== member.position + const roleBadge = member.jobTitle && member.jobTitle !== member.position ? member.jobTitle : member.position?.endsWith("장") ? member.position : null; return ( -
-
+
+
- + {member.name} - {member.position && member.position !== roleBadge && ( - - {member.position} + {roleBadge && ( + + {roleBadge} )}
- {roleBadge && ( - - {roleBadge} - - )} +
+
+ + {member.position || "사원"} +
); } + diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 3be0125b..57d5645a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -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) diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go index e4cd6fb8..fad84419 100644 --- a/backend/internal/bootstrap/bootstrap.go +++ b/backend/internal/bootstrap/bootstrap.go @@ -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 - ) -} + ) + } diff --git a/backend/internal/domain/shared_link.go b/backend/internal/domain/shared_link.go new file mode 100644 index 00000000..eca64173 --- /dev/null +++ b/backend/internal/domain/shared_link.go @@ -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 +} diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 03b4fb17..16e7641f 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -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, + }) +} diff --git a/backend/internal/repository/shared_link_repository.go b/backend/internal/repository/shared_link_repository.go new file mode 100644 index 00000000..1747ceae --- /dev/null +++ b/backend/internal/repository/shared_link_repository.go @@ -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 +} diff --git a/backend/internal/repository/user_repository.go b/backend/internal/repository/user_repository.go index a28089a2..4ccaffad 100644 --- a/backend/internal/repository/user_repository.go +++ b/backend/internal/repository/user_repository.go @@ -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 +} diff --git a/backend/internal/service/shared_link_service.go b/backend/internal/service/shared_link_service.go new file mode 100644 index 00000000..ff213c78 --- /dev/null +++ b/backend/internal/service/shared_link_service.go @@ -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함 +} diff --git a/compose.ory.yaml b/compose.ory.yaml index e589acb2..5747560d 100644 --- a/compose.ory.yaml +++ b/compose.ory.yaml @@ -206,6 +206,7 @@ services: - | hydra delete oauth2-client --endpoint http://hydra:4445 adminfront >/dev/null 2>&1 || true hydra delete oauth2-client --endpoint http://hydra:4445 devfront >/dev/null 2>&1 || true + hydra delete oauth2-client --endpoint http://hydra:4445 orgfront >/dev/null 2>&1 || true hydra delete oauth2-client --endpoint http://hydra:4445 ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} >/dev/null 2>&1 || true hydra create oauth2-client \ @@ -228,6 +229,16 @@ services: --token-endpoint-auth-method none \ --redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/auth/callback} + hydra create oauth2-client \ + --endpoint http://hydra:4445 \ + --id orgfront \ + --name "OrgFront" \ + --grant-type authorization_code,refresh_token \ + --response-type code \ + --scope openid,offline_access,profile,email \ + --token-endpoint-auth-method none \ + --redirect-uri ${ORGFRONT_CALLBACK_URLS:-http://localhost:5175/auth/callback} + hydra create oauth2-client \ --endpoint http://hydra:4445 \ --id ${OATHKEEPER_INTROSPECT_CLIENT_ID:-oathkeeper-introspect} \ diff --git a/docker-compose.yaml b/docker-compose.yaml index f7792782..e28bce48 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -86,6 +86,7 @@ services: networks: - baron_net + userfront: build: context: .