Initial commit

This commit is contained in:
2026-06-02 10:51:14 +02:00
committed by kawa
commit d19092c84e
182 changed files with 13757 additions and 0 deletions
+7
View File
@@ -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;
}
+41
View File
@@ -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);
}
}
+32
View File
@@ -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();
}
}
+74
View File
@@ -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);
+46
View File
@@ -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);
}
}
+32
View File
@@ -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"),
};
}
+7
View File
@@ -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();
}
+2
View File
@@ -0,0 +1,2 @@
// SystemGroupKind, SystemGroupClassification, and PermissionEntryHelper are defined in PermissionEntryHelper.cs
+7
View File
@@ -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";
}
+9
View File
@@ -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";
}
+11
View File
@@ -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; }
}
+14
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
namespace SharepointToolbox.Web.Core.Models;
public class BrandingSettings
{
public LogoData? MspLogo { get; set; }
}
+11
View File
@@ -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;
}
+29
View File
@@ -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; }
}
+13
View File
@@ -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;
}
+3
View File
@@ -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;
}
+22
View File
@@ -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);
}
+8
View File
@@ -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();
}
+15
View File
@@ -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;
}
+12
View File
@@ -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
);
+11
View File
@@ -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();
}
+17
View File
@@ -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);
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace SharepointToolbox.Web.Core.Models;
public record GraphDirectoryUser(
string DisplayName,
string UserPrincipalName,
string? Mail,
string? Department,
string? JobTitle,
string? UserType);
+9
View File
@@ -0,0 +1,9 @@
namespace SharepointToolbox.Web.Core.Models;
public record LocationInfo(
string SiteUrl,
string SiteTitle,
string ObjectTitle,
string ObjectUrl,
string ObjectType
);
+7
View File
@@ -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;
}
+7
View File
@@ -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);
}
+17
View File
@@ -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
);
+33
View File
@@ -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();
}
}
+3
View File
@@ -0,0 +1,3 @@
namespace SharepointToolbox.Web.Core.Models;
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);
+3
View File
@@ -0,0 +1,3 @@
namespace SharepointToolbox.Web.Core.Models;
public record ResolvedMember(string DisplayName, string Login);
+9
View File
@@ -0,0 +1,9 @@
namespace SharepointToolbox.Web.Core.Models;
public enum RiskLevel
{
High,
Medium,
Low,
ReadOnly
}
+8
View File
@@ -0,0 +1,8 @@
namespace SharepointToolbox.Web.Core.Models;
public record ScanOptions(
bool IncludeInherited = false,
bool ScanFolders = true,
int FolderDepth = 1,
bool IncludeSubsites = false
);
+15
View File
@@ -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
);
+13
View File
@@ -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; }
}
+11
View File
@@ -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;
}
+35
View File
@@ -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();
}
+38
View File
@@ -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"
};
}
+27
View File
@@ -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;
}
+10
View File
@@ -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;
}
+17
View File
@@ -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();
}
+11
View File
@@ -0,0 +1,11 @@
namespace SharepointToolbox.Web.Core.Models;
public enum StorageNodeKind
{
Library,
HiddenLibrary,
PreservationHold,
ListAttachments,
RecycleBin,
Subsite
}
+11
View File
@@ -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
);
+10
View File
@@ -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
);
+8
View File
@@ -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();
}
+9
View File
@@ -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();
}
+8
View File
@@ -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();
}
+18
View File
@@ -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; }
}
+16
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
namespace SharepointToolbox.Web.Core.Models;
public enum TransferMode { Copy, Move }
+26
View File
@@ -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
);
+8
View File
@@ -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
}
+9
View File
@@ -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);
}
+14
View File
@@ -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; }
}