1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -10,12 +10,15 @@ import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
@@ -28,6 +31,7 @@ type TenantHandler struct {
Service service.TenantService
UserRepo repository.UserRepository
UserProjectionRepo repository.UserProjectionRepository
OrgChartCache orgChartCacheStore
Keto service.KetoService
KetoOutbox repository.KetoOutboxRepository
KratosAdmin service.KratosAdminService
@@ -37,6 +41,11 @@ type TenantHandler struct {
ConsentRepo repository.ClientConsentRepository
}
type orgChartCacheStore interface {
Get(key string) (string, error)
Set(key string, value string, expiration time.Duration) error
}
func seedTenantDeleteError(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusConflict, "seed tenants cannot be deleted")
}
@@ -74,18 +83,19 @@ func (h *TenantHandler) SetWorksmobileSyncer(syncer service.WorksmobileSyncer) {
}
type tenantSummary struct {
ID string `json:"id"`
Type string `json:"type"`
ParentID *string `json:"parentId"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
Domains []string `json:"domains,omitempty"`
Config domain.JSONMap `json:"config,omitempty"`
MemberCount int64 `json:"memberCount"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
ID string `json:"id"`
Type string `json:"type"`
ParentID *string `json:"parentId"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
Domains []string `json:"domains,omitempty"`
Config domain.JSONMap `json:"config,omitempty"`
MemberCount int64 `json:"memberCount"`
TotalMemberCount int64 `json:"totalMemberCount"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type tenantListResponse struct {
@@ -97,6 +107,18 @@ type tenantListResponse struct {
NextCursor string `json:"nextCursor,omitempty"`
}
type orgChartSnapshotCacheInfo struct {
Source string `json:"source"`
Hit bool `json:"hit"`
TTLSeconds int `json:"ttlSeconds,omitempty"`
}
type orgChartSnapshotResponse struct {
Tenants []tenantSummary `json:"tenants"`
Users []userSummary `json:"users"`
Cache orgChartSnapshotCacheInfo `json:"cache"`
}
func pageTenantsByCursor(tenants []domain.Tenant, limit int, cursorRaw string) ([]domain.Tenant, string, error) {
ordered := append([]domain.Tenant(nil), tenants...)
pagination.SortByKeyDesc(ordered, func(tenant domain.Tenant) (time.Time, string) {
@@ -360,7 +382,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
}
}
memberCounts, err := h.countTenantMembersFromProjection(c.Context(), tenants)
memberCounts, totalMemberCounts, err := h.countTenantMembersFromProjection(c.Context(), tenants)
if err != nil {
return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
}
@@ -369,6 +391,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
for _, t := range tenants {
summary := mapTenantSummary(t)
summary.MemberCount = memberCounts[t.ID]
summary.TotalMemberCount = totalMemberCounts[t.ID]
items = append(items, summary)
}
@@ -1656,13 +1679,14 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
memberCounts, err := h.countTenantMembersFromProjection(c.Context(), []domain.Tenant{tenant})
memberCounts, totalMemberCounts, err := h.countTenantMembersFromProjection(c.Context(), []domain.Tenant{tenant})
if err != nil {
return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
}
summary := mapTenantSummary(tenant)
summary.MemberCount = memberCounts[tenant.ID]
summary.TotalMemberCount = totalMemberCounts[tenant.ID]
return c.JSON(summary)
}
@@ -1748,6 +1772,7 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
summary := mapTenantSummary(*tenant)
summary.MemberCount = 0
summary.TotalMemberCount = 0
if req.Config != nil {
config, err := normalizeTenantConfig(req.Config)
@@ -2658,25 +2683,33 @@ func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[
return build(rootID)
}
func (h *TenantHandler) countTenantMembersFromProjection(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
func (h *TenantHandler) countTenantMembersFromProjection(ctx context.Context, tenants []domain.Tenant) (map[string]int64, map[string]int64, error) {
counts := make(map[string]int64, len(tenants))
for _, tenant := range tenants {
counts[tenant.ID] = 0
}
if len(tenants) == 0 {
return counts, nil
return counts, counts, nil
}
if h.UserProjectionRepo == nil {
return nil, errors.New("user projection is not configured")
return nil, nil, errors.New("user projection is not configured")
}
ready, err := h.UserProjectionRepo.IsReady(ctx)
if err != nil {
return nil, fmt.Errorf("user projection status unavailable: %w", err)
return nil, nil, fmt.Errorf("user projection status unavailable: %w", err)
}
if !ready {
return nil, errors.New("user projection is not ready")
return nil, nil, errors.New("user projection is not ready")
}
return h.UserProjectionRepo.CountTenantMembers(ctx, tenants)
directCounts, err := h.UserProjectionRepo.CountTenantMembers(ctx, tenants)
if err != nil {
return nil, nil, err
}
totalCounts, err := h.UserProjectionRepo.CountTenantMembersRecursive(ctx, tenants)
if err != nil {
return nil, nil, err
}
return directCounts, totalCounts, nil
}
func normalizeTenantStatus(value string) string {
@@ -2736,6 +2769,230 @@ func (h *TenantHandler) DeleteShareLink(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "Share link deleted successfully"})
}
func (h *TenantHandler) GetOrgChartSnapshot(c *fiber.Ctx) error {
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
cacheMode := strings.ToLower(strings.TrimSpace(c.Query("cache")))
cacheKey := orgChartSnapshotCacheKey(profile, c.Get("X-Tenant-ID"))
ttl := orgChartSnapshotCacheTTL()
if cacheMode == "redis" && h.OrgChartCache != nil {
if raw, err := h.OrgChartCache.Get(cacheKey); err == nil && strings.TrimSpace(raw) != "" {
var cached orgChartSnapshotResponse
if err := json.Unmarshal([]byte(raw), &cached); err == nil {
cached.Cache = orgChartSnapshotCacheInfo{
Source: "redis",
Hit: true,
TTLSeconds: int(ttl.Seconds()),
}
c.Set("X-Orgfront-Cache", "HIT")
return c.JSON(cached)
}
}
}
snapshot, err := h.buildOrgChartSnapshot(c.Context(), profile)
if err != nil {
return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
}
snapshot.Cache = orgChartSnapshotCacheInfo{
Source: "database",
Hit: false,
TTLSeconds: int(ttl.Seconds()),
}
if cacheMode == "redis" && h.OrgChartCache != nil {
if raw, err := json.Marshal(snapshot); err == nil {
_ = h.OrgChartCache.Set(cacheKey, string(raw), ttl)
}
c.Set("X-Orgfront-Cache", "MISS")
} else {
c.Set("X-Orgfront-Cache", "BYPASS")
}
return c.JSON(snapshot)
}
func (h *TenantHandler) buildOrgChartSnapshot(ctx context.Context, profile *domain.UserProfileResponse) (orgChartSnapshotResponse, error) {
tenants, err := h.listOrgChartTenantsForProfile(ctx, profile)
if err != nil {
return orgChartSnapshotResponse{}, err
}
memberCounts, totalMemberCounts, err := h.countTenantMembersFromProjection(ctx, tenants)
if err != nil {
return orgChartSnapshotResponse{}, err
}
tenantSummaries := make([]tenantSummary, 0, len(tenants))
for _, tenant := range tenants {
summary := mapTenantSummary(tenant)
summary.MemberCount = memberCounts[tenant.ID]
summary.TotalMemberCount = totalMemberCounts[tenant.ID]
tenantSummaries = append(tenantSummaries, summary)
}
users, err := h.listOrgChartUsers(ctx, profile, tenants)
if err != nil {
return orgChartSnapshotResponse{}, err
}
return orgChartSnapshotResponse{
Tenants: tenantSummaries,
Users: users,
}, nil
}
func (h *TenantHandler) listOrgChartTenantsForProfile(ctx context.Context, profile *domain.UserProfileResponse) ([]domain.Tenant, error) {
if h.Service == nil {
return nil, errors.New("tenant service is not configured")
}
role := ""
if profile != nil {
role = domain.NormalizeRole(profile.Role)
}
if role == domain.RoleSuperAdmin {
tenants, _, err := h.Service.ListTenants(ctx, 10000, 0, "", "")
return tenants, err
}
allTenants, _, err := h.Service.ListTenants(ctx, 10000, 0, "", "")
if err != nil {
return nil, err
}
if profile == nil {
return []domain.Tenant{}, nil
}
baseTenantIDs := make([]string, 0, len(profile.ManageableTenants)+len(profile.JoinedTenants)+1)
for _, tenant := range profile.ManageableTenants {
baseTenantIDs = append(baseTenantIDs, tenant.ID)
}
for _, tenant := range profile.JoinedTenants {
baseTenantIDs = append(baseTenantIDs, tenant.ID)
}
if profile.TenantID != nil {
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
}
parentMap := make(map[string]string)
for _, tenant := range allTenants {
if tenant.ParentID != nil {
parentMap[tenant.ID] = *tenant.ParentID
}
}
findRoot := func(id string) string {
curr := id
for {
parentID, exists := parentMap[curr]
if !exists || parentID == "" {
return curr
}
curr = parentID
}
}
roots := make(map[string]bool)
for _, id := range baseTenantIDs {
if strings.TrimSpace(id) != "" {
roots[findRoot(id)] = true
}
}
tenants := make([]domain.Tenant, 0, len(allTenants))
for _, tenant := range allTenants {
if roots[findRoot(tenant.ID)] {
tenants = append(tenants, tenant)
}
}
return h.filterPrivateTenantsForProfile(ctx, tenants, profile)
}
func (h *TenantHandler) listOrgChartUsers(ctx context.Context, profile *domain.UserProfileResponse, tenants []domain.Tenant) ([]userSummary, error) {
if h.UserRepo == nil {
return nil, errors.New("user repository is not configured")
}
role := ""
if profile != nil {
role = domain.NormalizeRole(profile.Role)
}
tenantIDs := []string{}
if role != domain.RoleSuperAdmin {
tenantIDs = make([]string, 0, len(tenants))
for _, tenant := range tenants {
tenantIDs = append(tenantIDs, tenant.ID)
}
}
users, _, _, err := h.UserRepo.List(ctx, 0, 10000, "", tenantIDs, "")
if err != nil {
return nil, err
}
summaries := make([]userSummary, 0, len(users))
for _, user := range users {
summary := userSummary{
ID: user.ID,
Email: user.Email,
LoginID: user.Email,
Name: user.Name,
Phone: user.Phone,
Role: domain.NormalizeRole(user.Role),
Status: normalizeStatus(user.Status),
TenantSlug: userTenantSlug(user),
CompanyCode: userTenantSlug(user),
Metadata: user.Metadata,
Tenant: user.Tenant,
Department: user.Department,
Grade: user.Grade,
Position: user.Position,
JobTitle: user.JobTitle,
CreatedAt: formatTime(user.CreatedAt),
UpdatedAt: formatTime(user.UpdatedAt),
}
if h.Service != nil {
if joined, err := h.Service.ListJoinedTenants(ctx, user.ID); err == nil {
summary.JoinedTenants = joined
}
}
summaries = append(summaries, summary)
}
return summaries, nil
}
func orgChartSnapshotCacheKey(profile *domain.UserProfileResponse, tenantHeader string) string {
role := "anonymous"
userID := "anonymous"
tenantID := strings.TrimSpace(tenantHeader)
if profile != nil {
role = domain.NormalizeRole(profile.Role)
userID = strings.TrimSpace(profile.ID)
if tenantID == "" && profile.TenantID != nil {
tenantID = strings.TrimSpace(*profile.TenantID)
}
}
if userID == "" {
userID = "anonymous"
}
if tenantID == "" {
tenantID = "none"
}
return fmt.Sprintf("orgchart:snapshot:v1:%s:%s:%s", role, userID, tenantID)
}
func orgChartSnapshotCacheTTL() time.Duration {
const defaultTTL = 5 * time.Minute
raw := strings.TrimSpace(os.Getenv("ORGFRONT_ORGCHART_CACHE_TTL_SECONDS"))
if raw == "" {
return defaultTTL
}
seconds, err := strconv.Atoi(raw)
if err != nil || seconds <= 0 {
return defaultTTL
}
return time.Duration(seconds) * time.Second
}
func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
token := c.Query("token")
if token == "" {