package admin import ( "context" "sort" "strings" "time" "github.com/ultisuite/ulti-backend/internal/api/query" "github.com/ultisuite/ulti-backend/internal/nextcloud" "github.com/ultisuite/ulti-backend/internal/publicshare" ) type PublicSharesList struct { Shares []map[string]any `json:"shares"` Pagination query.PaginationMeta `json:"pagination,omitempty"` } func (s *Service) ListPublicShares(ctx context.Context, params query.ListParams) (PublicSharesList, error) { if s.nc == nil { return PublicSharesList{ Shares: []map[string]any{}, Pagination: params.Meta(ptrInt64(0)), }, nil } rows, err := s.db.Query(ctx, ` SELECT email, external_id FROM users WHERE status != 'disabled' `) if err != nil { return PublicSharesList{}, err } defer rows.Close() type platformUser struct { email string externalID string } users := make([]platformUser, 0) for rows.Next() { var u platformUser if err := rows.Scan(&u.email, &u.externalID); err != nil { return PublicSharesList{}, err } users = append(users, u) } if err := rows.Err(); err != nil { return PublicSharesList{}, err } q := strings.ToLower(strings.TrimSpace(params.Q)) all := make([]map[string]any, 0) for _, u := range users { ncUser := nextcloud.UserIDFromClaims(u.email, u.externalID) if ncUser == "" { continue } shares, err := s.nc.ListShares(ctx, ncUser, "") if err != nil { s.logger.Debug("list shares for admin audit", "nc_user", ncUser, "error", err) continue } for _, share := range shares { if !nextcloud.IsExternalShare(share) { continue } if q != "" && !publicShareMatchesQuery(share, u.email, q) { continue } entry := map[string]any{ "id": share.ID, "token": share.Token, "path": share.Path, "item_type": share.ItemType, "access_mode": share.AccessMode, "share_type": share.ShareType, "permissions": share.Permissions, "url": share.URL, "expires_at": share.ExpiresAt, "created_at": share.CreatedAt, "owner_nc_user_id": ncUser, "owner_email": strings.ToLower(strings.TrimSpace(u.email)), "owner_display_name": firstNonEmptyStr(share.OwnerDisplayName, share.FileOwnerDisplayName), "share_with": share.ShareWith, "share_with_display_name": share.ShareWithDisplayName, "has_password": share.HasPassword, "label": share.Label, } all = append(all, entry) } } sort.Slice(all, func(i, j int) bool { return shareCreatedAt(all[i]).After(shareCreatedAt(all[j])) }) tokens := make([]string, 0, len(all)) for _, item := range all { if t, _ := item["token"].(string); t != "" { tokens = append(tokens, t) } } access, err := publicshare.Lookup(ctx, s.db, tokens) if err != nil { return PublicSharesList{}, err } for _, item := range all { token, _ := item["token"].(string) if stats, ok := access[token]; ok { item["last_access_at"] = stats.LastAccessAt.UTC().Format(time.RFC3339) item["access_count"] = stats.AccessCount } else { item["last_access_at"] = nil item["access_count"] = int64(0) } } total := int64(len(all)) start := params.Offset() if start > len(all) { start = len(all) } end := start + params.Limit() if end > len(all) { end = len(all) } page := all[start:end] return PublicSharesList{ Shares: page, Pagination: params.Meta(&total), }, nil } func (s *Service) RevokePublicShare(ctx context.Context, actorSub, shareID, ownerNCUserID string) error { if s.nc == nil { return ErrNotFound } shareID = strings.TrimSpace(shareID) ownerNCUserID = strings.TrimSpace(ownerNCUserID) if shareID == "" || ownerNCUserID == "" { return ErrNotFound } if err := s.nc.DeleteShare(ctx, ownerNCUserID, shareID); err != nil { return err } s.logAudit(ctx, actorSub, "revoke_public_share", map[string]any{ "share_id": shareID, "owner_nc_user_id": ownerNCUserID, }) return nil } func publicShareMatchesQuery(share nextcloud.ShareInfo, ownerEmail, q string) bool { haystack := strings.ToLower(strings.Join([]string{ share.Path, share.Token, share.URL, share.ShareWith, share.ShareWithDisplayName, share.OwnerDisplayName, share.FileOwnerDisplayName, ownerEmail, share.Label, share.Note, }, " ")) return strings.Contains(haystack, q) } func shareCreatedAt(item map[string]any) time.Time { raw, _ := item["created_at"].(string) if raw == "" { return time.Time{} } t, err := time.Parse(time.RFC3339, raw) if err != nil { return time.Time{} } return t } func firstNonEmptyStr(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } } return "" } func ptrInt64(v int64) *int64 { return &v }