Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Web.Core.Config;
|
||||
|
||||
public class ClientConnectOptions
|
||||
{
|
||||
/// <summary>Must match the redirect URI registered in each client's app registration.</summary>
|
||||
public string RedirectUri { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class ExecuteQueryRetryHelper
|
||||
{
|
||||
private const int MaxRetries = 5;
|
||||
|
||||
public static async Task ExecuteQueryRetryAsync(
|
||||
ClientContext ctx,
|
||||
IProgress<OperationProgress>? progress = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
int attempt = 0;
|
||||
while (true)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
await ctx.ExecuteQueryAsync();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (IsThrottleException(ex) && attempt < MaxRetries)
|
||||
{
|
||||
attempt++;
|
||||
int delaySeconds = (int)Math.Pow(2, attempt) * 5;
|
||||
progress?.Report(OperationProgress.Indeterminate(
|
||||
$"Throttled by SharePoint — retrying in {delaySeconds}s (attempt {attempt}/{MaxRetries})…"));
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsThrottleException(Exception ex)
|
||||
{
|
||||
var msg = ex.Message;
|
||||
return msg.Contains("429") || msg.Contains("503") ||
|
||||
msg.Contains("throttl", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class PermissionConsolidator
|
||||
{
|
||||
internal static string MakeKey(UserAccessEntry entry)
|
||||
=> string.Join("|",
|
||||
entry.UserLogin.ToLowerInvariant(),
|
||||
entry.PermissionLevel.ToLowerInvariant(),
|
||||
entry.AccessType.ToString(),
|
||||
entry.GrantedThrough.ToLowerInvariant());
|
||||
|
||||
public static IReadOnlyList<ConsolidatedPermissionEntry> Consolidate(IReadOnlyList<UserAccessEntry> entries)
|
||||
{
|
||||
if (entries.Count == 0) return Array.Empty<ConsolidatedPermissionEntry>();
|
||||
return entries
|
||||
.GroupBy(e => MakeKey(e))
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
var locations = g.Select(e => new LocationInfo(
|
||||
e.SiteUrl, e.SiteTitle, e.ObjectTitle, e.ObjectUrl, e.ObjectType)).ToList();
|
||||
return new ConsolidatedPermissionEntry(
|
||||
first.UserDisplayName, first.UserLogin, first.PermissionLevel,
|
||||
first.AccessType, first.GrantedThrough, first.IsHighPrivilege,
|
||||
first.IsExternalUser, locations,
|
||||
first.TargetUrl, first.TargetLabel, first.SharingLinkType);
|
||||
})
|
||||
.OrderBy(c => c.UserLogin).ThenBy(c => c.PermissionLevel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class PermissionEntryHelper
|
||||
{
|
||||
private static readonly Regex LimitedAccessWebRegex = new(
|
||||
@"^Limited Access System Group For Web\s+(?<id>[0-9a-fA-F-]{36})\s*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex LimitedAccessListRegex = new(
|
||||
@"^Limited Access System Group For List\s+(?<id>[0-9a-fA-F-]{36})\s*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex SharingLinkRegex = new(
|
||||
@"^SharingLinks\.(?<item>[0-9a-fA-F-]{36})\.(?<type>[^.]+)\.(?<share>[0-9a-fA-F-]{36})\s*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public static bool IsExternalUser(string loginName) =>
|
||||
loginName.Contains("#EXT#", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static IReadOnlyList<string> FilterPermissionLevels(IEnumerable<string> levels) =>
|
||||
levels.Where(l => !string.Equals(l, "Limited Access", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
public static bool IsBareLimitedAccessSystemGroup(string name) =>
|
||||
name.Equals("Limited Access System Group", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static SystemGroupClassification Classify(string groupTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupTitle))
|
||||
return new SystemGroupClassification(SystemGroupKind.None, null, null, null, null, null);
|
||||
|
||||
var trimmed = groupTitle.Trim();
|
||||
|
||||
if (IsBareLimitedAccessSystemGroup(trimmed))
|
||||
return new SystemGroupClassification(SystemGroupKind.LimitedAccessBare, null, null, null, null, null);
|
||||
|
||||
var mWeb = LimitedAccessWebRegex.Match(trimmed);
|
||||
if (mWeb.Success && Guid.TryParse(mWeb.Groups["id"].Value, out var webId))
|
||||
return new SystemGroupClassification(SystemGroupKind.LimitedAccessWeb, webId, null, null, null, null);
|
||||
|
||||
var mList = LimitedAccessListRegex.Match(trimmed);
|
||||
if (mList.Success && Guid.TryParse(mList.Groups["id"].Value, out var listId))
|
||||
return new SystemGroupClassification(SystemGroupKind.LimitedAccessList, null, listId, null, null, null);
|
||||
|
||||
var mShare = SharingLinkRegex.Match(trimmed);
|
||||
if (mShare.Success
|
||||
&& Guid.TryParse(mShare.Groups["item"].Value, out var itemId)
|
||||
&& Guid.TryParse(mShare.Groups["share"].Value, out var shareId))
|
||||
{
|
||||
return new SystemGroupClassification(
|
||||
SystemGroupKind.SharingLink, null, null, itemId, mShare.Groups["type"].Value, shareId);
|
||||
}
|
||||
|
||||
return new SystemGroupClassification(SystemGroupKind.None, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SystemGroupKind
|
||||
{
|
||||
None,
|
||||
LimitedAccessBare,
|
||||
LimitedAccessWeb,
|
||||
LimitedAccessList,
|
||||
SharingLink
|
||||
}
|
||||
|
||||
public readonly record struct SystemGroupClassification(
|
||||
SystemGroupKind Kind,
|
||||
Guid? WebId,
|
||||
Guid? ListId,
|
||||
Guid? ItemUniqueId,
|
||||
string? LinkType,
|
||||
Guid? ShareId);
|
||||
@@ -0,0 +1,46 @@
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class PermissionLevelMapping
|
||||
{
|
||||
public record MappingResult(string Label, RiskLevel RiskLevel);
|
||||
|
||||
private static readonly Dictionary<string, MappingResult> Mappings = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Full Control"] = new("Full control (can manage everything)", RiskLevel.High),
|
||||
["Site Collection Administrator"] = new("Site collection admin (full control)", RiskLevel.High),
|
||||
["Contribute"] = new("Can edit files and list items", RiskLevel.Medium),
|
||||
["Edit"] = new("Can edit files, lists, and pages", RiskLevel.Medium),
|
||||
["Design"] = new("Can edit pages and use design tools", RiskLevel.Medium),
|
||||
["Approve"] = new("Can approve content and list items", RiskLevel.Medium),
|
||||
["Manage Hierarchy"] = new("Can create sites and manage pages", RiskLevel.Medium),
|
||||
["Read"] = new("Can view files and pages", RiskLevel.Low),
|
||||
["Restricted Read"] = new("Can view pages only (no download)", RiskLevel.Low),
|
||||
["View Only"] = new("Can view files in browser only", RiskLevel.ReadOnly),
|
||||
["Restricted View"] = new("Restricted view access", RiskLevel.ReadOnly),
|
||||
};
|
||||
|
||||
public static MappingResult GetMapping(string roleName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roleName)) return new(roleName, RiskLevel.Low);
|
||||
return Mappings.TryGetValue(roleName.Trim(), out var result) ? result : new(roleName.Trim(), RiskLevel.Medium);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<MappingResult> GetMappings(string permissionLevels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(permissionLevels)) return Array.Empty<MappingResult>();
|
||||
return permissionLevels.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(GetMapping).ToList();
|
||||
}
|
||||
|
||||
public static RiskLevel GetHighestRisk(string permissionLevels)
|
||||
{
|
||||
var mappings = GetMappings(permissionLevels);
|
||||
if (mappings.Count == 0) return RiskLevel.Low;
|
||||
return mappings.Min(m => m.RiskLevel);
|
||||
}
|
||||
|
||||
public static string GetSimplifiedLabels(string permissionLevels)
|
||||
=> string.Join("; ", GetMappings(permissionLevels).Select(m => m.Label));
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class SharePointPaginationHelper
|
||||
{
|
||||
private const int DefaultRowLimit = 5000;
|
||||
|
||||
public static async IAsyncEnumerable<ListItem> GetAllItemsAsync(
|
||||
ClientContext ctx,
|
||||
List list,
|
||||
CamlQuery? baseQuery = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var query = baseQuery ?? CamlQuery.CreateAllItemsQuery();
|
||||
query.ViewXml = BuildPagedViewXml(query.ViewXml, DefaultRowLimit);
|
||||
query.ListItemCollectionPosition = null;
|
||||
|
||||
do
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var items = list.GetItems(query);
|
||||
ctx.Load(items);
|
||||
await ctx.ExecuteQueryAsync();
|
||||
foreach (var item in items) yield return item;
|
||||
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
|
||||
}
|
||||
while (query.ListItemCollectionPosition != null);
|
||||
}
|
||||
|
||||
public static async IAsyncEnumerable<ListItem> GetItemsInFolderAsync(
|
||||
ClientContext ctx,
|
||||
List list,
|
||||
string folderServerRelativeUrl,
|
||||
bool recursive,
|
||||
string[]? viewFields = null,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var fields = viewFields ?? new[]
|
||||
{
|
||||
"FSObjType", "FileRef", "FileLeafRef", "FileDirRef", "File_x0020_Size"
|
||||
};
|
||||
var viewFieldsXml = string.Join(string.Empty, fields.Select(f => $"<FieldRef Name='{f}' />"));
|
||||
var scope = recursive ? " Scope='RecursiveAll'" : string.Empty;
|
||||
var viewXml =
|
||||
$"<View{scope}><Query></Query>" +
|
||||
$"<ViewFields>{viewFieldsXml}</ViewFields>" +
|
||||
$"<RowLimit Paged='TRUE'>{DefaultRowLimit}</RowLimit></View>";
|
||||
|
||||
var query = new CamlQuery
|
||||
{
|
||||
ViewXml = viewXml,
|
||||
FolderServerRelativeUrl = folderServerRelativeUrl,
|
||||
ListItemCollectionPosition = null
|
||||
};
|
||||
|
||||
do
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var items = list.GetItems(query);
|
||||
ctx.Load(items);
|
||||
await ctx.ExecuteQueryAsync();
|
||||
foreach (var item in items) yield return item;
|
||||
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
|
||||
}
|
||||
while (query.ListItemCollectionPosition != null);
|
||||
}
|
||||
|
||||
internal static string BuildPagedViewXml(string? existingXml, int rowLimit)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(existingXml))
|
||||
return $"<View><RowLimit Paged='TRUE'>{rowLimit}</RowLimit></View>";
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(
|
||||
existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
return System.Text.RegularExpressions.Regex.Replace(
|
||||
existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
|
||||
$"<RowLimit Paged='TRUE'>{rowLimit}</RowLimit>",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
}
|
||||
return existingXml.Replace("</View>",
|
||||
$"<RowLimit Paged='TRUE'>{rowLimit}</RowLimit></View>",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public enum SharingLinkRisk { Low, Medium, High, Unknown }
|
||||
|
||||
public static class SharingLinkLabels
|
||||
{
|
||||
public static (string Label, SharingLinkRisk Risk) Describe(string? rawLinkType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawLinkType)) return (string.Empty, SharingLinkRisk.Unknown);
|
||||
return rawLinkType.Trim() switch
|
||||
{
|
||||
"OrganizationView" => ("Org link · View", SharingLinkRisk.Low),
|
||||
"OrganizationEdit" => ("Org link · Edit", SharingLinkRisk.Medium),
|
||||
"AnonymousView" => ("Anyone · View", SharingLinkRisk.High),
|
||||
"AnonymousEdit" => ("Anyone · Edit", SharingLinkRisk.High),
|
||||
"Flexible" => ("Custom link", SharingLinkRisk.Medium),
|
||||
"Direct" => ("Specific people", SharingLinkRisk.Low),
|
||||
"Existing" => ("Existing access", SharingLinkRisk.Low),
|
||||
"Review" => ("Review only", SharingLinkRisk.Low),
|
||||
"Embed" => ("Embedded link", SharingLinkRisk.Medium),
|
||||
_ => (rawLinkType, SharingLinkRisk.Unknown)
|
||||
};
|
||||
}
|
||||
|
||||
public static (string Background, string Foreground) Colors(SharingLinkRisk risk) => risk switch
|
||||
{
|
||||
SharingLinkRisk.Low => ("#D1FAE5", "#065F46"),
|
||||
SharingLinkRisk.Medium => ("#FEF3C7", "#92400E"),
|
||||
SharingLinkRisk.High => ("#FEE2E2", "#991B1B"),
|
||||
_ => ("#F3F4F6", "#374151"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string? TrimOrNull(this string? s)
|
||||
=> string.IsNullOrWhiteSpace(s) ? null : s.Trim();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// SystemGroupKind, SystemGroupClassification, and PermissionEntryHelper are defined in PermissionEntryHelper.cs
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class AppConfiguration
|
||||
{
|
||||
public string DataFolder { get; set; } = "/data";
|
||||
public string ExportsFolder { get; set; } = "/data/exports";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string DataFolder { get; set; } = string.Empty;
|
||||
public string Lang { get; set; } = "en";
|
||||
public bool AutoTakeOwnership { get; set; } = false;
|
||||
public string Theme { get; set; } = "System";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class AppUser
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; } = UserRole.TechN0;
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? LastLogin { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class AuditEntry
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string UserEmail { get; set; } = string.Empty;
|
||||
public string UserDisplay { get; set; } = string.Empty;
|
||||
public UserRole UserRole { get; set; }
|
||||
public string Action { get; set; } = string.Empty;
|
||||
public string ClientName { get; set; } = string.Empty;
|
||||
public List<string> Sites { get; set; } = new();
|
||||
public string Details { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class BrandingSettings
|
||||
{
|
||||
public LogoData? MspLogo { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class BulkMemberRow
|
||||
{
|
||||
[Name("GroupName")] public string GroupName { get; set; } = string.Empty;
|
||||
[Name("GroupUrl")] public string GroupUrl { get; set; } = string.Empty;
|
||||
[Name("Email")] public string Email { get; set; } = string.Empty;
|
||||
[Name("Role")] public string Role { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class BulkItemResult<T>
|
||||
{
|
||||
public T Item { get; }
|
||||
public bool IsSuccess { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
private BulkItemResult(T item, bool success, string? error)
|
||||
{
|
||||
Item = item; IsSuccess = success; ErrorMessage = error; Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static BulkItemResult<T> Success(T item) => new(item, true, null);
|
||||
public static BulkItemResult<T> Failed(T item, string error) => new(item, false, error);
|
||||
}
|
||||
|
||||
public class BulkOperationSummary<T>
|
||||
{
|
||||
public IReadOnlyList<BulkItemResult<T>> Results { get; }
|
||||
public int TotalCount => Results.Count;
|
||||
public int SuccessCount => Results.Count(r => r.IsSuccess);
|
||||
public int FailedCount => Results.Count(r => !r.IsSuccess);
|
||||
public bool HasFailures => FailedCount > 0;
|
||||
public IReadOnlyList<BulkItemResult<T>> FailedItems => Results.Where(r => !r.IsSuccess).ToList();
|
||||
|
||||
public BulkOperationSummary(IReadOnlyList<BulkItemResult<T>> results) { Results = results; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class BulkSiteRow
|
||||
{
|
||||
[Name("Name")] public string Name { get; set; } = string.Empty;
|
||||
[Name("Alias")] public string Alias { get; set; } = string.Empty;
|
||||
[Name("Type")] public string Type { get; set; } = string.Empty;
|
||||
[Name("Template")] public string Template { get; set; } = string.Empty;
|
||||
[Name("Owners")] public string Owners { get; set; } = string.Empty;
|
||||
[Name("Members")] public string Members { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum ConflictPolicy { Skip, Overwrite, Rename }
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record ConsolidatedPermissionEntry(
|
||||
string UserDisplayName,
|
||||
string UserLogin,
|
||||
string PermissionLevel,
|
||||
AccessType AccessType,
|
||||
string GrantedThrough,
|
||||
bool IsHighPrivilege,
|
||||
bool IsExternalUser,
|
||||
IReadOnlyList<LocationInfo> Locations,
|
||||
string? TargetUrl = null,
|
||||
string? TargetLabel = null,
|
||||
string? SharingLinkType = null
|
||||
)
|
||||
{
|
||||
public int LocationCount => Locations.Count;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class CsvValidationRow<T>
|
||||
{
|
||||
public T? Record { get; }
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
public List<string> Errors { get; }
|
||||
public string? RawRecord { get; }
|
||||
|
||||
public CsvValidationRow(T record, List<string> errors)
|
||||
{
|
||||
Record = record; Errors = errors;
|
||||
}
|
||||
|
||||
private CsvValidationRow(string rawRecord, string parseError)
|
||||
{
|
||||
Record = default; RawRecord = rawRecord; Errors = new List<string> { parseError };
|
||||
}
|
||||
|
||||
public static CsvValidationRow<T> ParseError(string? rawRecord, string error)
|
||||
=> new(rawRecord ?? string.Empty, error);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class DuplicateGroup
|
||||
{
|
||||
public string GroupKey { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<DuplicateItem> Items { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class DuplicateItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string Library { get; set; } = string.Empty;
|
||||
public long? SizeBytes { get; set; }
|
||||
public DateTime? Created { get; set; }
|
||||
public DateTime? Modified { get; set; }
|
||||
public int? FolderCount { get; set; }
|
||||
public int? FileCount { get; set; }
|
||||
public string SiteUrl { get; set; } = string.Empty;
|
||||
public string SiteTitle { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record DuplicateScanOptions(
|
||||
string Mode = "Files",
|
||||
bool MatchSize = true,
|
||||
bool MatchCreated = false,
|
||||
bool MatchModified = false,
|
||||
bool MatchSubfolderCount = false,
|
||||
bool MatchFileCount = false,
|
||||
bool IncludeSubsites = false,
|
||||
string? Library = null
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record FileTypeMetric(
|
||||
string Extension,
|
||||
long TotalSizeBytes,
|
||||
int FileCount)
|
||||
{
|
||||
public string DisplayLabel => string.IsNullOrEmpty(Extension)
|
||||
? "No Extension"
|
||||
: Extension.TrimStart('.').ToUpperInvariant();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class FolderStructureRow
|
||||
{
|
||||
[Name("Level1")] public string Level1 { get; set; } = string.Empty;
|
||||
[Name("Level2")] public string Level2 { get; set; } = string.Empty;
|
||||
[Name("Level3")] public string Level3 { get; set; } = string.Empty;
|
||||
[Name("Level4")] public string Level4 { get; set; } = string.Empty;
|
||||
|
||||
public string BuildPath()
|
||||
{
|
||||
var parts = new[] { Level1, Level2, Level3, Level4 }.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||
return string.Join("/", parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record GraphDirectoryUser(
|
||||
string DisplayName,
|
||||
string UserPrincipalName,
|
||||
string? Mail,
|
||||
string? Department,
|
||||
string? JobTitle,
|
||||
string? UserType);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record LocationInfo(
|
||||
string SiteUrl,
|
||||
string SiteTitle,
|
||||
string ObjectTitle,
|
||||
string ObjectUrl,
|
||||
string ObjectType
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record LogoData
|
||||
{
|
||||
public string Base64 { get; init; } = string.Empty;
|
||||
public string MimeType { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record OperationProgress(int Current, int Total, string Message, bool IsIndeterminate = false)
|
||||
{
|
||||
public static OperationProgress Indeterminate(string message) =>
|
||||
new(0, 0, message, IsIndeterminate: true);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record PermissionEntry(
|
||||
string ObjectType,
|
||||
string Title,
|
||||
string Url,
|
||||
bool HasUniquePermissions,
|
||||
string Users,
|
||||
string UserLogins,
|
||||
string PermissionLevels,
|
||||
string GrantedThrough,
|
||||
string PrincipalType,
|
||||
bool WasAutoElevated = false,
|
||||
string? TargetUrl = null,
|
||||
string? TargetLabel = null,
|
||||
string? SharingLinkType = null
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record PermissionSummary(
|
||||
string Label,
|
||||
RiskLevel RiskLevel,
|
||||
int Count,
|
||||
int DistinctUsers
|
||||
);
|
||||
|
||||
public static class PermissionSummaryBuilder
|
||||
{
|
||||
private static readonly Dictionary<RiskLevel, string> Labels = new()
|
||||
{
|
||||
[RiskLevel.High] = "High Risk",
|
||||
[RiskLevel.Medium] = "Medium Risk",
|
||||
[RiskLevel.Low] = "Low Risk",
|
||||
[RiskLevel.ReadOnly] = "Read Only",
|
||||
};
|
||||
|
||||
public static IReadOnlyList<PermissionSummary> Build(IEnumerable<SimplifiedPermissionEntry> entries)
|
||||
{
|
||||
var grouped = entries.GroupBy(e => e.RiskLevel).ToDictionary(g => g.Key, g => g.ToList());
|
||||
return Enum.GetValues<RiskLevel>().Select(level =>
|
||||
{
|
||||
var items = grouped.GetValueOrDefault(level, new List<SimplifiedPermissionEntry>());
|
||||
var distinctUsers = items
|
||||
.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
.Select(u => u.Trim()).Where(u => u.Length > 0)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
||||
return new PermissionSummary(Labels[level], level, items.Count, distinctUsers);
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record ResolvedMember(string DisplayName, string Login);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum RiskLevel
|
||||
{
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
ReadOnly
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record ScanOptions(
|
||||
bool IncludeInherited = false,
|
||||
bool ScanFolders = true,
|
||||
int FolderDepth = 1,
|
||||
bool IncludeSubsites = false
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record SearchOptions(
|
||||
string[] Extensions,
|
||||
string? Regex,
|
||||
DateTime? CreatedAfter,
|
||||
DateTime? CreatedBefore,
|
||||
DateTime? ModifiedAfter,
|
||||
DateTime? ModifiedBefore,
|
||||
string? CreatedBy,
|
||||
string? ModifiedBy,
|
||||
string? Library,
|
||||
int MaxResults,
|
||||
string SiteUrl
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string FileExtension { get; set; } = string.Empty;
|
||||
public DateTime? Created { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string ModifiedBy { get; set; } = string.Empty;
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
/// <summary>Held in ProtectedSessionStorage — never persisted to disk.</summary>
|
||||
public class SessionTokens
|
||||
{
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string SpHost { get; set; } = string.Empty;
|
||||
public string UserPrincipalName { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class SimplifiedPermissionEntry
|
||||
{
|
||||
public PermissionEntry Inner { get; }
|
||||
public string SimplifiedLabels { get; }
|
||||
public RiskLevel RiskLevel { get; }
|
||||
public IReadOnlyList<PermissionLevelMapping.MappingResult> Mappings { get; }
|
||||
|
||||
public string ObjectType => Inner.ObjectType;
|
||||
public string Title => Inner.Title;
|
||||
public string Url => Inner.Url;
|
||||
public bool HasUniquePermissions => Inner.HasUniquePermissions;
|
||||
public string Users => Inner.Users;
|
||||
public string UserLogins => Inner.UserLogins;
|
||||
public string PermissionLevels => Inner.PermissionLevels;
|
||||
public string GrantedThrough => Inner.GrantedThrough;
|
||||
public string PrincipalType => Inner.PrincipalType;
|
||||
public string? TargetUrl => Inner.TargetUrl;
|
||||
public string? TargetLabel => Inner.TargetLabel;
|
||||
public string? SharingLinkType => Inner.SharingLinkType;
|
||||
|
||||
public SimplifiedPermissionEntry(PermissionEntry entry)
|
||||
{
|
||||
Inner = entry;
|
||||
Mappings = PermissionLevelMapping.GetMappings(entry.PermissionLevels);
|
||||
SimplifiedLabels = PermissionLevelMapping.GetSimplifiedLabels(entry.PermissionLevels);
|
||||
RiskLevel = PermissionLevelMapping.GetHighestRisk(entry.PermissionLevels);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<SimplifiedPermissionEntry> WrapAll(IEnumerable<PermissionEntry> entries)
|
||||
=> entries.Select(e => new SimplifiedPermissionEntry(e)).ToList();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record SiteInfo(string Url, string Title)
|
||||
{
|
||||
public long StorageUsedMb { get; init; }
|
||||
public long StorageQuotaMb { get; init; }
|
||||
public string Template { get; init; } = string.Empty;
|
||||
|
||||
public SiteKind Kind => SiteKindHelper.FromTemplate(Template);
|
||||
}
|
||||
|
||||
public enum SiteKind
|
||||
{
|
||||
Unknown,
|
||||
TeamSite,
|
||||
CommunicationSite,
|
||||
Classic
|
||||
}
|
||||
|
||||
public static class SiteKindHelper
|
||||
{
|
||||
public static SiteKind FromTemplate(string template)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template)) return SiteKind.Unknown;
|
||||
if (template.StartsWith("GROUP#", StringComparison.OrdinalIgnoreCase)) return SiteKind.TeamSite;
|
||||
if (template.StartsWith("SITEPAGEPUBLISHING#", StringComparison.OrdinalIgnoreCase)) return SiteKind.CommunicationSite;
|
||||
if (template.StartsWith("STS#", StringComparison.OrdinalIgnoreCase)) return SiteKind.Classic;
|
||||
return SiteKind.Unknown;
|
||||
}
|
||||
|
||||
public static string DisplayName(SiteKind kind) => kind switch
|
||||
{
|
||||
SiteKind.TeamSite => "Team site",
|
||||
SiteKind.CommunicationSite => "Communication site",
|
||||
SiteKind.Classic => "Classic site",
|
||||
_ => "Other"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class SiteTemplate
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string SourceUrl { get; set; } = string.Empty;
|
||||
public DateTime CapturedAt { get; set; }
|
||||
public string SiteType { get; set; } = string.Empty;
|
||||
public SiteTemplateOptions Options { get; set; } = new();
|
||||
public TemplateSettings? Settings { get; set; }
|
||||
public TemplateLogo? Logo { get; set; }
|
||||
public List<TemplateLibraryInfo> Libraries { get; set; } = new();
|
||||
public List<TemplatePermissionGroup> PermissionGroups { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TemplateSettings
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int Language { get; set; }
|
||||
}
|
||||
|
||||
public class TemplateLogo
|
||||
{
|
||||
public string LogoUrl { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class SiteTemplateOptions
|
||||
{
|
||||
public bool CaptureLibraries { get; set; } = true;
|
||||
public bool CaptureFolders { get; set; } = true;
|
||||
public bool CapturePermissionGroups { get; set; } = true;
|
||||
public bool CaptureLogo { get; set; } = true;
|
||||
public bool CaptureSettings { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class StorageNode
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string SiteTitle { get; set; } = string.Empty;
|
||||
public string Library { get; set; } = string.Empty;
|
||||
public long TotalSizeBytes { get; set; }
|
||||
public long FileStreamSizeBytes { get; set; }
|
||||
public long VersionSizeBytes => Math.Max(0L, TotalSizeBytes - FileStreamSizeBytes);
|
||||
public long TotalFileCount { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public int IndentLevel { get; set; }
|
||||
public StorageNodeKind Kind { get; set; } = StorageNodeKind.Library;
|
||||
public List<StorageNode> Children { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum StorageNodeKind
|
||||
{
|
||||
Library,
|
||||
HiddenLibrary,
|
||||
PreservationHold,
|
||||
ListAttachments,
|
||||
RecycleBin,
|
||||
Subsite
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record StorageScanOptions(
|
||||
bool PerLibrary = true,
|
||||
bool IncludeSubsites = false,
|
||||
int FolderDepth = 0,
|
||||
bool IncludeHiddenLibraries = true,
|
||||
bool IncludePreservationHold = true,
|
||||
bool IncludeListAttachments = true,
|
||||
bool IncludeRecycleBin = true
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
using SharepointToolbox.Web.Core.Helpers;
|
||||
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record SystemGroupTarget(
|
||||
SystemGroupKind Kind,
|
||||
string Label,
|
||||
string Url,
|
||||
string? LinkType = null
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class TemplateFolderInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public List<TemplateFolderInfo> Children { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class TemplateLibraryInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string BaseType { get; set; } = string.Empty;
|
||||
public int BaseTemplate { get; set; }
|
||||
public List<TemplateFolderInfo> Folders { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class TemplatePermissionGroup
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> RoleDefinitions { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class TenantProfile
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>https://contoso.sharepoint.com</summary>
|
||||
public string TenantUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Azure AD tenant GUID or domain (e.g. contoso.onmicrosoft.com). Required for app-only Graph calls.</summary>
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Azure AD app registration client (application) ID.</summary>
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
public LogoData? ClientLogo { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class TransferJob
|
||||
{
|
||||
public string SourceSiteUrl { get; set; } = string.Empty;
|
||||
public string SourceLibrary { get; set; } = string.Empty;
|
||||
public string SourceFolderPath { get; set; } = string.Empty;
|
||||
public string DestinationSiteUrl { get; set; } = string.Empty;
|
||||
public string DestinationLibrary { get; set; } = string.Empty;
|
||||
public string DestinationFolderPath { get; set; } = string.Empty;
|
||||
public TransferMode Mode { get; set; } = TransferMode.Copy;
|
||||
public ConflictPolicy ConflictPolicy { get; set; } = ConflictPolicy.Skip;
|
||||
public IReadOnlyList<string> SelectedFilePaths { get; set; } = Array.Empty<string>();
|
||||
public bool IncludeSourceFolder { get; set; }
|
||||
public bool CopyFolderContents { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum TransferMode { Copy, Move }
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum AccessType
|
||||
{
|
||||
Direct,
|
||||
Group,
|
||||
Inherited
|
||||
}
|
||||
|
||||
public record UserAccessEntry(
|
||||
string UserDisplayName,
|
||||
string UserLogin,
|
||||
string SiteUrl,
|
||||
string SiteTitle,
|
||||
string ObjectType,
|
||||
string ObjectTitle,
|
||||
string ObjectUrl,
|
||||
string PermissionLevel,
|
||||
AccessType AccessType,
|
||||
string GrantedThrough,
|
||||
bool IsHighPrivilege,
|
||||
bool IsExternalUser,
|
||||
string? TargetUrl = null,
|
||||
string? TargetLabel = null,
|
||||
string? SharingLinkType = null
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
TechN0 = 0, // Read-only
|
||||
TechN1 = 1, // Read/Write
|
||||
Admin = 2 // Read/Write + account management + client profiles
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public record VersionCleanupOptions(
|
||||
IReadOnlyList<string> LibraryTitles,
|
||||
int KeepLast,
|
||||
bool KeepFirst)
|
||||
{
|
||||
public static VersionCleanupOptions Default => new(Array.Empty<string>(), 5, false);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace SharepointToolbox.Web.Core.Models;
|
||||
|
||||
public class VersionCleanupResult
|
||||
{
|
||||
public string SiteUrl { get; init; } = string.Empty;
|
||||
public string Library { get; init; } = string.Empty;
|
||||
public string FileServerRelativeUrl { get; init; } = string.Empty;
|
||||
public string FileName { get; init; } = string.Empty;
|
||||
public int VersionsBefore { get; init; }
|
||||
public int VersionsDeleted { get; init; }
|
||||
public int VersionsRemaining { get; init; }
|
||||
public long BytesFreed { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user