package usecase import ( "context" "sort" "strings" memberentity "haixun-backend/internal/model/member/domain/entity" "haixun-backend/internal/model/permission/domain/entity" domrepo "haixun-backend/internal/model/permission/domain/repository" domusecase "haixun-backend/internal/model/permission/domain/usecase" ) type permissionUseCase struct { permissions domrepo.PermissionRepository rolePermissions domrepo.RolePermissionRepository } func NewUseCase(permissions domrepo.PermissionRepository, rolePermissions domrepo.RolePermissionRepository) domusecase.UseCase { return &permissionUseCase{permissions: permissions, rolePermissions: rolePermissions} } func (u *permissionUseCase) EnsureDefaultPermissions(ctx context.Context) error { for _, permission := range defaultPermissions() { if err := u.permissions.UpsertByName(ctx, permission); err != nil { return err } } return nil } func (u *permissionUseCase) EnsureDefaultRolePermissions(ctx context.Context, tenantID string) error { if tenantID == "" { return nil } perms, err := u.permissions.List(ctx, domrepo.PermissionFilter{Status: entity.StatusOpen}) if err != nil { return err } allIDs := make([]string, 0, len(perms)) byName := make(map[string]string, len(perms)) for _, item := range perms { id := item.ID.Hex() allIDs = append(allIDs, id) byName[item.Name] = id } if err := u.rolePermissions.SetForRole(ctx, tenantID, "admin", allIDs); err != nil { return err } userIDs := make([]string, 0, len(defaultUserPermissionNames())) for _, name := range defaultUserPermissionNames() { if id, ok := byName[name]; ok { userIDs = append(userIDs, id) } } return u.rolePermissions.SetForRole(ctx, tenantID, "user", userIDs) } func (u *permissionUseCase) Catalog(ctx context.Context, req domusecase.CatalogRequest) ([]domusecase.PermissionNode, []domusecase.PermissionNode, error) { filter := domrepo.PermissionFilter{} if req.Status != "" { filter.Status = entity.Status(req.Status) } if req.Type != "" { filter.Type = entity.Type(req.Type) } items, err := u.permissions.List(ctx, filter) if err != nil { return nil, nil, err } list := permissionNodes(items) if req.Tree { return buildTree(list), nil, nil } return nil, list, nil } func (u *permissionUseCase) Me(ctx context.Context, member *memberentity.Member, includeTree bool) (*domusecase.MePermissions, error) { if member == nil { return nil, nil } roles := member.Roles if len(roles) == 0 { roles = []string{"user"} } rolePermissions, err := u.rolePermissions.ListByRoles(ctx, member.TenantID, roles) if err != nil { return nil, err } ids := make([]string, 0, len(rolePermissions)) for _, item := range rolePermissions { ids = append(ids, item.PermissionID) } perms, err := u.permissions.ListByIDs(ctx, ids) if err != nil { return nil, err } if len(perms) == 0 { perms, err = u.permissions.List(ctx, domrepo.PermissionFilter{Status: entity.StatusOpen}) if err != nil { return nil, err } if !hasRole(roles, "admin") { perms = filterPermissionsByName(perms, defaultUserPermissionNames()) } } nodes := permissionNodes(perms) result := &domusecase.MePermissions{ TenantID: member.TenantID, UID: member.UID, Roles: roles, Permissions: map[string]string{}, } for _, node := range nodes { if node.HTTPPath == "" || node.HTTPMethods == "" { continue } if existing, ok := result.Permissions[node.HTTPPath]; ok { result.Permissions[node.HTTPPath] = mergeHTTPMethods(existing, node.HTTPMethods) continue } result.Permissions[node.HTTPPath] = node.HTTPMethods } if includeTree { result.Tree = buildTree(nodes) } return result, nil } func mergeHTTPMethods(existing, next string) string { seen := map[string]struct{}{} out := make([]string, 0, 8) for _, block := range []string{existing, next} { for _, item := range strings.Split(block, "|") { method := strings.ToUpper(strings.TrimSpace(item)) if method == "" { continue } if _, ok := seen[method]; ok { continue } seen[method] = struct{}{} out = append(out, method) } } return strings.Join(out, "|") } func hasRole(roles []string, role string) bool { for _, item := range roles { if item == role { return true } } return false } func filterPermissionsByName(items []*entity.Permission, names []string) []*entity.Permission { allowed := map[string]struct{}{} for _, name := range names { allowed[name] = struct{}{} } out := make([]*entity.Permission, 0, len(items)) for _, item := range items { if _, ok := allowed[item.Name]; ok { out = append(out, item) } } return out } func defaultUserPermissionNames() []string { return []string{ "auth.logout", "member.me.read", "member.me.update", "member.placement_settings.read", "member.placement_settings.update", "permission.me.read", "setting.manage", "ai.use", "brand.manage", "persona.manage", "placement_topic.manage", "threads_account.manage", "job.manage", "job.schedule.manage", } } func (u *permissionUseCase) ReplaceRolePermissions(ctx context.Context, tenantID, roleKey string, permissionIDs []string) error { return u.rolePermissions.SetForRole(ctx, tenantID, roleKey, permissionIDs) } func permissionNodes(items []*entity.Permission) []domusecase.PermissionNode { out := make([]domusecase.PermissionNode, 0, len(items)) for _, item := range items { out = append(out, domusecase.PermissionNode{ ID: item.ID.Hex(), Parent: item.Parent, Name: item.Name, HTTPMethods: item.HTTPMethods, HTTPPath: item.HTTPPath, Status: string(item.Status), Type: string(item.Type), }) } sort.SliceStable(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } func buildTree(items []domusecase.PermissionNode) []domusecase.PermissionNode { byParent := map[string][]domusecase.PermissionNode{} index := map[string]*domusecase.PermissionNode{} nodes := make([]domusecase.PermissionNode, len(items)) copy(nodes, items) for i := range nodes { index[nodes[i].ID] = &nodes[i] byParent[nodes[i].Parent] = append(byParent[nodes[i].Parent], nodes[i]) } for parent, children := range byParent { sort.SliceStable(children, func(i, j int) bool { return children[i].Name < children[j].Name }) byParent[parent] = children } for parent, children := range byParent { if node, ok := index[parent]; ok { node.Children = children } } return byParent[""] } func defaultPermissions() []*entity.Permission { allMethods := "GET|POST|PUT|PATCH|DELETE" return []*entity.Permission{ {Name: "auth.logout", HTTPMethods: "POST", HTTPPath: "/api/v1/auth/logout", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.me.read", HTTPMethods: "GET", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.me.update", HTTPMethods: "PATCH", HTTPPath: "/api/v1/members/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.placement_settings.read", HTTPMethods: "GET", HTTPPath: "/api/v1/members/me/placement-settings", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "member.placement_settings.update", HTTPMethods: "PATCH", HTTPPath: "/api/v1/members/me/placement-settings", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "permission.catalog.read", HTTPMethods: "GET", HTTPPath: "/api/v1/permissions/catalog", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "permission.me.read", HTTPMethods: "GET", HTTPPath: "/api/v1/permissions/me", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "setting.manage", HTTPMethods: "GET|PUT|DELETE", HTTPPath: "/api/v1/settings/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "ai.use", HTTPMethods: "GET|POST", HTTPPath: "/api/v1/ai/*", Status: entity.StatusOpen, Type: entity.TypeFrontendUser}, {Name: "brand.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/brands/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "persona.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/personas/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "placement_topic.manage", HTTPMethods: allMethods, HTTPPath: "/api/v1/placement/topics/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "threads_account.manage", HTTPMethods: "GET|POST|PUT|PATCH", HTTPPath: "/api/v1/threads-accounts/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "job.manage", HTTPMethods: "GET|POST", HTTPPath: "/api/v1/jobs/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "job.schedule.manage", HTTPMethods: "GET|POST|PUT|DELETE", HTTPPath: "/api/v1/job/schedules/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, {Name: "job.template.manage", HTTPMethods: "GET|PUT", HTTPPath: "/api/v1/job/templates/*", Status: entity.StatusOpen, Type: entity.TypeBackendUser}, } }