commit 5991d7fa5f59dc3d9ad40f7c97711fae281e5bcc Author: Hamid Reza Mohammadi Date: Fri Aug 14 21:06:22 2020 +0430 Add project files. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..256a1615 --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +/Museum/dist/js +/Museum/src/dist diff --git a/GanjoorService.sln b/GanjoorService.sln new file mode 100644 index 00000000..2a3d188a --- /dev/null +++ b/GanjoorService.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSecurityBackend", "RSecurityBackend\RSecurityBackend.csproj", "{1BF45DCD-40F4-4D6E-9134-D6B3DE39C56E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RMuseum", "RMuseum\RMuseum.csproj", "{113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1BF45DCD-40F4-4D6E-9134-D6B3DE39C56E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BF45DCD-40F4-4D6E-9134-D6B3DE39C56E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BF45DCD-40F4-4D6E-9134-D6B3DE39C56E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BF45DCD-40F4-4D6E-9134-D6B3DE39C56E}.Release|Any CPU.Build.0 = Release|Any CPU + {113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CC36BB5B-D678-42F3-9EF2-149A535A8191} + EndGlobalSection +EndGlobal diff --git a/RMuseum/Controllers/AppUserController.cs b/RMuseum/Controllers/AppUserController.cs new file mode 100644 index 00000000..b959b118 --- /dev/null +++ b/RMuseum/Controllers/AppUserController.cs @@ -0,0 +1,189 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Services; +using RSecurityBackend.Controllers; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Configuration; +using Audit.WebApi; + +namespace RMuseum.Controllers +{ + /// + /// User login/logout/register/... + /// + [Produces("application/json")] + [Route("api/users")] + public class AppUserController : AppUserControllerBase + { + /// + /// constructor + /// + /// + /// + /// + /// + /// + /// + /// + public AppUserController(IConfiguration configuration, IAppUserService appUserService, IHttpContextAccessor httpContextAccessor, IUserPermissionChecker userPermissionChecker, IEmailSender emailSender, IImageFileService imageFileService, ICaptchaService captchaService) + : base(configuration, appUserService, httpContextAccessor, userPermissionChecker, emailSender, imageFileService, captchaService) + { + + } + + + + + + /// + /// Sign Up Email Subject + /// + /// + /// subject + /// + /// + protected override string GetSignUpEmailSubject(string secretCode) + { + return $"{secretCode} کد ثبت نام شما در گنجینهٔ گنجور"; + } + + /// + /// Sign Up Email Html Content + /// + /// + /// html content + protected override string GetSignUpEmailHtmlContent(string secretCode) + { + string content = + "" + + + "" + + + "" + + + "" + + + $"کد ثبت نام شما در گنجینهٔ گنجور: {secretCode}" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "
" + + + "\"گنجینهٔ" + + + "
" + + + $"

لطفاً جهت تکمیل ثبت نام در گنجینهٔ گنجور اینجا کلیک کنید یا اگر صفحهٔ ثبت نام هنوز باز است کد {secretCode} را در آن وارد کنید.

" + + + "
" + + + "

اگر در گنجینهٔ گنجور ثبت نام نکرده‌اید لطفاً این نامه را نادیده بگیرید. به این نامه پاسخ ندهید، کسی پاسخگوی شما نخواهد بود.

" + + + "
" + + + "" + + + "" + ; + + return content; + } + + /// + /// Forgot Password Email Subject + /// + /// + /// subject + /// + /// + protected override string GetForgotPasswordEmailSubject(string secretCode) + { + return $"{secretCode} کد بازیابی کلمهٔ عبور شما در گنجینهٔ گنجور"; + } + + /// + /// Forgot Password Email Html Content + /// + /// + /// html content + protected override string GetForgotPasswordEmailHtmlContent(string secretCode) + { + string content = + "" + + + "" + + + "" + + + "" + + + $"کد تغییر گذرواژهٔ شما در گنجینهٔ گنجور: {secretCode}" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "" + + + "
" + + + "\"گنجینهٔ" + + + "
" + + + $"

لطفاً جهت تکمیل فرایند فراموشی گذرواژه در گنجینهٔ گنجور اینجا کلیک کنید یا اگر صفحهٔ فراموشی گذرواژه هنوز باز است کد {secretCode} را در آن وارد کنید.

" + + + "
" + + + "

اگر در گنجینهٔ گنجور فراموشی گذرواژه را نزده‌اید یا گذرواژه‌تان را به خاطر آوردید لطفاً این نامه را نادیده بگیرید. به این نامه پاسخ ندهید، کسی پاسخگوی شما نخواهد بود.

" + + + "
" + + + "" + + + "" + ; + + return content; + } + + /// + /// Is Sign-up enabled? + /// + /// + protected override bool IsSignupEnabled() + { + return true; + } + + } +} diff --git a/RMuseum/Controllers/ArtifactController.cs b/RMuseum/Controllers/ArtifactController.cs new file mode 100644 index 00000000..332fb979 --- /dev/null +++ b/RMuseum/Controllers/ArtifactController.cs @@ -0,0 +1,1947 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using RMuseum.Models.Artifact; +using RMuseum.Models.Artifact.ViewModels; +using RMuseum.Models.Auth.Memory; +using RMuseum.Models.Bookmark; +using RMuseum.Models.Bookmark.ViewModels; +using RMuseum.Models.GanjoorIntegration; +using RMuseum.Models.GanjoorIntegration.ViewModels; +using RMuseum.Models.ImportJob; +using RMuseum.Models.Note; +using RMuseum.Models.Note.ViewModels; +using RMuseum.Models.Notification; +using RMuseum.Services; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace RMuseum.Controllers +{ + [Produces("application/json")] + [Route("api/artifacts")] + public class ArtifactController : Controller + { + /// + /// get all published artifacts (including CoverImage info but not items info) + /// + /// + /// + + [HttpGet] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + + public async Task Get([FromQuery]PagingParameterModel paging) + { + var cacheKey = $"artifacts/all/{paging.PageSize}/{paging.PageNumber}"; + if (!_memoryCache.TryGetValue(cacheKey, out RServiceResult<(PaginationMetadata PagingMeta, RArtifactMasterRecord[] Items)> itemsInfo)) + { + itemsInfo = await _artifactService.GetAll(paging, new PublishStatus[] { PublishStatus.Published }); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + + _memoryCache.Set(cacheKey, itemsInfo); + } + + if(itemsInfo.Result.Items.Count() > 0) + { + DateTime lastModification = itemsInfo.Result.Items.Max(i => i.LastModified); + Response.GetTypedHeaders().LastModified = lastModification; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= lastModification) + { + return StatusCode(StatusCodes.Status304NotModified); + } + } + + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(itemsInfo.Result.PagingMeta)); + + return Ok(itemsInfo.Result.Items); + } + + /// + /// get list of artifact statuses user can see + /// + /// + /// + /// + private async Task> _GetUserVisibleArtifactStatusSet(Guid loggedOnUserId, Guid sessionId) + { + RServiceResult + canView = + await _userPermissionChecker.Check + ( + loggedOnUserId, + sessionId, + RMuseumSecurableItem.ArtifactEntityShortName, + RMuseumSecurableItem.ViewDraftOperationShortName + ); + if (!string.IsNullOrEmpty(canView.ExceptionString)) + return new RServiceResult(null, canView.ExceptionString); + + PublishStatus[] visibleItems = + canView.Result + ? + new PublishStatus[] + { + PublishStatus.Published, + PublishStatus.Restricted, + PublishStatus.Draft, + PublishStatus.Awaiting + } + : + new PublishStatus[] + { + PublishStatus.Published + }; + + return new RServiceResult(visibleItems); + } + + /// + /// get all artifacts visible by user (including CoverImage info but not items info) + /// + /// + /// + + [HttpGet] + [Route("secure")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserVisible([FromQuery]PagingParameterModel paging) + { + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value), + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + if (visibleItems.Length == 1 && visibleItems[0] == PublishStatus.Published) //Caching + { + return await Get(paging); + } + + RServiceResult<(PaginationMetadata PagingMeta, RArtifactMasterRecord[] Items)> itemsInfo = await _artifactService.GetAll(paging, visibleItems); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + + if (itemsInfo.Result.Items.Count() > 0) + { + DateTime lastModification = itemsInfo.Result.Items.Max(i => i.LastModified); + Response.GetTypedHeaders().LastModified = lastModification; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= lastModification) + { + return StatusCode(StatusCodes.Status304NotModified); + } + } + + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(itemsInfo.Result.PagingMeta)); + + return Ok(itemsInfo.Result.Items); + } + + [HttpGet("tagged/{tagUrl}/{valueUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetByTagValue(string tagUrl, string valueUrl) + { + RServiceResult itemsInfo = await _artifactService.GetByTagValue(tagUrl, valueUrl, new PublishStatus[] { PublishStatus.Published } ); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + + if (itemsInfo.Result.Length > 0) + { + DateTime lastModification = itemsInfo.Result.Max(i => i.LastModified); + Response.GetTypedHeaders().LastModified = lastModification; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= lastModification) + { + return StatusCode(StatusCodes.Status304NotModified); + } + } + + return Ok(itemsInfo.Result); + } + + + /// + /// gets specified publish artifact info (including CoverImage + images) + /// + /// + /// + [HttpGet("{friendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task Get(string friendlyUrl) + { + var cacheKey = $"artifacts/friendlyUrl/{friendlyUrl}"; + if (!_memoryCache.TryGetValue(cacheKey, out RServiceResult itemInfo)) + { + itemInfo = await _artifactService.GetByFriendlyUrl(friendlyUrl, new PublishStatus[] { PublishStatus.Published }); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + _memoryCache.Set(cacheKey, itemInfo); + } + + + Response.GetTypedHeaders().LastModified = itemInfo.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(itemInfo.Result); + } + + /// + /// remove unpublished artifact having no notes and not bookmarked + /// + /// + /// + [HttpDelete("{artifactId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.DeleteOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task RemoveArtifactHavingNoNoteAndBookmarks(Guid artifactId) + { + RServiceResult res = await _artifactService.RemoveArtifactHavingNoNoteAndBookmarks(artifactId, true); + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + /// + /// gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + /// + /// + /// + /// + [HttpGet("{friendlyUrl}/filteritemsbytag/{tagFriendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetByFriendlyUrlFilterItemsByTag(string friendlyUrl, string tagFriendlyUrl) + { + return await GetByFriendlyUrlFilterItemsByTag(friendlyUrl, tagFriendlyUrl, null); + } + + /// + /// gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + /// + /// + /// + /// + /// + [HttpGet("{friendlyUrl}/filteritemsbytag/{tagFriendlyUrl}/{valueFriendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetByFriendlyUrlFilterItemsByTag(string friendlyUrl, string tagFriendlyUrl, string valueFriendlyUrl) + { + var cacheKey = $"{friendlyUrl}/filteritemsbytag/{tagFriendlyUrl}"; + if(valueFriendlyUrl != null) + { + cacheKey += $"/{valueFriendlyUrl}"; + } + if (!_memoryCache.TryGetValue(cacheKey, out RServiceResult itemInfo)) + { + itemInfo = await _artifactService.GetByFriendlyUrlFilterItemsByTag(friendlyUrl, new PublishStatus[] { PublishStatus.Published }, tagFriendlyUrl, valueFriendlyUrl); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + _memoryCache.Set(cacheKey, itemInfo); + } + + Response.GetTypedHeaders().LastModified = itemInfo.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(itemInfo.Result); + } + + /// + /// gets specified publish artifact info (including CoverImage + images) + /// + /// + /// + [HttpGet("secure/{friendlyUrl}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetUserVisible(string friendlyUrl) + { + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value), + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + RServiceResult itemInfo = null; + if (visibleItems.Length == 1 && visibleItems[0] == PublishStatus.Published) + { + var cacheKey = $"artifacts/friendlyUrl/{friendlyUrl}"; + if (!_memoryCache.TryGetValue(cacheKey, out itemInfo)) + { + itemInfo = await _artifactService.GetByFriendlyUrl(friendlyUrl, new PublishStatus[] { PublishStatus.Published }); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + _memoryCache.Set(cacheKey, itemInfo); + } + } + if(itemInfo == null) + { + itemInfo = await _artifactService.GetByFriendlyUrl(friendlyUrl, visibleItems); + } + + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = itemInfo.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + + return Ok(itemInfo.Result); + } + + /// + /// gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + /// + /// + /// + /// + [HttpGet("secure/{friendlyUrl}/filteritemsbytag/{tagFriendlyUrl}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetUserVisibleByFriendlyUrlFilterItemsByTag(string friendlyUrl, string tagFriendlyUrl) + { + return await GetUserVisibleByFriendlyUrlFilterItemsByTag(friendlyUrl, tagFriendlyUrl, null); + } + + /// + /// gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + /// + /// + /// + /// + /// + [HttpGet("secure/{friendlyUrl}/filteritemsbytag/{tagFriendlyUrl}/{valueFriendlyUrl}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetUserVisibleByFriendlyUrlFilterItemsByTag(string friendlyUrl, string tagFriendlyUrl, string valueFriendlyUrl) + { + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value), + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + if(visibleItems.Length == 1 && visibleItems[0] == PublishStatus.Published) + { + return await GetByFriendlyUrlFilterItemsByTag(friendlyUrl, tagFriendlyUrl, valueFriendlyUrl); + } + RServiceResult itemInfo = await _artifactService.GetByFriendlyUrlFilterItemsByTag(friendlyUrl, visibleItems, tagFriendlyUrl, valueFriendlyUrl); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = itemInfo.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(itemInfo.Result); + } + + /// + /// edit artifactt master record (user should have additional permissions artifact:awaiting and artifact:publish to change status of artifact) + /// + /// + /// editing related collections such as images and attributed or complex properties such as CoverImage is ignored + /// + /// + /// + /// + [HttpPut("{id}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task Put(Guid id, [FromBody]RArtifactMasterRecord item) + { + if (id != item.Id) + { + return BadRequest(); + } + + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + Guid sessionId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); + + RServiceResult + canChangeStatusToAwaiting = + await _userPermissionChecker.Check + ( + loggedOnUserId, + sessionId, + RMuseumSecurableItem.ArtifactEntityShortName, + RMuseumSecurableItem.ToAwaitingStatusOperationShortName + ); + if (!string.IsNullOrEmpty(canChangeStatusToAwaiting.ExceptionString)) + return BadRequest(canChangeStatusToAwaiting.ExceptionString); + + RServiceResult + canPublish = + await _userPermissionChecker.Check + ( + loggedOnUserId, + sessionId, + RMuseumSecurableItem.ArtifactEntityShortName, + RMuseumSecurableItem.PublishOperationShortName + ); + if (!string.IsNullOrEmpty(canPublish.ExceptionString)) + return BadRequest(canPublish.ExceptionString); + + RServiceResult itemInfo = await _artifactService.EditMasterRecord(item, canChangeStatusToAwaiting.Result, canPublish.Result); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + + if(itemInfo == null) + { + return NotFound(); + } + + return Ok();; + } + + /// + /// Set Artifact Cover Item Index + /// + /// + /// + /// + [HttpPut("{id}/cover")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SetArtifactCoverItemIndex(Guid id, [FromBody]int itemIndex) + { + RServiceResult res = await _artifactService.SetArtifactCoverItemIndex(id, itemIndex); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(); + return Ok(); + } + + /// + /// get tag bundle by frindly url + /// + /// + /// + [HttpGet("tagbundle/{friendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTagBundleViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetTagBundleByFiendlyUrl(string friendlyUrl) + { + var cacheKey = $"tagbundle/{friendlyUrl}"; + if (!_memoryCache.TryGetValue(cacheKey, out RServiceResult tag)) + { + tag = await _artifactService.GetTagBundleByFiendlyUrl(friendlyUrl); + + if (!string.IsNullOrEmpty(tag.ExceptionString)) + { + return BadRequest(tag.ExceptionString); + } + if (tag.Result == null) + return NotFound(); + + _memoryCache.Set(cacheKey, tag); + } + + + + RServiceResult lastModified = await _artifactService.GetMaxArtifactLastModified(); + if(!string.IsNullOrEmpty(lastModified.ExceptionString)) + { + return BadRequest(lastModified.ExceptionString); + } + + DateTime lastModification = lastModified.Result; + Response.GetTypedHeaders().LastModified = lastModification; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= lastModification) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(tag.Result); + } + + /// + /// add new tag + /// + /// only name is processed + /// + [HttpPost("tag")] + [Authorize(Policy = RMuseumSecurableItem.TagEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTag))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task NewTag([FromBody]RTag tag) + { + RServiceResult res = await _artifactService.AddTag(tag.Name); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// get all tags + /// + /// + /// + [HttpGet("tag")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetTags([FromQuery]PagingParameterModel paging) + { + RServiceResult<(PaginationMetadata PagingMeta, RTag[] Items)> itemsInfo = await _artifactService.GetAllTags(paging); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(itemsInfo.Result.PagingMeta)); + + return Ok(itemsInfo.Result.Items); + } + + [HttpGet("tag/{friendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTag))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetTagByFriendlyUrl(string friendlyUrl) + { + RServiceResult tag = await _artifactService.GetTagByFriendlyUrl(friendlyUrl); + if (!string.IsNullOrEmpty(tag.ExceptionString)) + { + return BadRequest(tag.ExceptionString); + } + + return Ok(tag.Result); + } + + + /// + /// edit tag + /// + /// + /// editable fields are limited + /// + /// + /// + /// + [HttpPut("tag/{tagid}")] + [Authorize(Policy = RMuseumSecurableItem.TagEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task PutTag(Guid tagid, [FromBody]RTag tag) + { + if (tagid != tag.Id) + { + return BadRequest(); + } + + RServiceResult itemInfo = await _artifactService.EditTag(tag); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + + if (itemInfo == null) + { + return NotFound(); + } + + return Ok();; + } + + /// + /// changes order of tags based on their position in artifacts + /// + /// + /// + /// up / down + /// the other tag which its Order got replaced with the input id + [HttpPut("tag/move/{tagid}/in/{artifactId}/{upordown}")] + [Authorize(Policy = RMuseumSecurableItem.TagEntityShortName + ":" + RMuseumSecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task MoveTag(Guid tagId, Guid artifactId, string upordown) + { + + + RServiceResult otherTagId = await _artifactService.EditTagOrderBasedOnArtifact(tagId, artifactId, upordown == "up"); + if (!string.IsNullOrEmpty(otherTagId.ExceptionString)) + { + return BadRequest(otherTagId.ExceptionString); + } + + return Ok((Guid)otherTagId.Result); + } + + /// + /// changes order of tags based on their position in artifact items + /// + /// + /// + /// up / down + /// the other tag which its Order got replaced with the input id + [HttpPut("tag/move/{tagid}/in/item/{itemId}/{upordown}")] + [Authorize(Policy = RMuseumSecurableItem.TagEntityShortName + ":" + RMuseumSecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task MoveTagInItem(Guid tagId, Guid itemId, string upordown) + { + + + RServiceResult otherTagId = await _artifactService.EditTagOrderBasedOnItem(tagId, itemId, upordown == "up"); + if (!string.IsNullOrEmpty(otherTagId.ExceptionString)) + { + return BadRequest(otherTagId.ExceptionString); + } + + return Ok((Guid)otherTagId.Result); + } + + /// + /// get tag value bundle by frindly url + /// + /// + /// + /// + [HttpGet("tagbundle/{friendlyUrl}/{valueUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactTagViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetTagValueBundleByFiendlyUrl(string friendlyUrl, string valueUrl) + { + RServiceResult tag = await _artifactService.GetTagValueBundleByFiendlyUrl(friendlyUrl, valueUrl); + if (!string.IsNullOrEmpty(tag.ExceptionString)) + { + return BadRequest(tag.ExceptionString); + } + if (tag.Result == null) + return NotFound(); + return Ok(tag.Result); + } + + /// + /// get tag value by frindly url + /// + /// + /// + /// + [HttpGet("tagvalue/{tagId}/{friendlyUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTagValue))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetTagValueByFriendlyUrl(Guid tagId, string friendlyUrl) + { + RServiceResult tag = await _artifactService.GetTagValueByFriendlyUrl(tagId, friendlyUrl); + if (!string.IsNullOrEmpty(tag.ExceptionString)) + { + return BadRequest(tag.ExceptionString); + } + + return Ok(tag.Result); + } + + /// + /// add new tag value to artifact + /// + /// + /// only name is processed + /// + [HttpPost("tagvalue/{artifactid}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTagValue))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task NewArtifactTagValue(Guid artifactId, [FromBody]RTag tag) + { + RServiceResult res = await _artifactService.TagArtifact(artifactId, tag); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + + /// + /// edit artifact attribute value + /// + /// + /// editable fields are limited + /// + /// + /// + /// apply on all same value tags + /// + [HttpPut("tagvalue/{artifactid}/{global=true}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task PutAttributeValue(Guid artifactid, bool global, [FromBody]RTagValue tagvalue) + { + + RServiceResult itemInfo = await _artifactService.EditTagValue(artifactid, tagvalue, global); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + + if (itemInfo == null) + { + return NotFound(); + } + + return Ok();; + } + + /// + /// remove tag from artifact + /// + /// + /// + /// + [HttpDelete("tagvalue/{artifactId}/{tagValueId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task RemoveArtifactTagValue(Guid artifactId, Guid tagValueId) + { + RServiceResult res = await _artifactService.UnTagArtifact(artifactId, tagValueId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + /// + /// add new tag value to item + /// + /// + /// only name is processed + /// + [HttpPost("itemtagvalue/{itemId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RTagValue))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task NewItemTagValue(Guid itemId, [FromBody]RTag tag) + { + RServiceResult res = await _artifactService.TagItem(itemId, tag); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// edit item attribute value + /// + /// + /// editable fields are limited + /// + /// + /// + /// apply on all same value tags + /// + [HttpPut("itemtagvalue/{itemId}/{global=true}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task PutItemTagValue(Guid itemId, bool global, [FromBody]RTagValue tagvalue) + { + + RServiceResult itemInfo = await _artifactService.EditItemTagValue(itemId, tagvalue, global); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + + if (itemInfo == null) + { + return NotFound(); + } + + return Ok();; + } + + /// + /// remove tag from item + /// + /// + /// + /// + [HttpDelete("itemtagvalue/{itemId}/{tagValueId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task RemoveItemTagValue(Guid itemId, Guid tagValueId) + { + RServiceResult res = await _artifactService.UnTagItem(itemId, tagValueId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + /// + /// changes order of tag values based on their position in an artifact + /// + /// + /// + /// + /// up / down + /// the other tag which its Order got replaced with the input id + [HttpPut("tagvalue/{artifactid}/{tagId}/{valueId}/move/{upordown}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task MoveTagValue(Guid artifactId, Guid tagId, Guid valueId, string upordown) + { + + + RServiceResult otherValueId = await _artifactService.EditTagValueOrder(artifactId, tagId, valueId, upordown == "up"); + if (!string.IsNullOrEmpty(otherValueId.ExceptionString)) + { + return BadRequest(otherValueId.ExceptionString); + } + + return Ok((Guid)otherValueId.Result); + } + + /// + /// changes order of tag values based on their position in an item + /// + /// + /// + /// + /// up / down + /// the other tag which its Order got replaced with the input id + [HttpPut("itemtagvalue/{itemId}/{tagId}/{valueId}/move/{upordown}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.EditTagValueOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task MoveItemTagValue(Guid itemId, Guid tagId, Guid valueId, string upordown) + { + + + RServiceResult otherValueId = await _artifactService.EditItemTagValueOrder(itemId, tagId, valueId, upordown == "up"); + if (!string.IsNullOrEmpty(otherValueId.ExceptionString)) + { + return BadRequest(otherValueId.ExceptionString); + } + + return Ok((Guid)otherValueId.Result); + } + + + + /// + /// gets specified publish artifact item info (including images + attributes) + /// + /// + /// + /// + [HttpGet("{artifactUrl}/{itemUrl}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactItemRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetArtifactItemByFrienlyUrl(string artifactUrl, string itemUrl) + { + RServiceResult itemInfo = await _artifactService.GetArtifactItemByFrienlyUrl(artifactUrl, itemUrl, new PublishStatus[] { PublishStatus.Published }); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = itemInfo.Result.Item.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.Item.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(itemInfo.Result); + } + /// + /// gets specified publish artifact item info (including images + attributes) + /// + /// + /// + /// + [HttpGet("secure/{artifactUrl}/{itemUrl}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactItemRecordViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetArtifactItemByFrienlyUrlUserVisible(string artifactUrl, string itemUrl) + { + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value), + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + RServiceResult itemInfo = await _artifactService.GetArtifactItemByFrienlyUrl(artifactUrl, itemUrl, visibleItems); + if (!string.IsNullOrEmpty(itemInfo.ExceptionString)) + { + return BadRequest(itemInfo.ExceptionString); + } + if (itemInfo.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = itemInfo.Result.Item.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= itemInfo.Result.Item.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + return Ok(itemInfo.Result); + } + + + + /// + /// Add new artifact (multipart/form-data) + /// + /// + /// const data = new FormData(); + /// data.append('name', 'تست'); + /// data.append('description', ''); + /// data.append('srcUrl', ''); + /// data.append('picTitle', 'تست'); + /// data.append('picDescription', ''); + /// data.append('file', this.file); + /// data.append('picSrcUrl', ''); + /// + /// + /// + /// + + [HttpPost] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.AddOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RArtifactMasterRecord))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public virtual async Task Post() + { + + try + { + NewArtifact newArtifactInfo = + new NewArtifact() + { + Name = Request.Form["name"], + Description = Request.Form["description"], + SrcUrl = Request.Form["srcUrl"], + PicTitle = Request.Form["picTitle"], + PicDescription = Request.Form["picDescription"], + File = Request.Form.Files[0], + PicSrcUrl = Request.Form["picSrcUrl"] + }; + + RServiceResult result = + await _artifactService.Add + ( + newArtifactInfo.Name, newArtifactInfo.Description, newArtifactInfo.SrcUrl, + newArtifactInfo.PicTitle, newArtifactInfo.PicDescription, newArtifactInfo.File, + newArtifactInfo.PicSrcUrl, + null); + + if (result.Result == null) + return BadRequest(result.ExceptionString); + return Ok(result.Result); + } + catch(Exception exp) + { + return BadRequest(exp.ToString()); + } + + } + + /// + /// import from external resources + /// + /// loc/princeton/harvard/qajarwomen/hathitrust/penn/cam/bl/folder/walters/cbl + /// 119 + /// golestan-baysonghori + /// + /// + [HttpPost] + [Route("import")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Import(string srcType, string resourceNumber, string friendlyUrl, string resourcePrefix) + { + RServiceResult res = + srcType == "princeton" ? + await _artifactService.StartImportingFromPrinceton(resourceNumber, friendlyUrl) + : + srcType == "harvard" ? + await _artifactService.StartImportingFromHarvard(resourceNumber, friendlyUrl) + : + srcType == "qajarwomen" ? + await _artifactService.StartImportingFromHarvardDirectly(resourceNumber, friendlyUrl, resourcePrefix) + : + srcType == "hathitrust" ? + await _artifactService.StartImportingFromHathiTrust(resourceNumber, friendlyUrl) + : + srcType == "penn" ? + await _artifactService.StartImportingFromPenLibraries(resourceNumber, friendlyUrl) + : + srcType == "cam" ? + await _artifactService.StartImportingFromCambridge(resourceNumber, friendlyUrl) + : + srcType == "bl" ? + await _artifactService.StartImportingFromBritishLibrary(resourceNumber, friendlyUrl) + : + srcType == "folder" ? + await _artifactService.StartImportingFromServerFolder(resourceNumber, friendlyUrl, resourcePrefix) + : + srcType == "walters" ? + await _artifactService.StartImportingFromWalters(resourceNumber, friendlyUrl) + : + srcType == "cbl" ? + await _artifactService.StartImportingFromChesterBeatty(resourceNumber, friendlyUrl) + : + await _artifactService.StartImportingFromTheLibraryOfCongress(resourceNumber, friendlyUrl, resourcePrefix); + if (res.Result) + return Ok(); + return BadRequest(res.ExceptionString); + } + + /// + /// keep alive (for import background service) + /// + /// true + [HttpGet] + [Route("keep-alive")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public IActionResult KeepAlive() + { + return Ok(true); + } + + + /// + /// retry import + /// + /// + [HttpPost] + [Route("retryimport")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task RetryImport() + { + RServiceResult res = await _artifactService.RescheduleJobs(); + if (res.Result) + return Ok(); + return BadRequest(res.ExceptionString); + } + + /// + /// due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + /// + /// 123456 + /// + [HttpPost] + [Route("reexamineimport")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task ReExamineLocDownloads(string pass) + { + if(pass != "123456") //this is a heavy processor consuming call, so prevent mistaken call of it from swagger ui + return BadRequest("invalid password"); + RServiceResult res = await _artifactService.ReExamineLocDownloads(); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + [HttpGet] + [Route("jobs")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.AddOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetImportJobs([FromQuery]PagingParameterModel paging) + { + RServiceResult<(PaginationMetadata PagingMeta, ImportJob[] Items)> itemsInfo = await _artifactService.GetImportJobs(paging); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(itemsInfo.Result.PagingMeta)); + + return Ok(itemsInfo.Result.Items); + } + + /// + /// bookmark artifact + /// + /// + /// + [HttpPost] + [Route("bookmark/{artifactId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task BookmarkArtifact(Guid artifactId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.BookmarkArtifact(artifactId, loggedOnUserId, RBookmarkType.Bookmark); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// bookmark item + /// + /// + /// + [HttpPost] + [Route("bookmark/item/{itemId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task BookmarkItem(Guid itemId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.BookmarkItem(itemId, loggedOnUserId, RBookmarkType.Bookmark); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// item bookmarks info + /// + /// + /// + [HttpDelete] + [Route("bookmark/{bookmarkId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task DeleteBookmark(Guid bookmarkId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.DeleteUserBookmark(bookmarkId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok();; + } + + /// + /// fav artifact + /// + /// + /// + [HttpPost] + [Route("fav/{artifactId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task FavArtifact(Guid artifactId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.BookmarkArtifact(artifactId, loggedOnUserId, RBookmarkType.Favorite); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// fav item + /// + /// + /// + [HttpPost] + [Route("fav/item/{itemId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task FavItem(Guid itemId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.BookmarkItem(itemId, loggedOnUserId, RBookmarkType.Favorite); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// artifact bookmarks info + /// + /// + /// + [HttpGet] + [Route("bookmark/{artifactId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GeIArtifactUserBookmarks(Guid artifactId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GetArtifactUserBookmarks(artifactId, loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// item bookmarks info + /// + /// + /// + [HttpGet] + [Route("bookmark/item/{itemId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GeItemUserBookmarks(Guid itemId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GeItemUserBookmarks(itemId, loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// user bookmarks + /// + /// + [HttpGet] + [Route("bookmark")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmark[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserBookmarks([FromQuery]PagingParameterModel paging) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + + RServiceResult<(PaginationMetadata PagingMeta, RUserBookmarkViewModel[] Bookmarks)> res = await _artifactService.GetBookmarks(paging, loggedOnUserId, RBookmarkType.Bookmark, visibleItems); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); + return Ok(res.Result.Bookmarks); + } + + /// + /// user favorites + /// + /// + [HttpGet] + [Route("fav")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserBookmarkViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserFavorites([FromQuery]PagingParameterModel paging) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + RServiceResult<(PaginationMetadata PagingMeta, RUserBookmarkViewModel[] Bookmarks)> res = await _artifactService.GetBookmarks(paging, loggedOnUserId, RBookmarkType.Favorite, visibleItems); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); + return Ok(res.Result.Bookmarks); + } + + /// + /// add note for artifact + /// + /// + /// + [HttpPost] + [Route("note")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task AddUserNoteToArtifact([FromBody]PostUserNote note) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.AddUserNoteToArtifact(note.EntityId, loggedOnUserId, note.NoteType, note.Contents, note.ReferenceNoteId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// add note for artifact item + /// + /// + /// + [HttpPost] + [Route("note/item")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task AddUserNoteToArtifactItem([FromBody]PostUserNote note) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.AddUserNoteToArtifactItem(note.EntityId, loggedOnUserId, note.NoteType, note.Contents, note.ReferenceNoteId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + + /// + /// Edit User Notes + /// + /// a note can not be edited by a user other than its owner or another using having note:moderate permission + /// + /// only htmlContent is processed + /// + [HttpPut] + [Route("note/{noteId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task EditUserNote(Guid noteId, [FromBody]PostUserNote note) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult + canModerate = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + RMuseumSecurableItem.NoteEntityShortName, + RMuseumSecurableItem.ModerateCommentsOperationShortName + ); + if (!string.IsNullOrEmpty(canModerate.ExceptionString)) + return BadRequest(canModerate.ExceptionString); + + RServiceResult res = await _artifactService.EditUserNote(noteId, canModerate.Result ? (Guid?)null : loggedOnUserId, note.Contents); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// delete user notes + /// + /// + /// 1. a note can not be deleted by a user other than its owner or another using having note:moderate permission + /// 2. all notes which have refernce to deleting note (sent in reply to it) would be deleted irrelevant of their ownership + /// + /// + /// list of notes deleted + [HttpDelete] + [Route("note/{noteId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task DeleteUserNote(Guid noteId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult + canModerate = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + RMuseumSecurableItem.NoteEntityShortName, + RMuseumSecurableItem.ModerateCommentsOperationShortName + ); + if (!string.IsNullOrEmpty(canModerate.ExceptionString)) + return BadRequest(canModerate.ExceptionString); + + RServiceResult res = await _artifactService.DeleteUserNote(noteId, canModerate.Result ? (Guid?)null : loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// get private notes for artifact + /// + /// + /// + [HttpGet] + [Route("note/private/{artifactId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetArtifactUserNotes(Guid artifactId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GetArtifactUserNotes(artifactId, loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// get private notes for artifact + /// + /// + /// + [HttpGet] + [Route("note/{artifactId}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetArtifactPublicNotes(Guid artifactId) + { + RServiceResult res = await _artifactService.GetArtifactPublicNotes(artifactId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// get private notes for artifact item + /// + /// + /// + [HttpGet] + [Route("note/item/private/{itemId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetArtifactItemUserNotes(Guid itemId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GetArtifactItemUserNotes(itemId, loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// get public notes for artifact item + /// + /// + /// + [HttpGet] + [Route("note/item/{itemId}")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetArtifactItemPublicNotes(Guid itemId) + { + RServiceResult res = await _artifactService.GetArtifactItemPublicNotes(itemId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + return Ok(res.Result); + } + + /// + /// user public notes + /// + /// + [HttpGet] + [Route("note/public")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserPublicNotes([FromQuery]PagingParameterModel paging) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + + RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)> res = await _artifactService.GetUserNotes(loggedOnUserId, paging, RNoteType.Public, visibleItems); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); + return Ok(res.Result.Notes); + } + + /// + /// user public notes + /// + /// + [HttpGet] + [Route("note/private")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserPrivateNotes([FromQuery]PagingParameterModel paging) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult v = await _GetUserVisibleArtifactStatusSet + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value) + ); + if (!string.IsNullOrEmpty(v.ExceptionString)) + return BadRequest(v.ExceptionString); + PublishStatus[] visibleItems = v.Result; + + + RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)> res = await _artifactService.GetUserNotes(loggedOnUserId, paging, RNoteType.Private, visibleItems); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); + return Ok(res.Result.Notes); + } + + /// + /// all users public notes + /// + /// + [HttpGet] + [Route("note/all")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNoteViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetAllPublicNotes([FromQuery]PagingParameterModel paging) + { + RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)> res = await _artifactService.GetAllPublicNotes(paging); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(res.Result.PagingMeta)); + return Ok(res.Result.Notes); + } + + + /// + /// suggest ganjoor link + /// + /// + /// + [HttpPost] + [Route("ganjoor")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorLinkViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SuggestGanjoorLink([FromBody]LinkSuggestion link) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult suggestion = await _artifactService.SuggestGanjoorLink(loggedOnUserId, link); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(suggestion.Result); + } + + /// + /// get suggested ganjoor links + /// + /// + /// + /// + [HttpGet] + [Route("ganjoor")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorLinkViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetSuggestedLinks(ReviewResult status, bool notSynced) + { + RServiceResult res = await _artifactService.GetSuggestedLinks(status, notSynced); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + + /// + /// review suggested ganjoor link + /// + /// + /// + /// + [HttpPut] + [Route("ganjoor/review/{linkId}/{result}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ReviewGanjoorLinksOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task ReviewGanjoorLink(Guid linkId, ReviewResult result) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult suggestion = await _artifactService.ReviewSuggestedLink(linkId, loggedOnUserId, result); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(); + } + + /// + /// mark suggested ganjoor link as synchronized + /// + /// + /// + [HttpPut] + [Route("ganjoor/sync/{linkId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ReviewGanjoorLinksOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SynchronizeGanjoorLink(Guid linkId) + { + RServiceResult suggestion = await _artifactService.SynchronizeSuggestedLink(linkId); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(); + } + + /// + ///toc / temporary one time api / to be removed + /// + /// + [HttpPut] + [Route("ganjoor/toc")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ReviewGanjoorLinksOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task AddTOCForSuggestedLinks() + { + RServiceResult suggestion = await _artifactService.AddTOCForSuggestedLinks(); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(suggestion.Result); + } + + /// + /// get suggested pinterest links + /// + /// + /// + /// + [HttpGet] + [Route("pinterest")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(PinterestLinkViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetSuggestedPinterestLinks(ReviewResult status, bool notSynced) + { + RServiceResult res = await _artifactService.GetSuggestedPinterestLinks(status, notSynced); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// suggest pinterest link for ganjoor + /// + /// + /// + [HttpPost] + [Route("pinterest")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(PinterestLinkViewModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SuggestPinterestLink([FromBody]PinterestSuggestion suggestion) + { + RServiceResult captchaRes = await _captchaService.Evaluate(suggestion.CaptchaImageId, suggestion.CaptchaValue); + if (!string.IsNullOrEmpty(captchaRes.ExceptionString)) + return BadRequest(captchaRes.ExceptionString); + + if (!captchaRes.Result) + return BadRequest("مقدار تصویر امنیتی درست وارد نشده است."); + + RServiceResult res = await _artifactService.SuggestPinterestLink(suggestion); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + + + return Ok(res.Result); + } + /// + /// review suggested ganjoor pinterest link + /// + /// + /// + /// + /// + /// + /// + [HttpPut] + [Route("pinterest/review/{linkId}/{result}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ReviewGanjoorLinksOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task ReviewSuggestedPinterestLink(Guid linkId, string altText, ReviewResult result, string reviewDesc, string imageUrl) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult suggestion = await _artifactService.ReviewSuggestedPinterestLink(linkId, loggedOnUserId, altText, result, reviewDesc, imageUrl); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(); + } + + /// + /// mark suggested pinterest ganjoor link as synchronized + /// + /// + /// + [HttpPut] + [Route("pinterest/sync/{linkId}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + RMuseumSecurableItem.ReviewGanjoorLinksOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SynchronizeSuggestedPinterestLink(Guid linkId) + { + RServiceResult suggestion = await _artifactService.SynchronizeSuggestedPinterestLink(linkId); + if (!string.IsNullOrEmpty(suggestion.ExceptionString)) + return BadRequest(suggestion.ExceptionString); + return Ok(); + } + + /// + /// Get User Notifications + /// + /// + [HttpGet] + [Route("notifications")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNotification[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUserNotifications() + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GetUserNotifications(loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + [HttpGet] + [Route("notifications/unread/count")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(int))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetUnreadUserNotificationsCount() + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult res = await _artifactService.GetUnreadUserNotificationsCount(loggedOnUserId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// Switch Notification Status + /// + /// + /// + [HttpPut] + [Route("notifications/{notificationId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RUserNotification))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SwitchNotificationStatus(Guid notificationId) + { + RServiceResult res = await _artifactService.SwitchNotificationStatus(notificationId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + /// + /// Delete Notification + /// + /// + /// + [HttpDelete] + [Route("notifications/{notificationId}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task DeleteNotification(Guid notificationId) + { + RServiceResult res = await _artifactService.DeleteNotification(notificationId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(); + } + + /// + /// constructor + /// + /// + /// + /// + /// + /// + public ArtifactController(IArtifactService artifactService, IUserPermissionChecker userPermissionChecker, IMemoryCache memoryCache, ICaptchaService captchaService) + { + _artifactService = artifactService; + _userPermissionChecker = userPermissionChecker; + _memoryCache = memoryCache; + _captchaService = captchaService; + } + + /// + /// Artifact Service + /// + protected readonly IArtifactService _artifactService; + + /// + /// IUserPermissionChecker instance + /// + protected IUserPermissionChecker _userPermissionChecker; + + /// + /// IMemoryCache + /// + protected readonly IMemoryCache _memoryCache; + + /// + /// Captcha service + /// + protected readonly ICaptchaService _captchaService; + } +} diff --git a/RMuseum/Controllers/AuditLogController.cs b/RMuseum/Controllers/AuditLogController.cs new file mode 100644 index 00000000..230ad3bf --- /dev/null +++ b/RMuseum/Controllers/AuditLogController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Controllers; +using RSecurityBackend.Services; + + +namespace RMuseum.Controllers +{ + /// + /// Audit Log Controller Base + /// + [Produces("application/json")] + [Route("api/auditlogs")] + public class AuditLogController : AuditLogControllerBase + { + /// + /// constructor + /// + /// + /// + public AuditLogController(IAuditLogService auditLogService) + : base(auditLogService) + { + + } + } +} diff --git a/RMuseum/Controllers/ImageController.cs b/RMuseum/Controllers/ImageController.cs new file mode 100644 index 00000000..26ddd2e3 --- /dev/null +++ b/RMuseum/Controllers/ImageController.cs @@ -0,0 +1,121 @@ +using Audit.WebApi; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RMuseum.Models.Artifact; +using RMuseum.Models.Auth.Memory; +using RMuseum.Services; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +namespace RMuseum.Controllers +{ + [Produces("application/json")] + [Route("api/images")] + public class ImageController : Controller + { + /// + /// returns image stream + /// + /// + /// + /// + [HttpGet("{size}/{id}.jpg")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + + public async Task Get(Guid id, string size) + { + RServiceResult img = + await _pictureFileService.GetImage(id); + + if (!string.IsNullOrEmpty(img.ExceptionString)) + return BadRequest(img.ExceptionString); + + if (img.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = img.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= img.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + RServiceResult imgPath = _pictureFileService.GetImagePath(img.Result, size); + if(!string.IsNullOrEmpty(imgPath.ExceptionString)) + return BadRequest(imgPath.ExceptionString); + + + return new FileStreamResult(new FileStream(imgPath.Result, FileMode.Open, FileAccess.Read), "image/jpeg"); + + } + + /// + /// Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + /// + /// + /// + /// + [HttpPut("{id}/{degIn90mul}")] + [Authorize(Policy = RMuseumSecurableItem.ArtifactEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RPictureFile))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Rotate(Guid id, int degIn90mul) + { + RServiceResult res = await _pictureFileService.RotateImage(id, degIn90mul); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + + + /// + /// Generate Cropped Image Based On ThumbnailCoordinates For Notes + /// + /// + /// + /// + /// + /// + /// id of cropped image + [HttpGet("cropped/{id}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RImage))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GenerateCroppedImageBasedOnThumbnailCoordinates(Guid id, decimal left, decimal top, decimal width, decimal height) + { + RServiceResult cropped = await _pictureFileService.GenerateCroppedImageBasedOnThumbnailCoordinates(id, left, top, width, height); + if (!string.IsNullOrEmpty(cropped.ExceptionString)) + return BadRequest(cropped.ExceptionString); + return Ok(cropped.Result); + } + + + /// + /// constructor + /// + /// + /// + public ImageController(IPictureFileService pictureFileService) + { + _pictureFileService = pictureFileService; + } + + /// + /// Artifact Service + /// + protected readonly IPictureFileService _pictureFileService; + + + } +} diff --git a/RMuseum/Controllers/RImageController.cs b/RMuseum/Controllers/RImageController.cs new file mode 100644 index 00000000..9cc09f1f --- /dev/null +++ b/RMuseum/Controllers/RImageController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Services; +using RSecurityBackend.Controllers; +using Audit.WebApi; + +namespace RMuseum.Controllers +{ + /// + /// Generic Image Provider + /// + [Produces("application/json")] + [Route("api/rimages")] + public class RImageController : RImageControllerBase + { + /// + /// constructor + /// + /// + /// + public RImageController(IImageFileService pictureFileService) : base(pictureFileService) + { + + } + } +} diff --git a/RMuseum/Controllers/RoleController.cs b/RMuseum/Controllers/RoleController.cs new file mode 100644 index 00000000..74a77307 --- /dev/null +++ b/RMuseum/Controllers/RoleController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Services; +using RSecurityBackend.Controllers; +using Audit.WebApi; + +namespace RMuseum.Controllers +{ + /// + ///roles + /// + [Produces("application/json")] + [Route("api/roles")] + public class RoleController : RoleControllerBase + { + /// + /// constructor + /// + /// + public RoleController(IUserRoleService roleService) + : base(roleService) + { + } + } +} diff --git a/RMuseum/DbContext/RMuseumDbContext.cs b/RMuseum/DbContext/RMuseumDbContext.cs new file mode 100644 index 00000000..bd009b65 --- /dev/null +++ b/RMuseum/DbContext/RMuseumDbContext.cs @@ -0,0 +1,130 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using RMuseum.Models.Artifact; +using RMuseum.Models.Bookmark; +using RMuseum.Models.GanjoorIntegration; +using RMuseum.Models.ImportJob; +using RMuseum.Models.Note; +using RMuseum.Models.Notification; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using System; + +namespace RMuseum.DbContext +{ + /// + /// Museum Database Context + /// + public class RMuseumDbContext : RSecurityDbContext + { + /// + /// constructor + /// + public RMuseumDbContext(IConfiguration configuration) + { + Configuration = configuration; + Database.Migrate(); + } + + /// + /// + /// + public IConfiguration Configuration { get; } + + /// + /// + /// + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); + base.OnConfiguring(optionsBuilder); + } + + /// + /// + /// + /// + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity() + .HasIndex(m => m.FriendlyUrl) + .IsUnique(); + + builder.Entity() + .HasIndex(i => new { i.RArtifactMasterRecordId, i.FriendlyUrl }) + .IsUnique(); + + builder.Entity() + .HasIndex(i => new { i.RArtifactMasterRecordId, i.Order }) + .IsUnique(); + + builder.Entity() + .HasIndex(t => t.FriendlyUrl); + + builder.Entity() + .HasIndex(t => t.FriendlyUrl); + + } + + + /// + /// Picture Files + /// + public DbSet PictureFiles { get; set; } + + /// + /// Item Attributes + /// + public DbSet Tags { get; set; } + + /// + /// Artifacts + /// + public DbSet Artifacts { get; set; } + + /// + /// Items + /// + public DbSet Items { get; set; } + + + /// + /// Import Jobs + /// + public DbSet ImportJobs { get; set; } + + /// + /// Tags + /// + public DbSet TagValues { get; set; } + + /// + /// User Bookmarks + /// + public DbSet UserBookmarks { get; set; } + + /// + /// User Notes + /// + public DbSet UserNotes { get; set; } + + /// + /// Ganjoor Links + /// + public DbSet GanjoorLinks { get; set; } + + /// + /// Notifications + /// + public DbSet Notifications { get; set; } + + /// + /// Pinterest Links + /// + public DbSet PinterestLinks { get; set; } + + } +} diff --git a/RMuseum/Migrations/20190827145117_Initial.Designer.cs b/RMuseum/Migrations/20190827145117_Initial.Designer.cs new file mode 100644 index 00000000..c7e5b20c --- /dev/null +++ b/RMuseum/Migrations/20190827145117_Initial.Designer.cs @@ -0,0 +1,660 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20190827145117_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageIndex"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("RArtifactMasterRecordId"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageId"); + + b.Property("CoverItemIndex"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("ItemCount"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("GlobalValue"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("PluralName"); + + b.Property("PluralNameInEnglish"); + + b.Property("Status"); + + b.Property("TagType"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RTagId"); + + b.Property("Status"); + + b.Property("Value"); + + b.Property("ValueInEnglish"); + + b.Property("ValueSupplement"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("EndTime"); + + b.Property("Exception"); + + b.Property("FriendlyUrl"); + + b.Property("JobType"); + + b.Property("ProgressPercent"); + + b.Property("QueueTime"); + + b.Property("ResourceNumber"); + + b.Property("SrcContent"); + + b.Property("SrcUrl"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Description"); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("CreateDate"); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FirstName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RImageId"); + + b.Property("SecurityStamp"); + + b.Property("Status"); + + b.Property("SureName"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("RImageId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("OperationShortName"); + + b.Property("RAppRoleId"); + + b.Property("SecurableItemShortName"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("Language"); + + b.Property("LastRenewal"); + + b.Property("LoginTime"); + + b.Property("RAppUserId"); + + b.Property("Token"); + + b.Property("ValidUntil"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("DateTime"); + + b.Property("Email"); + + b.Property("Language"); + + b.Property("PhoneNumber"); + + b.Property("QueueType"); + + b.Property("Secret"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("DataTime"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("FileSizeInBytes"); + + b.Property("FolderName"); + + b.Property("ImageHeight"); + + b.Property("ImageWidth"); + + b.Property("LastModified"); + + b.Property("OriginalFileName"); + + b.Property("StoredFileName"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("LastModifiedMeta"); + + b.Property("NormalSizeImageHeight"); + + b.Property("NormalSizeImageStoredFileName"); + + b.Property("NormalSizeImageWidth"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("SrcUrl"); + + b.Property("Status"); + + b.Property("ThumbnailImageHeight"); + + b.Property("ThumbnailImageStoredFileName"); + + b.Property("ThumbnailImageWidth"); + + b.Property("Title"); + + b.Property("TitleInEnglish"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20190827145117_Initial.cs b/RMuseum/Migrations/20190827145117_Initial.cs new file mode 100644 index 00000000..4f03cdf0 --- /dev/null +++ b/RMuseum/Migrations/20190827145117_Initial.cs @@ -0,0 +1,624 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + Description = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false), + Order = table.Column(nullable: false), + TagType = table.Column(nullable: false), + FriendlyUrl = table.Column(nullable: true), + Status = table.Column(nullable: false), + Name = table.Column(nullable: true), + NameInEnglish = table.Column(nullable: true), + PluralName = table.Column(nullable: true), + PluralNameInEnglish = table.Column(nullable: true), + GlobalValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "VerifyQueueItems", + columns: table => new + { + Id = table.Column(nullable: false), + QueueType = table.Column(nullable: false), + Email = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + DateTime = table.Column(nullable: false), + Secret = table.Column(nullable: true), + ClientIPAddress = table.Column(nullable: true), + ClientAppName = table.Column(nullable: true), + Language = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_VerifyQueueItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + Id = table.Column(nullable: false), + SecurableItemShortName = table.Column(nullable: true), + OperationShortName = table.Column(nullable: true), + RAppRoleId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + table.ForeignKey( + name: "FK_Permissions_AspNetRoles_RAppRoleId", + column: x => x.RAppRoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ImportJobs", + columns: table => new + { + Id = table.Column(nullable: false), + JobType = table.Column(nullable: false), + ResourceNumber = table.Column(nullable: true), + FriendlyUrl = table.Column(nullable: true), + SrcUrl = table.Column(nullable: true), + QueueTime = table.Column(nullable: false), + StartTime = table.Column(nullable: false), + EndTime = table.Column(nullable: false), + Status = table.Column(nullable: false), + ProgressPercent = table.Column(nullable: false), + Exception = table.Column(nullable: true), + ArtifactId = table.Column(nullable: true), + SrcContent = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportJobs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Items", + columns: table => new + { + Id = table.Column(nullable: false), + RArtifactMasterRecordId = table.Column(nullable: false), + Order = table.Column(nullable: false), + FriendlyUrl = table.Column(nullable: true), + Name = table.Column(nullable: true), + NameInEnglish = table.Column(nullable: true), + Description = table.Column(nullable: true), + DescriptionInEnglish = table.Column(nullable: true), + CoverImageIndex = table.Column(nullable: false), + LastModified = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Items", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "GeneralImages", + columns: table => new + { + Id = table.Column(nullable: false), + OriginalFileName = table.Column(nullable: true), + ContentType = table.Column(nullable: true), + FileSizeInBytes = table.Column(nullable: false), + ImageWidth = table.Column(nullable: false), + ImageHeight = table.Column(nullable: false), + FolderName = table.Column(nullable: true), + StoredFileName = table.Column(nullable: true), + DataTime = table.Column(nullable: false), + LastModified = table.Column(nullable: false), + Discriminator = table.Column(nullable: false), + Title = table.Column(nullable: true), + TitleInEnglish = table.Column(nullable: true), + Description = table.Column(nullable: true), + DescriptionInEnglish = table.Column(nullable: true), + Status = table.Column(nullable: true), + Order = table.Column(nullable: true), + NormalSizeImageStoredFileName = table.Column(nullable: true), + ThumbnailImageStoredFileName = table.Column(nullable: true), + NormalSizeImageWidth = table.Column(nullable: true), + NormalSizeImageHeight = table.Column(nullable: true), + ThumbnailImageWidth = table.Column(nullable: true), + ThumbnailImageHeight = table.Column(nullable: true), + SrcUrl = table.Column(nullable: true), + LastModifiedMeta = table.Column(nullable: true), + RArtifactItemRecordId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_GeneralImages", x => x.Id); + table.ForeignKey( + name: "FK_GeneralImages_Items_RArtifactItemRecordId", + column: x => x.RArtifactItemRecordId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Artifacts", + columns: table => new + { + Id = table.Column(nullable: false), + FriendlyUrl = table.Column(nullable: true), + Status = table.Column(nullable: false), + Name = table.Column(nullable: true), + NameInEnglish = table.Column(nullable: true), + Description = table.Column(nullable: true), + DescriptionInEnglish = table.Column(nullable: true), + DateTime = table.Column(nullable: false), + LastModified = table.Column(nullable: false), + CoverItemIndex = table.Column(nullable: false), + CoverImageId = table.Column(nullable: false), + ItemCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Artifacts", x => x.Id); + table.ForeignKey( + name: "FK_Artifacts_GeneralImages_CoverImageId", + column: x => x.CoverImageId, + principalTable: "GeneralImages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + FirstName = table.Column(nullable: true), + SureName = table.Column(nullable: true), + CreateDate = table.Column(nullable: false), + Status = table.Column(nullable: false), + RImageId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUsers_GeneralImages_RImageId", + column: x => x.RImageId, + principalTable: "GeneralImages", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "CaptchaImages", + columns: table => new + { + Id = table.Column(nullable: false), + RImageId = table.Column(nullable: false), + Value = table.Column(nullable: true), + DateTime = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CaptchaImages", x => x.Id); + table.ForeignKey( + name: "FK_CaptchaImages_GeneralImages_RImageId", + column: x => x.RImageId, + principalTable: "GeneralImages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TagValues", + columns: table => new + { + Id = table.Column(nullable: false), + RTagId = table.Column(nullable: false), + Order = table.Column(nullable: false), + FriendlyUrl = table.Column(nullable: true), + Status = table.Column(nullable: false), + Value = table.Column(nullable: true), + ValueInEnglish = table.Column(nullable: true), + ValueSupplement = table.Column(nullable: true), + RArtifactItemRecordId = table.Column(nullable: true), + RArtifactMasterRecordId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TagValues", x => x.Id); + table.ForeignKey( + name: "FK_TagValues_Items_RArtifactItemRecordId", + column: x => x.RArtifactItemRecordId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TagValues_Artifacts_RArtifactMasterRecordId", + column: x => x.RArtifactMasterRecordId, + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TagValues_Tags_RTagId", + column: x => x.RTagId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Sessions", + columns: table => new + { + Id = table.Column(nullable: false), + RAppUserId = table.Column(nullable: false), + ClientIPAddress = table.Column(nullable: true), + ClientAppName = table.Column(nullable: true), + Language = table.Column(nullable: true), + LoginTime = table.Column(nullable: false), + LastRenewal = table.Column(nullable: false), + ValidUntil = table.Column(nullable: false), + Token = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sessions", x => x.Id); + table.ForeignKey( + name: "FK_Sessions_AspNetUsers_RAppUserId", + column: x => x.RAppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Artifacts_CoverImageId", + table: "Artifacts", + column: "CoverImageId"); + + migrationBuilder.CreateIndex( + name: "IX_Artifacts_FriendlyUrl", + table: "Artifacts", + column: "FriendlyUrl", + unique: true, + filter: "[FriendlyUrl] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUsers_RImageId", + table: "AspNetUsers", + column: "RImageId"); + + migrationBuilder.CreateIndex( + name: "IX_CaptchaImages_RImageId", + table: "CaptchaImages", + column: "RImageId"); + + migrationBuilder.CreateIndex( + name: "IX_GeneralImages_RArtifactItemRecordId", + table: "GeneralImages", + column: "RArtifactItemRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_ImportJobs_ArtifactId", + table: "ImportJobs", + column: "ArtifactId"); + + migrationBuilder.CreateIndex( + name: "IX_Items_RArtifactMasterRecordId_FriendlyUrl", + table: "Items", + columns: new[] { "RArtifactMasterRecordId", "FriendlyUrl" }, + unique: true, + filter: "[FriendlyUrl] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Items_RArtifactMasterRecordId_Order", + table: "Items", + columns: new[] { "RArtifactMasterRecordId", "Order" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_RAppRoleId", + table: "Permissions", + column: "RAppRoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Sessions_RAppUserId", + table: "Sessions", + column: "RAppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_FriendlyUrl", + table: "Tags", + column: "FriendlyUrl"); + + migrationBuilder.CreateIndex( + name: "IX_TagValues_FriendlyUrl", + table: "TagValues", + column: "FriendlyUrl"); + + migrationBuilder.CreateIndex( + name: "IX_TagValues_RArtifactItemRecordId", + table: "TagValues", + column: "RArtifactItemRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_TagValues_RArtifactMasterRecordId", + table: "TagValues", + column: "RArtifactMasterRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_TagValues_RTagId", + table: "TagValues", + column: "RTagId"); + + migrationBuilder.CreateIndex( + name: "IX_VerifyQueueItems_Secret", + table: "VerifyQueueItems", + column: "Secret", + unique: true, + filter: "[Secret] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_ImportJobs_Artifacts_ArtifactId", + table: "ImportJobs", + column: "ArtifactId", + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Items_Artifacts_RArtifactMasterRecordId", + table: "Items", + column: "RArtifactMasterRecordId", + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Artifacts_GeneralImages_CoverImageId", + table: "Artifacts"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "CaptchaImages"); + + migrationBuilder.DropTable( + name: "ImportJobs"); + + migrationBuilder.DropTable( + name: "Permissions"); + + migrationBuilder.DropTable( + name: "Sessions"); + + migrationBuilder.DropTable( + name: "TagValues"); + + migrationBuilder.DropTable( + name: "VerifyQueueItems"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "GeneralImages"); + + migrationBuilder.DropTable( + name: "Items"); + + migrationBuilder.DropTable( + name: "Artifacts"); + } + } +} diff --git a/RMuseum/Migrations/20190908153305_Bookmarks.Designer.cs b/RMuseum/Migrations/20190908153305_Bookmarks.Designer.cs new file mode 100644 index 00000000..bae9573e --- /dev/null +++ b/RMuseum/Migrations/20190908153305_Bookmarks.Designer.cs @@ -0,0 +1,704 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20190908153305_Bookmarks")] + partial class Bookmarks + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageIndex"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("RArtifactMasterRecordId"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageId"); + + b.Property("CoverItemIndex"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("ItemCount"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("GlobalValue"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("PluralName"); + + b.Property("PluralNameInEnglish"); + + b.Property("Status"); + + b.Property("TagType"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RTagId"); + + b.Property("Status"); + + b.Property("Value"); + + b.Property("ValueInEnglish"); + + b.Property("ValueSupplement"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("Note"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RBookmarkType"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("EndTime"); + + b.Property("Exception"); + + b.Property("FriendlyUrl"); + + b.Property("JobType"); + + b.Property("ProgressPercent"); + + b.Property("QueueTime"); + + b.Property("ResourceNumber"); + + b.Property("SrcContent"); + + b.Property("SrcUrl"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Description"); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("CreateDate"); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FirstName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RImageId"); + + b.Property("SecurityStamp"); + + b.Property("Status"); + + b.Property("SureName"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("RImageId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("OperationShortName"); + + b.Property("RAppRoleId"); + + b.Property("SecurableItemShortName"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("Language"); + + b.Property("LastRenewal"); + + b.Property("LoginTime"); + + b.Property("RAppUserId"); + + b.Property("Token"); + + b.Property("ValidUntil"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("DateTime"); + + b.Property("Email"); + + b.Property("Language"); + + b.Property("PhoneNumber"); + + b.Property("QueueType"); + + b.Property("Secret"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("DataTime"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("FileSizeInBytes"); + + b.Property("FolderName"); + + b.Property("ImageHeight"); + + b.Property("ImageWidth"); + + b.Property("LastModified"); + + b.Property("OriginalFileName"); + + b.Property("StoredFileName"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("LastModifiedMeta"); + + b.Property("NormalSizeImageHeight"); + + b.Property("NormalSizeImageStoredFileName"); + + b.Property("NormalSizeImageWidth"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("SrcUrl"); + + b.Property("Status"); + + b.Property("ThumbnailImageHeight"); + + b.Property("ThumbnailImageStoredFileName"); + + b.Property("ThumbnailImageWidth"); + + b.Property("Title"); + + b.Property("TitleInEnglish"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20190908153305_Bookmarks.cs b/RMuseum/Migrations/20190908153305_Bookmarks.cs new file mode 100644 index 00000000..abf2ee07 --- /dev/null +++ b/RMuseum/Migrations/20190908153305_Bookmarks.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class Bookmarks : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserBookmarks", + columns: table => new + { + Id = table.Column(nullable: false), + RAppUserId = table.Column(nullable: false), + RArtifactMasterRecordId = table.Column(nullable: true), + RArtifactItemRecordId = table.Column(nullable: true), + DateTime = table.Column(nullable: false), + RBookmarkType = table.Column(nullable: false), + Note = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserBookmarks", x => x.Id); + table.ForeignKey( + name: "FK_UserBookmarks_AspNetUsers_RAppUserId", + column: x => x.RAppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserBookmarks_Items_RArtifactItemRecordId", + column: x => x.RArtifactItemRecordId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserBookmarks_Artifacts_RArtifactMasterRecordId", + column: x => x.RArtifactMasterRecordId, + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserBookmarks_RAppUserId", + table: "UserBookmarks", + column: "RAppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserBookmarks_RArtifactItemRecordId", + table: "UserBookmarks", + column: "RArtifactItemRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_UserBookmarks_RArtifactMasterRecordId", + table: "UserBookmarks", + column: "RArtifactMasterRecordId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserBookmarks"); + } + } +} diff --git a/RMuseum/Migrations/20190909123824_UserNotes.Designer.cs b/RMuseum/Migrations/20190909123824_UserNotes.Designer.cs new file mode 100644 index 00000000..798f0038 --- /dev/null +++ b/RMuseum/Migrations/20190909123824_UserNotes.Designer.cs @@ -0,0 +1,762 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20190909123824_UserNotes")] + partial class UserNotes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageIndex"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("RArtifactMasterRecordId"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageId"); + + b.Property("CoverItemIndex"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("ItemCount"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("GlobalValue"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("PluralName"); + + b.Property("PluralNameInEnglish"); + + b.Property("Status"); + + b.Property("TagType"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RTagId"); + + b.Property("Status"); + + b.Property("Value"); + + b.Property("ValueInEnglish"); + + b.Property("ValueSupplement"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("Note"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RBookmarkType"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("EndTime"); + + b.Property("Exception"); + + b.Property("FriendlyUrl"); + + b.Property("JobType"); + + b.Property("ProgressPercent"); + + b.Property("QueueTime"); + + b.Property("ResourceNumber"); + + b.Property("SrcContent"); + + b.Property("SrcUrl"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("HtmlContent"); + + b.Property("LastModified"); + + b.Property("Modified"); + + b.Property("NoteType"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("ReferenceNoteId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Description"); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("CreateDate"); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FirstName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RImageId"); + + b.Property("SecurityStamp"); + + b.Property("Status"); + + b.Property("SureName"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("RImageId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("OperationShortName"); + + b.Property("RAppRoleId"); + + b.Property("SecurableItemShortName"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("Language"); + + b.Property("LastRenewal"); + + b.Property("LoginTime"); + + b.Property("RAppUserId"); + + b.Property("Token"); + + b.Property("ValidUntil"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("DateTime"); + + b.Property("Email"); + + b.Property("Language"); + + b.Property("PhoneNumber"); + + b.Property("QueueType"); + + b.Property("Secret"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("DataTime"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("FileSizeInBytes"); + + b.Property("FolderName"); + + b.Property("ImageHeight"); + + b.Property("ImageWidth"); + + b.Property("LastModified"); + + b.Property("OriginalFileName"); + + b.Property("StoredFileName"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("LastModifiedMeta"); + + b.Property("NormalSizeImageHeight"); + + b.Property("NormalSizeImageStoredFileName"); + + b.Property("NormalSizeImageWidth"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("SrcUrl"); + + b.Property("Status"); + + b.Property("ThumbnailImageHeight"); + + b.Property("ThumbnailImageStoredFileName"); + + b.Property("ThumbnailImageWidth"); + + b.Property("Title"); + + b.Property("TitleInEnglish"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20190909123824_UserNotes.cs b/RMuseum/Migrations/20190909123824_UserNotes.cs new file mode 100644 index 00000000..7c5ac635 --- /dev/null +++ b/RMuseum/Migrations/20190909123824_UserNotes.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class UserNotes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserNotes", + columns: table => new + { + Id = table.Column(nullable: false), + RAppUserId = table.Column(nullable: false), + RArtifactMasterRecordId = table.Column(nullable: true), + RArtifactItemRecordId = table.Column(nullable: true), + DateTime = table.Column(nullable: false), + Modified = table.Column(nullable: false), + LastModified = table.Column(nullable: false), + NoteType = table.Column(nullable: false), + HtmlContent = table.Column(nullable: true), + ReferenceNoteId = table.Column(nullable: true), + Status = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserNotes", x => x.Id); + table.ForeignKey( + name: "FK_UserNotes_AspNetUsers_RAppUserId", + column: x => x.RAppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserNotes_Items_RArtifactItemRecordId", + column: x => x.RArtifactItemRecordId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserNotes_Artifacts_RArtifactMasterRecordId", + column: x => x.RArtifactMasterRecordId, + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserNotes_UserNotes_ReferenceNoteId", + column: x => x.ReferenceNoteId, + principalTable: "UserNotes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserNotes_RAppUserId", + table: "UserNotes", + column: "RAppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserNotes_RArtifactItemRecordId", + table: "UserNotes", + column: "RArtifactItemRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_UserNotes_RArtifactMasterRecordId", + table: "UserNotes", + column: "RArtifactMasterRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_UserNotes_ReferenceNoteId", + table: "UserNotes", + column: "ReferenceNoteId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserNotes"); + } + } +} diff --git a/RMuseum/Migrations/20190916070256_GanjoorLink.Designer.cs b/RMuseum/Migrations/20190916070256_GanjoorLink.Designer.cs new file mode 100644 index 00000000..3e76003c --- /dev/null +++ b/RMuseum/Migrations/20190916070256_GanjoorLink.Designer.cs @@ -0,0 +1,823 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20190916070256_GanjoorLink")] + partial class GanjoorLink + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageIndex"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("RArtifactMasterRecordId"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageId"); + + b.Property("CoverItemIndex"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("ItemCount"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("GlobalValue"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("PluralName"); + + b.Property("PluralNameInEnglish"); + + b.Property("Status"); + + b.Property("TagType"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RTagId"); + + b.Property("Status"); + + b.Property("Value"); + + b.Property("ValueInEnglish"); + + b.Property("ValueSupplement"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("Note"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RBookmarkType"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("GanjoorPostId"); + + b.Property("GanjoorTitle"); + + b.Property("GanjoorUrl"); + + b.Property("ItemId"); + + b.Property("ReviewDate"); + + b.Property("ReviewResult"); + + b.Property("ReviewerId"); + + b.Property("SuggestedById"); + + b.Property("SuggestionDate"); + + b.Property("Synchronized"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("GanjoorLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("EndTime"); + + b.Property("Exception"); + + b.Property("FriendlyUrl"); + + b.Property("JobType"); + + b.Property("ProgressPercent"); + + b.Property("QueueTime"); + + b.Property("ResourceNumber"); + + b.Property("SrcContent"); + + b.Property("SrcUrl"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("HtmlContent"); + + b.Property("LastModified"); + + b.Property("Modified"); + + b.Property("NoteType"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("ReferenceNoteId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Description"); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("CreateDate"); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FirstName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RImageId"); + + b.Property("SecurityStamp"); + + b.Property("Status"); + + b.Property("SureName"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("RImageId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("OperationShortName"); + + b.Property("RAppRoleId"); + + b.Property("SecurableItemShortName"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("Language"); + + b.Property("LastRenewal"); + + b.Property("LoginTime"); + + b.Property("RAppUserId"); + + b.Property("Token"); + + b.Property("ValidUntil"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("DateTime"); + + b.Property("Email"); + + b.Property("Language"); + + b.Property("PhoneNumber"); + + b.Property("QueueType"); + + b.Property("Secret"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("DataTime"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("FileSizeInBytes"); + + b.Property("FolderName"); + + b.Property("ImageHeight"); + + b.Property("ImageWidth"); + + b.Property("LastModified"); + + b.Property("OriginalFileName"); + + b.Property("StoredFileName"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("LastModifiedMeta"); + + b.Property("NormalSizeImageHeight"); + + b.Property("NormalSizeImageStoredFileName"); + + b.Property("NormalSizeImageWidth"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("SrcUrl"); + + b.Property("Status"); + + b.Property("ThumbnailImageHeight"); + + b.Property("ThumbnailImageStoredFileName"); + + b.Property("ThumbnailImageWidth"); + + b.Property("Title"); + + b.Property("TitleInEnglish"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20190916070256_GanjoorLink.cs b/RMuseum/Migrations/20190916070256_GanjoorLink.cs new file mode 100644 index 00000000..87d7ce4f --- /dev/null +++ b/RMuseum/Migrations/20190916070256_GanjoorLink.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class GanjoorLink : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GanjoorLinks", + columns: table => new + { + Id = table.Column(nullable: false), + GanjoorPostId = table.Column(nullable: false), + GanjoorUrl = table.Column(nullable: true), + GanjoorTitle = table.Column(nullable: true), + ArtifactId = table.Column(nullable: false), + ItemId = table.Column(nullable: true), + SuggestedById = table.Column(nullable: false), + SuggestionDate = table.Column(nullable: false), + ReviewerId = table.Column(nullable: true), + ReviewDate = table.Column(nullable: false), + ReviewResult = table.Column(nullable: false), + Synchronized = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GanjoorLinks", x => x.Id); + table.ForeignKey( + name: "FK_GanjoorLinks_Artifacts_ArtifactId", + column: x => x.ArtifactId, + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GanjoorLinks_Items_ItemId", + column: x => x.ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_GanjoorLinks_AspNetUsers_ReviewerId", + column: x => x.ReviewerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_GanjoorLinks_AspNetUsers_SuggestedById", + column: x => x.SuggestedById, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GanjoorLinks_ArtifactId", + table: "GanjoorLinks", + column: "ArtifactId"); + + migrationBuilder.CreateIndex( + name: "IX_GanjoorLinks_ItemId", + table: "GanjoorLinks", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_GanjoorLinks_ReviewerId", + table: "GanjoorLinks", + column: "ReviewerId"); + + migrationBuilder.CreateIndex( + name: "IX_GanjoorLinks_SuggestedById", + table: "GanjoorLinks", + column: "SuggestedById"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GanjoorLinks"); + } + } +} diff --git a/RMuseum/Migrations/20190918162149_Notifications.Designer.cs b/RMuseum/Migrations/20190918162149_Notifications.Designer.cs new file mode 100644 index 00000000..da724357 --- /dev/null +++ b/RMuseum/Migrations/20190918162149_Notifications.Designer.cs @@ -0,0 +1,853 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20190918162149_Notifications")] + partial class Notifications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageIndex"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("RArtifactMasterRecordId"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CoverImageId"); + + b.Property("CoverItemIndex"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("FriendlyUrl"); + + b.Property("ItemCount"); + + b.Property("LastModified"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("GlobalValue"); + + b.Property("Name"); + + b.Property("NameInEnglish"); + + b.Property("Order"); + + b.Property("PluralName"); + + b.Property("PluralNameInEnglish"); + + b.Property("Status"); + + b.Property("TagType"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FriendlyUrl"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RTagId"); + + b.Property("Status"); + + b.Property("Value"); + + b.Property("ValueInEnglish"); + + b.Property("ValueSupplement"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("Note"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("RBookmarkType"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("GanjoorPostId"); + + b.Property("GanjoorTitle"); + + b.Property("GanjoorUrl"); + + b.Property("ItemId"); + + b.Property("ReviewDate"); + + b.Property("ReviewResult"); + + b.Property("ReviewerId"); + + b.Property("SuggestedById"); + + b.Property("SuggestionDate"); + + b.Property("Synchronized"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("GanjoorLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ArtifactId"); + + b.Property("EndTime"); + + b.Property("Exception"); + + b.Property("FriendlyUrl"); + + b.Property("JobType"); + + b.Property("ProgressPercent"); + + b.Property("QueueTime"); + + b.Property("ResourceNumber"); + + b.Property("SrcContent"); + + b.Property("SrcUrl"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("HtmlContent"); + + b.Property("LastModified"); + + b.Property("Modified"); + + b.Property("NoteType"); + + b.Property("RAppUserId"); + + b.Property("RArtifactItemRecordId"); + + b.Property("RArtifactMasterRecordId"); + + b.Property("ReferenceNoteId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("HtmlText"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Description"); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("CreateDate"); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FirstName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RImageId"); + + b.Property("SecurityStamp"); + + b.Property("Status"); + + b.Property("SureName"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateTime"); + + b.Property("RImageId"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("OperationShortName"); + + b.Property("RAppRoleId"); + + b.Property("SecurableItemShortName"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("Language"); + + b.Property("LastRenewal"); + + b.Property("LoginTime"); + + b.Property("RAppUserId"); + + b.Property("Token"); + + b.Property("ValidUntil"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientAppName"); + + b.Property("ClientIPAddress"); + + b.Property("DateTime"); + + b.Property("Email"); + + b.Property("Language"); + + b.Property("PhoneNumber"); + + b.Property("QueueType"); + + b.Property("Secret"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("DataTime"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("FileSizeInBytes"); + + b.Property("FolderName"); + + b.Property("ImageHeight"); + + b.Property("ImageWidth"); + + b.Property("LastModified"); + + b.Property("OriginalFileName"); + + b.Property("StoredFileName"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description"); + + b.Property("DescriptionInEnglish"); + + b.Property("LastModifiedMeta"); + + b.Property("NormalSizeImageHeight"); + + b.Property("NormalSizeImageStoredFileName"); + + b.Property("NormalSizeImageWidth"); + + b.Property("Order"); + + b.Property("RArtifactItemRecordId"); + + b.Property("SrcUrl"); + + b.Property("Status"); + + b.Property("ThumbnailImageHeight"); + + b.Property("ThumbnailImageStoredFileName"); + + b.Property("ThumbnailImageWidth"); + + b.Property("Title"); + + b.Property("TitleInEnglish"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord") + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole") + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord") + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20190918162149_Notifications.cs b/RMuseum/Migrations/20190918162149_Notifications.cs new file mode 100644 index 00000000..28974a57 --- /dev/null +++ b/RMuseum/Migrations/20190918162149_Notifications.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class Notifications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(nullable: false), + UserId = table.Column(nullable: false), + DateTime = table.Column(nullable: false), + Status = table.Column(nullable: false), + Subject = table.Column(nullable: true), + HtmlText = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId", + table: "Notifications", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } +} diff --git a/RMuseum/Migrations/20200325142334_AuditLogs.Designer.cs b/RMuseum/Migrations/20200325142334_AuditLogs.Designer.cs new file mode 100644 index 00000000..500c29ca --- /dev/null +++ b/RMuseum/Migrations/20200325142334_AuditLogs.Designer.cs @@ -0,0 +1,1093 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20200325142334_AuditLogs")] + partial class AuditLogs + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageIndex") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("CoverItemIndex") + .HasColumnType("int"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("ItemCount") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("GlobalValue") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PluralName") + .HasColumnType("nvarchar(max)"); + + b.Property("PluralNameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TagType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RTagId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueSupplement") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Note") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RBookmarkType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("GanjoorPostId") + .HasColumnType("int"); + + b.Property("GanjoorTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("GanjoorUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("ReviewResult") + .HasColumnType("int"); + + b.Property("ReviewerId") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestedById") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestionDate") + .HasColumnType("datetime2"); + + b.Property("Synchronized") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("GanjoorLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("JobType") + .HasColumnType("int"); + + b.Property("ProgressPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("QueueTime") + .HasColumnType("datetime2"); + + b.Property("ResourceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcContent") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlContent") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Modified") + .HasColumnType("bit"); + + b.Property("NoteType") + .HasColumnType("int"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReferenceNoteId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlText") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Audit.Db.REvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventType") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonData") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseStatusCode") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SureName") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("OperationShortName") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppRoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurableItemShortName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("LastRenewal") + .HasColumnType("datetime2"); + + b.Property("LoginTime") + .HasColumnType("datetime2"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Token") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidUntil") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("QueueType") + .HasColumnType("int"); + + b.Property("Secret") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("DataTime") + .HasColumnType("datetime2"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeInBytes") + .HasColumnType("bigint"); + + b.Property("FolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("OriginalFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("StoredFileName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModifiedMeta") + .HasColumnType("datetime2"); + + b.Property("NormalSizeImageHeight") + .HasColumnType("int"); + + b.Property("NormalSizeImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalSizeImageWidth") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThumbnailImageHeight") + .HasColumnType("int"); + + b.Property("ThumbnailImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailImageWidth") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("TitleInEnglish") + .HasColumnType("nvarchar(max)"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20200325142334_AuditLogs.cs b/RMuseum/Migrations/20200325142334_AuditLogs.cs new file mode 100644 index 00000000..d2752d6e --- /dev/null +++ b/RMuseum/Migrations/20200325142334_AuditLogs.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class AuditLogs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AuditLogs", + columns: table => new + { + Id = table.Column(nullable: false), + EventType = table.Column(nullable: true), + StartDate = table.Column(nullable: false), + EndDate = table.Column(nullable: true), + Duration = table.Column(nullable: false), + UserName = table.Column(nullable: true), + IpAddress = table.Column(nullable: true), + ResponseStatusCode = table.Column(nullable: false), + RequestUrl = table.Column(nullable: true), + JsonData = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AuditLogs", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AuditLogs"); + } + } +} diff --git a/RMuseum/Migrations/20200416095631_PinterestLinks.Designer.cs b/RMuseum/Migrations/20200416095631_PinterestLinks.Designer.cs new file mode 100644 index 00000000..81349fe5 --- /dev/null +++ b/RMuseum/Migrations/20200416095631_PinterestLinks.Designer.cs @@ -0,0 +1,1179 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + [Migration("20200416095631_PinterestLinks")] + partial class PinterestLinks + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageIndex") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("CoverItemIndex") + .HasColumnType("int"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("ItemCount") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("GlobalValue") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PluralName") + .HasColumnType("nvarchar(max)"); + + b.Property("PluralNameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TagType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RTagId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueSupplement") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Note") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RBookmarkType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("GanjoorPostId") + .HasColumnType("int"); + + b.Property("GanjoorTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("GanjoorUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("ReviewResult") + .HasColumnType("int"); + + b.Property("ReviewerId") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestedById") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestionDate") + .HasColumnType("datetime2"); + + b.Property("Synchronized") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("GanjoorLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.PinterestLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AltText") + .HasColumnType("nvarchar(max)"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("GanjoorPostId") + .HasColumnType("int"); + + b.Property("GanjoorTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("GanjoorUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("LinkType") + .HasColumnType("int"); + + b.Property("PinterestImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("PinterestUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("ReviewDesc") + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewResult") + .HasColumnType("int"); + + b.Property("ReviewerId") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestedById") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestionDate") + .HasColumnType("datetime2"); + + b.Property("Synchronized") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("PinterestLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("JobType") + .HasColumnType("int"); + + b.Property("ProgressPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("QueueTime") + .HasColumnType("datetime2"); + + b.Property("ResourceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcContent") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlContent") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Modified") + .HasColumnType("bit"); + + b.Property("NoteType") + .HasColumnType("int"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReferenceNoteId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlText") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Audit.Db.REvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventType") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonData") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseStatusCode") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SureName") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("OperationShortName") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppRoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurableItemShortName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("LastRenewal") + .HasColumnType("datetime2"); + + b.Property("LoginTime") + .HasColumnType("datetime2"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Token") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidUntil") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("QueueType") + .HasColumnType("int"); + + b.Property("Secret") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("DataTime") + .HasColumnType("datetime2"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeInBytes") + .HasColumnType("bigint"); + + b.Property("FolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("OriginalFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("StoredFileName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModifiedMeta") + .HasColumnType("datetime2"); + + b.Property("NormalSizeImageHeight") + .HasColumnType("int"); + + b.Property("NormalSizeImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalSizeImageWidth") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThumbnailImageHeight") + .HasColumnType("int"); + + b.Property("ThumbnailImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailImageWidth") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("TitleInEnglish") + .HasColumnType("nvarchar(max)"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.PinterestLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Migrations/20200416095631_PinterestLinks.cs b/RMuseum/Migrations/20200416095631_PinterestLinks.cs new file mode 100644 index 00000000..db442308 --- /dev/null +++ b/RMuseum/Migrations/20200416095631_PinterestLinks.cs @@ -0,0 +1,88 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RMuseum.Migrations +{ + public partial class PinterestLinks : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PinterestLinks", + columns: table => new + { + Id = table.Column(nullable: false), + GanjoorPostId = table.Column(nullable: false), + GanjoorUrl = table.Column(nullable: true), + GanjoorTitle = table.Column(nullable: true), + AltText = table.Column(nullable: true), + LinkType = table.Column(nullable: false), + PinterestUrl = table.Column(nullable: true), + PinterestImageUrl = table.Column(nullable: true), + SuggestedById = table.Column(nullable: true), + SuggestionDate = table.Column(nullable: false), + ReviewerId = table.Column(nullable: true), + ReviewDate = table.Column(nullable: false), + ReviewResult = table.Column(nullable: false), + ReviewDesc = table.Column(nullable: true), + ArtifactId = table.Column(nullable: true), + ItemId = table.Column(nullable: true), + Synchronized = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PinterestLinks", x => x.Id); + table.ForeignKey( + name: "FK_PinterestLinks_Artifacts_ArtifactId", + column: x => x.ArtifactId, + principalTable: "Artifacts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_PinterestLinks_Items_ItemId", + column: x => x.ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_PinterestLinks_AspNetUsers_ReviewerId", + column: x => x.ReviewerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_PinterestLinks_AspNetUsers_SuggestedById", + column: x => x.SuggestedById, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_PinterestLinks_ArtifactId", + table: "PinterestLinks", + column: "ArtifactId"); + + migrationBuilder.CreateIndex( + name: "IX_PinterestLinks_ItemId", + table: "PinterestLinks", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_PinterestLinks_ReviewerId", + table: "PinterestLinks", + column: "ReviewerId"); + + migrationBuilder.CreateIndex( + name: "IX_PinterestLinks_SuggestedById", + table: "PinterestLinks", + column: "SuggestedById"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PinterestLinks"); + } + } +} diff --git a/RMuseum/Migrations/RMuseumDbContextModelSnapshot.cs b/RMuseum/Migrations/RMuseumDbContextModelSnapshot.cs new file mode 100644 index 00000000..0689bdfb --- /dev/null +++ b/RMuseum/Migrations/RMuseumDbContextModelSnapshot.cs @@ -0,0 +1,1177 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RMuseum.DbContext; + +namespace RMuseum.Migrations +{ + [DbContext(typeof(RMuseumDbContext))] + partial class RMuseumDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageIndex") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RArtifactMasterRecordId", "FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.HasIndex("RArtifactMasterRecordId", "Order") + .IsUnique(); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CoverImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("CoverItemIndex") + .HasColumnType("int"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("ItemCount") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CoverImageId"); + + b.HasIndex("FriendlyUrl") + .IsUnique() + .HasFilter("[FriendlyUrl] IS NOT NULL"); + + b.ToTable("Artifacts"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("GlobalValue") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PluralName") + .HasColumnType("nvarchar(max)"); + + b.Property("PluralNameInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TagType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(450)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RTagId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("ValueSupplement") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FriendlyUrl"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("RTagId"); + + b.ToTable("TagValues"); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Note") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RBookmarkType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.ToTable("UserBookmarks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("GanjoorPostId") + .HasColumnType("int"); + + b.Property("GanjoorTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("GanjoorUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("ReviewResult") + .HasColumnType("int"); + + b.Property("ReviewerId") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestedById") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestionDate") + .HasColumnType("datetime2"); + + b.Property("Synchronized") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("GanjoorLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.PinterestLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AltText") + .HasColumnType("nvarchar(max)"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("GanjoorPostId") + .HasColumnType("int"); + + b.Property("GanjoorTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("GanjoorUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("LinkType") + .HasColumnType("int"); + + b.Property("PinterestImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("PinterestUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewDate") + .HasColumnType("datetime2"); + + b.Property("ReviewDesc") + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewResult") + .HasColumnType("int"); + + b.Property("ReviewerId") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestedById") + .HasColumnType("uniqueidentifier"); + + b.Property("SuggestionDate") + .HasColumnType("datetime2"); + + b.Property("Synchronized") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SuggestedById"); + + b.ToTable("PinterestLinks"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArtifactId") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FriendlyUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("JobType") + .HasColumnType("int"); + + b.Property("ProgressPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("QueueTime") + .HasColumnType("datetime2"); + + b.Property("ResourceNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcContent") + .HasColumnType("nvarchar(max)"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtifactId"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlContent") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Modified") + .HasColumnType("bit"); + + b.Property("NoteType") + .HasColumnType("int"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("RArtifactMasterRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReferenceNoteId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasIndex("RArtifactMasterRecordId"); + + b.HasIndex("ReferenceNoteId"); + + b.ToTable("UserNotes"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("HtmlText") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Audit.Db.REvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventType") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonData") + .HasColumnType("nvarchar(max)"); + + b.Property("RequestUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseStatusCode") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreateDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SureName") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("RImageId"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("RImageId") + .HasColumnType("uniqueidentifier"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RImageId"); + + b.ToTable("CaptchaImages"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("OperationShortName") + .HasColumnType("nvarchar(max)"); + + b.Property("RAppRoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("SecurableItemShortName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RAppRoleId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("LastRenewal") + .HasColumnType("datetime2"); + + b.Property("LoginTime") + .HasColumnType("datetime2"); + + b.Property("RAppUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Token") + .HasColumnType("nvarchar(max)"); + + b.Property("ValidUntil") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("RAppUserId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RVerifyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientAppName") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientIPAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("DateTime") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Language") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("QueueType") + .HasColumnType("int"); + + b.Property("Secret") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Secret") + .IsUnique() + .HasFilter("[Secret] IS NOT NULL"); + + b.ToTable("VerifyQueueItems"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Image.RImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("DataTime") + .HasColumnType("datetime2"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeInBytes") + .HasColumnType("bigint"); + + b.Property("FolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("ImageHeight") + .HasColumnType("int"); + + b.Property("ImageWidth") + .HasColumnType("int"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("OriginalFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("StoredFileName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GeneralImages"); + + b.HasDiscriminator("Discriminator").HasValue("RImage"); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasBaseType("RSecurityBackend.Models.Image.RImage"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DescriptionInEnglish") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModifiedMeta") + .HasColumnType("datetime2"); + + b.Property("NormalSizeImageHeight") + .HasColumnType("int"); + + b.Property("NormalSizeImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("NormalSizeImageWidth") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("RArtifactItemRecordId") + .HasColumnType("uniqueidentifier"); + + b.Property("SrcUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("ThumbnailImageHeight") + .HasColumnType("int"); + + b.Property("ThumbnailImageStoredFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailImageWidth") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("TitleInEnglish") + .HasColumnType("nvarchar(max)"); + + b.HasIndex("RArtifactItemRecordId"); + + b.HasDiscriminator().HasValue("RPictureFile"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactItemRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Items") + .HasForeignKey("RArtifactMasterRecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RArtifactMasterRecord", b => + { + b.HasOne("RMuseum.Models.Artifact.RPictureFile", "CoverImage") + .WithMany() + .HasForeignKey("CoverImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RTagValue", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", null) + .WithMany("Tags") + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RTag", "RTag") + .WithMany() + .HasForeignKey("RTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Bookmark.RUserBookmark", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.GanjoorLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.GanjoorIntegration.PinterestLink", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "Item") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId"); + + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "SuggestedBy") + .WithMany() + .HasForeignKey("SuggestedById"); + }); + + modelBuilder.Entity("RMuseum.Models.ImportJob.ImportJob", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "Artifact") + .WithMany() + .HasForeignKey("ArtifactId"); + }); + + modelBuilder.Entity("RMuseum.Models.Note.RUserNote", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", "RArtifactItemRecord") + .WithMany() + .HasForeignKey("RArtifactItemRecordId"); + + b.HasOne("RMuseum.Models.Artifact.RArtifactMasterRecord", "RArtifactMasterRecord") + .WithMany() + .HasForeignKey("RArtifactMasterRecordId"); + + b.HasOne("RMuseum.Models.Note.RUserNote", "ReferenceNote") + .WithMany() + .HasForeignKey("ReferenceNoteId"); + }); + + modelBuilder.Entity("RMuseum.Models.Notification.RUserNotification", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RAppUser", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RCaptchaImage", b => + { + b.HasOne("RSecurityBackend.Models.Image.RImage", "RImage") + .WithMany() + .HasForeignKey("RImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RPermission", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppRole", null) + .WithMany("Permissions") + .HasForeignKey("RAppRoleId"); + }); + + modelBuilder.Entity("RSecurityBackend.Models.Auth.Db.RTemporaryUserSession", b => + { + b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "RAppUser") + .WithMany() + .HasForeignKey("RAppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RMuseum.Models.Artifact.RPictureFile", b => + { + b.HasOne("RMuseum.Models.Artifact.RArtifactItemRecord", null) + .WithMany("Images") + .HasForeignKey("RArtifactItemRecordId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RMuseum/Models/Artifact/PublishStatus.cs b/RMuseum/Models/Artifact/PublishStatus.cs new file mode 100644 index 00000000..b2eaeed6 --- /dev/null +++ b/RMuseum/Models/Artifact/PublishStatus.cs @@ -0,0 +1,16 @@ +namespace RMuseum.Models.Artifact +{ + /// + /// Items or pictures status + /// + public enum PublishStatus + { + Draft = 0, + Awaiting = 1, + Refused = 2, + Published = 4, + Restricted = 8, + Hidden = 16, + Deleted = 32 + } +} diff --git a/RMuseum/Models/Artifact/RArtifactItemRecord.cs b/RMuseum/Models/Artifact/RArtifactItemRecord.cs new file mode 100644 index 00000000..30578a66 --- /dev/null +++ b/RMuseum/Models/Artifact/RArtifactItemRecord.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace RMuseum.Models.Artifact +{ + public class RArtifactItemRecord + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// parent id + /// + public Guid RArtifactMasterRecordId { get; set; } + + /// + /// Order in Collections + /// + public int Order { get; set; } + + /// + /// Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Name In English + /// + public string NameInEnglish { get; set; } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// Description In English + /// + public string DescriptionInEnglish { get; set; } + + /// + /// Main Image + /// + public int CoverImageIndex { get; set; } + + /// + /// Last Modified for caching purposes + /// + public DateTime LastModified { get; set; } + + /// + /// All Images + /// + public ICollection Images { get; set; } + + /// + /// Attributes + /// + public ICollection Tags { get; set; } + + + } +} diff --git a/RMuseum/Models/Artifact/RArtifactMasterRecord.cs b/RMuseum/Models/Artifact/RArtifactMasterRecord.cs new file mode 100644 index 00000000..31c22c10 --- /dev/null +++ b/RMuseum/Models/Artifact/RArtifactMasterRecord.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; + +namespace RMuseum.Models.Artifact +{ + /// + /// Museum Master Item + /// + public class RArtifactMasterRecord + { + + public RArtifactMasterRecord() { } + public RArtifactMasterRecord(string name, string description) + { + Name = NameInEnglish = name; + Description = DescriptionInEnglish = description; + } + + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Publish Status + /// + public PublishStatus Status { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Name In English + /// + public string NameInEnglish { get; set; } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// Description In English + /// + public string DescriptionInEnglish { get; set; } + + + /// + /// Date/Time + /// + public DateTime DateTime { get; set; } + + /// + /// Last Modified for caching purposes + /// + public DateTime LastModified { get; set; } + + /// + /// Cover Item Index + /// + public int CoverItemIndex { get; set; } + + /// + /// Cover Image + /// + public RPictureFile CoverImage { get; set; } + + /// + /// Cover Image Id + /// + public Guid CoverImageId { get; set; } + + /// + /// Parts of this item + /// + public ICollection Items { get; set; } + + /// + /// Item Count (for lists and queries) + /// + public int ItemCount { get; set; } + + /// + /// Attributes + /// + public ICollection Tags { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/RPictureFile.cs b/RMuseum/Models/Artifact/RPictureFile.cs new file mode 100644 index 00000000..914384a6 --- /dev/null +++ b/RMuseum/Models/Artifact/RPictureFile.cs @@ -0,0 +1,131 @@ +using RSecurityBackend.Models.Image; +using System; + + +namespace RMuseum.Models.Artifact +{ + /// + /// Artifact Picture Files + /// + public class RPictureFile : RImage + { + + /// + /// Title + /// + public string Title { get; set; } + + /// + /// Title in English + /// + public string TitleInEnglish { set; get; } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// Description in English + /// + public string DescriptionInEnglish { get; set; } + + /// + /// Publish status + /// + public PublishStatus Status { get; set; } + + /// + /// order in the collection it belongs to it + /// + public int Order { get; set; } + + /// + /// نام فایل ذخیره شده با بالاترین کیفیت + /// + /// + /// e0d7050a-7e30-4a2f-b181-0daa4a1e1068.jpg + /// + public string NormalSizeImageStoredFileName { get; set; } + + /// + /// نام فایل ذخیره شده با بالاترین کیفیت + /// + /// + /// fbe9cc8e-12fa-4cb9-8f09-bbf353333383.jpg + /// + public string ThumbnailImageStoredFileName { get; set; } + + /// + /// عرض تصویر مناسب نمایش در صفحهٔ اختصاصی + /// + public int NormalSizeImageWidth { get; set; } + + /// + /// طول تصویر مناسب نمایش در صفحهٔ اختصاصی + /// + public int NormalSizeImageHeight { get; set; } + + /// + ///عرض تصویر با اندازه مناسب نمایش در صفحات لیستی + /// + public int ThumbnailImageWidth { get; set; } + + /// + ///طول تصویر با اندازه مناسب نمایش در صفحات لیستی + /// + public int ThumbnailImageHeight { get; set; } + + /// + /// source url + /// + public string SrcUrl { get; set; } + + /// + /// Last Modified for caching purposes + /// + public DateTime LastModifiedMeta { get; set; } + + + + /// + /// duplicated a picture record (Id is missing so you should store this to get a new Id) + /// + /// + /// + public static RPictureFile Duplicate(RPictureFile src) + { + if (src == null) + return null; + return + new RPictureFile() + { + Title = src.Title, + TitleInEnglish = src.TitleInEnglish, + Description = src.Description, + DescriptionInEnglish = src.DescriptionInEnglish, + FolderName = src.FolderName, + DataTime = src.DataTime, + LastModified = src.LastModified, + LastModifiedMeta = src.LastModifiedMeta, + NormalSizeImageHeight = src.NormalSizeImageHeight, + NormalSizeImageStoredFileName = src.NormalSizeImageStoredFileName, + NormalSizeImageWidth = src.NormalSizeImageWidth, + Order = src.Order, + OriginalFileName = src.OriginalFileName, + ContentType = src.ContentType, + FileSizeInBytes = src.FileSizeInBytes, + ImageHeight = src.ImageHeight, + StoredFileName = src.StoredFileName, + ImageWidth = src.ImageWidth, + SrcUrl = src.SrcUrl, + Status = src.Status, + ThumbnailImageHeight = src.ThumbnailImageHeight, + ThumbnailImageStoredFileName = src.ThumbnailImageStoredFileName, + ThumbnailImageWidth = src.ThumbnailImageWidth + }; + } + + + } +} diff --git a/RMuseum/Models/Artifact/RTag.cs b/RMuseum/Models/Artifact/RTag.cs new file mode 100644 index 00000000..5b32374e --- /dev/null +++ b/RMuseum/Models/Artifact/RTag.cs @@ -0,0 +1,61 @@ +using System; + +namespace RMuseum.Models.Artifact +{ + /// + /// Item Attribute + /// + public class RTag + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Order + /// + public int Order { get; set; } + + /// + /// tag type + /// + public RTagType TagType { get; set; } + + /// + /// Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Publish Status + /// + public PublishStatus Status { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Name In English + /// + public string NameInEnglish { get; set; } + + /// + /// Plural Name + /// + public string PluralName { get; set; } + + /// + /// Plural Name In English + /// + public string PluralNameInEnglish { get; set; } + + /// + /// Value is normally changed globally + /// + public bool GlobalValue { get; set; } + + } +} diff --git a/RMuseum/Models/Artifact/RTagType.cs b/RMuseum/Models/Artifact/RTagType.cs new file mode 100644 index 00000000..ae845830 --- /dev/null +++ b/RMuseum/Models/Artifact/RTagType.cs @@ -0,0 +1,37 @@ +namespace RMuseum.Models.Artifact +{ + /// + /// نوع ویژگی + /// + public enum RTagType + { + /// + /// عادی + /// + Ordinary = 0, + /// + /// پیوند + /// + Link = 1, + /// + /// جستجو + /// + Search = 2, + /// + /// پیوند و جستجو + /// + LinkSearch = 3, + /// + /// متن چپ به راست + /// + LeftToRightText = 4, + /// + /// بدون مقدار + /// + Binary = 5, + /// + /// عنوان در فهرست + /// + TitleInContents = 6, + } +} diff --git a/RMuseum/Models/Artifact/RTagValue.cs b/RMuseum/Models/Artifact/RTagValue.cs new file mode 100644 index 00000000..0b17f9d3 --- /dev/null +++ b/RMuseum/Models/Artifact/RTagValue.cs @@ -0,0 +1,46 @@ +using System; + +namespace RMuseum.Models.Artifact +{ + /// + /// Attribute Value + /// + public class RTagValue + { + /// + /// Id + /// + public Guid Id { get; set; } + + public Guid RTagId { get; set; } + + public RTag RTag { get; set; } + + public int Order { get; set; } + + /// + /// Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Publish Status + /// + public PublishStatus Status { get; set; } + + /// + /// Value + /// + public string Value { get; set; } + + /// + /// Values In English + /// + public string ValueInEnglish { get; set; } + + /// + /// link or .... + /// + public string ValueSupplement { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/NewArtifact.cs b/RMuseum/Models/Artifact/ViewModels/NewArtifact.cs new file mode 100644 index 00000000..8bcad697 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/NewArtifact.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; + + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// new artifact view model + /// + public class NewArtifact + { + + public string Name { get; set; } + + public string Description { get; set; } + + public string SrcUrl { get; set; } + + public string PicTitle { get; set; } + + public string PicDescription { get; set; } + + public IFormFile File { get; set; } + + public string PicSrcUrl { get; set; } + + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RArtifactItemRecordViewModel.cs b/RMuseum/Models/Artifact/ViewModels/RArtifactItemRecordViewModel.cs new file mode 100644 index 00000000..9f73694a --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RArtifactItemRecordViewModel.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// RArtifactItemRecord with additional info + /// + public class RArtifactItemRecordViewModel + { + + private RArtifactItemRecord _Item; + + /// + /// main item info + /// + public RArtifactItemRecord Item + { + get + { + return _Item; + } + set + { + _Item = value; + + List rItemTags = new List(); + if (_Item != null && _Item.Tags != null) + { + foreach (RTagValue tag in _Item.Tags) + { + RArtifactTagViewModel related = rItemTags.Where(t => t.Id == tag.RTagId).SingleOrDefault(); + List values = (related == null) ? new List() : new List(related.Values); + if (related == null) + { + related = new RArtifactTagViewModel(tag.RTag); + rItemTags.Add(related); + + } + values.Add(tag); + values.Sort((a, b) => a.Order - b.Order); + related.Values = values; + } + + rItemTags.Sort((a, b) => a.Order - b.Order); + } + + FormattedTags = rItemTags; + } + } + + /// + /// parent + /// + public string ParentFriendlyUrl { get; set; } + + /// + /// parent name + /// + public string ParentName { get; set; } + + /// + /// parent image + /// + public Guid ParentImageId { get; set; } + + /// + /// parent item count + /// + public int ParentItemCount { get; set; } + + /// + /// empty or null means this is last item + /// + public string NextItemFriendlyUrl { get; set; } + + /// + /// next image + /// + public Guid? NextItemImageId { get; set; } + + /// + /// empty or null means this is first item + /// + public string PreviousItemFriendlyUrl { get; set; } + + /// + /// prev image + /// + public Guid? PrevItemImageId { get; set; } + + /// + /// Formatted Tags + /// + public ICollection FormattedTags { get; set; } + + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RArtifactMasterRecordViewModel.cs b/RMuseum/Models/Artifact/ViewModels/RArtifactMasterRecordViewModel.cs new file mode 100644 index 00000000..9512fa86 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RArtifactMasterRecordViewModel.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Museum Master Item View Model + /// + public class RArtifactMasterRecordViewModel : RArtifactMasterRecord + { + /// + /// constructor + /// + /// + public RArtifactMasterRecordViewModel(RArtifactMasterRecord src) + { + Id = src.Id; + FriendlyUrl = src.FriendlyUrl; + Status = src.Status; + Name = src.Name; + NameInEnglish = src.NameInEnglish; + Description = src.Description; + DescriptionInEnglish = src.DescriptionInEnglish; + DateTime = src.DateTime; + LastModified = src.LastModified; + CoverItemIndex = src.CoverItemIndex; + CoverImage = src.CoverImage; + CoverImageId = src.CoverImageId; + ItemCount = src.ItemCount; + Tags = null; + + List items = new List(); + List sums = new List(); + List contents = new List(); + int orderOfContents = 0; + if(src.Items != null) + foreach(RArtifactItemRecord item in src.Items) + { + if(item.Tags != null) + { + var OrderedTags = item.Tags.OrderBy(t => t.RTag.Order).ThenBy(t => t.Order); + foreach (RTagValue value in OrderedTags) + { + if (value.RTag != null) + { + switch(value.RTag.TagType) + { + case RTagType.Binary: + { + RTagSum sum = sums.Where(s => s.TagFriendlyUrl == value.RTag.FriendlyUrl).SingleOrDefault(); + if (sum != null) + { + sum.ItemCount++; + } + else + { + sums.Add(new RTagSum() + { + TagName = value.RTag.Name, + TagFriendlyUrl = value.RTag.FriendlyUrl, + ItemCount = 1 + }); + } + } + break; + case RTagType.TitleInContents: + { + if(!int.TryParse(value.ValueSupplement, out int level)) + { + level = 1; + } + if(contents.Where(c => c.Title == value.Value).SingleOrDefault() == null) + { + contents.Add + ( + new RTitleInContents() + { + Title = value.Value, + Order = ++orderOfContents, + Level = level, + ItemFriendlyUrl = item.FriendlyUrl + } + ); + } + } + break; + } + + } + } + } + + item.Tags = null; + items.Add(item); + } + Items = items; + RTagSums = sums; + Contents = contents; + + List rArtifactTags = new List(); + if(src.Tags != null) + { + foreach (RTagValue tag in src.Tags) + { + RArtifactTagViewModel related = rArtifactTags.Where(t => t.Id == tag.RTagId).SingleOrDefault(); + List values = (related == null) ? new List() : new List(related.Values); + if (related == null) + { + related = new RArtifactTagViewModel(tag.RTag); + rArtifactTags.Add(related); + + } + values.Add(tag); + values.Sort((a, b) => a.Order - b.Order); + related.Values = values; + } + + rArtifactTags.Sort((a, b) => a.Order - b.Order); + } + + ArtifactTags = rArtifactTags; + } + + /// + /// Tags + /// + public ICollection ArtifactTags { get; set; } + + /// + /// Binary Tagged Items + /// + public ICollection RTagSums { get; set; } + + /// + /// Titles of Items in Contents + /// + public ICollection Contents { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RArtifactTagViewModel.cs b/RMuseum/Models/Artifact/ViewModels/RArtifactTagViewModel.cs new file mode 100644 index 00000000..142723d5 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RArtifactTagViewModel.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Artifact Tag (RTag with grouped related RTagValues of Artifact) + /// + public class RArtifactTagViewModel : RTag + { + public RArtifactTagViewModel(RTag src) + { + Id = src.Id; + Order = src.Order; + TagType = src.TagType; + FriendlyUrl = src.FriendlyUrl; + Status = src.Status; + Name = src.Name; + NameInEnglish = src.NameInEnglish; + GlobalValue = src.GlobalValue; + PluralName = src.PluralName; + PluralNameInEnglish = src.PluralNameInEnglish; + } + + public ICollection Values { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RTagBundleValueViewModel.cs b/RMuseum/Models/Artifact/ViewModels/RTagBundleValueViewModel.cs new file mode 100644 index 00000000..63e107b4 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RTagBundleValueViewModel.cs @@ -0,0 +1,31 @@ +using System; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Tag Bundle View Model + /// + public class RTagBundleValueViewModel + { + /// + /// Item Title + /// + public string Name { get; set; } + + /// + /// Item Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Items Count + /// + public int Count { get; set; } + + /// + /// Item Image Id + /// + public Guid ImageId { get; set; } + + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RTagBundleViewModel.cs b/RMuseum/Models/Artifact/ViewModels/RTagBundleViewModel.cs new file mode 100644 index 00000000..85aecc16 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RTagBundleViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Tag Bundle View Model + /// + public class RTagBundleViewModel + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Friendly Url + /// + public string FriendlyUrl { get; set; } + + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Plural Name + /// + public string PluralName { get; set; } + + /// + /// Values + /// + public RTagBundleValueViewModel[] Values { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RTagSum.cs b/RMuseum/Models/Artifact/ViewModels/RTagSum.cs new file mode 100644 index 00000000..4df214e0 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RTagSum.cs @@ -0,0 +1,23 @@ +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Artifact Binary Tagged items Summation + /// + public class RTagSum + { + /// + /// Tag Name + /// + public string TagName { get; set; } + + /// + /// Tag Friendly Url + /// + public string TagFriendlyUrl{ get; set; } + + /// + /// Item Count + /// + public int ItemCount { get; set; } + } +} diff --git a/RMuseum/Models/Artifact/ViewModels/RTitleInContents.cs b/RMuseum/Models/Artifact/ViewModels/RTitleInContents.cs new file mode 100644 index 00000000..26f0b865 --- /dev/null +++ b/RMuseum/Models/Artifact/ViewModels/RTitleInContents.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Models.Artifact.ViewModels +{ + /// + /// Title of an Item in Artifact Contents + /// + public class RTitleInContents + { + /// + /// Title + /// + public string Title { get; set; } + + /// + /// Order + /// + public int Order { get; set; } + + /// + /// Level + /// + public int Level { get; set; } + + /// + /// Item Friendly Url + /// + public string ItemFriendlyUrl { get; set; } + } +} diff --git a/RMuseum/Models/Auth/Memory/RMuseumSecurableItem.cs b/RMuseum/Models/Auth/Memory/RMuseumSecurableItem.cs new file mode 100644 index 00000000..c1f95675 --- /dev/null +++ b/RMuseum/Models/Auth/Memory/RMuseumSecurableItem.cs @@ -0,0 +1,115 @@ +using RSecurityBackend.Models.Auth.Memory; +using System.Collections.Generic; + +namespace RMuseum.Models.Auth.Memory +{ + /// + /// specific forms and permission + /// + public class RMuseumSecurableItem : SecurableItem + { + /// + /// artifact + /// + public const string ArtifactEntityShortName = "artifact"; + + /// + /// tag + /// + public const string TagEntityShortName = "tag"; + + /// + /// note + /// + public const string NoteEntityShortName = "note"; + + /// + /// view drafts + /// + public const string ViewDraftOperationShortName = "viewdraft"; + + /// + /// edittag + /// + public const string EditTagValueOperationShortName = "edittag"; + + /// + /// awaiting + /// + public const string ToAwaitingStatusOperationShortName = "awaiting"; + + /// + /// publish + /// + public const string PublishOperationShortName = "publish"; + + /// + /// import + /// + public const string ImportOperationShortName = "import"; + + /// + /// moderate + /// + public const string ModerateCommentsOperationShortName = "moderate"; + + /// + /// review suggested ganjoor links + /// + public const string ReviewGanjoorLinksOperationShortName = "ganjoor"; + + /// + /// list of forms and their permissions + /// + public new static SecurableItem[] Items + { + get + { + List lst = new List(SecurableItem.Items); + lst.AddRange( + new SecurableItem[] + { + new SecurableItem() + { + ShortName = ArtifactEntityShortName, + Description = "اشیاء گنجینه", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(AddOperationShortName, "ایجاد", false), + new SecurableItemOperation(ImportOperationShortName, "ورود اطلاعات", false), + new SecurableItemOperation(ModifyOperationShortName, "اصلاح", false), + new SecurableItemOperation(DeleteOperationShortName, "حذف", false), + new SecurableItemOperation(ViewDraftOperationShortName, "مشاهدهٔ پیش‌نویس‌ها", false), + new SecurableItemOperation(EditTagValueOperationShortName, "اصلاح مقدار ویژگی", false), + new SecurableItemOperation(ToAwaitingStatusOperationShortName, "درخواست بازبینی", false), + new SecurableItemOperation(PublishOperationShortName, "انتشار", false), + new SecurableItemOperation(ReviewGanjoorLinksOperationShortName, "بررسی شعرهای پیشنهادی گنجور", false), + } + }, + new SecurableItem() + { + ShortName = TagEntityShortName, + Description = "انواع ویژگیها", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(AddOperationShortName, "ایجاد", false), + new SecurableItemOperation(ModifyOperationShortName, "اصلاح", false), + new SecurableItemOperation(DeleteOperationShortName, "حذف", false) + } + }, + new SecurableItem() + { + ShortName = NoteEntityShortName, + Description = "یادداشتها", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(ModerateCommentsOperationShortName, "بررسی", false) + } + }, + + }); + return lst.ToArray(); + } + } + } +} diff --git a/RMuseum/Models/Bookmark/RBookmarkType.cs b/RMuseum/Models/Bookmark/RBookmarkType.cs new file mode 100644 index 00000000..f577c325 --- /dev/null +++ b/RMuseum/Models/Bookmark/RBookmarkType.cs @@ -0,0 +1,17 @@ +namespace RMuseum.Models.Bookmark +{ + /// + /// Bookmark Type + /// + public enum RBookmarkType + { + /// + /// Private Bookmark + /// + Bookmark = 0, + /// + /// Public Favorite + /// + Favorite = 1 + } +} diff --git a/RMuseum/Models/Bookmark/RUserBookmark.cs b/RMuseum/Models/Bookmark/RUserBookmark.cs new file mode 100644 index 00000000..1d5f665e --- /dev/null +++ b/RMuseum/Models/Bookmark/RUserBookmark.cs @@ -0,0 +1,63 @@ +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Auth.Db; +using System; + + +namespace RMuseum.Models.Bookmark +{ + /// + /// User Bookmarks + /// + public class RUserBookmark + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid RAppUserId { get; set; } + + /// + /// User + /// + public RAppUser RAppUser { get; set; } + + /// + /// Master Record Id + /// + public Guid? RArtifactMasterRecordId { get; set; } + + /// + /// Master Record + /// + public virtual RArtifactMasterRecord RArtifactMasterRecord { get; set; } + + /// + /// Item Record Id + /// + public Guid? RArtifactItemRecordId { get; set; } + + /// + /// Item Record + /// + public virtual RArtifactItemRecord RArtifactItemRecord { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + + /// + /// Type + /// + public RBookmarkType RBookmarkType { get; set; } + + /// + /// User Note + /// + public string Note { get; set; } + } +} diff --git a/RMuseum/Models/Bookmark/ViewModels/RUserBookmarkViewModel.cs b/RMuseum/Models/Bookmark/ViewModels/RUserBookmarkViewModel.cs new file mode 100644 index 00000000..d9136225 --- /dev/null +++ b/RMuseum/Models/Bookmark/ViewModels/RUserBookmarkViewModel.cs @@ -0,0 +1,56 @@ +using RMuseum.Models.Artifact; +using RMuseum.Models.Artifact.ViewModels; +using System; + +namespace RMuseum.Models.Bookmark.ViewModels +{ + public class RUserBookmarkViewModel + { + public RUserBookmarkViewModel(RUserBookmark src) + { + Id = src.Id; + RAppUserId = src.RAppUserId; + RArtifactMasterRecord = src.RArtifactMasterRecord; + RArtifactItemRecord = null;//this should be filled by an external call + DateTime = src.DateTime; + RBookmarkType = src.RBookmarkType; + Note = src.Note; + } + + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid RAppUserId { get; set; } + + /// + /// Master Record + /// + public virtual RArtifactMasterRecord RArtifactMasterRecord { get; set; } + + + /// + /// Item Record + /// + public virtual RArtifactItemRecordViewModel RArtifactItemRecord { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + + /// + /// Type + /// + public RBookmarkType RBookmarkType { get; set; } + + /// + /// User Note + /// + public string Note { get; set; } + } +} diff --git a/RMuseum/Models/GanjoorIntegration/GanjoorLink.cs b/RMuseum/Models/GanjoorIntegration/GanjoorLink.cs new file mode 100644 index 00000000..6b15a7be --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/GanjoorLink.cs @@ -0,0 +1,92 @@ +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Auth.Db; +using System; + +namespace RMuseum.Models.GanjoorIntegration +{ + /// + /// ganjoor link for artifacts and items + /// + public class GanjoorLink + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// Artifact Id + /// + public Guid ArtifactId { get; set; } + + /// + /// artifact + /// + public RArtifactMasterRecord Artifact { get; set; } + + /// + /// Artifact Item Id + /// + public Guid? ItemId { get; set; } + + /// + /// Artifact Item + /// + public virtual RArtifactItemRecord Item { get; set; } + + /// + /// User Id who suggested the link + /// + public Guid SuggestedById { get; set; } + + /// + /// Suggestion Date + /// + public DateTime SuggestionDate { get; set; } + + /// + /// User who suggested the link + /// + public RAppUser SuggestedBy { get; set; } + + /// + /// User id who reviewed the link + /// + public Guid? ReviewerId { get; set; } + + /// + /// User who reviewed the link + /// + public virtual RAppUser Reviewer { get; set; } + + /// + /// Review Date + /// + public DateTime ReviewDate { get; set; } + + /// + /// review result + /// + public ReviewResult ReviewResult { get; set; } + + /// + /// Synchronized with ganjoor + /// + public bool Synchronized { get; set; } + } +} diff --git a/RMuseum/Models/GanjoorIntegration/LinkType.cs b/RMuseum/Models/GanjoorIntegration/LinkType.cs new file mode 100644 index 00000000..2715dd83 --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/LinkType.cs @@ -0,0 +1,25 @@ +namespace RMuseum.Models.GanjoorIntegration +{ + /// + /// Link Type + /// + public enum LinkType + { + /// + /// Custom + /// + Custom = 0, + /// + /// Museum + /// + Museum = 1, + /// + /// Pinterest + /// + Pinterest = 2, + /// + /// Instagram + /// + Instagram = 3 + } +} diff --git a/RMuseum/Models/GanjoorIntegration/PinterestLink.cs b/RMuseum/Models/GanjoorIntegration/PinterestLink.cs new file mode 100644 index 00000000..bc5787b1 --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/PinterestLink.cs @@ -0,0 +1,118 @@ +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Auth.Db; +using System; + + +namespace RMuseum.Models.GanjoorIntegration +{ + /// + /// pinterest link + /// + public class PinterestLink + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// alt text + /// + public string AltText { get; set; } + + /// + /// link type + /// + public LinkType LinkType { get; set; } + + /// + /// pinterest url + /// + public string PinterestUrl { get; set; } + + /// + /// pinterest image url + /// + public string PinterestImageUrl { get; set; } + + /// + /// User Id who suggested the link (this would always be null for anonyous suggestions, kept it so that someday it would be used to integerate GanjoorLink into this class) + /// + public Guid? SuggestedById { get; set; } + + /// + /// Suggestion Date + /// + public DateTime SuggestionDate { get; set; } + + /// + /// User who suggested the link + /// + public virtual RAppUser SuggestedBy { get; set; } + + /// + /// User id who reviewed the link + /// + public Guid? ReviewerId { get; set; } + + /// + /// User who reviewed the link + /// + public virtual RAppUser Reviewer { get; set; } + + /// + /// Review Date + /// + public DateTime ReviewDate { get; set; } + + /// + /// review result + /// + public ReviewResult ReviewResult { get; set; } + + /// + /// review description + /// + public string ReviewDesc { get; set; } + + /// + /// Artifact Id + /// + public Guid? ArtifactId { get; set; } + + /// + /// artifact + /// + public virtual RArtifactMasterRecord Artifact { get; set; } + + /// + /// Artifact Item Id + /// + public Guid? ItemId { get; set; } + + /// + /// Artifact Item + /// + public virtual RArtifactItemRecord Item { get; set; } + + /// + /// Synchronized with ganjoor + /// + public bool Synchronized { get; set; } + } +} diff --git a/RMuseum/Models/GanjoorIntegration/ReviewResult.cs b/RMuseum/Models/GanjoorIntegration/ReviewResult.cs new file mode 100644 index 00000000..83e1796e --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/ReviewResult.cs @@ -0,0 +1,21 @@ +namespace RMuseum.Models.GanjoorIntegration +{ + /// + /// Review Result + /// + public enum ReviewResult + { + /// + /// Awaiting (default) + /// + Awaiting = 0, + /// + /// Approved + /// + Approved = 1, + /// + /// Rejected + /// + Rejected = 2 + } +} diff --git a/RMuseum/Models/GanjoorIntegration/ViewModels/GanjoorLinkViewModel.cs b/RMuseum/Models/GanjoorIntegration/ViewModels/GanjoorLinkViewModel.cs new file mode 100644 index 00000000..70d71b56 --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/ViewModels/GanjoorLinkViewModel.cs @@ -0,0 +1,75 @@ +using RSecurityBackend.Models.Auth.ViewModels; +using System; + +namespace RMuseum.Models.GanjoorIntegration.ViewModels +{ + /// + /// Ganjoor Link View Model + /// + public class GanjoorLinkViewModel + { + public GanjoorLinkViewModel(GanjoorLink src, string entityName, string entityFriendlyUrl, Guid entityImageId) + { + Id = src.Id; + GanjoorPostId = src.GanjoorPostId; + GanjoorUrl = src.GanjoorUrl; + GanjoorTitle = src.GanjoorTitle; + EntityName = entityName; + EntityFriendlyUrl = entityFriendlyUrl; + EntityImageId = entityImageId; + ReviewResult = src.ReviewResult; + Synchronized = src.Synchronized; + SuggestedBy = new PublicRAppUser(src.SuggestedBy); + } + + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// entity name + /// + public string EntityName { get; set; } + + /// + /// entity friendly url + /// + public string EntityFriendlyUrl { get; set; } + + /// + /// entity image id + /// + public Guid EntityImageId { get; set; } + + /// + /// review result + /// + public ReviewResult ReviewResult { get; set; } + + /// + /// Synchronized with ganjoor + /// + public bool Synchronized { get; set; } + + /// + /// suggested by + /// + public PublicRAppUser SuggestedBy { get; set; } + } +} diff --git a/RMuseum/Models/GanjoorIntegration/ViewModels/LinkSuggestion.cs b/RMuseum/Models/GanjoorIntegration/ViewModels/LinkSuggestion.cs new file mode 100644 index 00000000..891bf21a --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/ViewModels/LinkSuggestion.cs @@ -0,0 +1,35 @@ +using System; + +namespace RMuseum.Models.GanjoorIntegration.ViewModels +{ + /// + /// Link Suggestion + /// + public class LinkSuggestion + { + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// Artifact Friendly Url + /// + public string ArtifactFriendlyUrl { get; set; } + + /// + /// Artifact Item Id + /// + public Guid? ItemId { get; set; } + } +} diff --git a/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestLinkViewModel.cs b/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestLinkViewModel.cs new file mode 100644 index 00000000..33557e27 --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestLinkViewModel.cs @@ -0,0 +1,142 @@ +using System; + +namespace RMuseum.Models.GanjoorIntegration.ViewModels +{ + + /// + /// Safe pinterest link view model + /// + public class PinterestLinkViewModel + { + /// + /// cosntructor + /// + /// + /// + /// + /// + public PinterestLinkViewModel(PinterestLink src, string entityName, string entityFriendlyUrl, Guid entityImageId) + { + Id = src.Id; + GanjoorPostId = src.GanjoorPostId; + GanjoorUrl = src.GanjoorUrl; + GanjoorTitle = src.GanjoorTitle; + AltText = src.AltText; + LinkType = src.LinkType; + PinterestImageUrl = src.PinterestImageUrl; + PinterestUrl = src.PinterestUrl; + SuggestionDate = src.SuggestionDate; + ReviewerId = src.ReviewerId; + ReviewDate = src.ReviewDate; + ReviewResult = src.ReviewResult; + ReviewDesc = src.ReviewDesc; + ArtifactId = src.ArtifactId; + ItemId = src.ItemId; + Synchronized = src.Synchronized; + EntityName = entityName; + EntityFriendlyUrl = entityFriendlyUrl; + EntityImageId = entityImageId; + } + + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// alt text + /// + public string AltText { get; set; } + + /// + /// link type + /// + public LinkType LinkType { get; set; } + + /// + /// pinterest url + /// + public string PinterestUrl { get; set; } + + /// + /// pinterest image url + /// + public string PinterestImageUrl { get; set; } + + + /// + /// Suggestion Date + /// + public DateTime SuggestionDate { get; set; } + + + /// + /// User id who reviewed the link + /// + public Guid? ReviewerId { get; set; } + + + /// + /// Review Date + /// + public DateTime ReviewDate { get; set; } + + /// + /// review result + /// + public ReviewResult ReviewResult { get; set; } + + /// + /// review description + /// + public string ReviewDesc { get; set; } + + /// + /// Artifact Id + /// + public Guid? ArtifactId { get; set; } + + + /// + /// Artifact Item Id + /// + public Guid? ItemId { get; set; } + + /// + /// Synchronized with ganjoor + /// + public bool Synchronized { get; set; } + + /// + /// entity name + /// + public string EntityName { get; set; } + + /// + /// entity friendly url + /// + public string EntityFriendlyUrl { get; set; } + + /// + /// entity image id + /// + public Guid EntityImageId { get; set; } + + + } +} diff --git a/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestSuggestion.cs b/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestSuggestion.cs new file mode 100644 index 00000000..0014f769 --- /dev/null +++ b/RMuseum/Models/GanjoorIntegration/ViewModels/PinterestSuggestion.cs @@ -0,0 +1,55 @@ +using System; + +namespace RMuseum.Models.GanjoorIntegration.ViewModels +{ + /// + /// pinterest / ganjoor link suggestion view model + /// + public class PinterestSuggestion + { + /// + /// Ganjoor Post Id + /// + public int GanjoorPostId { get; set; } + + /// + /// ganjoor url + /// + public string GanjoorUrl { get; set; } + + /// + /// ganjoor title + /// + public string GanjoorTitle { get; set; } + + /// + /// alt text + /// + public string AltText { get; set; } + + /// + /// link type + /// + public LinkType LinkType { get; set; } + + /// + /// pinterest url + /// + public string PinterestUrl { get; set; } + + /// + /// pinterest image url + /// + public string PinterestImageUrl { get; set; } + + /// + /// Captcha Image Id + /// + public Guid CaptchaImageId { get; set; } + + /// + /// Captcha Value + /// + public string CaptchaValue { get; set; } + } +} diff --git a/RMuseum/Models/ImportJob/ImportJob.cs b/RMuseum/Models/ImportJob/ImportJob.cs new file mode 100644 index 00000000..f227cb39 --- /dev/null +++ b/RMuseum/Models/ImportJob/ImportJob.cs @@ -0,0 +1,39 @@ +using RMuseum.Models.Artifact; +using System; + +namespace RMuseum.Models.ImportJob +{ + /// + /// Import Job + /// + public class ImportJob + { + public Guid Id { get; set; } + + public JobType JobType { get; set; } + + public string ResourceNumber { get; set; } + + public string FriendlyUrl { get; set; } + + public string SrcUrl { get; set; } + + public DateTime QueueTime { get; set; } + + public DateTime StartTime { get; set; } + + public DateTime EndTime { get; set; } + + public ImportJobStatus Status { get; set; } + + public decimal ProgressPercent { get; set; } + + public string Exception { get; set; } + + public virtual RArtifactMasterRecord Artifact { get; set; } + + public Guid? ArtifactId { get; set; } + + public string SrcContent { get; set; } + } +} diff --git a/RMuseum/Models/ImportJob/ImportJobStatus.cs b/RMuseum/Models/ImportJob/ImportJobStatus.cs new file mode 100644 index 00000000..c367f949 --- /dev/null +++ b/RMuseum/Models/ImportJob/ImportJobStatus.cs @@ -0,0 +1,13 @@ + + +namespace RMuseum.Models.ImportJob +{ + public enum ImportJobStatus + { + NotStarted = 0, + Running = 1, + Succeeded =2, + Failed = 3, + Aborted = 4 + } +} diff --git a/RMuseum/Models/ImportJob/JobType.cs b/RMuseum/Models/ImportJob/JobType.cs new file mode 100644 index 00000000..e99fb175 --- /dev/null +++ b/RMuseum/Models/ImportJob/JobType.cs @@ -0,0 +1,21 @@ +namespace RMuseum.Models.ImportJob +{ + /// + /// Job Type + /// + public enum JobType + { + Pdf = 0, + Loc = 1, + Princeton = 2, + Harvard = 3, + HarvardDirect = 4, + HathiTrust = 5, + PennLibraries = 6, + Cambridge = 7, + BritishLibrary = 8, + ServerFolder = 9, + Walters = 10, + ChesterBeatty = 11 + } +} diff --git a/RMuseum/Models/Note/RNoteType.cs b/RMuseum/Models/Note/RNoteType.cs new file mode 100644 index 00000000..88f2b540 --- /dev/null +++ b/RMuseum/Models/Note/RNoteType.cs @@ -0,0 +1,17 @@ +namespace RMuseum.Models.Note +{ + /// + /// Note Type + /// + public enum RNoteType + { + /// + /// private + /// + Private = 0, + /// + /// public + /// + Public = 1 + } +} diff --git a/RMuseum/Models/Note/RUserNote.cs b/RMuseum/Models/Note/RUserNote.cs new file mode 100644 index 00000000..e71957f3 --- /dev/null +++ b/RMuseum/Models/Note/RUserNote.cs @@ -0,0 +1,87 @@ +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Auth.Db; +using System; + +namespace RMuseum.Models.Note +{ + /// + /// User Note + /// + public class RUserNote + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid RAppUserId { get; set; } + + /// + /// User + /// + public RAppUser RAppUser { get; set; } + + /// + /// Master Record Id + /// + public Guid? RArtifactMasterRecordId { get; set; } + + /// + /// Master Record + /// + public virtual RArtifactMasterRecord RArtifactMasterRecord { get; set; } + + /// + /// Item Record Id + /// + public Guid? RArtifactItemRecordId { get; set; } + + /// + /// Item Record + /// + public virtual RArtifactItemRecord RArtifactItemRecord { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + + /// + /// Is Updated by User + /// + public bool Modified { get; set; } + + /// + /// Last Modified + /// + public DateTime LastModified { get; set; } + + /// + /// Private / Public + /// + public RNoteType NoteType { get; set; } + + /// + /// content + /// + public string HtmlContent { get; set; } + + /// + /// In Reply to Other Note + /// + public virtual RUserNote ReferenceNote { get; set; } + + /// + /// Reference Note Id + /// + public Guid? ReferenceNoteId { get; set; } + + /// + /// Status + /// + public PublishStatus Status { get; set; } + } +} diff --git a/RMuseum/Models/Note/ViewModels/PostUserNote.cs b/RMuseum/Models/Note/ViewModels/PostUserNote.cs new file mode 100644 index 00000000..421109cb --- /dev/null +++ b/RMuseum/Models/Note/ViewModels/PostUserNote.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Models.Note.ViewModels +{ + /// + /// New User Note For Artifact + /// + public class PostUserNote + { + /// + /// Artifact / Item Id + /// + public Guid EntityId { get; set; } + + /// + /// Note Type + /// + public RNoteType NoteType { get; set; } + + /// + /// Contents + /// + public string Contents { get; set; } + + /// + /// Reference Note Id + /// + public Guid? ReferenceNoteId { get; set; } + } +} diff --git a/RMuseum/Models/Note/ViewModels/RUserNoteViewModel.cs b/RMuseum/Models/Note/ViewModels/RUserNoteViewModel.cs new file mode 100644 index 00000000..5e00caec --- /dev/null +++ b/RMuseum/Models/Note/ViewModels/RUserNoteViewModel.cs @@ -0,0 +1,134 @@ +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Auth.ViewModels; +using System; +using System.Globalization; + +namespace RMuseum.Models.Note.ViewModels +{ + /// + /// Safe User Note View Model + /// + public class RUserNoteViewModel + { + /// + /// Constructor + /// + /// + /// + public RUserNoteViewModel(RUserNote src, PublicRAppUser user) + { + Id = src.Id; + RAppUserId = src.RAppUserId; + UserName = (user.FirstName + " " + user.SureName); + RUserImageId = user.RImageId; + Modified = src.Modified; + NoteType = src.NoteType; + HtmlContent = src.HtmlContent; + ReferenceNoteId = src.ReferenceNoteId; + Status = src.Status; + Notes = new RUserNoteViewModel[] { }; + + + PersianCalendar pc = new PersianCalendar(); + + DateTime = $"{pc.GetYear(src.DateTime)}/{pc.GetMonth(src.DateTime)}/{pc.GetDayOfMonth(src.DateTime)} {pc.GetHour(src.DateTime)}:{pc.GetMinute(src.DateTime)}"; + LastModified = $"{pc.GetYear(src.LastModified)}/{pc.GetMonth(src.LastModified)}/{pc.GetDayOfMonth(src.LastModified)} {pc.GetHour(src.LastModified)}:{pc.GetMinute(src.LastModified)}"; + + } + + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid RAppUserId { get; set; } + + /// + /// User Name (FirstName + SureName) + /// + public string UserName { get; set; } + + /// + /// user image id + /// + public Guid? RUserImageId { get; set; } + + /// + /// DateTime + /// + public string DateTime { get; set; } + + /// + /// Is Updated by User + /// + public bool Modified { get; set; } + + /// + /// Last Modified + /// + public string LastModified { get; set; } + + /// + /// Private / Public + /// + public RNoteType NoteType { get; set; } + + /// + /// content + /// + public string HtmlContent { get; set; } + + /// + /// Reference Note Id + /// + public Guid? ReferenceNoteId { get; set; } + + /// + /// Status + /// + public PublishStatus Status { get; set; } + + /// + /// Related Entity (Artifact/Item) Name + /// + /// + /// It is set only in cummulative api calls + /// + public string RelatedEntityName { get; set; } + + /// + /// Realated entity Image Id + /// + /// + /// It is set only in cummulative api calls + /// + public Guid RelatedEntityImageId { get; set; } + + /// + /// Realated entity Friendly Url (for artifact items it would be ParentFiendlyUrl/ItemFriendlyUrl ) + /// + /// + /// It is set only in cummulative api calls + /// + public string RelatedEntityFriendlyUrl { get; set; } + + /// + /// Related Item Artifact Name + /// + /// + /// It is set only in cummulative api calls + /// + public string RelatedItemParentName { get; set; } + + /// + /// child notes + /// + /// + /// It os set only in certain cases + /// + public RUserNoteViewModel[] Notes { get; set; } + } +} diff --git a/RMuseum/Models/Notification/NotificationStatus.cs b/RMuseum/Models/Notification/NotificationStatus.cs new file mode 100644 index 00000000..6cad9cfc --- /dev/null +++ b/RMuseum/Models/Notification/NotificationStatus.cs @@ -0,0 +1,13 @@ +namespace RMuseum.Models.Notification +{ + /// + /// Notifaction Status + /// + public enum NotificationStatus + { + Unread = 0, + Read = 1, + Archived = 2, + Deleted = 3 + } +} diff --git a/RMuseum/Models/Notification/RUserNotification.cs b/RMuseum/Models/Notification/RUserNotification.cs new file mode 100644 index 00000000..67327404 --- /dev/null +++ b/RMuseum/Models/Notification/RUserNotification.cs @@ -0,0 +1,46 @@ +using RSecurityBackend.Models.Auth.Db; +using System; + +namespace RMuseum.Models.Notification +{ + /// + /// User Notification + /// + public class RUserNotification + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid UserId { get; set; } + + /// + /// User + /// + public RAppUser User { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + + /// + /// Status + /// + public NotificationStatus Status { get; set; } + + /// + /// Subject + /// + public string Subject { get; set; } + + /// + /// Text + /// + public string HtmlText { get; set; } + } +} diff --git a/RMuseum/Program.cs b/RMuseum/Program.cs new file mode 100644 index 00000000..4010c6d4 --- /dev/null +++ b/RMuseum/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace RMuseum +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/RMuseum/Properties/PublishProfiles/FolderProfile.pubxml b/RMuseum/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..915ce764 --- /dev/null +++ b/RMuseum/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,21 @@ + + + + + FileSystem + FileSystem + Release + Any CPU + + True + False + netcoreapp3.1 + 089caebe-24cf-438a-80f9-7e457a6e77d2 + false + bin\Release\netcoreapp3.1\publish\ + True + + \ No newline at end of file diff --git a/RMuseum/Properties/launchSettings.json b/RMuseum/Properties/launchSettings.json new file mode 100644 index 00000000..8e87fcc9 --- /dev/null +++ b/RMuseum/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3439", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "RMuseum": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/RMuseum/RMuseum.csproj b/RMuseum/RMuseum.csproj new file mode 100644 index 00000000..5c1be78e --- /dev/null +++ b/RMuseum/RMuseum.csproj @@ -0,0 +1,40 @@ + + + + netcoreapp3.1 + InProcess + + + + RMuseum.xml + 1701;1702;1591 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + Always + + + + diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml new file mode 100644 index 00000000..45f437d6 --- /dev/null +++ b/RMuseum/RMuseum.xml @@ -0,0 +1,3797 @@ + + + + RMuseum + + + + + User login/logout/register/... + + + + + constructor + + + + + + + + + + + + Sign Up Email Subject + + + subject + + + + + + Sign Up Email Html Content + + + html content + + + + Forgot Password Email Subject + + + subject + + + + + + Forgot Password Email Html Content + + + html content + + + + Is Sign-up enabled? + + + + + + get all published artifacts (including CoverImage info but not items info) + + + + + + + get list of artifact statuses user can see + + + + + + + + get all artifacts visible by user (including CoverImage info but not items info) + + + + + + + gets specified publish artifact info (including CoverImage + images) + + + + + + + remove unpublished artifact having no notes and not bookmarked + + + + + + + gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + + + + + + + + gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + + + + + + + + + gets specified publish artifact info (including CoverImage + images) + + + + + + + gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + + + + + + + + gets specified publish artifact info (including CoverImage + images) where its items are filteted by tag + + + + + + + + + edit artifactt master record (user should have additional permissions artifact:awaiting and artifact:publish to change status of artifact) + + + editing related collections such as images and attributed or complex properties such as CoverImage is ignored + + + + + + + + Set Artifact Cover Item Index + + + + + + + + get tag bundle by frindly url + + + + + + + add new tag + + only name is processed + + + + + get all tags + + + + + + + edit tag + + + editable fields are limited + + + + + + + + changes order of tags based on their position in artifacts + + + + up / down + the other tag which its Order got replaced with the input id + + + + changes order of tags based on their position in artifact items + + + + up / down + the other tag which its Order got replaced with the input id + + + + get tag value bundle by frindly url + + + + + + + + get tag value by frindly url + + + + + + + + add new tag value to artifact + + + only name is processed + + + + + edit artifact attribute value + + + editable fields are limited + + + + apply on all same value tags + + + + + remove tag from artifact + + + + + + + + add new tag value to item + + + only name is processed + + + + + edit item attribute value + + + editable fields are limited + + + + apply on all same value tags + + + + + remove tag from item + + + + + + + + changes order of tag values based on their position in an artifact + + + + + up / down + the other tag which its Order got replaced with the input id + + + + changes order of tag values based on their position in an item + + + + + up / down + the other tag which its Order got replaced with the input id + + + + gets specified publish artifact item info (including images + attributes) + + + + + + + + gets specified publish artifact item info (including images + attributes) + + + + + + + + Add new artifact (multipart/form-data) + + + const data = new FormData(); + data.append('name', 'تست'); + data.append('description', ''); + data.append('srcUrl', ''); + data.append('picTitle', 'تست'); + data.append('picDescription', ''); + data.append('file', this.file); + data.append('picSrcUrl', ''); + + + + + + + + import from external resources + + loc/princeton/harvard/qajarwomen/hathitrust/penn/cam/bl/folder/walters/cbl + 119 + golestan-baysonghori + + + + + + keep alive (for import background service) + + true + + + + retry import + + + + + + due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + + 123456 + + + + + bookmark artifact + + + + + + + bookmark item + + + + + + + item bookmarks info + + + + + + + fav artifact + + + + + + + fav item + + + + + + + artifact bookmarks info + + + + + + + item bookmarks info + + + + + + + user bookmarks + + + + + + user favorites + + + + + + add note for artifact + + + + + + + add note for artifact item + + + + + + + Edit User Notes + + a note can not be edited by a user other than its owner or another using having note:moderate permission + + only htmlContent is processed + + + + + delete user notes + + + 1. a note can not be deleted by a user other than its owner or another using having note:moderate permission + 2. all notes which have refernce to deleting note (sent in reply to it) would be deleted irrelevant of their ownership + + + list of notes deleted + + + + get private notes for artifact + + + + + + + get private notes for artifact + + + + + + + get private notes for artifact item + + + + + + + get public notes for artifact item + + + + + + + user public notes + + + + + + user public notes + + + + + + all users public notes + + + + + + suggest ganjoor link + + + + + + + get suggested ganjoor links + + + + + + + + review suggested ganjoor link + + + + + + + + mark suggested ganjoor link as synchronized + + + + + + + toc / temporary one time api / to be removed + + + + + + get suggested pinterest links + + + + + + + + suggest pinterest link for ganjoor + + + + + + + review suggested ganjoor pinterest link + + + + + + + + + + + mark suggested pinterest ganjoor link as synchronized + + + + + + + Get User Notifications + + + + + + Switch Notification Status + + + + + + + Delete Notification + + + + + + + constructor + + + + + + + + + + Artifact Service + + + + + IUserPermissionChecker instance + + + + + IMemoryCache + + + + + Captcha service + + + + + Audit Log Controller Base + + + + + constructor + + + + + + + returns image stream + + + + + + + + Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + + + + + + + + Generate Cropped Image Based On ThumbnailCoordinates For Notes + + + + + + + id of cropped image + + + + constructor + + + + + + + Artifact Service + + + + + Generic Image Provider + + + + + constructor + + + + + + + roles + + + + + constructor + + + + + + Museum Database Context + + + + + constructor + + + + + + + + + + + + + + + + + + + + + + Picture Files + + + + + Item Attributes + + + + + Artifacts + + + + + Items + + + + + Import Jobs + + + + + Tags + + + + + User Bookmarks + + + + + User Notes + + + + + Ganjoor Links + + + + + Notifications + + + + + Pinterest Links + + + + + Items or pictures status + + + + + Id + + + + + parent id + + + + + Order in Collections + + + + + Friendly Url + + + + + Name + + + + + Name In English + + + + + Description + + + + + Description In English + + + + + Main Image + + + + + Last Modified for caching purposes + + + + + All Images + + + + + Attributes + + + + + Museum Master Item + + + + + Id + + + + + Friendly Url + + + + + Publish Status + + + + + Name + + + + + Name In English + + + + + Description + + + + + Description In English + + + + + Date/Time + + + + + Last Modified for caching purposes + + + + + Cover Item Index + + + + + Cover Image + + + + + Cover Image Id + + + + + Parts of this item + + + + + Item Count (for lists and queries) + + + + + Attributes + + + + + Artifact Picture Files + + + + + Title + + + + + Title in English + + + + + Description + + + + + Description in English + + + + + Publish status + + + + + order in the collection it belongs to it + + + + + نام فایل ذخیره شده با بالاترین کیفیت + + + e0d7050a-7e30-4a2f-b181-0daa4a1e1068.jpg + + + + + نام فایل ذخیره شده با بالاترین کیفیت + + + fbe9cc8e-12fa-4cb9-8f09-bbf353333383.jpg + + + + + عرض تصویر مناسب نمایش در صفحهٔ اختصاصی + + + + + طول تصویر مناسب نمایش در صفحهٔ اختصاصی + + + + + عرض تصویر با اندازه مناسب نمایش در صفحات لیستی + + + + + طول تصویر با اندازه مناسب نمایش در صفحات لیستی + + + + + source url + + + + + Last Modified for caching purposes + + + + + duplicated a picture record (Id is missing so you should store this to get a new Id) + + + + + + + Item Attribute + + + + + Id + + + + + Order + + + + + tag type + + + + + Friendly Url + + + + + Publish Status + + + + + Name + + + + + Name In English + + + + + Plural Name + + + + + Plural Name In English + + + + + Value is normally changed globally + + + + + نوع ویژگی + + + + + عادی + + + + + پیوند + + + + + جستجو + + + + + پیوند و جستجو + + + + + متن چپ به راست + + + + + بدون مقدار + + + + + عنوان در فهرست + + + + + Attribute Value + + + + + Id + + + + + Friendly Url + + + + + Publish Status + + + + + Value + + + + + Values In English + + + + + link or .... + + + + + new artifact view model + + + + + RArtifactItemRecord with additional info + + + + + main item info + + + + + parent + + + + + parent name + + + + + parent image + + + + + parent item count + + + + + empty or null means this is last item + + + + + next image + + + + + empty or null means this is first item + + + + + prev image + + + + + Formatted Tags + + + + + Museum Master Item View Model + + + + + constructor + + + + + + Tags + + + + + Binary Tagged Items + + + + + Titles of Items in Contents + + + + + Artifact Tag (RTag with grouped related RTagValues of Artifact) + + + + + Tag Bundle View Model + + + + + Item Title + + + + + Item Friendly Url + + + + + Items Count + + + + + Item Image Id + + + + + Tag Bundle View Model + + + + + Id + + + + + Friendly Url + + + + + Name + + + + + Plural Name + + + + + Values + + + + + Artifact Binary Tagged items Summation + + + + + Tag Name + + + + + Tag Friendly Url + + + + + Item Count + + + + + Title of an Item in Artifact Contents + + + + + Title + + + + + Order + + + + + Level + + + + + Item Friendly Url + + + + + specific forms and permission + + + + + artifact + + + + + tag + + + + + note + + + + + view drafts + + + + + edittag + + + + + awaiting + + + + + publish + + + + + import + + + + + moderate + + + + + review suggested ganjoor links + + + + + list of forms and their permissions + + + + + Bookmark Type + + + + + Private Bookmark + + + + + Public Favorite + + + + + User Bookmarks + + + + + Id + + + + + User Id + + + + + User + + + + + Master Record Id + + + + + Master Record + + + + + Item Record Id + + + + + Item Record + + + + + DateTime + + + + + Type + + + + + User Note + + + + + Id + + + + + User Id + + + + + Master Record + + + + + Item Record + + + + + DateTime + + + + + Type + + + + + User Note + + + + + ganjoor link for artifacts and items + + + + + Id + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + Artifact Id + + + + + artifact + + + + + Artifact Item Id + + + + + Artifact Item + + + + + User Id who suggested the link + + + + + Suggestion Date + + + + + User who suggested the link + + + + + User id who reviewed the link + + + + + User who reviewed the link + + + + + Review Date + + + + + review result + + + + + Synchronized with ganjoor + + + + + Link Type + + + + + Custom + + + + + Museum + + + + + Pinterest + + + + + Instagram + + + + + pinterest link + + + + + Id + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + alt text + + + + + link type + + + + + pinterest url + + + + + pinterest image url + + + + + User Id who suggested the link (this would always be null for anonyous suggestions, kept it so that someday it would be used to integerate GanjoorLink into this class) + + + + + Suggestion Date + + + + + User who suggested the link + + + + + User id who reviewed the link + + + + + User who reviewed the link + + + + + Review Date + + + + + review result + + + + + review description + + + + + Artifact Id + + + + + artifact + + + + + Artifact Item Id + + + + + Artifact Item + + + + + Synchronized with ganjoor + + + + + Review Result + + + + + Awaiting (default) + + + + + Approved + + + + + Rejected + + + + + Ganjoor Link View Model + + + + + Id + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + entity name + + + + + entity friendly url + + + + + entity image id + + + + + review result + + + + + Synchronized with ganjoor + + + + + suggested by + + + + + Link Suggestion + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + Artifact Friendly Url + + + + + Artifact Item Id + + + + + Safe pinterest link view model + + + + + cosntructor + + + + + + + + + Id + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + alt text + + + + + link type + + + + + pinterest url + + + + + pinterest image url + + + + + Suggestion Date + + + + + User id who reviewed the link + + + + + Review Date + + + + + review result + + + + + review description + + + + + Artifact Id + + + + + Artifact Item Id + + + + + Synchronized with ganjoor + + + + + entity name + + + + + entity friendly url + + + + + entity image id + + + + + pinterest / ganjoor link suggestion view model + + + + + Ganjoor Post Id + + + + + ganjoor url + + + + + ganjoor title + + + + + alt text + + + + + link type + + + + + pinterest url + + + + + pinterest image url + + + + + Captcha Image Id + + + + + Captcha Value + + + + + Import Job + + + + + Job Type + + + + + Note Type + + + + + private + + + + + public + + + + + User Note + + + + + Id + + + + + User Id + + + + + User + + + + + Master Record Id + + + + + Master Record + + + + + Item Record Id + + + + + Item Record + + + + + DateTime + + + + + Is Updated by User + + + + + Last Modified + + + + + Private / Public + + + + + content + + + + + In Reply to Other Note + + + + + Reference Note Id + + + + + Status + + + + + New User Note For Artifact + + + + + Artifact / Item Id + + + + + Note Type + + + + + Contents + + + + + Reference Note Id + + + + + Safe User Note View Model + + + + + Constructor + + + + + + + Id + + + + + User Id + + + + + User Name (FirstName + SureName) + + + + + user image id + + + + + DateTime + + + + + Is Updated by User + + + + + Last Modified + + + + + Private / Public + + + + + content + + + + + Reference Note Id + + + + + Status + + + + + Related Entity (Artifact/Item) Name + + + It is set only in cummulative api calls + + + + + Realated entity Image Id + + + It is set only in cummulative api calls + + + + + Realated entity Friendly Url (for artifact items it would be ParentFiendlyUrl/ItemFriendlyUrl ) + + + It is set only in cummulative api calls + + + + + Related Item Artifact Name + + + It is set only in cummulative api calls + + + + + child notes + + + It os set only in certain cases + + + + + Notifaction Status + + + + + User Notification + + + + + Id + + + + + User Id + + + + + User + + + + + DateTime + + + + + Status + + + + + Subject + + + + + Text + + + + + Artifact Item Service + + + + + get all artifacts (including CoverImage info but not items or attributes info) + + + + + + + + get tagged publish artifacts (including CoverImage info but not items or tagibutes info) + + + + + + + + + gets specified artifact info (including CoverImage + images + attributes) + + + + + + + + gets specified artifact info (including CoverImage + images + tagibutes) + + + + + + + + + + edit master record + + + + + + + + + Set Artifact Cover Item Index + + + + + + + + get tag bundle by frindly url + + + + + + + get max lastmodified artifact date for caching purposes + + + + + + get all tags + + + + + + + get tag bu friendly url + + + + + + + add tag + + + + + + edit tag + + + + + + + changes order of tags based on their position in artifacts + + + + true => up, false => down + the other tag which its Order has been changed + + + + changes order of tags based on their position in artifact items + + + + true => up, false => down + the other tag which its Order has been changed + + + + add artifact tag value + + + + + + + + changes order of tag values based on their position in an artifact + + + + + true => up, false => down + the other tag value which its Order has been changed + + + + changes order of tag values based on their position in an item + + + + + true => up, false => down + the other tag value which its Order has been changed + + + + get tag value bundle by frindly url + + + + + + + + get tag value by frindly url + + + + + + + + edit artifact tagibute value + + + + apply on all same value tags + + + + + remove artfiact tag value + + + + + + + + add item tag value + + + + + + + + edit item tag value + + + + apply on all same value tags + + + + + remove item tag value + + + + + + + + gets specified artifact item info (including images + attributes) + + + + + + + + + add new artifact + + + + + + + + + + + + + + for now it only supports https://www.loc.gov + + + + m084 + + + + + boostan1207 + + + + + plmp + + + + + + from http://pudl.princeton.edu/ + + dj52w476m + + + + + + from https://curiosity.lib.harvard.edu + + example: https://curiosity.lib.harvard.edu/islamic-heritage-project/catalog/40-990114893240203941 + + + + + + import from http://www.qajarwomen.org + + 43117279 + atame + http://www.qajarwomen.org/fa/items/1018A10.html + + + + + from https://catalog.hathitrust.org + + 006814127 + + + + + + from http://www.library.upenn.edu/ + + MEDREN_9949222153503681 + + + + + + from http://cudl.lib.cam.ac.uk + + MS-RAS-00258 + + + + + + from http://www.bl.uk + + grenville_xli_f001r + + + + + + import from server folder + + C:\Tools\batches\florence + shahname-florence + https://t.me/dr_khatibi_abolfazl/888 + + + + + from http://www.thedigitalwalters.org/01_ACCESS_WALTERS_MANUSCRIPTS.html + + W619 + golestan-walters-01 + + + + + import from https://viewer.cbl.ie + + 119 + golestan-baysonghori + + + + + reschedule jobs + + + + + + due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + + + + + + import jobs + + + + + + + Bookmark Artifact + + + + + + + + + get artifact user bookmarks + + + + + + + + Bookmark Item + + + + + + + + + get item user bookmarks + + + + + + + + update bookmark note + + + + + + + + delete user bookmark + /// + + + + + + get user bookmarks (artifacts and items) + + + + + + + + + + Add Note to Artifact + + + + + + + + + + + Add Note to Artifact Item + + + + + + + + + + + Edit Note + + + sending null here means user is a moderator + + + + + + delete note + + + sending null here means user is a moderator or the note is being deleted in a recursive delete of referenced notes + list of notes deleted + + + + get artifact private user notes + + + + + + + + get artifact public user notes + + + + + + + get item artifact private user notes + + + + + + + + get item artifact item public user notes + + + + + + + Get All USer Notes + + + + + + + + + + Get All Public Notes + + + + + + + suggest ganjoor link + + + + + + + + get suggested ganjoor links + + + + + + + + Review Suggested Link + + + + + + + + + Temporary api + + + + + + Synchronize suggested link + + + + + + + get suggested pinterest links + + + + + + + + suggest pinterest link + + + + + + + Review Suggested Pinterest Link + + + + + + + + + + + + Synchronize suggested pinterest link + + + + + + + Add Notification + + + + + + + + + Get User Notifications + + + + + + + Get Unread User Notifications Count + + + + + + + Switch Notification Status + + + updated notification object + + + + Delete Notification + + + + + + + an incomplete prototype for removing artifacts + + + + + + + + IArtifactService implementation + + + + + get all artifacts (including CoverImage info but not items or tagibutes info) + + + + + + + + get tagged publish artifacts (including CoverImage info but not items or tagibutes info) + + + + + + + + + gets specified artifact info (including CoverImage + images + tagibutes) + + + + + + + + gets specified artifact info (including CoverImage + images + tagibutes) + + + + + + + + + + edit master record + + + + + + + + + Set Artifact Cover Item Index + + + + + + + + get tag bundle by frindly url + + + + + + + get max lastmodified artifact date for caching purposes + + + + + + get all tags + + + + + + + get tag bu friendly url + + + + + + + get tag value by frindly url + + + + + + + + add tag + + + + + + edit tag + + + + + + + changes LastModified field value for tagged artifacts and items + + + + + + + changes order of tags based on their position in artifacts + + + + true => up, false => down + the other tag which its Order has been changed + + + + changes order of tags based on their position in artifact items + + + + true => up, false => down + the other tag which its Order has been changed + + + + get tag value bundle by frindly url + + + + + + + + add artifact tag value + + + + + + + + remove artfiact tag value + + + + + + + + edit artifact tag value + + + + apply on all same value tags + + + + + add item tag value + + + + + + + + remove item tag value + + + + + + + + edit item tagibute value + + + + apply on all same value tags + + + + + changes order of tag values based on their position in an artifact + + + + + true => up, false => down + the other tag value which its Order has been changed + + + + changes order of tag values based on their position in an item + + + + + true => up, false => down + the other tag value which its Order has been changed + + + + gets specified artifact item info (including images + tagibutes) + + + + + + + + + add new artifact + + + + + + + + + + + + + + from https://www.loc.gov + + + + m084 + + + + + boostan1207 + + + + + plmp + + + + + + from http://pudl.princeton.edu/ + + dj52w476m + + + + + + from https://curiosity.lib.harvard.edu + + example: https://curiosity.lib.harvard.edu/islamic-heritage-project/catalog/40-990114893240203941 + + + + + + import from http://www.qajarwomen.org + + 43117279 + atame + http://www.qajarwomen.org/fa/items/1018A10.html + + + + + from https://catalog.hathitrust.org + + 006814127 + + + + + + from http://www.library.upenn.edu/ + + MEDREN_9949222153503681 + + + + + + from http://cudl.lib.cam.ac.uk + + MS-RAS-00258 + + + + + + from http://www.bl.uk + + grenville_xli_f001r + + + + + + import from server folder + + C:\Tools\batches\florence + shahname-florence + https://t.me/dr_khatibi_abolfazl/888 + + + + + from http://www.thedigitalwalters.org/01_ACCESS_WALTERS_MANUSCRIPTS.html + + W619 + golestan-walters-01 + + + + + import from https://viewer.cbl.ie + + 119 + golestan-baysonghori + + + + + reschedule jobs + + + + + + an incomplete prototype for removing artifacts + + + + + + + + due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + + + + + + import jobs + + + + + + + Bookmark Artifact + + + + + + + + + get artifact user bookmarks + + + + + + + + Bookmark Item + + + + + + + + + get item user bookmarks + + + + + + + + update bookmark note + + + + + + + + delete user bookmark + /// + + + + + + get user bookmarks (artifacts and items) + + + + + + + + + + Add Note to Artifact + + + + + + + + + + + Add Note to Artifact Item + + + + + + + + + + + Edit Note + + + + + + + + + delete note + + + sending null here means user is a moderator or the note is being deleted in a recursive delete of referenced notes + list of notes deleted + + + + get artifact private user notes + + + + + + + + recursvie call to build notes tree + + + + + + + + get artifact public user notes + + + + + + + get item artifact private user notes + + + + + + + + recursvie call to build notes tree for iten + + + + + + + + get item artifact item public user notes + + + + + + + Get All USer Notes + + + + + + + + + + Get All Public Notes + + + + + + + suggest ganjoor link + + + + + + + + get suggested ganjoor links + + + + + + + + Review Suggested Link + + + + + + + + + Temporary api + + + + + + Synchronize suggested link + + + + + + + get suggested pinterest links + + + + + + + + suggest pinterest link + + + + + + + Review Suggested Pinterest Link + + + + + + + + + + + + Synchronize suggested pinterest link + + + + + + + Add Notification + + + + + + + + + Switch Notification Status + + + updated notification object + + + + Delete Notification + + + + + + + Get User Notifications + + + + + + + Get Unread User Notifications Count + + + + + + + Database Contetxt + + + + + Picture File Service + + + + + Configuration + + + + + Background Task Queue Instance + + + + + User Service + + + + + constructor + + + + + + + + + + British Library Tile Downloader And Mixer + + + + + Constructor + + + + + find image encodder + + + + + + + SmptConfig + + + + + manipulating picture files + + + + + recover from files + + + + + + + + + + + + + + + add new picture + + + + + + + + + pass empty if you want a generic date based folder + + + + + returns image info + + + + + + + Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + + + + + + + + find image encodder + + + + + + + returns image file stream + + + + + + + + فرمت تصاویر بندانگشتی و ... + + image/jpeg + + + + عرض تصویر با اندازه نرمال - جهت نمایش در صفحات + + + + + /// طول تصویر با اندازه نرمال - جهت نمایش در صفحات + + + + + عرض تصویر بندانگشتی + + + + + طول تصویر بندانگشتی + + + + + Image Storage Path + + + + + Generated Cropped Image Based On ThumbnailCoordinates For Notes + + + + + + + + + + + Database Contetxt + + + + + Configuration + + + + + Simple Image Storage (not storing multiple image sizes) for creating small cropped images + + + + + constructor + + + + + + + + Role Service Implementation + + + + + gets list of SecurableItem, should be reimplemented in end user applications + + + + + + constructor + + + + + + manipulating picture files + + + + + add new picture + + + + + + + + + pass empty if you want a generic date based folder + + + + + recover from files + + + + + + + + + + + + + + + returns image info + + + + + + + returns image file stream + + + + + + + + Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + + + + + + + + Get Encoder + + + + + + + Generate Cropped Image Based On ThumbnailCoordinates For Notes + + + + + + + + + + + Image Storage Path + + + + + //https://mattfrear.com/2018/07/21/add-an-authorization-header-to-your-swagger-ui-with-swashbuckle-revisited/ + + + + + + + + + + + diff --git a/RMuseum/Services/IArtifactService.cs b/RMuseum/Services/IArtifactService.cs new file mode 100644 index 00000000..65de007f --- /dev/null +++ b/RMuseum/Services/IArtifactService.cs @@ -0,0 +1,640 @@ +using Microsoft.AspNetCore.Http; +using RMuseum.Models.Artifact; +using RMuseum.Models.Artifact.ViewModels; +using RMuseum.Models.Bookmark; +using RMuseum.Models.Bookmark.ViewModels; +using RMuseum.Models.GanjoorIntegration; +using RMuseum.Models.GanjoorIntegration.ViewModels; +using RMuseum.Models.ImportJob; +using RMuseum.Models.Note; +using RMuseum.Models.Note.ViewModels; +using RMuseum.Models.Notification; +using RSecurityBackend.Models.Generic; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace RMuseum.Services +{ + /// + /// Artifact Item Service + /// + public interface IArtifactService + { + /// + /// get all artifacts (including CoverImage info but not items or attributes info) + /// + /// + /// + /// + Task> GetAll(PagingParameterModel paging, PublishStatus[] statusArray); + + /// + /// get tagged publish artifacts (including CoverImage info but not items or tagibutes info) + /// + /// + /// + /// + /// + Task> GetByTagValue(string tagUrl, string valueUrl, PublishStatus[] statusArray); + + + /// + /// gets specified artifact info (including CoverImage + images + attributes) + /// + /// + /// + /// + Task> GetByFriendlyUrl(string friendlyUrl, PublishStatus[] statusArray); + + /// + /// gets specified artifact info (including CoverImage + images + tagibutes) + /// + /// + /// + /// + /// + /// + Task> GetByFriendlyUrlFilterItemsByTag(string friendlyUrl, PublishStatus[] statusArray, string tagFriendlyUrl, string tagValueFriendlyUrl); + + /// + /// edit master record + /// + /// + /// + /// + /// + Task> EditMasterRecord(RArtifactMasterRecord edited, bool canChangeStatusToAwaiting, bool canPublish); + + /// + /// Set Artifact Cover Item Index + /// + /// + /// + /// + Task> SetArtifactCoverItemIndex(Guid artifactId, int itemIndex); + + /// + /// get tag bundle by frindly url + /// + /// + /// + Task> GetTagBundleByFiendlyUrl(string friendlyUrl); + + /// + /// get max lastmodified artifact date for caching purposes + /// + /// + Task> GetMaxArtifactLastModified(); + + /// + /// get all tags + /// + /// + /// + Task> GetAllTags(PagingParameterModel paging); + + /// + /// get tag bu friendly url + /// + /// + /// + Task> GetTagByFriendlyUrl(string friendlyUrl); + + /// + /// add tag + /// + /// + Task> AddTag(string tagName); + + /// + /// edit tag + /// + /// + /// + Task> EditTag(RTag edited); + + /// + /// changes order of tags based on their position in artifacts + /// + /// + /// + /// true => up, false => down + /// the other tag which its Order has been changed + Task> EditTagOrderBasedOnArtifact(Guid tagId, Guid artifactId, bool up); + + /// + /// changes order of tags based on their position in artifact items + /// + /// + /// + /// true => up, false => down + /// the other tag which its Order has been changed + Task> EditTagOrderBasedOnItem(Guid tagId, Guid itemId, bool up); + + /// + /// add artifact tag value + /// + /// + /// + /// + Task> TagArtifact(Guid artifactId, RTag tag); + + /// + /// changes order of tag values based on their position in an artifact + /// + /// + /// + /// + /// true => up, false => down + /// the other tag value which its Order has been changed + Task> EditTagValueOrder(Guid artifactId, Guid tagId, Guid valueId, bool up); + + /// + /// changes order of tag values based on their position in an item + /// + /// + /// + /// + /// true => up, false => down + /// the other tag value which its Order has been changed + Task> EditItemTagValueOrder(Guid itemId, Guid tagId, Guid valueId, bool up); + + /// + /// get tag value bundle by frindly url + /// + /// + /// + /// + Task> GetTagValueBundleByFiendlyUrl(string friendlyUrl, string valueUrl); + + /// + /// get tag value by frindly url + /// + /// + /// + /// + Task> GetTagValueByFriendlyUrl(Guid tagId, string friendlyUrl); + + /// + /// edit artifact tagibute value + /// + /// + /// + /// apply on all same value tags + /// + Task> EditTagValue(Guid artifactId, RTagValue edited, bool global); + + /// + /// remove artfiact tag value + /// + /// + /// + /// + Task> UnTagArtifact(Guid artifactId, Guid tagValueId); + + /// + /// add item tag value + /// + /// + /// + /// + Task> TagItem(Guid itemId, RTag tag); + + /// + /// edit item tag value + /// + /// + /// + /// apply on all same value tags + /// + Task> EditItemTagValue(Guid itemId, RTagValue edited, bool global); + + + /// + /// remove item tag value + /// + /// + /// + /// + Task> UnTagItem(Guid itemId, Guid tagValueId); + + + + /// + /// gets specified artifact item info (including images + attributes) + /// + /// + /// + /// + /// + Task> GetArtifactItemByFrienlyUrl(string artifactUrl, string itemUrl, PublishStatus[] statusArray); + + + + /// + /// add new artifact + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task> Add + ( + string name, string description, string srcurl, + string pictitle, string picdescription, IFormFile file, string picsrcurl, Stream stream + ); + + + /// + /// for now it only supports https://www.loc.gov + /// + /// + /// + /// m084 + /// + /// + /// + /// + /// boostan1207 + /// + /// + /// + /// + /// plmp + /// + /// + Task> StartImportingFromTheLibraryOfCongress(string resourceNumber, string fiendlyUrl, string resourcePrefix); + + /// + /// from http://pudl.princeton.edu/ + /// + /// dj52w476m + /// + /// + Task> StartImportingFromPrinceton(string resourceNumber, string friendlyUrl); + + /// + /// from https://curiosity.lib.harvard.edu + /// + /// example: https://curiosity.lib.harvard.edu/islamic-heritage-project/catalog/40-990114893240203941 + /// + /// + Task> StartImportingFromHarvard(string url, string friendlyUrl); + + /// + /// import from http://www.qajarwomen.org + /// + /// 43117279 + /// atame + /// http://www.qajarwomen.org/fa/items/1018A10.html + /// + Task> StartImportingFromHarvardDirectly(string hardvardResourceNumber, string friendlyUrl, string srcUrl); + + /// + /// from https://catalog.hathitrust.org + /// + /// 006814127 + /// + /// + Task> StartImportingFromHathiTrust(string resourceNumber, string friendlyUrl); + + /// + /// from http://www.library.upenn.edu/ + /// + /// MEDREN_9949222153503681 + /// + /// + Task> StartImportingFromPenLibraries(string resourceNumber, string friendlyUrl); + + /// + /// from http://cudl.lib.cam.ac.uk + /// + /// MS-RAS-00258 + /// + /// + Task> StartImportingFromCambridge(string resourceNumber, string friendlyUrl); + + /// + /// from http://www.bl.uk + /// + /// grenville_xli_f001r + /// + /// + Task> StartImportingFromBritishLibrary(string resourceNumber, string friendlyUrl); + + /// + /// import from server folder + /// + /// C:\Tools\batches\florence + /// shahname-florence + /// https://t.me/dr_khatibi_abolfazl/888 + /// + Task> StartImportingFromServerFolder(string folderPath, string friendlyUrl, string srcUrl); + + /// + /// from http://www.thedigitalwalters.org/01_ACCESS_WALTERS_MANUSCRIPTS.html + /// + /// W619 + /// golestan-walters-01 + /// + Task> StartImportingFromWalters(string resourceNumber, string friendlyUrl); + + /// + /// import from https://viewer.cbl.ie + /// + /// 119 + /// golestan-baysonghori + /// + Task> StartImportingFromChesterBeatty(string resourceNumber, string friendlyUrl); + + /// + /// reschedule jobs + /// + /// + Task> RescheduleJobs(); + + /// + /// due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + /// + /// + Task> ReExamineLocDownloads(); + + /// + /// import jobs + /// + /// + /// + Task> GetImportJobs(PagingParameterModel paging); + + /// + /// Bookmark Artifact + /// + /// + /// + /// + /// + Task> BookmarkArtifact(Guid artifactId, Guid userId, RBookmarkType type); + + /// + /// get artifact user bookmarks + /// + /// + /// + /// + Task> GetArtifactUserBookmarks(Guid artifactId, Guid userId); + + /// + /// Bookmark Item + /// + /// + /// + /// + /// + Task> BookmarkItem(Guid itemId, Guid userId, RBookmarkType type); + + /// + /// get item user bookmarks + /// + /// + /// + /// + Task> GeItemUserBookmarks(Guid itemId, Guid userId); + + /// + /// update bookmark note + /// + /// + /// + /// + Task> UpdateUserBookmark(Guid bookmarkId, string note); + + /// + /// delete user bookmark + /// /// + /// + /// + Task> DeleteUserBookmark(Guid bookmarkId); + + /// + /// get user bookmarks (artifacts and items) + /// + /// + /// + /// + /// + /// + Task> GetBookmarks(PagingParameterModel paging, Guid userId, RBookmarkType type, PublishStatus[] statusArray); + + /// + /// Add Note to Artifact + /// + /// + /// + /// + /// + /// + /// + Task> AddUserNoteToArtifact(Guid artifactId, Guid userId, RNoteType type, string noteContents, Guid? referenceNoteId); + + /// + /// Add Note to Artifact Item + /// + /// + /// + /// + /// + /// + /// + Task> AddUserNoteToArtifactItem(Guid itemId, Guid userId, RNoteType type, string noteContents, Guid? referenceNoteId); + + /// + /// Edit Note + /// + /// + /// sending null here means user is a moderator + /// + /// + Task> EditUserNote(Guid noteId, Guid? userId, string noteContents); + + + /// + /// delete note + /// + /// + /// sending null here means user is a moderator or the note is being deleted in a recursive delete of referenced notes + /// list of notes deleted + Task> DeleteUserNote(Guid noteId, Guid? userId); + + /// + /// get artifact private user notes + /// + /// + /// + /// + Task> GetArtifactUserNotes(Guid artifactId, Guid userId); + + /// + /// get artifact public user notes + /// + /// + /// + Task> GetArtifactPublicNotes(Guid artifactId); + + /// + /// get item artifact private user notes + /// + /// + /// + /// + Task> GetArtifactItemUserNotes(Guid itemId, Guid userId); + + /// + /// get item artifact item public user notes + /// + /// + /// + Task> GetArtifactItemPublicNotes(Guid itemId); + + /// + /// Get All USer Notes + /// + /// + /// + /// + /// + /// + Task> GetUserNotes(Guid userId, PagingParameterModel paging, RNoteType type, PublishStatus[] statusArray); + + /// + /// Get All Public Notes + /// + /// + /// + Task> GetAllPublicNotes(PagingParameterModel paging); + + /// + /// suggest ganjoor link + /// + /// + /// + /// + Task> SuggestGanjoorLink(Guid userId, LinkSuggestion link); + + /// + /// get suggested ganjoor links + /// + /// + /// + /// + Task> GetSuggestedLinks(ReviewResult status, bool notSynced); + + /// + /// Review Suggested Link + /// + /// + /// + /// + /// + Task> ReviewSuggestedLink(Guid linkId, Guid userId, ReviewResult result); + + /// + /// Temporary api + /// + /// + Task> AddTOCForSuggestedLinks(); + + /// + /// Synchronize suggested link + /// + /// + /// + Task> SynchronizeSuggestedLink(Guid linkId); + + /// + /// get suggested pinterest links + /// + /// + /// + /// + Task> GetSuggestedPinterestLinks(ReviewResult status, bool notSynced); + + /// + /// suggest pinterest link + /// + /// + /// + Task> SuggestPinterestLink(PinterestSuggestion suggestion); + + /// + /// Review Suggested Pinterest Link + /// + /// + /// + /// + /// + /// + /// + /// + Task> ReviewSuggestedPinterestLink(Guid linkId, Guid userId, string altText, ReviewResult result, string reviewDesc, string imageUrl); + + /// + /// Synchronize suggested pinterest link + /// + /// + /// + Task> SynchronizeSuggestedPinterestLink(Guid linkId); + + + /// + /// Add Notification + /// + /// + /// + /// + /// + Task> PushNotification(Guid userId, string subject, string htmlText); + + /// + /// Get User Notifications + /// + /// + /// + Task> GetUserNotifications(Guid userId); + + /// + /// Get Unread User Notifications Count + /// + /// + /// + Task> GetUnreadUserNotificationsCount(Guid userId); + + /// + /// Switch Notification Status + /// + /// + /// updated notification object + Task> SwitchNotificationStatus(Guid notificationId); + + /// + /// Delete Notification + /// + /// + /// + Task> DeleteNotification(Guid notificationId); + + /// + /// an incomplete prototype for removing artifacts + /// + /// + /// + /// + Task> RemoveArtifactHavingNoNoteAndBookmarks(Guid artifactId, bool checkJobs); + + } +} diff --git a/RMuseum/Services/IBackgroundTaskQueue.cs b/RMuseum/Services/IBackgroundTaskQueue.cs new file mode 100644 index 00000000..65b76bab --- /dev/null +++ b/RMuseum/Services/IBackgroundTaskQueue.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace RMuseum.Services +{ + public interface IBackgroundTaskQueue + { + void QueueBackgroundWorkItem(Func workItem); + + Task> DequeueAsync( + CancellationToken cancellationToken); + } +} diff --git a/RMuseum/Services/IPictureFileService.cs b/RMuseum/Services/IPictureFileService.cs new file mode 100644 index 00000000..ece63058 --- /dev/null +++ b/RMuseum/Services/IPictureFileService.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.Drawing.Imaging; +using System.IO; +using System.Threading.Tasks; + +namespace RMuseum.Services +{ + /// + /// manipulating picture files + /// + public interface IPictureFileService + { + /// + /// add new picture + /// + /// + /// + /// + /// + /// + /// + /// + /// pass empty if you want a generic date based folder + /// + Task> Add + ( + string title, string description, int order, IFormFile file, string srcurl, Stream stream, string originalFileNameForStreams, string imageFolderName + ); + + /// + /// recover from files + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task> RecoverFromeFiles + ( + string title, string description, int order, string srcurl, string orignalFilePath, string normalFilePath, string thumbFilePath, string originalFileNameForStreams, string imageFolderName + ); + + /// + /// returns image info + /// + /// + /// + Task> GetImage(Guid id); + + /// + /// returns image file stream + /// + /// + /// + /// + RServiceResult GetImagePath(RPictureFile image, string sz = "orig"); + + + /// + /// Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + /// + /// + /// + /// + Task> RotateImage(Guid id, int degIn90mul); + + /// + /// Get Encoder + /// + /// + /// + ImageCodecInfo GetEncoder(ImageFormat format); + + /// + /// Generate Cropped Image Based On ThumbnailCoordinates For Notes + /// + /// + /// + /// + /// + /// + /// + Task> GenerateCroppedImageBasedOnThumbnailCoordinates(Guid id, decimal left, decimal top, decimal width, decimal height); + + + /// + /// Image Storage Path + /// + string ImageStoragePath { get; } + } +} diff --git a/RMuseum/Services/Implementation/ArtifactService.cs b/RMuseum/Services/Implementation/ArtifactService.cs new file mode 100644 index 00000000..7f979c0b --- /dev/null +++ b/RMuseum/Services/Implementation/ArtifactService.cs @@ -0,0 +1,7942 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using RMuseum.DbContext; +using RMuseum.Models.Artifact; +using RMuseum.Models.Artifact.ViewModels; +using RMuseum.Models.Bookmark; +using RMuseum.Models.Bookmark.ViewModels; +using RMuseum.Models.GanjoorIntegration; +using RMuseum.Models.GanjoorIntegration.ViewModels; +using RMuseum.Models.ImportJob; +using RMuseum.Models.Note; +using RMuseum.Models.Note.ViewModels; +using RMuseum.Models.Notification; +using RSecurityBackend.Models.Auth.ViewModels; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Services; +using RSecurityBackend.Services.Implementation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Drawing; +using System.Drawing.Imaging; + +namespace RMuseum.Services.Implementation +{ + /// + /// IArtifactService implementation + /// + public class ArtifactService : IArtifactService + { + + /// + /// get all artifacts (including CoverImage info but not items or tagibutes info) + /// + /// + /// + /// + public async Task> GetAll(PagingParameterModel paging, PublishStatus[] statusArray) + { + try + { + var source = + _context.Artifacts + .Include(a => a.CoverImage) + .Where(a => statusArray.Contains(a.Status)) + .OrderByDescending(t => t.DateTime) + .AsQueryable(); + + (PaginationMetadata PagingMeta, RArtifactMasterRecord[] Items) paginatedResult = + await QueryablePaginator.Paginate(source, paging); + + + + return new RServiceResult<(PaginationMetadata PagingMeta, RArtifactMasterRecord[] Items)>(paginatedResult); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, RArtifactMasterRecord[] Items)>((PagingMeta: null, Items: null), exp.ToString()); + } + } + + /// + /// get tagged publish artifacts (including CoverImage info but not items or tagibutes info) + /// + /// + /// + /// + /// + public async Task> GetByTagValue(string tagUrl, string valueUrl, PublishStatus[] statusArray) + { + try + { + RTag tag = + await _context.Tags + .Where(a => a.FriendlyUrl == tagUrl) + .SingleOrDefaultAsync(); + if(tag == null) + return new RServiceResult(new RArtifactMasterRecord[] { }); + + + RArtifactMasterRecord[] taggedItems = + await _context.Artifacts.Include(a => a.Tags) + .Include(a => a.CoverImage) + .Where(a => statusArray.Contains(a.Status) && a.Tags != null && a.Tags.Any(v => v.RTagId == tag.Id && v.FriendlyUrl == valueUrl)) + .OrderByDescending(t => t.DateTime) + .AsNoTracking() + .ToArrayAsync(); + + foreach (RArtifactMasterRecord taggedItem in taggedItems) + taggedItem.Tags = null; + + return new RServiceResult(taggedItems); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// gets specified artifact info (including CoverImage + images + tagibutes) + /// + /// + /// + /// + public async Task> GetByFriendlyUrl(string friendlyUrl, PublishStatus[] statusArray) + { + try + { + RArtifactMasterRecord item = + await _context.Artifacts + .Include(a => a.CoverImage) + .Include(a => a.Items).ThenInclude(i => i.Images) + .Include(a => a.Items).ThenInclude(i => i.Tags).ThenInclude(t => t.RTag) + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => statusArray.Contains(a.Status) && a.FriendlyUrl == friendlyUrl) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if(item != null) + { + + return new RServiceResult(new RArtifactMasterRecordViewModel(item)); + } + + + return new RServiceResult(null); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// gets specified artifact info (including CoverImage + images + tagibutes) + /// + /// + /// + /// + /// + /// + public async Task> GetByFriendlyUrlFilterItemsByTag(string friendlyUrl, PublishStatus[] statusArray, string tagFriendlyUrl, string tagValueFriendlyUrl) + { + try + { + RTag rTag = await _context.Tags.Where(t => t.FriendlyUrl == tagFriendlyUrl).SingleOrDefaultAsync(); + + if (rTag == null) + return new RServiceResult(null, "Tag not found!"); + + RArtifactMasterRecord artifact = + await _context.Artifacts + .Include(a => a.CoverImage) + .Include(a => a.Items).ThenInclude(i => i.Images) + .Include(a => a.Items).ThenInclude(i => i.Tags) + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => statusArray.Contains(a.Status) && a.FriendlyUrl == friendlyUrl) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (artifact != null) + { + List filteredItems = new List(); + + foreach(RArtifactItemRecord item in artifact.Items) + { + if(item.Tags.Any(v => v.RTagId == rTag.Id && (string.IsNullOrEmpty(tagValueFriendlyUrl) || v.FriendlyUrl == tagValueFriendlyUrl) )) + { + item.Tags = null; + filteredItems.Add(item); + } + } + + artifact.Items = filteredItems; + + return new RServiceResult(new RArtifactMasterRecordViewModel(artifact)); + } + + + return new RServiceResult(null); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// edit master record + /// + /// + /// + /// + /// + public async Task> EditMasterRecord(RArtifactMasterRecord edited, bool canChangeStatusToAwaiting, bool canPublish) + { + try + { + + if(string.IsNullOrEmpty(edited.Name)) + { + return new RServiceResult(null, "Name could not be empty."); + } + + RArtifactMasterRecord item = + await _context.Artifacts + .Where(a => a.Id == edited.Id) + .SingleOrDefaultAsync(); + + + if (item != null) + { + if(item.Status != edited.Status) + { + if(!canChangeStatusToAwaiting) + { + return new RServiceResult(null, "User should be able to change status to Awaiting to complete this operation."); + } + + if( + ! + ( + (item.Status == PublishStatus.Draft && edited.Status == PublishStatus.Awaiting) + || + (item.Status == PublishStatus.Awaiting && edited.Status == PublishStatus.Draft) + ) + ) + { + if(!canPublish) + { + return new RServiceResult(null, "User should have Publish permission to complete this operation."); + } + } + } + + item.FriendlyUrl = edited.FriendlyUrl; + item.Status = edited.Status; + item.Name = edited.Name; + item.NameInEnglish = edited.Name; + item.Description = edited.Description; + item.DescriptionInEnglish = edited.DescriptionInEnglish; + item.LastModified = DateTime.Now; + + _context.Update(item); + await _context.SaveChangesAsync(); + } + + + return new RServiceResult(item); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Set Artifact Cover Item Index + /// + /// + /// + /// + public async Task> SetArtifactCoverItemIndex(Guid artifactId, int itemIndex) + { + try + { + RArtifactMasterRecord artifact = await _context + .Artifacts.Where(a => a.Id == artifactId) + .Include(a => a.Items).ThenInclude(i => i.Images) + .SingleOrDefaultAsync(); + if(artifact == null) + return new RServiceResult(false, "Artifact not found."); + + if (itemIndex == artifact.CoverItemIndex) + return new RServiceResult(true); + + if(itemIndex < 0 || itemIndex >= artifact.Items.Count()) + return new RServiceResult(false, "Item not found."); + + artifact.CoverItemIndex = itemIndex; + artifact.CoverImage = RPictureFile.Duplicate(artifact.Items.ToArray()[itemIndex].Images.First()); + + artifact.LastModified = DateTime.Now; + + _context.Artifacts.Update(artifact); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// get tag bundle by frindly url + /// + /// + /// + public async Task> GetTagBundleByFiendlyUrl(string friendlyUrl) + { + try + { + RTag tag = + await _context.Tags + .Where(a => a.FriendlyUrl == friendlyUrl) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (tag != null) + { + RTagBundleViewModel viewModel = new RTagBundleViewModel() + { + Id = tag.Id, + Name = tag.Name, + PluralName = tag.PluralName, + FriendlyUrl = friendlyUrl + }; + List + values = new List( + await _context.TagValues + .Where(value => value.RTagId == tag.Id && !string.IsNullOrEmpty(value.FriendlyUrl)) + .GroupBy(value => new { value.Value, value.FriendlyUrl, value.RTagId }) + .Select + ( + g => + new RTagBundleValueViewModel() + { + FriendlyUrl = g.Key.FriendlyUrl, + Name = g.Key.Value, + Count = g.Count(), + ImageId = _context.Artifacts.Include(a => a.Tags).Where(a => a.Status == PublishStatus.Published && a.Tags.Any(t => t.RTagId == g.Key.RTagId && t.Value == g.Key.Value)).FirstOrDefault().CoverImageId + } + ) + .OrderByDescending(g => g.Count) + .ToArrayAsync() + ); + + + viewModel.Values = values.ToArray(); + + return new RServiceResult(viewModel); + } + + + return new RServiceResult(null); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get max lastmodified artifact date for caching purposes + /// + /// + public async Task> GetMaxArtifactLastModified() + { + try + { + return new RServiceResult(await _context.Artifacts.MaxAsync(a => a.LastModified)); + } + catch(Exception exp) + { + return new RServiceResult(DateTime.Now, exp.ToString()); + } + } + + /// + /// get all tags + /// + /// + /// + public async Task> GetAllTags(PagingParameterModel paging) + { + try + { + var source = + _context.Tags + .OrderByDescending(t => t.Order) + .AsQueryable(); + + (PaginationMetadata PagingMeta, RTag[] Items) paginatedResult = + await QueryablePaginator.Paginate(source, paging); + + + + return new RServiceResult<(PaginationMetadata PagingMeta, RTag[] Items)>(paginatedResult); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, RTag[] Items)>((PagingMeta: null, Items: null), exp.ToString()); + } + } + + /// + /// get tag bu friendly url + /// + /// + /// + public async Task> GetTagByFriendlyUrl(string friendlyUrl) + { + try + { + return new RServiceResult(await _context.Tags.Where(t => t.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get tag value by frindly url + /// + /// + /// + /// + public async Task> GetTagValueByFriendlyUrl(Guid tagId, string friendlyUrl) + { + try + { + RTagValue value = await _context.TagValues.Where(t => t.RTagId == tagId && t.FriendlyUrl == friendlyUrl).FirstOrDefaultAsync(); + if (value != null) + return new RServiceResult(value); + return new RServiceResult(null, "Tag value not found"); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// add tag + /// + /// + public async Task> AddTag(string tagName) + { + try + { + + if (string.IsNullOrEmpty(tagName)) + { + return new RServiceResult(null, "Name could not be empty."); + } + + RTag existingTag = + await _context.Tags + .Where(a => a.Name == tagName || a.NameInEnglish == tagName) + .SingleOrDefaultAsync(); + if(existingTag != null) + { + return new RServiceResult(null, "Duplicated Name or English Name for tag."); + } + + int maxOrder = await _context.Tags.CountAsync() == 0 ? 0 : await _context.Tags.MaxAsync(a => a.Order); + RTag type = new RTag() + { + Name = tagName, + NameInEnglish = tagName, + PluralName = $"{tagName}s", + PluralNameInEnglish = $"{tagName}s", + Order = maxOrder + 1, + Status = PublishStatus.Published, + GlobalValue = true + }; + await _context.Tags.AddAsync(type); + await _context.SaveChangesAsync(); + + + return new RServiceResult(type); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// edit tag + /// + /// + /// + public async Task> EditTag(RTag edited) + { + try + { + if (string.IsNullOrEmpty(edited.Name)) + { + return new RServiceResult(null, "Name could not be empty."); + } + + RTag tag = + await _context.Tags + .Where(a => a.Id == edited.Id) + .SingleOrDefaultAsync(); + + + if (tag != null) + { + tag.Name = edited.Name; + tag.Order = edited.Order; + tag.FriendlyUrl = edited.FriendlyUrl; + tag.TagType = edited.TagType; + tag.Status = edited.Status; + tag.NameInEnglish = edited.NameInEnglish; + tag.GlobalValue = edited.GlobalValue; + tag.PluralName = edited.PluralName; + tag.PluralNameInEnglish = edited.PluralNameInEnglish; + _context.Update(tag); + + + + RArtifactMasterRecord[] taggedItems = + await _context.Artifacts.Include(a => a.Tags) + .Where(a => a.Tags != null && a.Tags.Any(v => v.RTagId == tag.Id)) + .ToArrayAsync(); + + foreach(RArtifactMasterRecord taggedItem in taggedItems) + { + taggedItem.LastModified = DateTime.Now; + _context.Update(taggedItem); + } + + RArtifactItemRecord[] taggedPages = + await _context.Items.Include(a => a.Tags) + .Where(a => a.Tags != null && a.Tags.Any(v => v.RTagId == tag.Id)) + .ToArrayAsync(); + + foreach (RArtifactItemRecord taggedItem in taggedPages) + { + taggedItem.LastModified = DateTime.Now; + _context.Update(taggedItem); + } + + await UpdateTaggedItems(tag); + + await _context.SaveChangesAsync(); + } + + + return new RServiceResult(tag); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// changes LastModified field value for tagged artifacts and items + /// + /// + /// + private async Task UpdateTaggedItems(RTag tag) + { + RArtifactMasterRecord[] taggedItems = + await _context.Artifacts.Include(a => a.Tags) + .Where(a => a.Tags != null && a.Tags.Any(v => v.RTagId == tag.Id)) + .ToArrayAsync(); + + foreach (RArtifactMasterRecord taggedItem in taggedItems) + { + taggedItem.LastModified = DateTime.Now; + _context.Update(taggedItem); + } + + RArtifactItemRecord[] taggedPages = + await _context.Items.Include(a => a.Tags) + .Where(a => a.Tags != null && a.Tags.Any(v => v.RTagId == tag.Id)) + .ToArrayAsync(); + + foreach (RArtifactItemRecord taggedItem in taggedPages) + { + taggedItem.LastModified = DateTime.Now; + _context.Update(taggedItem); + } + } + + /// + /// changes order of tags based on their position in artifacts + /// + /// + /// + /// true => up, false => down + /// the other tag which its Order has been changed + public async Task> EditTagOrderBasedOnArtifact(Guid tagId, Guid artifactId, bool up) + { + try + { + RArtifactMasterRecord item = + await _context.Artifacts + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => a.Id == artifactId) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (item == null) + { + return new RServiceResult(null, "artifact not found"); + } + + RArtifactMasterRecordViewModel viewModel = new RArtifactMasterRecordViewModel(item); //tags are sorted in this method + + int tagOrder = viewModel.ArtifactTags.Where(tag => tag.Id == tagId).FirstOrDefault().Order; + RArtifactTagViewModel otherTagViewModel; + if(up) + { + otherTagViewModel = viewModel.ArtifactTags.Where(tag => tag.Order < tagOrder).OrderByDescending(tag => tag.Order).FirstOrDefault(); + } + else + { + otherTagViewModel = viewModel.ArtifactTags.Where(tag => tag.Order > tagOrder).OrderBy(tag => tag.Order).FirstOrDefault(); + } + + if(otherTagViewModel == null) + { + return new RServiceResult(null, "Invalid movement"); + } + + RTag rTag1 = await _context.Tags.Where(tag => tag.Id == tagId).FirstOrDefaultAsync(); + RTag rTag2 = await _context.Tags.Where(tag => tag.Id == otherTagViewModel.Id ).FirstOrDefaultAsync(); + + rTag1.Order = otherTagViewModel.Order; + rTag2.Order = tagOrder; + + + _context.Tags.UpdateRange(new RTag[] { rTag1, rTag2 }); + + + await UpdateTaggedItems(rTag1); + await UpdateTaggedItems(rTag2); + + await _context.SaveChangesAsync(); + + return new RServiceResult((Guid?)rTag2.Id); + + + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// changes order of tags based on their position in artifact items + /// + /// + /// + /// true => up, false => down + /// the other tag which its Order has been changed + public async Task> EditTagOrderBasedOnItem(Guid tagId, Guid itemId, bool up) + { + try + { + RArtifactItemRecord item = + await _context.Items + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => a.Id == itemId) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (item == null) + { + return new RServiceResult(null, "item not found"); + } + + RArtifactItemRecordViewModel viewModel = new RArtifactItemRecordViewModel(); //tags are sorted in this method + viewModel.Item = item; + + int tagOrder = viewModel.FormattedTags.Where(tag => tag.Id == tagId).FirstOrDefault().Order; + RArtifactTagViewModel otherTagViewModel; + if (up) + { + otherTagViewModel = viewModel.FormattedTags.Where(tag => tag.Order < tagOrder).OrderByDescending(tag => tag.Order).FirstOrDefault(); + } + else + { + otherTagViewModel = viewModel.FormattedTags.Where(tag => tag.Order > tagOrder).OrderBy(tag => tag.Order).FirstOrDefault(); + } + + if (otherTagViewModel == null) + { + return new RServiceResult(null, "Invalid movement"); + } + + RTag rTag1 = await _context.Tags.Where(tag => tag.Id == tagId).FirstOrDefaultAsync(); + RTag rTag2 = await _context.Tags.Where(tag => tag.Id == otherTagViewModel.Id).FirstOrDefaultAsync(); + + rTag1.Order = otherTagViewModel.Order; + rTag2.Order = tagOrder; + + + _context.Tags.UpdateRange(new RTag[] { rTag1, rTag2 }); + + + await UpdateTaggedItems(rTag1); + await UpdateTaggedItems(rTag2); + + await _context.SaveChangesAsync(); + + return new RServiceResult((Guid?)rTag2.Id); + + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// get tag value bundle by frindly url + /// + /// + /// + /// + public async Task> GetTagValueBundleByFiendlyUrl(string friendlyUrl, string valueUrl) + { + try + { + RTag tag = + await _context.Tags + .Where(a => a.FriendlyUrl == friendlyUrl) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (tag != null) + { + RArtifactTagViewModel viewModel = new RArtifactTagViewModel(tag); + viewModel.Values = + new RTagValue[] + { + await _context.TagValues + .Where(value => value.RTagId == tag.Id && value.FriendlyUrl == valueUrl) + .FirstOrDefaultAsync() + }; + + + return new RServiceResult(viewModel); + } + + + return new RServiceResult(null); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// add artifact tag value + /// + /// + /// + /// + public async Task> TagArtifact(Guid artifactId, RTag rTag) + { + try + { + RTag type = await _context.Tags.Where(a => a.Id == rTag.Id).SingleOrDefaultAsync(); + + RArtifactMasterRecord item = await _context.Artifacts.Include(i => i.Tags).Where(i => i.Id == artifactId).SingleOrDefaultAsync(); + + int order = item.Tags.Where(t => t.RTagId == type.Id).Count() == 0 ? 1 : item.Tags.Where(t => t.RTagId == type.Id).OrderByDescending(t => t.Order).FirstOrDefault().Order + 1; + + RTagValue tag = + new RTagValue() + { + Order = order, + Value = "", + ValueInEnglish = "", + ValueSupplement = "", + RTag = type, + Status = PublishStatus.Published + }; + + item.Tags.Add(tag); + item.LastModified = DateTime.Now; + _context.Artifacts.Update(item); + await _context.SaveChangesAsync(); + + return new RServiceResult(tag); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// remove artfiact tag value + /// + /// + /// + /// + public async Task> UnTagArtifact(Guid artifactId, Guid tagValueId) + { + try + { + + RArtifactMasterRecord item = await _context.Artifacts.Include(i => i.Tags).Where(i => i.Id == artifactId).SingleOrDefaultAsync(); + item.Tags.Remove(item.Tags.Where(t => t.Id == tagValueId).SingleOrDefault()); + item.LastModified = DateTime.Now; + _context.Artifacts.Update(item); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// edit artifact tag value + /// + /// + /// + /// apply on all same value tags + /// + public async Task> EditTagValue(Guid artifactId, RTagValue edited, bool global) + { + try + { + if (string.IsNullOrEmpty(edited.Value)) + { + return new RServiceResult(null, "Value could not be empty."); + } + + RArtifactMasterRecord artifact = + await _context.Artifacts + .Include(a => a.Tags) + .Where(a => a.Id == artifactId) + .SingleOrDefaultAsync(); + if(artifact == null) + return new RServiceResult(null); + + RTagValue tag = + artifact.Tags.Where(a => a.Id == edited.Id) + .SingleOrDefault(); + + + if (tag != null) + { + tag.Order = edited.Order; + tag.ValueSupplement = edited.ValueSupplement; + _context.Update(tag); + + if (global) + { + RTagValue[] sameValueTags = await _context.TagValues.Where(v => v.Value == tag.Value && v.RTagId == tag.RTagId).ToArrayAsync(); + foreach(RTagValue sameValueTag in sameValueTags) + { + sameValueTag.Value = edited.Value; + sameValueTag.ValueInEnglish = edited.ValueInEnglish; + sameValueTag.Status = edited.Status; + sameValueTag.FriendlyUrl = edited.FriendlyUrl; + _context.Update(sameValueTag); + + RArtifactMasterRecord correspondingArtifact = + await _context.Artifacts.Include(a => a.Tags).Where(a => a.Tags.Contains(sameValueTag)).SingleOrDefaultAsync(); + if(correspondingArtifact != null) + { + correspondingArtifact.LastModified = DateTime.Now; + _context.Update(correspondingArtifact); + } + + RArtifactItemRecord correspondingItem = + await _context.Items.Include(a => a.Tags).Where(a => a.Tags.Contains(sameValueTag)).SingleOrDefaultAsync(); + if(correspondingItem != null) + { + correspondingItem.LastModified = DateTime.Now; + _context.Update(correspondingItem); + } + + } + await _context.SaveChangesAsync(); + } + else + { + tag.Value = edited.Value; + tag.ValueInEnglish = edited.ValueInEnglish; + tag.Order = edited.Order; + tag.FriendlyUrl = edited.FriendlyUrl; + tag.Status = edited.Status; + tag.ValueSupplement = edited.ValueSupplement; + _context.Update(tag); + artifact.LastModified = DateTime.Now; + _context.Update(artifact); + await _context.SaveChangesAsync(); + } + } + + + return new RServiceResult(tag); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// add item tag value + /// + /// + /// + /// + public async Task> TagItem(Guid itemId, RTag rTag) + { + try + { + RTag type = await _context.Tags.Where(a => a.Id == rTag.Id).SingleOrDefaultAsync(); + + RArtifactItemRecord item = await _context.Items.Include(i => i.Tags).Where(i=> i.Id == itemId).SingleOrDefaultAsync(); + + int order = item.Tags.Where(t => t.RTagId == type.Id).Count() == 0 ? 1 : item.Tags.Where(t => t.RTagId == type.Id).OrderByDescending(t => t.Order).FirstOrDefault().Order + 1; + + RTagValue tag = + new RTagValue() + { + Order = order, + Value = "", + ValueInEnglish = "", + ValueSupplement = "", + RTag = type, + Status = PublishStatus.Published + }; + + item.Tags.Add(tag); + item.LastModified = DateTime.Now; + _context.Items.Update(item); + await _context.SaveChangesAsync(); + + return new RServiceResult(tag); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// remove item tag value + /// + /// + /// + /// + public async Task> UnTagItem(Guid itemId, Guid tagValueId) + { + try + { + + RArtifactItemRecord item = await _context.Items.Include(i => i.Tags).Where(i => i.Id == itemId).SingleOrDefaultAsync(); + item.Tags.Remove(item.Tags.Where(t => t.Id == tagValueId).SingleOrDefault()); + item.LastModified = DateTime.Now; + _context.Items.Update(item); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// edit item tagibute value + /// + /// + /// + /// apply on all same value tags + /// + public async Task> EditItemTagValue(Guid itemtId, RTagValue edited, bool global) + { + try + { + if (string.IsNullOrEmpty(edited.Value)) + { + return new RServiceResult(null, "Value could not be empty."); + } + + RArtifactItemRecord item = + await _context.Items + .Include(a => a.Tags) + .Where(a => a.Id == itemtId) + .SingleOrDefaultAsync(); + if (item == null) + return new RServiceResult(null); + + RTagValue tag = + item.Tags.Where(a => a.Id == edited.Id) + .SingleOrDefault(); + + + if (tag != null) + { + tag.Order = edited.Order; + tag.ValueSupplement = edited.ValueSupplement; + _context.Update(tag); + + if (global) + { + RTagValue[] sameValueTags = await _context.TagValues.Where(v => v.Value == tag.Value && v.RTagId == tag.RTagId).ToArrayAsync(); + foreach (RTagValue sameValueTag in sameValueTags) + { + sameValueTag.Value = edited.Value; + sameValueTag.ValueInEnglish = edited.ValueInEnglish; + sameValueTag.Status = edited.Status; + sameValueTag.FriendlyUrl = edited.FriendlyUrl; + + _context.Update(sameValueTag); + + RArtifactMasterRecord correspondingArtifact = + await _context.Artifacts.Include(a => a.Tags).Where(a => a.Tags.Contains(sameValueTag)).SingleOrDefaultAsync(); + if (correspondingArtifact != null) + { + correspondingArtifact.LastModified = DateTime.Now; + _context.Update(correspondingArtifact); + } + + RArtifactItemRecord correspondingItem = + await _context.Items.Include(a => a.Tags).Where(a => a.Tags.Contains(sameValueTag)).SingleOrDefaultAsync(); + if (correspondingItem != null) + { + correspondingItem.LastModified = DateTime.Now; + _context.Update(correspondingItem); + } + + } + await _context.SaveChangesAsync(); + } + else + { + tag.Value = edited.Value; + tag.ValueInEnglish = edited.ValueInEnglish; + tag.Order = edited.Order; + tag.FriendlyUrl = edited.FriendlyUrl; + tag.Status = edited.Status; + tag.ValueSupplement = edited.ValueSupplement; + _context.Update(tag); + item.LastModified = DateTime.Now; + _context.Update(item); + await _context.SaveChangesAsync(); + } + } + + + return new RServiceResult(tag); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// changes order of tag values based on their position in an artifact + /// + /// + /// + /// + /// true => up, false => down + /// the other tag value which its Order has been changed + public async Task> EditTagValueOrder(Guid artifactId, Guid tagId, Guid valueId, bool up) + { + try + { + RArtifactMasterRecord item = + await _context.Artifacts + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => a.Id == artifactId) + .SingleOrDefaultAsync(); + + if (item == null) + { + return new RServiceResult(null, "artifact not found"); + } + + RArtifactMasterRecordViewModel viewModel = new RArtifactMasterRecordViewModel(item); //tags are sorted in this method + + RArtifactTagViewModel tag = viewModel.ArtifactTags.Where(t => t.Id == tagId).FirstOrDefault(); + RTagValue value1 = tag.Values.Where(v => v.Id == valueId).FirstOrDefault(); + RTagValue value2; + if (up) + { + value2 = tag.Values.Where(v => v.Order < value1.Order).OrderByDescending(v => v.Order).FirstOrDefault(); + } + else + { + value2 = tag.Values.Where(v => v.Order > value1.Order).OrderBy(v => v.Order).FirstOrDefault(); + } + + if (value2 == null) + { + return new RServiceResult(null, "Invalid movement"); + } + + + int nOrder = value1.Order; + value1.Order = value2.Order; + value2.Order = nOrder; + + _context.TagValues.UpdateRange(new RTagValue[] { value1, value2 }); + + item.LastModified = DateTime.Now; + _context.Artifacts.Update(item); + + await _context.SaveChangesAsync(); + + return new RServiceResult((Guid?)value2.Id); + + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// changes order of tag values based on their position in an item + /// + /// + /// + /// + /// true => up, false => down + /// the other tag value which its Order has been changed + public async Task> EditItemTagValueOrder(Guid itemId, Guid tagId, Guid valueId, bool up) + { + try + { + RArtifactItemRecord item = + await _context.Items + .Include(a => a.Tags).ThenInclude(t => t.RTag) + .Where(a => a.Id == itemId) + .SingleOrDefaultAsync(); + + if (item == null) + { + return new RServiceResult(null, "item not found"); + } + + RArtifactItemRecordViewModel viewModel = new RArtifactItemRecordViewModel(); //tags are sorted in this method + viewModel.Item = item; + + RArtifactTagViewModel tag = viewModel.FormattedTags.Where(t => t.Id == tagId).FirstOrDefault(); + RTagValue value1 = tag.Values.Where(v => v.Id == valueId).FirstOrDefault(); + RTagValue value2; + if (up) + { + value2 = tag.Values.Where(v => v.Order < value1.Order).OrderByDescending(v => v.Order).FirstOrDefault(); + } + else + { + value2 = tag.Values.Where(v => v.Order > value1.Order).OrderBy(v => v.Order).FirstOrDefault(); + } + + if (value2 == null) + { + return new RServiceResult(null, "Invalid movement"); + } + + + int nOrder = value1.Order; + value1.Order = value2.Order; + value2.Order = nOrder; + + _context.TagValues.UpdateRange(new RTagValue[] { value1, value2 }); + + item.LastModified = DateTime.Now; + _context.Items.Update(item); + + await _context.SaveChangesAsync(); + + return new RServiceResult((Guid?)value2.Id); + + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// gets specified artifact item info (including images + tagibutes) + /// + /// + /// + /// + /// + public async Task> GetArtifactItemByFrienlyUrl(string artifactUrl, string itemUrl, PublishStatus[] statusArray) + { + try + { + RArtifactMasterRecord parent = + await _context.Artifacts + .Include(a => a.CoverImage) + .Include(a => a.Items).ThenInclude(i => i.Images) + .Include(a => a.Items).ThenInclude(i => i.Tags).ThenInclude(t => t.RTag) + .Where(a => statusArray.Contains(a.Status) && a.FriendlyUrl == artifactUrl) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (parent == null) + return new RServiceResult(null); + + if(parent.Items != null) + parent.Items = parent.Items.OrderBy(i => i.Order).ToArray(); + if(parent.Tags != null) + parent.Tags = parent.Tags.OrderBy(a => a.RTag.Order).ToArray(); + + + + RArtifactItemRecord item = + parent.Items + .Where(i => i.FriendlyUrl == itemUrl) + .SingleOrDefault(); + + if (item == null) + return new RServiceResult(null); + + if (item.Images != null) + item.Images = item.Images.OrderBy(i => i.Order).ToArray(); + if (item.Tags != null) + item.Tags = item.Tags.OrderBy(a => a.RTag.Order).ToArray(); + + RArtifactItemRecord nextItem = parent.Items.Where(i => i.Order > item.Order).OrderBy(i => i.Order).FirstOrDefault(); + RArtifactItemRecord prevItem = parent.Items.Where(i => i.Order < item.Order).OrderByDescending(i => i.Order).FirstOrDefault(); + + RArtifactItemRecordViewModel res = new RArtifactItemRecordViewModel() + { + Item = item, + ParentFriendlyUrl = parent.FriendlyUrl, + ParentName = parent.Name, + ParentImageId = parent.CoverImage.Id, + ParentItemCount = parent.Items.Count(), + NextItemFriendlyUrl = nextItem == null ? "" : nextItem.FriendlyUrl, + NextItemImageId = nextItem == null ? (Guid?)null : nextItem.Images.First().Id, + PreviousItemFriendlyUrl = prevItem == null ? "" : prevItem.FriendlyUrl, + PrevItemImageId = prevItem == null ? (Guid?)null : prevItem.Images.First().Id, + }; + + return new RServiceResult(res); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + + /// + /// add new artifact + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> Add + ( + string name, string description, string srcurl, + string pictitle, string picdescription, IFormFile file, string picsrcurl, Stream stream + ) + { + try + { + RServiceResult picture = await _pictureFileService.Add(pictitle, picdescription, 1, file, picsrcurl, stream, "", ""); + if (picture.Result == null) + { + return new RServiceResult(null, $"_pictureFileService.Add : {picture.ExceptionString}"); + } + + RArtifactItemRecord item = new RArtifactItemRecord() + { + Order = 1, + CoverImageIndex = 0, + Images = new RPictureFile[] { picture.Result } + }; + + RArtifactMasterRecord artifact = new RArtifactMasterRecord(name, description) + { + Name = name, + NameInEnglish = name, + Description = description, + DescriptionInEnglish = description, + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + Items = new RArtifactItemRecord[] { item }, + ItemCount = 1, + CoverImage = picture.Result + }; + + + await _context.Artifacts.AddAsync(artifact); + await _context.SaveChangesAsync(); + + return new RServiceResult(artifact); + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + + } + + + + /// + /// from https://www.loc.gov + /// + /// + /// + /// m084 + /// + /// + /// + /// + /// boostan1207 + /// + /// + /// + /// + /// plmp + /// + /// + public async Task> StartImportingFromTheLibraryOfCongress(string resourceNumber, string friendlyUrl, string resourcePrefix) + { + string url = $"https://www.loc.gov/resource/{resourcePrefix}.{resourceNumber}/?fo=json&st=gallery"; + + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.Loc && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if(string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.Loc, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + using(RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration) ) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + int pageCount = 0; + int representative_index = 0; + //اول یک صفحه را می‌خوانیم تا تعداد صفحات را مشخص کنیم + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + string json = await result.Content.ReadAsStringAsync(); + var parsed = JObject.Parse(json); + + pageCount = parsed.SelectToken("resource.segment_count").Value(); + representative_index = parsed.SelectToken("resource.representative_index").Value(); + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + + //here might be problems: loc json does not return correct answer when number of segments are more than 1000 + /* + if (pageCount > 1000) + { + job.Exception = $"Page count ({pageCount}) was cut to 1000 for this artifact due to loc bug."; + pageCount = 1000; + } + */ + + //حالا که تعداد صفحات را داریم دوباره می‌خوانیم + url = $"https://www.loc.gov/resource/{resourcePrefix}.{resourceNumber}/?c={pageCount}&fo=json&st=gallery"; + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + + //here is a problem, this method could be called from a background service where _context is disposed, so I need to renew it + using(RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = representative_index, + FriendlyUrl = friendlyUrl + }; + + string json = await result.Content.ReadAsStringAsync(); + + job.SrcContent = json; + + var parsed = JObject.Parse(json); + + var segmentsArray = parsed.SelectToken("segments").ToArray(); + //here might be problems: loc json does not return correct answer when number of segments are more than 1000 + //I've added some temporary solutions prior + //Here I want to log any paradox I encounter: + if(segmentsArray.Length != pageCount) + { + job.Exception = $"Page count ({pageCount}) is not equal to number of returned resources ({segmentsArray.Length})."; + } + + + + List meta = new List(); + + string string_value = await HandleSimpleValue(context, parsed, meta, "item.title", "Title"); + if (!string.IsNullOrWhiteSpace(string_value)) + { + book.Name = string_value; + book.NameInEnglish = string_value; + } + await HandleSimpleValue(context, parsed, meta, "item.date", "Date"); + string_value = await HandleListValue(context, parsed, meta, "item.other_title", "Other Title"); + if (!string.IsNullOrWhiteSpace(string_value)) + { + book.Name = string_value; + } + await HandleListValue(context, parsed, meta, "item.contributor_names", "Contributor Names"); + await HandleSimpleValue(context, parsed, meta, "item.shelf_id", "Shelf ID"); + await HandleListValue(context, parsed, meta, "item.created_published", "Created / Published"); + await HandleListValue(context, parsed, meta, "item.subject_headings", "Subject Headings"); + await HandleListValue(context, parsed, meta, "item.notes", "Notes"); + await HandleListValue(context, parsed, meta, "item.medium", "Medium"); + await HandleListValue(context, parsed, meta, "item.call_number", "Call Number/Physical Location"); + await HandleListValue(context, parsed, meta, "item.digital_id", "Digital Id"); + await HandleSimpleValue(context, parsed, meta, "item.library_of_congress_control_number", "Library of Congress Control Number"); + await HandleChildrenValue(context, parsed, meta, "item.language", "Language"); + await HandleListValue(context, parsed, meta, "item.online_format", "Online Format"); + await HandleListValue(context, parsed, meta, "item.number_oclc", "OCLC Number"); + string_value = await HandleListValue(context, parsed, meta, "item.description", "Description"); + if(!string.IsNullOrEmpty(string_value)) + { + book.Description = string_value; + book.DescriptionInEnglish = string_value; + } + await HandleSimpleValue(context, parsed, meta, "cite_this.chicago", "Chicago citation style"); + await HandleSimpleValue(context, parsed, meta, "cite_this.apa", "APA citation style"); + await HandleSimpleValue(context, parsed, meta, "cite_this.mla", "MLA citation style"); + await HandleChildrenValue(context, parsed, meta, "item.dates", "Dates"); + await HandleChildrenValue(context, parsed, meta, "item.contributors", "Contributors"); + await HandleChildrenValue(context, parsed, meta, "item.location", "Location"); + await HandleListValue(context, parsed, meta, "item.rights", "Rights & Access"); + + RTagValue tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "Library of Congress, African and Middle East Division, Near East Section Persian Manuscript Collection", 1); + tag.ValueSupplement = url; + string_value = parsed.SelectToken("item.id").Value(); + if (!string.IsNullOrWhiteSpace(string_value)) + { + tag.ValueSupplement = string_value; + } + + meta.Add(tag); + + + book.Tags = meta.ToArray(); + + + + + int order = 0; + List pages = new List(); + + //due to loc bug for books with more than 1000 pages relying on segmentsArray changed to hard coded image urls and .... + //foreach (JToken segment in segmentsArray) + for(int pageIndex = 1; pageIndex <= pageCount; pageIndex++) + { + /* + using (var fakeClient = new HttpClient()) + { + + using (var fakeRes = await fakeClient.GetAsync("https://ganjgah.ir/api/artifacts/keep-alive")) + { + if (fakeRes.IsSuccessStatusCode) + { + await fakeRes.Content.ReadAsStringAsync(); + } + } + } + */ + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order * 100 / (decimal)pageCount; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + order++; + + + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {pageIndex} of {book.NameInEnglish}",//segment.SelectToken("title").Value(), + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + tag = await TagHandler.PrepareAttribute(context, "Source", "Library of Congress, African and Middle East Division, Near East Section Persian Manuscript Collection", 1); + tag.ValueSupplement = $"http://www.loc.gov/resource/{resourcePrefix}.{resourceNumber}/?sp={pageIndex}";//segment.SelectToken("id").Value(); + page.Tags = new RTagValue[] { tag }; + + string imageUrlPart = $"{pageIndex}".PadLeft(4, '0'); + //string imageUrl = $"https://tile.loc.gov/image-services/iiif/service:amed:{resourcePrefix}:{resourceNumber}:{imageUrlPart}/full/pct:100/0/default.jpg"; + string imageUrl = $"https://tile.loc.gov/image-services/iiif/service:rbc:{resourcePrefix}:2015:{resourceNumber}:{imageUrlPart}/full/pct:100/0/default.jpg"; + /* + List list = segment.SelectToken("image_url").ToObject>(); + if (list != null && list.Count > 0) + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].IndexOf(".jpg") != -1) + { + if (imageUrl == "") + imageUrl = list[i]; + else + { + if (imageUrl.IndexOf("#h=") != -1 && imageUrl.IndexOf("&w=", imageUrl.IndexOf("#h=")) != -1) + { + int h1 = int.Parse(imageUrl.Substring(imageUrl.IndexOf("#h=") + "#h=".Length, imageUrl.IndexOf("&w=") - imageUrl.IndexOf("#h=") - "&w=".Length)); + if (list[i].IndexOf("#h=") != -1 && list[i].IndexOf("&w=", list[i].IndexOf("#h=")) != -1) + { + int h2 = int.Parse(list[i].Substring(list[i].IndexOf("#h=") + "#h=".Length, list[i].IndexOf("&w=") - list[i].IndexOf("#h=") - "&w=".Length)); + + if (h2 > h1) + { + imageUrl = list[i]; + } + } + } + else + imageUrl = list[i]; + + } + } + } + } + */ + + + + if (!string.IsNullOrEmpty(imageUrl)) + { + //imageUrl = "https:" + imageUrl.Substring(0, imageUrl.IndexOf('#')); + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if(!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); +} + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + + + + + + + } + } + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (book.CoverImage == null && pages.Count > 0) + { + book.CoverImage = RPictureFile.Duplicate(pages[0].Images.First()); + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + } + + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + } + + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// from http://pudl.princeton.edu/ + /// + /// dj52w476m + /// + /// + public async Task> StartImportingFromPrinceton(string resourceNumber, string friendlyUrl) + { + string url = $"http://pudl.princeton.edu/mdCompiler2.php?obj={resourceNumber}"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.Princeton && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.Princeton, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string xml = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = xml; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + XElement elObject = XDocument.Parse(xml).Root; + foreach(var prop in elObject.Element("dmd").Element("properties").Elements("property")) + { + if (prop.Element("label") == null) + continue; + string label = prop.Element("label").Value.Replace(":", ""); + int order = 1; + foreach (var value in prop.Elements("valueGrp").Elements("value")) + { + tag = await TagHandler.PrepareAttribute(context, label, value.Value, order); + if(value.Attribute("href") != null) + { + if(value.Attribute("href").Value.IndexOf("http://localhost") != 0) + { + tag.ValueSupplement = value.Attribute("href").Value; + } + } + meta.Add(tag); + + if(label == "Title") + { + book.Name = book.NameInEnglish = + book.Description = book.DescriptionInEnglish = + value.Value; + } + order++; + } + } + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "Princeton Digital Library of Islamic Manuscripts", 1); + tag.ValueSupplement = $"http://pudl.princeton.edu/objects/{job.ResourceNumber}"; + + meta.Add(tag); + + + book.Tags = meta.ToArray(); + List pages = new List(); + + foreach (var structure in elObject.Elements("structure")) + { + if (structure.Attribute("type") != null && structure.Attribute("type").Value == "RelatedObjects") + { + if (structure.Element("div") == null || structure.Element("div").Element("OrderedList") == null) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "structure[RelatedObjects].div.OrderedList is null"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + return; + } + } + + + int pageCount = structure.Element("div").Element("OrderedList").Elements("div").Count(); + int inlineOrder = 0; + + foreach (var div in structure.Element("div").Element("OrderedList").Elements("div")) + { + inlineOrder++; + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = inlineOrder * 100 / (decimal)pageCount; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + int order = int.Parse(div.Attribute("order").Value); + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = div.Attribute("label").Value, + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + string imageUrl = div.Attribute("img").Value; + imageUrl = "https://libimages.princeton.edu/loris/" + imageUrl.Substring(imageUrl.LastIndexOf(":") + 1); + imageUrl += $"/full/,{div.Attribute("h").Value}/0/default.jpg"; + + tag = await TagHandler.PrepareAttribute(context, "Source", "Princeton Digital Library of Islamic Manuscripts", 1); + tag.ValueSupplement = imageUrl; + page.Tags = new RTagValue[] { tag }; + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + + } + + + + } + + } + } + + foreach (var structure in elObject.Elements("structure")) + { + if (structure.Attribute("type") != null && structure.Attribute("type").Value == "Physical") + { + if(structure.Element("RTLBoundManuscript") != null) + { + foreach (var leaf in structure.Element("RTLBoundManuscript").Elements("Leaf")) + { + foreach (var side in leaf.Elements("Side")) + { + int pageOrder = int.Parse(side.Attribute("order").Value); + tag = await TagHandler.PrepareAttribute(context, "Leaf Side", side.Attribute("label").Value, 100); + RArtifactItemRecord page = pages.Where(p => p.Order == pageOrder).SingleOrDefault(); + if (page != null) + { + List tags = new List(page.Tags); + tags.Add(tag); + page.Tags = tags; + } + } + } + foreach (var folio in structure.Element("RTLBoundManuscript").Elements("Folio")) + { + foreach (var side in folio.Elements("Side")) + { + int pageOrder = int.Parse(side.Attribute("order").Value); + tag = await TagHandler.PrepareAttribute(context, "Folio Side", folio.Attribute("label").Value + ":" + side.Attribute("label").Value, 101); + RArtifactItemRecord page = pages.Where(p => p.Order == pageOrder).SingleOrDefault(); + if (page != null) + { + List tags = new List(page.Tags); + tags.Add(tag); + page.Tags = tags; + } + } + } + } + } + } + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if(pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "ages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// from https://curiosity.lib.harvard.edu + /// + /// example: https://curiosity.lib.harvard.edu/islamic-heritage-project/catalog/40-990114893240203941 + /// + /// + public async Task> StartImportingFromHarvard(string url, string friendlyUrl) + { + try + { + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.Harvard && j.ResourceNumber == url && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + return new RServiceResult(false, $"Friendly url is empty, url = {url}"); + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.Harvard, + ResourceNumber = url, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string html = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = html; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + int nStartIndex = html.IndexOf("", nStartIndex); + if (nStartIndex == -1) + break; + nStartIndex++; + string tagName = html.Substring(nStartIndex, html.IndexOf(":", nStartIndex) - nStartIndex); + nStartIndex = html.IndexOf("", nStartIndex); + if (nStartIndex == -1) + break; + nStartIndex++; + string tagValues = html.Substring(nStartIndex, html.IndexOf("", nStartIndex) - nStartIndex); + foreach(string tagValuePart in tagValues.Split("
", StringSplitOptions.RemoveEmptyEntries)) + { + string tagValue = tagValuePart; + bool href = false; + if(tagValue.IndexOf(" pages = (await _InternalHarvardJsonImport(hardvardResourceNumber, job, friendlyUrl, context, book, meta)).Result; + if(pages == null) + { + return; + } + + + book.Tags = meta.ToArray(); + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + private async Task>> _InternalHarvardJsonImport(string hardvardResourceNumber, ImportJob job, string friendlyUrl, RMuseumDbContext context, RArtifactMasterRecord book, List meta) + { + List pages = new List(); + + using (var client = new HttpClient()) + { + using (var jsonResult = await client.GetAsync($"https://iiif.lib.harvard.edu/manifests/drs:{hardvardResourceNumber}")) + { + if (jsonResult.IsSuccessStatusCode) + { + string json = await jsonResult.Content.ReadAsStringAsync(); + var parsed = JObject.Parse(json); + book.Name = book.NameInEnglish = book.Description = book.DescriptionInEnglish = + parsed.SelectToken("label").Value(); + + + RTagValue tag; + + tag = await TagHandler.PrepareAttribute(context, "Title", book.Name, 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", "تعیین نشده", 1); + meta.Add(tag); + + List labels = new List(); + foreach (JToken structure in parsed.SelectTokens("$.structures[*].label")) + { + labels.Add(structure.Value()); + } + + int order = 0; + var canvases = parsed.SelectToken("sequences").First().SelectToken("canvases").ToArray(); + int pageCount = canvases.Length; + foreach (JToken canvas in canvases) + { + order++; + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order * 100 / (decimal)pageCount; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + string label = canvas.SelectToken("label").Value(); + if (labels.Where(l => l.IndexOf(label) != -1).SingleOrDefault() != null) + label = labels.Where(l => l.IndexOf(label) != -1).SingleOrDefault(); + string imageUrl = canvas.SelectTokens("images[*]").First().SelectToken("resource").SelectToken("@id").Value(); + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = label, + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + + + tag = await TagHandler.PrepareAttribute(context, "Source", "Harvard University Islamic Heritage Project", 1); + tag.ValueSupplement = imageUrl; + page.Tags = new RTagValue[] { tag }; + + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return new RServiceResult>(null, "failed"); + } + + imageResult.Dispose(); + GC.Collect(); + } + } + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({jsonResult.StatusCode}) for https://iiif.lib.harvard.edu/manifests/drs:{hardvardResourceNumber}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return new RServiceResult>(null, "failed"); + } + } + } + + return new RServiceResult>(pages); + + + } + + /// + /// import from http://www.qajarwomen.org + /// + /// 43117279 + /// atame + /// http://www.qajarwomen.org/fa/items/1018A10.html + /// + public async Task> StartImportingFromHarvardDirectly(string hardvardResourceNumber, string friendlyUrl, string srcUrl) + { + try + { + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.HarvardDirect && j.ResourceNumber == hardvardResourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing harvard direct resource number {hardvardResourceNumber}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + return new RServiceResult(false, $"Friendly url is empty, harvard direct resource number {hardvardResourceNumber}"); + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.HarvardDirect, + ResourceNumber = hardvardResourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = srcUrl, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from harvard resource number {job.ResourceNumber}", $"extracted from harvard resource number {job.ResourceNumber}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + + + + + tag = await TagHandler.PrepareAttribute(context, "Notes", "وارد شده از سایت دنیای زنان در عصر قاجار", 1); + meta.Add(tag); + + + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "دنیای زنان در عصر قاجار", 1); + tag.ValueSupplement = $"{job.SrcUrl}"; + + meta.Add(tag); + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = $"https://iiif.lib.harvard.edu/manifests/drs:{hardvardResourceNumber}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + List pages = (await _InternalHarvardJsonImport(hardvardResourceNumber, job, friendlyUrl, context, book, meta)).Result; + if (pages == null) + { + return; + } + + + book.Tags = meta.ToArray(); + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// from https://catalog.hathitrust.org + /// + /// 006814127 + /// + /// + public async Task> StartImportingFromHathiTrust(string resourceNumber, string friendlyUrl) + { + string url = $"https://catalog.hathitrust.org/Record/{resourceNumber}.xml"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.HathiTrust && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated artifact friendly url '{friendlyUrl}'"); + } + + + + ImportJob job = new ImportJob() + { + JobType = JobType.HathiTrust, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + if ( + (await context.Artifacts.Where(a => a.FriendlyUrl == job.FriendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "aborted because of duplicated friendly url"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string xml = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = xml; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + string title = ""; + string author = ""; + string pdfResourceNumber = ""; + int tagOrder = 1; + XElement elObject = XDocument.Parse(xml).Root; + foreach (var datafield in elObject.Element("record").Elements("datafield")) + { + tagOrder++; + if (datafield.Attribute("tag") == null) + continue; + string hathiTrustTag = datafield.Attribute("tag").Value; + switch(hathiTrustTag) + { + case "245": + case "246": + foreach (var subfield in datafield.Elements("subfield")) + { + if(subfield.Attribute("code") != null) + { + if (subfield.Attribute("code").Value == "a" || subfield.Attribute("code").Value == "f") + title = (title + " " + subfield.Value).Trim(); + } + } + break; + case "100": + foreach (var subfield in datafield.Elements("subfield")) + { + if (subfield.Attribute("code") != null) + { + if (subfield.Attribute("code").Value == "a" || subfield.Attribute("code").Value == "d") + author = (author + " " + subfield.Value).Trim(); + } + } + break; + case "HOL": + foreach (var subfield in datafield.Elements("subfield")) + { + if (subfield.Attribute("code") != null) + { + if (subfield.Attribute("code").Value == "p") + pdfResourceNumber = subfield.Value; + } + } + break; + + default: + { + if(int.TryParse(hathiTrustTag, out int tmp)) + { + if(tmp >= 100 && tmp <= 900) + { + string note = ""; + foreach (var subfield in datafield.Elements("subfield")) + { + if (subfield.Attribute("code") != null) + { + note = (note + " " + subfield.Value).Trim(); + } + } + + tag = await TagHandler.PrepareAttribute(context, "Notes", note, tagOrder); + meta.Add(tag); + } + } + } + break; + } + + } + + if(string.IsNullOrEmpty(pdfResourceNumber)) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "pdfResourceNumber not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + tag = await TagHandler.PrepareAttribute(context, "Title", title, 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", author, 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "HathiTrust Digital Library", 1); + string viewerUrl = $"https://babel.hathitrust.org/cgi/pt?id={pdfResourceNumber}"; + tag.ValueSupplement = viewerUrl; + + meta.Add(tag); + + book.Name = book.NameInEnglish = book.Description = book.DescriptionInEnglish = title; + book.Tags = meta.ToArray(); + + List pages = new List(); + + string lastMD5hash = ""; + int order = 0; + while(true) + { + order++; + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + string imageUrl = $"https://babel.hathitrust.org/cgi/imgsrv/image?id={pdfResourceNumber};seq={order};size=1000;rotation=0"; + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + tag = await TagHandler.PrepareAttribute(context, "Source", "HathiTrust Digital Library", 1); + tag.ValueSupplement = viewerUrl; + page.Tags = new RTagValue[] { tag }; + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + + int _ImportRetryCount = 200; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + bool lastPage = false; + using (var md5 = MD5.Create()) + { + string md5hash = string.Join("", md5.ComputeHash(File.ReadAllBytes(Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"))).Select(x => x.ToString("X2"))); + if (md5hash == lastMD5hash) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + lastPage = true; + } + lastMD5hash = md5hash; + } + + if(!lastPage) + { + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + else + { + break; + } + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + } + } + + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "ages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// from http://www.library.upenn.edu/ + /// + /// MEDREN_9949222153503681 + /// + /// + public async Task> StartImportingFromPenLibraries(string resourceNumber, string friendlyUrl) + { + string url = $"http://dla.library.upenn.edu/dla/medren/pageturn.html?id={resourceNumber}&rotation=0&size=0"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.PennLibraries && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated artifact friendly url '{friendlyUrl}'"); + } + + + + ImportJob job = new ImportJob() + { + JobType = JobType.PennLibraries, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromMinutes(5); + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + if ( + (await context.Artifacts.Where(a => a.FriendlyUrl == job.FriendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "aborted because of duplicated friendly url"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string html = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = html; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + string title = ""; + string author = ""; + int tagOrder = 1; + + int nIdxStart = html.IndexOf("https://repo.library.upenn.edu/djatoka/resolver?"); + if(nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "https://repo.library.upenn.edu/djatoka/resolver? not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + string firstImageUrl = html.Substring(nIdxStart, html.IndexOf('"', nIdxStart) - nIdxStart).Replace("&", "&"); + + nIdxStart = html.IndexOf("recordinfolabel"); + while(nIdxStart != -1) + { + nIdxStart += "recordinfolabel\">".Length; + int nIdxEnd = html.IndexOf(":", nIdxStart); + string recordinfolabel = html.Substring(nIdxStart, nIdxEnd - nIdxStart); + nIdxStart = html.IndexOf("recordinfotext", nIdxEnd); + nIdxStart += "recordinfotext\">".Length; + nIdxEnd = html.IndexOf("", nIdxStart); + string recordinfotext = html.Substring(nIdxStart, nIdxEnd - nIdxStart).Replace("", "
").Replace("\n", "").Replace("\r", "").Trim(); + + string[] values = recordinfotext.Split("
", StringSplitOptions.RemoveEmptyEntries); + + foreach(string value in values) + { + if (value.Trim().Length == 0) + continue; + if (recordinfolabel == "Title") + { + title = value.Trim(); + tag = await TagHandler.PrepareAttribute(context, "Title", title, 1); + meta.Add(tag); + } + else + if (recordinfolabel == "Author") + { + author = value.Trim(); + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", author, 1); + meta.Add(tag); + } + else + { + tag = await TagHandler.PrepareAttribute(context, recordinfolabel, value.Trim(), tagOrder++); + meta.Add(tag); + } + } + + nIdxStart = html.IndexOf("recordinfolabel", nIdxEnd); + + } + + + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "Penn Libraries", 1); + string viewerUrl = $"http://dla.library.upenn.edu/dla/medren/detail.html?id={resourceNumber}"; + tag.ValueSupplement = viewerUrl; + + meta.Add(tag); + + book.Name = book.NameInEnglish = book.Description = book.DescriptionInEnglish = title; + book.Tags = meta.ToArray(); + + List pages = new List(); + + int order = 0; + while (true) + { + order++; + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + string imageUrl = firstImageUrl; + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + tag = await TagHandler.PrepareAttribute(context, "Source", "Penn Libraries", 1); + tag.ValueSupplement = viewerUrl; + page.Tags = new RTagValue[] { tag }; + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + + if (order > 1) + { + string pageUrl = $"http://dla.library.upenn.edu/dla/medren/pageturn.html?id={resourceNumber}&doubleside=0&rotation=0&size=0¤tpage={order}"; + var pageResult = await client.GetAsync(pageUrl); + + if (pageResult.StatusCode == HttpStatusCode.NotFound) + { + break;//finished + } + + string pageHtml = await pageResult.Content.ReadAsStringAsync(); + nIdxStart = pageHtml.IndexOf("https://repo.library.upenn.edu/djatoka/resolver?"); + if (nIdxStart == -1) + { + if (order > 1) + break; //finished + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"https://repo.library.upenn.edu/djatoka/resolver? not found on page {order}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + imageUrl = pageHtml.Substring(nIdxStart, pageHtml.IndexOf('"', nIdxStart) - nIdxStart).Replace("&", "&"); + + } + var imageResult = await client.GetAsync(imageUrl); + + if(imageResult.StatusCode == HttpStatusCode.NotFound) + { + break;//finished + } + + + int _ImportRetryCount = 200; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + } + } + + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// from http://cudl.lib.cam.ac.uk + /// + /// MS-RAS-00258 + /// + /// + public async Task> StartImportingFromCambridge(string resourceNumber, string friendlyUrl) + { + string url = $"http://cudl.lib.cam.ac.uk/view/{resourceNumber}.json"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.Cambridge && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated artifact friendly url '{friendlyUrl}'"); + } + + + + ImportJob job = new ImportJob() + { + JobType = JobType.Cambridge, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + if ( + (await context.Artifacts.Where(a => a.FriendlyUrl == job.FriendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "aborted because of duplicated friendly url"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string json = await result.Content.ReadAsStringAsync(); + var parsed = JObject.Parse(json); + book.Name = book.NameInEnglish = + parsed.SelectToken("logicalStructures[*].label").Value(); + + book.Description = book.DescriptionInEnglish = + Regex.Replace( + parsed.SelectToken("descriptiveMetadata[*].abstract.displayForm").Value(), + "<.*?>", string.Empty); + + int tagOrder = 1; + foreach (JToken descriptiveMetadata in parsed.SelectTokens("$.descriptiveMetadata[*]").Children()) + { + foreach(JToken child in descriptiveMetadata.Children()) + { + if (child.SelectToken("label") != null && child.SelectToken("display") != null) + { + if (child.SelectToken("display").Value() == "True") + { + string metaName = child.SelectToken("label").Value(); + string metaValue = ""; + if (child.SelectToken("displayForm") != null) + { + metaValue = Regex.Replace( + child.SelectToken("displayForm").Value(), + "<.*?>", string.Empty); + tag = await TagHandler.PrepareAttribute(context, metaName, metaValue, tagOrder++); + meta.Add(tag); + } + else + if (child.SelectToken("value") != null) + { + foreach (JToken value in child.SelectTokens("value").Children()) + { + if (value.SelectToken("displayForm") != null) + { + metaValue = Regex.Replace( + value.SelectToken("displayForm").Value(), + "<.*?>", string.Empty); + tag = await TagHandler.PrepareAttribute(context, metaName, metaValue, tagOrder++); + meta.Add(tag); + } + } + } + } + } + } + + } + + string imageReproPageURL = "https://image01.cudl.lib.cam.ac.uk"; + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = json; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + + + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "University of Cambridge Digital Library", 1); + string viewerUrl = $"http://cudl.lib.cam.ac.uk/view/{resourceNumber}"; + tag.ValueSupplement = viewerUrl; + + meta.Add(tag); + + book.Tags = meta.ToArray(); + + List pages = new List(); + + int order = 0; + foreach (JToken pageToken in parsed.SelectTokens("$.pages").Children()) + { + order++; + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + string imageUrl = imageReproPageURL + pageToken.SelectToken("downloadImageURL").Value(); + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + List pageMata = new List(); + tag = await TagHandler.PrepareAttribute(context, "Source", "University of Cambridge Digital Library", 1); + tag.ValueSupplement = $"{viewerUrl}/{order}"; + pageMata.Add(tag); + + if(pageToken.SelectToken("label") != null) + { + tag = await TagHandler.PrepareAttribute(context, "Label", pageToken.SelectToken("label").Value(), 1); + pageMata.Add(tag); + } + + + page.Tags = pageMata.ToArray(); + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + + var imageResult = await client.GetAsync(imageUrl); + + if (imageResult.StatusCode == HttpStatusCode.NotFound) + { + break;//finished + } + + + int _ImportRetryCount = 200; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + } + } + + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// from http://www.bl.uk + /// + /// grenville_xli_f001r + /// + /// + public async Task> StartImportingFromBritishLibrary(string resourceNumber, string friendlyUrl) + { + string url = $"http://www.bl.uk/manuscripts/Viewer.aspx?ref={resourceNumber}"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.BritishLibrary && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated artifact friendly url '{friendlyUrl}'"); + } + + + + ImportJob job = new ImportJob() + { + JobType = JobType.BritishLibrary, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromMinutes(5); + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + if ( + (await context.Artifacts.Where(a => a.FriendlyUrl == job.FriendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "aborted because of duplicated friendly url"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string html = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = html; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + + int nIdxStart = html.IndexOf("PageList"); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "PageList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart = html.IndexOf("value=\"", nIdxStart); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "value after PageList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart += "value=\"".Length; + + string strPageList = html.Substring(nIdxStart, html.IndexOf('"', nIdxStart) - nIdxStart); + + nIdxStart = html.IndexOf("TextList"); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "TextList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart = html.IndexOf("value=\"", nIdxStart); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "value after TextList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart += "value=\"".Length; + + string strTextList = html.Substring(nIdxStart, html.IndexOf('"', nIdxStart) - nIdxStart); + + nIdxStart = html.IndexOf("TitleList"); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "TitleList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart = html.IndexOf("value=\"", nIdxStart); + if (nIdxStart == -1) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "value after TitleList not found"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + nIdxStart += "value=\"".Length; + + string strTitleList = html.Substring(nIdxStart, html.IndexOf('"', nIdxStart) - nIdxStart); + + string[] PageUrls = strPageList.Split("||", StringSplitOptions.None); + string[] PageTexts = strTextList.Split("||", StringSplitOptions.None); + string[] PageTitles = strTitleList.Split("||", StringSplitOptions.None); + + if(PageUrls.Length != PageTexts.Length || PageTexts.Length != PageTitles.Length) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "PageUrls.Length != PageTexts.Length || PageTexts.Length != PageTitles.Length"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + tag = await TagHandler.PrepareAttribute(context, "Title", "Untitled", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", "Unknown", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + book.Tags = meta.ToArray(); + + + tag = await TagHandler.PrepareAttribute(context, "Source", "British Library", 1); + string viewerUrl = $"http://www.bl.uk/manuscripts/FullDisplay.aspx?ref={resourceNumber.Substring(0, resourceNumber.LastIndexOf('_'))}"; + tag.ValueSupplement = viewerUrl; + + meta.Add(tag); + + List pages = new List(); + + int order = 0; + for(int i = 0; i < PageUrls.Length; i++) + { + if (PageUrls[i] == "##") + continue; + order++; + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + + List pageTags = new List(); + tag = await TagHandler.PrepareAttribute(context, "Source", "British Library", 1); + tag.ValueSupplement = $"http://www.bl.uk/manuscripts/Viewer.aspx?ref={PageUrls[i]}"; + pageTags.Add(tag); + + + if (!string.IsNullOrEmpty(PageTitles[i])) + { + RTagValue toc = await TagHandler.PrepareAttribute(context, "Title in TOC", PageTitles[i], 1); + toc.ValueSupplement = "1";//font size + pageTags.Add(toc); + } + + if (!string.IsNullOrEmpty(PageTexts[i])) + { + tag = await TagHandler.PrepareAttribute(context, "Label", PageTexts[i], 1); + pageTags.Add(tag); + + } + + page.Tags = pageTags.ToArray(); + + + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + viewerUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + + /* + failed multithread attempt: + + BLTileMixer mixer = new BLTileMixer(); + RServiceResult blResult = await mixer.DownloadMix(PageUrls[i], order); + */ + + + Dictionary<(int x, int y), Image> tiles = new Dictionary<(int x, int y), Image>(); + int max_x = -1; + for(int x = 0; ;x++) + { + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={PageUrls[i]}_files/13/{x}_0.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + + imageStream.Position = 0; + try + { + Image tile = Image.FromStream(imageStream); + tiles.Add((x, 0), tile); + max_x = x; + } + catch(Exception aexp) + { + if (aexp is ArgumentException) + { + break; + } + throw aexp; + } + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + } + + int max_y = -1; + for (int y = 1; ; y++) + { + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={PageUrls[i]}_files/13/0_{y}.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + if (imageStream.Length <= 248) + break; + imageStream.Position = 0; + try + { + Image tile = Image.FromStream(imageStream); + tiles.Add((0, y), tile); + max_y = y; + } + catch (Exception aexp) + { + if (aexp is ArgumentException) + { + break; + } + throw aexp; + } + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + } + + for(int x = 0; x <= max_x; x++) + for(int y = 0; y <= max_y; y++) + if(tiles.TryGetValue((x, y), out Image tmp) == false) + { + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={PageUrls[i]}_files/13/{x}_{y}.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + if (imageStream.Length == 0) + break; + imageStream.Position = 0; + tiles.Add((x, y), Image.FromStream(imageStream)); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + } + + int tileWidth = tiles[(0, 0)].Width; + int tileHeight = tiles[(0, 0)].Height; + + int imageWidth = tileWidth * (max_x + 1); + int imageHeight = tileHeight * (max_y + 1); + + using(Image image = new Bitmap(imageWidth, imageHeight)) + { + using(Graphics g = Graphics.FromImage(image)) + { + for(int x = 0; x <= max_x; x++) + for(int y = 0; y < max_y; y++) + { + g.DrawImage(tiles[(x, y)], new Point(x * tileWidth, y * tileHeight)); + tiles[(x, y)].Dispose(); + } + + using (Stream imageStream = new MemoryStream()) + { + ImageCodecInfo jpgEncoder = _pictureFileService.GetEncoder(ImageFormat.Jpeg); + + Encoder myEncoder = + Encoder.Quality; + EncoderParameters jpegParameters = new EncoderParameters(1); + + EncoderParameter qualityParameter = new EncoderParameter(myEncoder, 92L); + jpegParameters.Param[0] = qualityParameter; + + image.Save(imageStream, jpgEncoder, jpegParameters); + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, $"http://www.bl.uk/manuscripts/Viewer.aspx?ref={PageUrls[i]}", imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + } + } + + GC.Collect(); + + + + } + } + + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + + /// + /// import from server folder + /// + /// C:\Tools\batches\florence + /// shahname-florence + /// https://t.me/dr_khatibi_abolfazl/888 + /// + public async Task> StartImportingFromServerFolder(string folderPath, string friendlyUrl, string srcUrl) + { + try + { + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.ServerFolder && j.ResourceNumber == folderPath && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing server folder {folderPath}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + return new RServiceResult(false, $"Friendly url is empty, server folder {folderPath}"); + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.ServerFolder, + ResourceNumber = folderPath, + FriendlyUrl = friendlyUrl, + SrcUrl = srcUrl, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from server folder {job.ResourceNumber}", $"extracted from server folder {job.ResourceNumber}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + + meta.Add(tag); + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = ""; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + List pages = new List(); + string[] fileNames = Directory.GetFiles(job.ResourceNumber, "*.jpg"); + int order = 0; + foreach (string fileName in fileNames) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order * 100 / (decimal)fileNames.Length; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + order++; + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order} of {book.NameInEnglish}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + + page.Tags = new RTagValue[] { }; + + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + using (FileStream imageStream = new FileStream(fileName, FileMode.Open)) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, job.SrcUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + } + + + pages.Add(page); + } + + + book.Tags = meta.ToArray(); + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// from http://www.thedigitalwalters.org/01_ACCESS_WALTERS_MANUSCRIPTS.html + /// + /// W619 + /// golestan-walters-01 + /// + public async Task> StartImportingFromWalters(string resourceNumber, string friendlyUrl) + { + string url = $"http://www.thedigitalwalters.org/Data/WaltersManuscripts/ManuscriptDescriptions/{resourceNumber}_tei.xml"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.Walters && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {url}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + friendlyUrl = resourceNumber; + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.Walters, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = url, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + + try + { + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + + + using (var client = new HttpClient()) + { + + using (var result = await client.GetAsync(url)) + { + if (result.IsSuccessStatusCode) + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from {url}", $"extracted from {url}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + + List meta = new List(); + RTagValue tag; + + string xml = await result.Content.ReadAsStringAsync(); + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = xml; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + XElement elObject = XDocument.Parse(xml).Root; + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + + + + + + try + { + foreach (var prop in elObject + .Elements("{http://www.tei-c.org/ns/1.0}teiHeader").First() + .Elements("{http://www.tei-c.org/ns/1.0}fileDesc").First() + .Elements("{http://www.tei-c.org/ns/1.0}titleStmt").First() + .Elements("{http://www.tei-c.org/ns/1.0}title")) + { + string label = prop.Value; + book.Name = book.NameInEnglish = + book.Description = book.DescriptionInEnglish = + label; + + tag = await TagHandler.PrepareAttribute(context, "Title", label, 1); + meta.Add(tag); + break; + } + } + catch + { + //ignore non-existing = null tags + } + + try + { + foreach (var prop in elObject + .Elements("{http://www.tei-c.org/ns/1.0}teiHeader").First() + .Elements("{http://www.tei-c.org/ns/1.0}fileDesc").First() + .Elements("{http://www.tei-c.org/ns/1.0}titleStmt").First() + .Elements("{http://www.tei-c.org/ns/1.0}author")) + { + string label = prop.Value; + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", label, 1); + meta.Add(tag); + break; + } + } + catch + { + //ignore non-existing = null tags + } + + try + { + foreach (var prop in elObject + .Elements("{http://www.tei-c.org/ns/1.0}teiHeader").First() + .Elements("{http://www.tei-c.org/ns/1.0}fileDesc").First() + .Elements("{http://www.tei-c.org/ns/1.0}titleStmt").First() + .Elements("{http://www.tei-c.org/ns/1.0}respStmt")) + { + string label = prop.Elements("{http://www.tei-c.org/ns/1.0}name").First().Value; + tag = await TagHandler.PrepareAttribute(context, "Contributor Names", label, 1); + meta.Add(tag); + break; + + } + } + catch + { + //ignore non-existing = null tags + } + + try + { + foreach (var prop in elObject + .Elements("{http://www.tei-c.org/ns/1.0}teiHeader").First() + .Elements("{http://www.tei-c.org/ns/1.0}fileDesc").First() + .Elements("{http://www.tei-c.org/ns/1.0}notesStmt").First() + .Elements("{http://www.tei-c.org/ns/1.0}note")) + { + string label = prop.Value; + tag = await TagHandler.PrepareAttribute(context, "Notes", label, 1); + meta.Add(tag); + } + } + catch + { + //ignore non-existing = null tags + } + + + + + tag = await TagHandler.PrepareAttribute(context, "Source", "Digitized Walters Manuscripts", 1); + tag.ValueSupplement = $"http://www.thedigitalwalters.org/Data/WaltersManuscripts/html/{job.ResourceNumber}/"; + + meta.Add(tag); + + + book.Tags = meta.ToArray(); + List pages = new List(); + + int order = 0; + + foreach (var surface in elObject + .Elements("{http://www.tei-c.org/ns/1.0}facsimile").First() + .Elements("{http://www.tei-c.org/ns/1.0}surface")) + { + foreach(var graphic in surface.Elements("{http://www.tei-c.org/ns/1.0}graphic")) + if (graphic.Attribute("url").Value.Contains("sap.jpg")) + { + + order++; + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + + + string imageUrl = $"http://www.thedigitalwalters.org/Data/WaltersManuscripts/{resourceNumber}/data/W.{resourceNumber.Substring(1)}/{graphic.Attribute("url").Value}"; + + tag = await TagHandler.PrepareAttribute(context, "Source", "Digitized Walters Manuscripts", 1); + tag.ValueSupplement = imageUrl; + page.Tags = new RTagValue[] { tag }; + + if (!string.IsNullOrEmpty(imageUrl)) + { + bool recovered = false; + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + + } + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + pages.Add(page); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + imageResult.Dispose(); + GC.Collect(); + } + + } + + + + + } + } + + + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({result.StatusCode}) for {url}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + } + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// import from https://viewer.cbl.ie + /// + /// 119 + /// golestan-baysonghori + /// + public async Task> StartImportingFromChesterBeatty(string resourceNumber, string friendlyUrl) + { + try + { + string srcUrl = $"https://viewer.cbl.ie/viewer/object/Per_{resourceNumber}/1/"; + if ( + ( + await _context.ImportJobs + .Where(j => j.JobType == JobType.ChesterBeatty && j.ResourceNumber == resourceNumber && !(j.Status == ImportJobStatus.Failed || j.Status == ImportJobStatus.Aborted)) + .SingleOrDefaultAsync() + ) + != + null + ) + { + return new RServiceResult(false, $"Job is already scheduled or running for importing {srcUrl}"); + } + + if (string.IsNullOrEmpty(friendlyUrl)) + { + return new RServiceResult(false, $"Friendly url is empty, server folder {srcUrl}"); + } + + if ( + (await _context.Artifacts.Where(a => a.FriendlyUrl == friendlyUrl).SingleOrDefaultAsync()) + != + null + ) + { + return new RServiceResult(false, $"duplicated friendly url '{friendlyUrl}'"); + } + + ImportJob job = new ImportJob() + { + JobType = JobType.ChesterBeatty, + ResourceNumber = resourceNumber, + FriendlyUrl = friendlyUrl, + SrcUrl = srcUrl, + QueueTime = DateTime.Now, + ProgressPercent = 0, + Status = ImportJobStatus.NotStarted + }; + + + await _context.ImportJobs.AddAsync + ( + job + ); + + await _context.SaveChangesAsync(); + + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + try + { + using (RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + RArtifactMasterRecord book = new RArtifactMasterRecord($"extracted from url {job.ResourceNumber}", $"extracted from url {job.ResourceNumber}") + { + Status = PublishStatus.Draft, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl, + }; + + + List meta = new List(); + RTagValue tag; + + + tag = await TagHandler.PrepareAttribute(context, "Type", "Book", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Type", "Manuscript", 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(context, "Source", "Chester Beatty Digital Collections", 1); + tag.ValueSupplement = srcUrl; + + meta.Add(tag); + + + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.StartTime = DateTime.Now; + job.Status = ImportJobStatus.Running; + job.SrcContent = ""; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + List pages = new List(); + int order = 0; + using (var client = new HttpClient()) + do + { + + + order++; + + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.ProgressPercent = order; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = $"تصویر {order}", + NameInEnglish = $"Image {order} of {book.NameInEnglish}", + Description = "", + DescriptionInEnglish = "", + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + string imageUrl = $"https://viewer.cbl.ie/viewer/rest/image/Per_{resourceNumber}/Per{resourceNumber}_{$"{order}".PadLeft(3, '0')}.jpg/full/!10000,10000/0/default.jpg?ignoreWatermark=true"; + + + page.Tags = new RTagValue[] { }; + + bool recovered = false; + + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + && + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + RServiceResult picture = await _pictureFileService.RecoverFromeFiles(page.Name, page.Description, 1, + imageUrl, + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg"), + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg"), + $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result != null) + { + recovered = true; + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + + tag = await TagHandler.PrepareAttribute(context, "Source", "Chester Beatty Digital Collections", 1); + tag.ValueSupplement = $"https://viewer.cbl.ie/viewer/object/Per_{resourceNumber}/{$"{order}".PadLeft(3, '0')}/"; ; + page.Tags = new RTagValue[] { tag }; + + pages.Add(page); + } + + } + + if (!recovered) + { + if ( + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "orig"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "norm"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + if ( + + File.Exists + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ) + ) + { + File.Delete + ( + Path.Combine(Path.Combine(Path.Combine(_pictureFileService.ImageStoragePath, friendlyUrl), "thumb"), $"{order}".PadLeft(4, '0') + ".jpg") + ); + } + var imageResult = await client.GetAsync(imageUrl); + + if (imageResult.StatusCode == HttpStatusCode.Forbidden || imageResult.StatusCode == HttpStatusCode.NotFound) + break; + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + RServiceResult picture = await _pictureFileService.Add(page.Name, page.Description, 1, null, imageUrl, imageStream, $"{order}".PadLeft(4, '0') + ".jpg", friendlyUrl); + if (picture.Result == null) + { + throw new Exception($"_pictureFileService.Add : {picture.ExceptionString}"); + } + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + if (book.CoverItemIndex == (order - 1)) + { + book.CoverImage = RPictureFile.Duplicate(picture.Result); + } + tag = await TagHandler.PrepareAttribute(context, "Source", "Chester Beatty Digital Collections", 1); + tag.ValueSupplement = $"https://viewer.cbl.ie/viewer/object/Per_{resourceNumber}/{$"{order}".PadLeft(3, '0')}/"; + page.Tags = new RTagValue[] { tag }; + + pages.Add(page); + } + } + else + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + imageResult.Dispose(); + return; + } + + + imageResult.Dispose(); + GC.Collect(); + } + + + pages.Add(page); + } + while (true); + + + book.Tags = meta.ToArray(); + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + if (pages.Count == 0) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = "Pages.Count == 0"; + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + return; + } + + await context.Artifacts.AddAsync(book); + await context.SaveChangesAsync(); + + job.ProgressPercent = 100; + job.Status = ImportJobStatus.Succeeded; + job.ArtifactId = book.Id; + job.EndTime = DateTime.Now; + context.Update(job); + await context.SaveChangesAsync(); + + + + } + } + catch (Exception exp) + { + using (RMuseumDbContext importJobUpdaterDb = new RMuseumDbContext(Configuration)) + { + job.EndTime = DateTime.Now; + job.Status = ImportJobStatus.Failed; + job.Exception = exp.ToString(); + importJobUpdaterDb.Update(job); + await importJobUpdaterDb.SaveChangesAsync(); + } + + } + } + ); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// reschedule jobs + /// + /// + public async Task> RescheduleJobs() + { + try + { + + + ImportJob[] jobs = await _context.ImportJobs.Where(j => j.Status != ImportJobStatus.Succeeded && j.JobType == JobType.ChesterBeatty).OrderByDescending(j => j.ProgressPercent).ToArrayAsync(); + + List scheduled = new List(); + + foreach(ImportJob job in jobs) + { + if(job.Status != ImportJobStatus.Failed) + { + job.Status = ImportJobStatus.Aborted; + job.EndTime = DateTime.Now; + job.Exception = "Aborted by RescheduleJobs"; + _context.Update(job); + await _context.SaveChangesAsync(); + } + + if ( + await _context.ImportJobs.Where(j => j.ResourceNumber == job.ResourceNumber && j.Status == ImportJobStatus.Succeeded).SingleOrDefaultAsync() + != + null + ) + continue; + + if (scheduled.IndexOf(job.ResourceNumber) == -1) + { + scheduled.Add(job.ResourceNumber); + + RServiceResult rescheduled = await StartImportingFromChesterBeatty(job.ResourceNumber, job.FriendlyUrl); + if(rescheduled.Result) + { + _context.ImportJobs.Remove(job); + await _context.SaveChangesAsync(); + } + + } + } + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(true, exp.ToString()); + } + } + + /// + /// an incomplete prototype for removing artifacts + /// + /// + /// + /// + public async Task> RemoveArtifactHavingNoNoteAndBookmarks(Guid artifactId, bool checkJobs) + { + try + { + RArtifactMasterRecord record = await _context.Artifacts + .Include(a => a.Items).ThenInclude(i => i.Images) + .Include(a => a.Items).ThenInclude(i => i.Tags) + .Include(a => a.Tags) + .Where(a => a.Id == artifactId) + .SingleOrDefaultAsync(); + if (record == null) + { + return new RServiceResult(false, "Artifact not found."); + } + if (record.Status == PublishStatus.Published) + { + return new RServiceResult(false, "Can not delete published artifact"); + } + + if(checkJobs) + { + var jobs = await _context.ImportJobs.Where(j => j.ArtifactId == artifactId).ToArrayAsync(); + if (jobs.Length > 0) + { + _context.ImportJobs.RemoveRange(jobs); + } + } + + var pins = await _context.PinterestLinks.Where(j => j.ArtifactId == artifactId).ToArrayAsync(); + if (pins.Length > 0) + { + _context.PinterestLinks.RemoveRange(pins); + } + + + foreach (RArtifactItemRecord item in record.Items) + { + _context.PictureFiles.RemoveRange(item.Images); + _context.TagValues.RemoveRange(item.Tags); + } + + _context.Items.RemoveRange(record.Items); + _context.TagValues.RemoveRange(record.Tags); + _context.Artifacts.Remove(record); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// due to a bug in loc json outputs some artifacts with more than 1000 pages were downloaded incompletely + /// + /// + public async Task> ReExamineLocDownloads() + { + try + { + ImportJob[] jobs = await _context.ImportJobs + .Include(j => j.Artifact) + .Where(j => j.Status == ImportJobStatus.Succeeded && j.JobType == JobType.Loc).ToArrayAsync(); + + List scheduled = new List(); + List rescheduledJobs = new List(); + foreach (ImportJob job in jobs) + { + + int pageCount = 0; + //اول یک صفحه را می‌خوانیم تا تعداد صفحات را مشخص کنیم + using (var client = new HttpClient()) + { + if (job.Artifact == null) + continue; + + string url = $"https://www.loc.gov/resource/rbc0001.{job.ResourceNumber}/?fo=json&st=gallery"; //plmp + + var result = await client.GetAsync(url); + //using (var result = await client.GetAsync(url)) + { + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !result.IsSuccessStatusCode && result.StatusCode == HttpStatusCode.ServiceUnavailable) + { + result.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + result = await client.GetAsync(url); + retryCount++; + } + + if (result.IsSuccessStatusCode) + { + string json = await result.Content.ReadAsStringAsync(); + var parsed = JObject.Parse(json); + + pageCount = parsed.SelectToken("resource.segment_count").Value(); + } + else + { + return new RServiceResult(null, $"{job.ResourceNumber}: Http result is not ok ({result.StatusCode}) for {url}"); + } + + if(pageCount != job.Artifact.ItemCount) + { + if (scheduled.IndexOf(job.ResourceNumber) == -1) + { + scheduled.Add(job.ResourceNumber); + rescheduledJobs.Add(job); + + } + } + result.Dispose(); + } + + } + } + + scheduled = new List(); + foreach (ImportJob job in rescheduledJobs) + { + await RemoveArtifactHavingNoNoteAndBookmarks((Guid)job.ArtifactId, false); + _context.ImportJobs.Remove(job); + await _context.SaveChangesAsync(); + RServiceResult rescheduled = await StartImportingFromTheLibraryOfCongress(job.ResourceNumber, job.FriendlyUrl, "rbc0001");//plmp + if (rescheduled.Result) + { + + scheduled.Add(job.ResourceNumber); + } + } + return new RServiceResult(scheduled.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + private async Task HandleSimpleValue(RMuseumDbContext context, JObject parsed, List meta, string path, string aName) + { + try + { + string string_value = parsed.SelectToken(path).Value(); + if (!string.IsNullOrWhiteSpace(string_value)) + { + meta.Add + ( + await TagHandler.PrepareAttribute(context, aName, string_value, 1) + ); + } + return string_value; + } + catch + { + return ""; + } + } + + private async Task HandleListValue(RMuseumDbContext context, JObject parsed, List meta, string path, string aName) + { + try + { + string value = ""; + List list = parsed.SelectToken(path).ToObject>(); + if (list != null && list.Count > 0) + { + value = list[0]; + meta.Add + ( + await TagHandler.PrepareAttribute(context, aName, list[0], 1) + ); + + for (int i = 1; i < list.Count; i++) + { + value += $"
{Environment.NewLine}{list[i]}"; + meta.Add + ( + await TagHandler.PrepareAttribute(context, aName, list[i], i + 1) + ); + + } + } + return value; + } + catch + { + return ""; + } + } + + private async Task HandleChildrenValue(RMuseumDbContext context, JObject parsed, List meta, string path, string aName) + { + try + { + string value = ""; + var children = parsed.SelectToken(path).Children().ToList(); + if (children.Count > 0) + { + value = children[0].First.Path.Replace($"{path}[0].", "").Replace($"{path}[0]", ""); + meta.Add + ( + await TagHandler.PrepareAttribute(context, aName, value, 1) + ); + for (int i = 1; i < children.Count; i++) + { + value += $"
{Environment.NewLine}{children[0].First.Path.Replace($"{path}[{i}].", "")}"; + meta.Add + ( + await TagHandler.PrepareAttribute(context, aName, children[0].First.Path.Replace($"{path}[{i}].", ""), i + 1) + ); + } + + + } + return value; + } + catch + { + return ""; + } + } + + + + /// + /// import jobs + /// + /// + /// + public async Task> GetImportJobs(PagingParameterModel paging) + { + try + { + var source = + _context.ImportJobs + .Select(j => new ImportJob() { Id = j.Id, Artifact = j.Artifact, ArtifactId = j.ArtifactId, EndTime = j.EndTime, Exception = j.Exception, ProgressPercent = j.ProgressPercent, QueueTime = j.QueueTime, FriendlyUrl = j.FriendlyUrl, JobType = j.JobType, ResourceNumber = j.ResourceNumber, SrcContent = "--omitted--", SrcUrl = j.SrcUrl, StartTime = j.StartTime, Status = j.Status }) + .OrderByDescending(t => t.QueueTime) + .AsQueryable(); + + (PaginationMetadata PagingMeta, ImportJob[] Items) paginatedResult = + await QueryablePaginator.Paginate(source, paging); + + + + return new RServiceResult<(PaginationMetadata PagingMeta, ImportJob[] Items)>(paginatedResult); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, ImportJob[] Items)>((PagingMeta: null, Items: null), exp.ToString()); + } + } + + /// + /// Bookmark Artifact + /// + /// + /// + /// + /// + public async Task> BookmarkArtifact(Guid artifactId, Guid userId, RBookmarkType type) + { + try + { + if ((await _context.UserBookmarks.Where(b => b.RAppUserId == userId && b.RArtifactMasterRecordId == artifactId && b.RBookmarkType == type).SingleOrDefaultAsync()) != null) + { + return new RServiceResult(null, "Artifact is already bookmarked/faved."); + } + + RUserBookmark bookmark = + new RUserBookmark() + { + RAppUserId = userId, + RArtifactMasterRecordId = artifactId, + RBookmarkType = type, + DateTime = DateTime.Now, + Note = "" + }; + + _context.UserBookmarks.Add(bookmark); + await _context.SaveChangesAsync(); + + return new RServiceResult(bookmark); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get artifact user bookmarks + /// + /// + /// + /// + public async Task> GetArtifactUserBookmarks(Guid artifactId, Guid userId) + { + try + { + RUserBookmark[] bookmarks = await _context.UserBookmarks.Where(b => b.RArtifactMasterRecordId == artifactId && b.RAppUserId == userId).ToArrayAsync(); + return new RServiceResult(bookmarks); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Bookmark Item + /// + /// + /// + /// + /// + public async Task> BookmarkItem(Guid itemId, Guid userId, RBookmarkType type) + { + try + { + if((await _context.UserBookmarks.Where(b => b.RAppUserId == userId && b.RArtifactItemRecordId == itemId && b.RBookmarkType == type).SingleOrDefaultAsync()) != null) + { + return new RServiceResult(null, "Item is already bookmarked/faved."); + } + + RUserBookmark bookmark = + new RUserBookmark() + { + RAppUserId = userId, + RArtifactItemRecordId = itemId, + RBookmarkType = type, + DateTime = DateTime.Now, + Note = "" + }; + + _context.UserBookmarks.Add(bookmark); + await _context.SaveChangesAsync(); + + return new RServiceResult(bookmark); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get item user bookmarks + /// + /// + /// + /// + public async Task> GeItemUserBookmarks(Guid itemId, Guid userId) + { + try + { + RUserBookmark[] bookmarks = await _context.UserBookmarks.Where(b => b.RArtifactItemRecordId == itemId && b.RAppUserId == userId).ToArrayAsync(); + return new RServiceResult(bookmarks); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// update bookmark note + /// + /// + /// + /// + public async Task> UpdateUserBookmark(Guid bookmarkId, string note) + { + try + { + RUserBookmark bookmark = await _context.UserBookmarks.Where(b => b.Id == bookmarkId).SingleOrDefaultAsync(); + if(bookmark == null) + { + return new RServiceResult(false, "bookmark not found"); + } + bookmark.Note = note; + _context.UserBookmarks.Update(bookmark); + await _context.SaveChangesAsync(); + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// delete user bookmark + /// /// + /// + /// + public async Task> DeleteUserBookmark(Guid bookmarkId) + { + try + { + RUserBookmark bookmark = await _context.UserBookmarks.Where(b => b.Id == bookmarkId).SingleOrDefaultAsync(); + if (bookmark == null) + { + return new RServiceResult(false, "bookmark not found"); + } + _context.UserBookmarks.Remove(bookmark); + await _context.SaveChangesAsync(); + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// get user bookmarks (artifacts and items) + /// + /// + /// + /// + /// + /// + public async Task> GetBookmarks(PagingParameterModel paging, Guid userId, RBookmarkType type, PublishStatus[] statusArray) + { + try + { + var source = + _context.UserBookmarks + .Include(b => b.RArtifactMasterRecord).ThenInclude(a => a.CoverImage) + .Include(b => b.RArtifactItemRecord).ThenInclude(b => b.Images) + .Where(b => b.RAppUserId == userId && b.RBookmarkType == type) + .OrderByDescending(b => b.DateTime) + .AsQueryable(); + + (PaginationMetadata PagingMeta, RUserBookmark[] Bookmarks) paginatedResult1 = + await QueryablePaginator.Paginate(source, paging); + + + List finalList = new List(); + foreach(RUserBookmark bookmark in paginatedResult1.Bookmarks) + { + RUserBookmarkViewModel model = new RUserBookmarkViewModel(bookmark); + if(bookmark.RArtifactMasterRecord != null) + { + if (!statusArray.Contains(bookmark.RArtifactMasterRecord.Status)) + continue; //this may result in paging bugs + } + if(bookmark.RArtifactItemRecord != null) + { + RArtifactMasterRecord parent = + await _context.Artifacts + .Where(a => statusArray.Contains(a.Status) && a.Id == bookmark.RArtifactItemRecord.RArtifactMasterRecordId) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (parent == null) + continue; + + model.RArtifactItemRecord = new RArtifactItemRecordViewModel() + { + Item = bookmark.RArtifactItemRecord + }; + model.RArtifactItemRecord.ParentFriendlyUrl = parent.FriendlyUrl; + model.RArtifactItemRecord.ParentName = parent.Name; + } + finalList.Add(model); + } + + + return new RServiceResult<(PaginationMetadata PagingMeta, RUserBookmarkViewModel[] Bookmarks)>((paginatedResult1.PagingMeta, finalList.ToArray())); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, RUserBookmarkViewModel[] Bookmarks)>((PagingMeta: null, Bookmarks: null), exp.ToString()); + } + } + + /// + /// Add Note to Artifact + /// + /// + /// + /// + /// + /// + /// + public async Task> AddUserNoteToArtifact(Guid artifactId, Guid userId, RNoteType type, string noteContents, Guid? referenceNoteId) + { + try + { + + RServiceResult userInfo = await _userService.GetUserInformation(userId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult(null, userInfo.ExceptionString); + + RUserNote note = + new RUserNote() + { + RAppUserId = userId, + RArtifactMasterRecordId = artifactId, + NoteType = type, + HtmlContent = noteContents, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + Modified = false, + Status = PublishStatus.Published, + ReferenceNoteId = referenceNoteId + }; + + _context.UserNotes.Add(note); + await _context.SaveChangesAsync(); //we need note.Id for the next step + + if (referenceNoteId != null) + { + RUserNote referenceNote = await _context.UserNotes.Where(n => n.Id == referenceNoteId).SingleOrDefaultAsync(); + if(referenceNote == null) + { + return new RServiceResult(null, "Reference note not found!"); + } + + if(referenceNote.RAppUserId != userId) + { + RArtifactMasterRecord artificat = await _context.Artifacts.Where(a => a.Id == artifactId).SingleOrDefaultAsync(); + await PushNotification + ( + referenceNote.RAppUserId, + $"پاسخگویی {userInfo.Result.FirstName} {userInfo.Result.SureName} به یادداشت شما دربارهٔ {artificat.Name}", + $"برای مشاهدهٔ پاسخ ارائه شده
اینجا را ببینید.
" + + $"پاسخ داده شده:
" + + $"
{note.HtmlContent}

" + + $"یادداشت شما:
" + + $"
{referenceNote.HtmlContent}
" + ); + } + } + return new RServiceResult(new RUserNoteViewModel(note, userInfo.Result)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Add Note to Artifact Item + /// + /// + /// + /// + /// + /// + /// + public async Task> AddUserNoteToArtifactItem(Guid itemId, Guid userId, RNoteType type, string noteContents, Guid? referenceNoteId) + { + try + { + + RServiceResult userInfo = await _userService.GetUserInformation(userId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult(null, userInfo.ExceptionString); + + RUserNote note = + new RUserNote() + { + RAppUserId = userId, + RArtifactItemRecordId = itemId, + NoteType = type, + HtmlContent = noteContents, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + Modified = false, + Status = PublishStatus.Published, + ReferenceNoteId = referenceNoteId + }; + + _context.UserNotes.Add(note); + await _context.SaveChangesAsync();//we need note.Id for the next step + + if (referenceNoteId != null) + { + RUserNote referenceNote = await _context.UserNotes.Where(n => n.Id == referenceNoteId).SingleOrDefaultAsync(); + if (referenceNote == null) + { + return new RServiceResult(null, "Reference note not found!"); + } + + if (referenceNote.RAppUserId != userId) + { + RArtifactItemRecord item = await _context.Items.Where(a => a.Id == itemId).SingleOrDefaultAsync(); + RArtifactMasterRecord artificat = await _context.Artifacts.Where(a => a.Id == item.RArtifactMasterRecordId).SingleOrDefaultAsync(); + await PushNotification + ( + referenceNote.RAppUserId, + $"پاسخگویی {userInfo.Result.FirstName} {userInfo.Result.SureName} به یادداشت شما دربارهٔ {artificat.Name} « {item.Name}", + $"برای مشاهدهٔ پاسخ ارائه شده اینجا را ببینید.
" + + $"پاسخ داده شده:
" + + $"
{note.HtmlContent}

" + + $"یادداشت شما:
" + + $"
{referenceNote.HtmlContent}
" + ); + } + } + + + return new RServiceResult(new RUserNoteViewModel(note, userInfo.Result)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Edit Note + /// + /// + /// + /// + /// + public async Task> EditUserNote(Guid noteId, Guid? userId, string noteContents) + { + try + { + RUserNote note = await _context.UserNotes. + Where(n => n.Id == noteId). + SingleOrDefaultAsync(); + if (note == null) + { + return new RServiceResult(null, "Note not found."); + } + + if(userId != null) //sending null here means user is a moderator + { + if (userId != note.RAppUserId) + { + return new RServiceResult(null, "Note only can be edited by its owner"); + } + } + + + + RServiceResult userInfo = await _userService.GetUserInformation(note.RAppUserId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult(null, userInfo.ExceptionString); + + + + note.HtmlContent = noteContents; + note.LastModified = DateTime.Now; + + _context.UserNotes.Update(note); + await _context.SaveChangesAsync(); + + + return new RServiceResult(new RUserNoteViewModel(note, userInfo.Result)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// delete note + /// + /// + /// sending null here means user is a moderator or the note is being deleted in a recursive delete of referenced notes + /// list of notes deleted + public async Task> DeleteUserNote(Guid noteId, Guid? userId) + { + try + { + RUserNote note = await _context.UserNotes. + Where(n => n.Id == noteId). + SingleOrDefaultAsync(); + if (note == null) + { + return new RServiceResult(null, "Note not found."); + } + + if (userId != null) //sending null here means user is a moderator or the note is being deleted in a recursive delete of referenced notes + { + if (userId != note.RAppUserId) + { + return new RServiceResult(null, "Note only can be deleted by its owner"); + } + } + + List deletedNotesIdSet = new List(); + + RUserNote[] relatedNotes = await _context.UserNotes.Where(n => n.ReferenceNoteId == noteId).ToArrayAsync(); + foreach(RUserNote relatedNote in relatedNotes) + { + deletedNotesIdSet.Add(relatedNote.Id); + + RServiceResult refDel = await DeleteUserNote(relatedNote.Id, null); + if (!string.IsNullOrEmpty(refDel.ExceptionString)) + return new RServiceResult(null, refDel.ExceptionString); + } + + deletedNotesIdSet.Add(noteId); + + _context.UserNotes.Remove(note); + await _context.SaveChangesAsync(); + + return new RServiceResult(deletedNotesIdSet.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// get artifact private user notes + /// + /// + /// + /// + public async Task> GetArtifactUserNotes(Guid artifactId, Guid userId) + { + try + { + RUserNote[] notes = await _context.UserNotes. + Where(b => b.RArtifactMasterRecordId == artifactId && b.RAppUserId == userId && b.NoteType == RNoteType.Private). + OrderBy(n => n.DateTime).ToArrayAsync(); + + RServiceResult userInfo = await _userService.GetUserInformation(userId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult(null, userInfo.ExceptionString); + + List res = new List(); + foreach (RUserNote note in notes) + res.Add(new RUserNoteViewModel(note, userInfo.Result)); + + return new RServiceResult(res.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// recursvie call to build notes tree + /// + /// + /// + /// + private async Task _GetArtifactPublicNotes(Guid artifactId, Guid? parentNoteId) + { + RUserNote[] notes = await _context.UserNotes. + Where(b => b.RArtifactMasterRecordId == artifactId && b.NoteType == RNoteType.Public && b.ReferenceNoteId == parentNoteId) + .Include(n => n.RAppUser) + .OrderBy(n => n.DateTime).ToArrayAsync(); + + List res = new List(); + foreach (RUserNote note in notes) + { + RUserNoteViewModel viewModel = new RUserNoteViewModel(note, new PublicRAppUser(note.RAppUser)); + viewModel.Notes = await _GetArtifactPublicNotes(artifactId, note.Id); + res.Add(viewModel); + } + return res.ToArray(); + } + + /// + /// get artifact public user notes + /// + /// + /// + public async Task> GetArtifactPublicNotes(Guid artifactId) + { + try + { + return new RServiceResult(await _GetArtifactPublicNotes(artifactId, null)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get item artifact private user notes + /// + /// + /// + /// + public async Task> GetArtifactItemUserNotes(Guid itemId, Guid userId) + { + try + { + RUserNote[] notes = await _context.UserNotes. + Where(b => b.RArtifactItemRecordId == itemId && b.RAppUserId == userId && b.NoteType == RNoteType.Private). + OrderBy(n => n.DateTime).ToArrayAsync(); + + RServiceResult userInfo = await _userService.GetUserInformation(userId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult(null, userInfo.ExceptionString); + + List res = new List(); + foreach (RUserNote note in notes) + res.Add(new RUserNoteViewModel(note, userInfo.Result)); + + return new RServiceResult(res.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// recursvie call to build notes tree for iten + /// + /// + /// + /// + private async Task _GetArtifactItemPublicNotes(Guid itemId, Guid? parentNoteId) + { + RUserNote[] notes = await _context.UserNotes. + Where(b => b.RArtifactItemRecordId == itemId && b.NoteType == RNoteType.Public && b.ReferenceNoteId == parentNoteId) + .Include(n => n.RAppUser) + .OrderBy(n => n.DateTime).ToArrayAsync(); + + List res = new List(); + foreach (RUserNote note in notes) + { + RUserNoteViewModel viewModel = new RUserNoteViewModel(note, new PublicRAppUser(note.RAppUser)); + viewModel.Notes = await _GetArtifactItemPublicNotes(itemId, note.Id); + res.Add(viewModel); + } + return res.ToArray(); + } + + /// + /// get item artifact item public user notes + /// + /// + /// + public async Task> GetArtifactItemPublicNotes(Guid itemId) + { + try + { + + return new RServiceResult(await _GetArtifactItemPublicNotes(itemId, null)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Get All USer Notes + /// + /// + /// + /// + /// + /// + public async Task> GetUserNotes(Guid userId, PagingParameterModel paging, RNoteType type, PublishStatus[] statusArray) + { + try + { + var source = + _context.UserNotes + .Include(b => b.RArtifactMasterRecord).ThenInclude(a => a.CoverImage) + .Include(b => b.RArtifactItemRecord).ThenInclude(b => b.Images) + .Where(b => b.RAppUserId == userId && b.NoteType == type) + .OrderByDescending(b => b.DateTime) + .AsQueryable(); + + (PaginationMetadata PagingMeta, RUserNote[] Notes) paginatedResult1 = + await QueryablePaginator.Paginate(source, paging); + + RServiceResult userInfo = await _userService.GetUserInformation(userId); + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return new RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)>((PagingMeta: null, Notes: null), userInfo.ExceptionString); + + PublicRAppUser user = userInfo.Result; + + List finalList = new List(); + foreach (RUserNote note in paginatedResult1.Notes) + { + RUserNoteViewModel model = new RUserNoteViewModel(note, user); + if (note.RArtifactMasterRecord != null) + { + if (!statusArray.Contains(note.RArtifactMasterRecord.Status)) + continue; //this may result in paging bugs + } + + if (note.RArtifactItemRecord != null) + { + + RArtifactMasterRecord parent = + await _context.Artifacts + .Where(a => statusArray.Contains(a.Status) && a.Id == note.RArtifactItemRecord.RArtifactMasterRecordId) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (parent == null) + continue; + + model.RelatedEntityName = note.RArtifactItemRecord.Name; + model.RelatedEntityImageId = note.RArtifactItemRecord.Images.FirstOrDefault().Id; + model.RelatedEntityFriendlyUrl = parent.FriendlyUrl + "/" + note.RArtifactItemRecord.FriendlyUrl; + model.RelatedItemParentName = parent.Name; + } + + if(note.RArtifactMasterRecord != null) + { + model.RelatedEntityName = note.RArtifactMasterRecord.Name; + model.RelatedEntityImageId = note.RArtifactMasterRecord.CoverImage.Id; + model.RelatedEntityFriendlyUrl = note.RArtifactMasterRecord.FriendlyUrl; + } + finalList.Add(model); + } + + + return new RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)>((paginatedResult1.PagingMeta, finalList.ToArray())); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)>((PagingMeta: null, Notes: null), exp.ToString()); + } + } + + /// + /// Get All Public Notes + /// + /// + /// + public async Task> GetAllPublicNotes(PagingParameterModel paging) + { + try + { + var source = + _context.UserNotes + .Include(b => b.RArtifactMasterRecord).ThenInclude(a => a.CoverImage) + .Include(b => b.RArtifactItemRecord).ThenInclude(b => b.Images) + .Include(n => n.RAppUser) + .Where(b => b.NoteType == RNoteType.Public) + .OrderByDescending(b => b.DateTime) + .AsQueryable(); + + (PaginationMetadata PagingMeta, RUserNote[] Notes) paginatedResult1 = + await QueryablePaginator.Paginate(source, paging); + + + + List finalList = new List(); + foreach (RUserNote note in paginatedResult1.Notes) + { + RUserNoteViewModel model = new RUserNoteViewModel(note, new PublicRAppUser(note.RAppUser)); + if (note.RArtifactMasterRecord != null) + { + if (note.RArtifactMasterRecord.Status != PublishStatus.Published) + continue; //this may result in paging bugs + } + + if (note.RArtifactItemRecord != null) + { + + RArtifactMasterRecord parent = + await _context.Artifacts + .Where(a => a.Status == PublishStatus.Published && a.Id == note.RArtifactItemRecord.RArtifactMasterRecordId) + .AsNoTracking() + .SingleOrDefaultAsync(); + + if (parent == null) + continue; + + model.RelatedEntityName = note.RArtifactItemRecord.Name; + model.RelatedEntityImageId = note.RArtifactItemRecord.Images.FirstOrDefault().Id; + model.RelatedEntityFriendlyUrl = parent.FriendlyUrl + "/" + note.RArtifactItemRecord.FriendlyUrl; + model.RelatedItemParentName = parent.Name; + } + + if (note.RArtifactMasterRecord != null) + { + model.RelatedEntityName = note.RArtifactMasterRecord.Name; + model.RelatedEntityImageId = note.RArtifactMasterRecord.CoverImage.Id; + model.RelatedEntityFriendlyUrl = note.RArtifactMasterRecord.FriendlyUrl; + } + finalList.Add(model); + } + + + return new RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)>((paginatedResult1.PagingMeta, finalList.ToArray())); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, RUserNoteViewModel[] Notes)>((PagingMeta: null, Notes: null), exp.ToString()); + } + } + + + + /// + /// suggest ganjoor link + /// + /// + /// + /// + public async Task> SuggestGanjoorLink(Guid userId, LinkSuggestion link) + { + try + { + RArtifactMasterRecord artifact = await _context.Artifacts.Where(a => a.FriendlyUrl == link.ArtifactFriendlyUrl).SingleOrDefaultAsync(); + + GanjoorLink alreaySuggest = + await _context.GanjoorLinks. + Where(l => l.GanjoorPostId == link.GanjoorPostId && l.ArtifactId == artifact.Id && l.ItemId == link.ItemId && l.ReviewResult != ReviewResult.Rejected) + .SingleOrDefaultAsync(); + if (alreaySuggest != null) + return new RServiceResult(null, "این مورد پیشتر پیشنهاد شده است."); + + GanjoorLink suggestion = + new GanjoorLink() + { + GanjoorPostId = link.GanjoorPostId, + GanjoorTitle = link.GanjoorTitle, + GanjoorUrl = link.GanjoorUrl, + ArtifactId = artifact.Id, + ItemId = link.ItemId, + SuggestedById = userId, + SuggestionDate = DateTime.Now, + ReviewResult = ReviewResult.Awaiting + }; + + _context.GanjoorLinks.Add(suggestion); + await _context.SaveChangesAsync(); + + string entityName, entityFriendlyUrl; + Guid entityImageId; + + if (suggestion.ItemId == null) + { + entityName = artifact.Name; + entityFriendlyUrl = $"/items/{artifact.FriendlyUrl}"; + entityImageId = artifact.CoverImageId; + } + else + { + RArtifactItemRecord item = await _context.Items.Include(i => i.Images).Where(i => i.Id == suggestion.ItemId).SingleOrDefaultAsync(); + entityName = artifact.Name + " » " + item.Name; + entityFriendlyUrl = $"/items/{artifact.FriendlyUrl}/{item.FriendlyUrl}"; + entityImageId = item.Images.First().Id; + } + + GanjoorLinkViewModel viewModel = new GanjoorLinkViewModel(suggestion, entityName, entityFriendlyUrl, entityImageId); + return new RServiceResult(viewModel); + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// get suggested ganjoor links + /// + /// + /// + /// + public async Task> GetSuggestedLinks(ReviewResult status, bool notSynced) + { + try + { + GanjoorLink[] links = + await _context.GanjoorLinks + .Include(l => l.SuggestedBy) + .Include(l => l.Artifact) + .Include(l => l.Item).ThenInclude(i => i.Images) + .Where(l => l.ReviewResult == status && (notSynced == false || !l.Synchronized)) + .OrderBy( l => l.SuggestionDate) + .ToArrayAsync(); + List result = new List(); + foreach(GanjoorLink link in links) + { + result.Add + ( + new GanjoorLinkViewModel + ( + link, + link.Item == null ? link.Artifact.Name : link.Artifact.Name + " » " + link.Item.Name, + link.Item == null ? $"/items/{link.Artifact.FriendlyUrl}" : $"/items/{link.Artifact.FriendlyUrl}/{link.Item.FriendlyUrl}", + link.Item == null ? link.Artifact.CoverImageId : link.Item.Images.First().Id + ) + ); + } + return new RServiceResult(result.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Review Suggested Link + /// + /// + /// + /// + /// + public async Task> ReviewSuggestedLink(Guid linkId, Guid userId, ReviewResult result) + { + try + { + GanjoorLink link = + await _context.GanjoorLinks + .Include(l => l.Artifact).ThenInclude(a => a.Tags).ThenInclude(t => t.RTag) + .Include(l => l.Item).ThenInclude(i => i.Tags).ThenInclude(t => t.RTag) + .Where(l => l.Id == linkId) + .SingleOrDefaultAsync(); + + link.ReviewResult = result; + link.ReviewerId = userId; + link.ReviewDate = DateTime.Now; + + _context.GanjoorLinks.Update(link); + + if(link.ReviewResult == ReviewResult.Approved) + { + RTagValue tag = await TagHandler.PrepareAttribute(_context, "Ganjoor Link", link.GanjoorTitle, 1); + tag.ValueSupplement = link.GanjoorUrl; + if(link.Item == null) + { + link.Artifact.Tags.Add(tag); + _context.Artifacts.Update(link.Artifact); + } + else + { + link.Item.Tags.Add(tag); + _context.Items.Update(link.Item); + } + + //add TOC: + using (var client = new HttpClient()) + { + using (var httpResult = await client.GetAsync(link.GanjoorUrl)) + { + if (httpResult.IsSuccessStatusCode) + { + string html = await httpResult.Content.ReadAsStringAsync(); + int nIndexFirstParagraphOpenning = html.IndexOf("

"); + if (nIndexFirstParagraphOpenning != -1) + { + int nIndexFirstParagraphClosing = html.IndexOf("

"); + if (nIndexFirstParagraphClosing > nIndexFirstParagraphOpenning) + { + string paragraphContent = html.Substring(nIndexFirstParagraphOpenning + "

".Length, 1 + nIndexFirstParagraphClosing - (nIndexFirstParagraphOpenning + "

".Length)); + if (paragraphContent.Length != 0) + { + int openIndex = paragraphContent.IndexOf('<'); + while (paragraphContent.Length > 0 && (openIndex != -1)) + { + string afterClosing = ""; + int closeIndex = paragraphContent.IndexOf('>', openIndex); + if (closeIndex != -1) + afterClosing = paragraphContent.Substring(closeIndex + 1); + paragraphContent = paragraphContent.Substring(0, openIndex) + afterClosing; + openIndex = paragraphContent.IndexOf('<'); + } + paragraphContent = paragraphContent.Replace("<", "").Replace(">", ""); + if (paragraphContent.Length > 100) + paragraphContent = paragraphContent.Substring(0, 100) + " ..."; + RTagValue toc = await TagHandler.PrepareAttribute(_context, "Title in TOC", paragraphContent, 1); + toc.ValueSupplement = "1";//font size + if (link.Item == null) + { + if (link.Artifact.Tags.Where(t => t.RTag.Name == "Title in TOC" && t.Value == toc.Value).Count() == 0) + { + toc.Order = 1 + link.Artifact.Tags.Where(t => t.RTag.NameInEnglish == "Title in TOC").Count(); + link.Artifact.Tags.Add(toc); + _context.Artifacts.Update(link.Artifact); + } + } + else + { + if (link.Item.Tags.Where(t => t.RTag.Name == "Title in TOC" && t.Value == toc.Value).Count() == 0) + { + toc.Order = 1 + link.Item.Tags.Where(t => t.RTag.NameInEnglish == "Title in TOC").Count(); + link.Item.Tags.Add(toc); + _context.Items.Update(link.Item); + } + + } + } + + } + } + } + + } + } + } + + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Temporary api + /// + /// + public async Task> AddTOCForSuggestedLinks() + { + try + { + GanjoorLink[] links = + await _context.GanjoorLinks + .Include(l => l.Artifact).ThenInclude(a => a.Tags).ThenInclude(t => t.RTag) + .Include(l => l.Item).ThenInclude(i => i.Tags).ThenInclude(t => t.RTag) + .Where(l => l.ReviewResult == ReviewResult.Approved) + .ToArrayAsync(); + + List lst = new List(); + + foreach(GanjoorLink link in links) + { + using(var client = new HttpClient()) + { + using (var result = await client.GetAsync(link.GanjoorUrl)) + { + if (result.IsSuccessStatusCode) + { + string html = await result.Content.ReadAsStringAsync(); + int nIndexFirstParagraphOpenning = html.IndexOf("

"); + if(nIndexFirstParagraphOpenning != -1) + { + int nIndexFirstParagraphClosing = html.IndexOf("

"); + if(nIndexFirstParagraphClosing > nIndexFirstParagraphOpenning) + { + string paragraphContent = html.Substring(nIndexFirstParagraphOpenning + "

".Length, 1 + nIndexFirstParagraphClosing - (nIndexFirstParagraphOpenning + "

".Length)); + if (paragraphContent.Length == 0) + continue; + if (paragraphContent.Length > 100) + paragraphContent = paragraphContent.Substring(0, 100) + " ..."; + lst.Add(paragraphContent); + RTagValue tag = await TagHandler.PrepareAttribute(_context, "Title in TOC", paragraphContent, 1); + tag.ValueSupplement = "1";//font size + if (link.Item == null) + { + if(link.Artifact.Tags.Where(t => t.RTag.Name == "Title in TOC" && t.Value == tag.Value).Count() == 0) + { + tag.Order = 1 + link.Artifact.Tags.Where(t => t.RTag.NameInEnglish == "Title in TOC").Count(); + link.Artifact.Tags.Add(tag); + _context.Artifacts.Update(link.Artifact); + } + } + else + { + if (link.Item.Tags.Where(t => t.RTag.Name == "Title in TOC" && t.Value == tag.Value).Count() == 0) + { + tag.Order = 1 + link.Item.Tags.Where(t => t.RTag.NameInEnglish == "Title in TOC").Count(); + link.Item.Tags.Add(tag); + _context.Items.Update(link.Item); + } + + } + } + } + } + + } + } + } + + + + await _context.SaveChangesAsync(); + + return new RServiceResult(lst.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Synchronize suggested link + /// + /// + /// + public async Task> SynchronizeSuggestedLink(Guid linkId) + { + try + { + GanjoorLink link = + await _context.GanjoorLinks + .Where(l => l.Id == linkId) + .SingleOrDefaultAsync(); + + link.Synchronized = true; + + _context.GanjoorLinks.Update(link); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// get suggested pinterest links + /// + /// + /// + /// + public async Task> GetSuggestedPinterestLinks(ReviewResult status, bool notSynced) + { + try + { + PinterestLink[] links = + await _context.PinterestLinks + .Include(l => l.Artifact) + .Include(l => l.Item).ThenInclude(i => i.Images) + .Where(l => l.ReviewResult == status && (notSynced == false || !l.Synchronized)) + .OrderBy(l => l.SuggestionDate) + .ToArrayAsync(); + List result = new List(); + foreach (PinterestLink link in links) + { + result.Add + ( + new PinterestLinkViewModel + ( + link, + link.Artifact == null? null : link.Artifact.Name + " » " + link.Item.Name, + link.Artifact == null ? null : $"/items/{link.Artifact.FriendlyUrl}/{link.Item.FriendlyUrl}", + link.Item == null ? Guid.Empty : link.Item.Images.First().Id + ) + ); + } + return new RServiceResult(result.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// suggest pinterest link + /// + /// + /// + public async Task> SuggestPinterestLink(PinterestSuggestion suggestion) + { + try + { + + PinterestLink existingLink = + await _context.PinterestLinks.Where + ( + p => + p.GanjoorPostId == suggestion.GanjoorPostId + && + p.PinterestUrl == suggestion.PinterestUrl + && + (p.ReviewResult == ReviewResult.Approved || p.ReviewResult == ReviewResult.Awaiting) + ).FirstOrDefaultAsync(); + if (existingLink != null) + return new RServiceResult(null, "این مورد پیشتر پیشنهاد شده است."); + + PinterestLink link = new PinterestLink() + { + GanjoorPostId = suggestion.GanjoorPostId, + GanjoorTitle = suggestion.GanjoorTitle, + GanjoorUrl = suggestion.GanjoorUrl, + AltText = suggestion.AltText, + LinkType = suggestion.LinkType, + PinterestUrl = suggestion.PinterestUrl, + PinterestImageUrl = suggestion.PinterestImageUrl, + ReviewResult = ReviewResult.Awaiting, + SuggestionDate = DateTime.Now, + Synchronized = false + }; + _context.PinterestLinks.Add(link); + await _context.SaveChangesAsync(); + return new RServiceResult(new PinterestLinkViewModel(link, null, null, Guid.Empty) ); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Review Suggested Pinterest Link + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> ReviewSuggestedPinterestLink(Guid linkId, Guid userId, string altText, ReviewResult result, string reviewDesc, string imageUrl) + { + try + { + PinterestLink link = + await _context.PinterestLinks + .Where(l => l.Id == linkId) + .SingleOrDefaultAsync(); + + link.ReviewResult = result; + link.ReviewerId = userId; + link.ReviewDate = DateTime.Now; + if(!string.IsNullOrEmpty(altText)) + link.AltText = altText; + if (!string.IsNullOrEmpty(imageUrl)) + link.PinterestImageUrl = imageUrl; + link.ReviewDesc = reviewDesc; + + if (link.ReviewResult == ReviewResult.Approved) + { + PinterestLink relatedArtifactLink = + await _context.PinterestLinks + .Where(p => p.PinterestUrl == link.PinterestUrl && p.PinterestImageUrl == link.PinterestImageUrl + && link.Id != p.Id + && p.ReviewResult == ReviewResult.Approved) + .FirstOrDefaultAsync(); + string titleInToc = link.AltText; + if (titleInToc.IndexOfAny(new char[] { '\n', '\r' }) != -1) + { + string[] lines = titleInToc.Replace('\n', '\r').Split('\r', StringSplitOptions.RemoveEmptyEntries); + titleInToc = lines[0]; + } + if (relatedArtifactLink != null) + { + + link.ArtifactId = relatedArtifactLink.ArtifactId; + link.ItemId = relatedArtifactLink.ItemId; + + //TODO: append tags to this existing artifact and its items + + + } + else + { + using (var client = new HttpClient()) + { + var imageResult = await client.GetAsync(link.PinterestImageUrl); + + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(link.PinterestImageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + string friendlyUrl = Guid.NewGuid().ToString(); + string fileName = friendlyUrl + ".jpg"; + while((await _context.PictureFiles.Where(p => p.OriginalFileName == fileName).FirstOrDefaultAsync()) != null) + { + fileName = Guid.NewGuid() + "-" + friendlyUrl + ".jpg"; + } + RServiceResult picture = await _pictureFileService.Add(link.GanjoorTitle, link.AltText, 1, null, link.PinterestUrl, imageStream, fileName, "Pinterest"); + if (picture.Result == null) + { + return new RServiceResult(false, $"_pictureFileService.Add : {picture.ExceptionString}"); + } + + + + RArtifactMasterRecord book = new RArtifactMasterRecord(titleInToc, titleInToc) + { + Status = PublishStatus.Published, + DateTime = DateTime.Now, + LastModified = DateTime.Now, + CoverItemIndex = 0, + FriendlyUrl = friendlyUrl + }; + + List meta = new List(); + + RTagValue tag = await TagHandler.PrepareAttribute(_context, "Title", titleInToc, 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(_context, "Description", link.AltText, 1); + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(_context, "Source", link.LinkType == LinkType.Pinterest ? "Pinterest" : link.LinkType == LinkType.Instagram ? "Instagram" : link.PinterestUrl, 1); + tag.ValueSupplement = link.PinterestUrl; + + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(_context, "Ganjoor Link", link.GanjoorTitle, 1); + tag.ValueSupplement = link.GanjoorUrl; + + meta.Add(tag); + + tag = await TagHandler.PrepareAttribute(_context, "Title in TOC", titleInToc, 1); + tag.ValueSupplement = "1";//font size + + meta.Add(tag); + + book.Tags = meta.ToArray(); + + List pages = new List(); + + int order = 1; + + RArtifactItemRecord page = new RArtifactItemRecord() + { + Name = titleInToc, + NameInEnglish = titleInToc, + Description = link.AltText, + DescriptionInEnglish = link.AltText, + Order = order, + FriendlyUrl = $"p{$"{order}".PadLeft(4, '0')}", + LastModified = DateTime.Now + }; + + page.Images = new RPictureFile[] { picture.Result }; + page.CoverImageIndex = 0; + + page.Tags = meta.ToArray(); + + book.CoverImage = RPictureFile.Duplicate(picture.Result); + + pages.Add(page); + + book.Items = pages.ToArray(); + book.ItemCount = pages.Count; + + await _context.Artifacts.AddAsync(book); + await _context.SaveChangesAsync(); + + link.ArtifactId = book.Id; + link.ItemId = (await _context.Items.Where(i => i.RArtifactMasterRecordId == link.ArtifactId).FirstOrDefaultAsync()).Id; + } + } + else + { + return new RServiceResult(false, $"Http result is not ok (url {link.PinterestImageUrl}"); + } + } + + } + } + + _context.PinterestLinks.Update(link); + + + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Synchronize suggested pinterest link + /// + /// + /// + public async Task> SynchronizeSuggestedPinterestLink(Guid linkId) + { + try + { + PinterestLink link = + await _context.PinterestLinks + .Where(l => l.Id == linkId) + .SingleOrDefaultAsync(); + + link.Synchronized = true; + + _context.PinterestLinks.Update(link); + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Add Notification + /// + /// + /// + /// + /// + public async Task> PushNotification(Guid userId, string subject, string htmlText) + { + try + { + RUserNotification notification = + new RUserNotification() + { + UserId = userId, + DateTime = DateTime.Now, + Status = NotificationStatus.Unread, + Subject = subject, + HtmlText = htmlText + }; + _context.Notifications.Add(notification); + await _context.SaveChangesAsync(); + return new RServiceResult(notification); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Switch Notification Status + /// + /// + /// updated notification object + public async Task> SwitchNotificationStatus(Guid notificationId) + { + try + { + RUserNotification notification = + await _context.Notifications.Where(n => n.Id == notificationId).SingleAsync(); + notification.Status = notification.Status == NotificationStatus.Unread ? NotificationStatus.Read : NotificationStatus.Unread; + _context.Notifications.Update(notification); + await _context.SaveChangesAsync(); + return new RServiceResult(notification); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// Delete Notification + /// + /// + /// + public async Task> DeleteNotification(Guid notificationId) + { + try + { + RUserNotification notification = + await _context.Notifications.Where(n => n.Id == notificationId).SingleAsync(); + _context.Notifications.Remove(notification); + await _context.SaveChangesAsync(); + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Get User Notifications + /// + /// + /// + public async Task> GetUserNotifications(Guid userId) + { + try + { + return new RServiceResult + ( + await _context.Notifications + .Where(n => n.UserId == userId) + .OrderByDescending(n => n.DateTime) + .ToArrayAsync() + ); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Get Unread User Notifications Count + /// + /// + /// + public async Task> GetUnreadUserNotificationsCount(Guid userId) + { + try + { + return new RServiceResult + ( + await _context.Notifications + .Where(n => n.UserId == userId && n.Status == NotificationStatus.Unread) + .CountAsync() + ); + } + catch (Exception exp) + { + return new RServiceResult(0, exp.ToString()); + } + } + + + /// + /// Database Contetxt + /// + protected readonly RMuseumDbContext _context; + + /// + /// Picture File Service + /// + protected readonly IPictureFileService _pictureFileService; + + + /// + /// Configuration + /// + protected IConfiguration Configuration { get; } + + /// + /// Background Task Queue Instance + /// + protected readonly IBackgroundTaskQueue _backgroundTaskQueue; + + /// + /// User Service + /// + protected readonly IAppUserService _userService; + + /// + /// constructor + /// + /// + /// + /// + /// + /// + public ArtifactService(RMuseumDbContext context, IConfiguration configuration, IPictureFileService pictureFileService, IBackgroundTaskQueue backgroundTaskQueue, IAppUserService userService) + { + _context = context; + _pictureFileService = pictureFileService; + Configuration = configuration; + _backgroundTaskQueue = backgroundTaskQueue; + _userService = userService; + } + } +} diff --git a/RMuseum/Services/Implementation/BLTileMixer.cs b/RMuseum/Services/Implementation/BLTileMixer.cs new file mode 100644 index 00000000..66fd077d --- /dev/null +++ b/RMuseum/Services/Implementation/BLTileMixer.cs @@ -0,0 +1,258 @@ +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + /// + /// British Library Tile Downloader And Mixer + /// + public class BLTileMixer + { + + /// + /// Constructor + /// + public BLTileMixer() + { + + } + + private Dictionary<(int x, int y), Image> _tiles = null; + + public async Task> DownloadMix(string pageUrl, int order) + { + try + { + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromMinutes(5); + + _tiles = new Dictionary<(int x, int y), Image>(); + + + int max_x = -1; + for (int x = 0; ; x++) + { + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={pageUrl}_files/13/{x}_0.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + + imageStream.Position = 0; + try + { + Image tile = Image.FromStream(imageStream); + _tiles.Add((x, 0), tile); + max_x = x; + } + catch (Exception aexp) + { + if (aexp is ArgumentException) + { + break; + } + throw aexp; + } + } + } + else + { + + imageResult.Dispose(); + return new RServiceResult(null, $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"); + } + } + + int max_y = -1; + for (int y = 1; ; y++) + { + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={pageUrl}_files/13/0_{y}.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + imageStream.Position = 0; + try + { + Image tile = Image.FromStream(imageStream); + _tiles.Add((0, y), tile); + max_y = y; + } + catch (Exception aexp) + { + if (aexp is ArgumentException) + { + break; + } + throw aexp; + } + } + } + else + { + imageResult.Dispose(); + return new RServiceResult(null, $"Http result is not ok ({imageResult.StatusCode}) for page {order}, url {imageUrl}"); + } + } + + List threadsList = new List(); + + List<(int x, int y)> unprocessedtiles = new List<(int x, int y)>(); + for (int x = 0; x <= max_x; x++) + for (int y = 0; y <= max_y; y++) + if (_tiles.TryGetValue((x, y), out Image tmp) == false) + { + unprocessedtiles.Add((x, y)); + } + + foreach((int x, int y) unprocessedtile in unprocessedtiles) + { + Thread th = new Thread + ( + new ThreadStart + ( + async () => + { + await _ProcessTile(pageUrl, unprocessedtile.x, unprocessedtile.y); + } + ) + ); + th.Start(); + Thread.Sleep(100); + threadsList.Add(th); + } + + + foreach (Thread th in threadsList) + th.Join(); + + int tileWidth = _tiles[(0, 0)].Width; + int tileHeight = _tiles[(0, 0)].Height; + + int imageWidth = tileWidth * (max_x + 1); + int imageHeight = tileHeight * (max_y + 1); + + using (Image image = new Bitmap(imageWidth, imageHeight)) + { + using (Graphics g = Graphics.FromImage(image)) + { + for (int x = 0; x <= max_x; x++) + for (int y = 0; y < max_y; y++) + { + g.DrawImage(_tiles[(x, y)], new Point(x * tileWidth, y * tileHeight)); + _tiles[(x, y)].Dispose(); + } + + Stream imageStream = new MemoryStream(); + ImageCodecInfo jpgEncoder = _GetEncoder(ImageFormat.Jpeg); + + Encoder myEncoder = + Encoder.Quality; + EncoderParameters jpegParameters = new EncoderParameters(1); + + EncoderParameter qualityParameter = new EncoderParameter(myEncoder, 92L); + jpegParameters.Param[0] = qualityParameter; + + image.Save(imageStream, jpgEncoder, jpegParameters); + imageStream.Position = 0; + return new RServiceResult(imageStream); + } + } + } + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + private async Task _ProcessTile(string pageUrl, int x, int y) + { + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromMinutes(5); + string imageUrl = $"http://www.bl.uk/manuscripts/Proxy.ashx?view={pageUrl}_files/13/{x}_{y}.jpg"; + var imageResult = await client.GetAsync(imageUrl); + + int _ImportRetryCount = 5; + int _ImportRetryInitialSleep = 500; + int retryCount = 0; + while (retryCount < _ImportRetryCount && !imageResult.IsSuccessStatusCode && imageResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + imageResult.Dispose(); + Thread.Sleep(_ImportRetryInitialSleep * (retryCount + 1)); + imageResult = await client.GetAsync(imageUrl); + retryCount++; + } + + if (imageResult.IsSuccessStatusCode) + { + using (Stream imageStream = await imageResult.Content.ReadAsStreamAsync()) + { + if (imageStream.Length == 0) + return; + imageStream.Position = 0; + _tiles.Add((x, y), Image.FromStream(imageStream)); + } + } + else + { + imageResult.Dispose(); + } + } + } + + /// + /// find image encodder + /// + /// + /// + private ImageCodecInfo _GetEncoder(ImageFormat format) + { + ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); + foreach (ImageCodecInfo codec in codecs) + { + if (codec.FormatID == format.Guid) + { + return codec; + } + } + return null; + } + } +} diff --git a/RMuseum/Services/Implementation/BackgroundTaskQueue.cs b/RMuseum/Services/Implementation/BackgroundTaskQueue.cs new file mode 100644 index 00000000..e8c48102 --- /dev/null +++ b/RMuseum/Services/Implementation/BackgroundTaskQueue.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + internal class BackgroundTaskQueue : IBackgroundTaskQueue + { + private ConcurrentQueue> _workItems = + new ConcurrentQueue>(); + private SemaphoreSlim _signal = new SemaphoreSlim(0); + + public void QueueBackgroundWorkItem( + Func workItem) + { + if (workItem == null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + _workItems.Enqueue(workItem); + _signal.Release(); + } + + public async Task> DequeueAsync( + CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out var workItem); + + return workItem; + } + } +} diff --git a/RMuseum/Services/Implementation/MailKitEmailSender.cs b/RMuseum/Services/Implementation/MailKitEmailSender.cs new file mode 100644 index 00000000..8b858e37 --- /dev/null +++ b/RMuseum/Services/Implementation/MailKitEmailSender.cs @@ -0,0 +1,33 @@ +using RSecurityBackend.Models.Mail; +using RSecurityBackend.Services.Implementation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + public class MailKitEmailSender : MailKitEmailSenderBase + { + /// + /// SmptConfig + /// + public override SmptConfig SmptConfig + { + get + { + return + new SmptConfig() + { + port = 25, + server = "ganjgah.ir", + smtpUsername = "noreply@ganjgah.ir", + smtpPassword = "sahY%6&$OnD67i0978", + from = "noreply@ganjgah.ir", + useSsl = false + }; + } + } + } + +} diff --git a/RMuseum/Services/Implementation/PictureFileService.cs b/RMuseum/Services/Implementation/PictureFileService.cs new file mode 100644 index 00000000..b704180e --- /dev/null +++ b/RMuseum/Services/Implementation/PictureFileService.cs @@ -0,0 +1,588 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using RMuseum.DbContext; +using RMuseum.Models.Artifact; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using RSecurityBackend.Services; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + /// + /// manipulating picture files + /// + public class PictureFileService : IPictureFileService + { + + /// + /// recover from files + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> RecoverFromeFiles + ( + string title, string description, int order, string srcurl, string orignalFilePath, string normalFilePath, string thumbFilePath, string originalFileNameForStreams, string imageFolderName + ) + { + try + { + RPictureFile pictureFile = + new RPictureFile() + { + Title = title, + TitleInEnglish = title, + Description = description, + DescriptionInEnglish = description, + Order = order, + SrcUrl = srcurl, + Status = PublishStatus.Draft, + DataTime = DateTime.Now, + LastModified = DateTime.Now, + LastModifiedMeta = DateTime.Now, + FolderName = string.IsNullOrEmpty(imageFolderName) ? DateTime.Now.ToString("yyyy-MM") : imageFolderName, + ContentType = "image/jpeg", + OriginalFileName = originalFileNameForStreams, + StoredFileName = orignalFilePath, + NormalSizeImageStoredFileName = normalFilePath, + ThumbnailImageStoredFileName = thumbFilePath, + FileSizeInBytes = (await File.ReadAllBytesAsync(orignalFilePath)).Length + + }; + + using(Image img = Image.FromFile(orignalFilePath)) + { + pictureFile.ImageWidth = img.Width; + pictureFile.ImageHeight = img.Height; + } + + using (Image img = Image.FromFile(normalFilePath)) + { + pictureFile.NormalSizeImageWidth = img.Width; + pictureFile.NormalSizeImageHeight = img.Height; + } + + using (Image img = Image.FromFile(thumbFilePath)) + { + pictureFile.ThumbnailImageWidth = img.Width; + pictureFile.ThumbnailImageHeight = img.Height; + } + + + //here is a problem, this method could be called from a background service where _context is disposed, so I need to renew it + /*using(RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + await context.PictureFiles.AddAsync(pictureFile.Result); + await context.SaveChangesAsync(); + }*/ + return new RServiceResult(pictureFile); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// add new picture + /// + /// + /// + /// + /// + /// + /// + /// + /// pass empty if you want a generic date based folder + /// + public async Task> Add + ( + string title, string description, int order, IFormFile file, string srcurl, Stream stream, string originalFileNameForStreams, string imageFolderName + ) + { + try + { + + RServiceResult + pictureFile = + await ProcessImage + ( + file, + new RPictureFile() + { + Title = title, + TitleInEnglish = title, + Description = description, + DescriptionInEnglish = description, + Order = order, + SrcUrl = srcurl, + Status = PublishStatus.Draft, + DataTime = DateTime.Now, + LastModified = DateTime.Now, + LastModifiedMeta = DateTime.Now, + FolderName = string.IsNullOrEmpty(imageFolderName) ? DateTime.Now.ToString("yyyy-MM") : imageFolderName + + }, + stream, + originalFileNameForStreams + ); + if (!string.IsNullOrEmpty(pictureFile.ExceptionString)) + return new RServiceResult(null, pictureFile.ExceptionString); + + //here is a problem, this method could be called from a background service where _context is disposed, so I need to renew it + /*using(RMuseumDbContext context = new RMuseumDbContext(Configuration)) + { + await context.PictureFiles.AddAsync(pictureFile.Result); + await context.SaveChangesAsync(); + }*/ + return new RServiceResult(pictureFile.Result); + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// returns image info + /// + /// + /// + public async Task> GetImage(Guid id) + { + try + { + return new RServiceResult( + await _context.PictureFiles + .Where(p => p.Id == id) + .SingleOrDefaultAsync() + ); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Rotate Image in 90 deg. multiplicants: 90, 180 or 270 + /// + /// + /// + /// + public async Task> RotateImage(Guid id, int degIn90mul) + { + try + { + if(degIn90mul != 90 && degIn90mul != 180 && degIn90mul != 270) + { + return new RServiceResult(null, "degIn90mul could only be equal to these 3 values: 90, 180 and 270"); + } + RPictureFile rPictureFile = + await _context.PictureFiles + .Where(p => p.Id == id) + .SingleOrDefaultAsync(); + + string origPath = GetImagePath(rPictureFile).Result; + using(MemoryStream msRotated = new MemoryStream()) + { + using (Image img = Image.FromFile(origPath)) + { + img.RotateFlip(degIn90mul == 90 ? RotateFlipType.Rotate90FlipNone : degIn90mul == 180 ? RotateFlipType.Rotate180FlipNone : RotateFlipType.Rotate270FlipNone); + + ImageCodecInfo jpgEncoder = GetEncoder(ImageFormat.Jpeg); + + Encoder myEncoder = + Encoder.Quality; + EncoderParameters jpegParameters = new EncoderParameters(1); + + EncoderParameter qualityParameter = new EncoderParameter(myEncoder, 92L); + jpegParameters.Param[0] = qualityParameter; + + img.Save(msRotated, jpgEncoder, jpegParameters); + } + + File.Move(GetImagePath(rPictureFile, "thumb").Result, GetImagePath(rPictureFile, "thumb").Result + ".bak"); + File.Move(GetImagePath(rPictureFile, "norm").Result, GetImagePath(rPictureFile, "norm").Result + ".bak"); + File.Move(origPath, origPath + ".bak"); + + RServiceResult + result = + await ProcessImage + ( + null, + rPictureFile, + msRotated, + rPictureFile.OriginalFileName + ); + if (!string.IsNullOrEmpty(result.ExceptionString)) + return new RServiceResult(null, result.ExceptionString); + result.Result.LastModified = DateTime.Now; + _context.PictureFiles.Update(result.Result); + await _context.SaveChangesAsync(); + + File.Delete(GetImagePath(rPictureFile, "thumb").Result + ".bak"); + File.Delete(GetImagePath(rPictureFile, "norm").Result + ".bak"); + File.Delete(origPath + ".bak"); + + return result; + + } + + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// find image encodder + /// + /// + /// + public ImageCodecInfo GetEncoder(ImageFormat format) + { + ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); + foreach (ImageCodecInfo codec in codecs) + { + if (codec.FormatID == format.Guid) + { + return codec; + } + } + return null; + } + + + /// + /// returns image file stream + /// + /// + /// + /// + public RServiceResult GetImagePath(RPictureFile image, string sz = "orig") + { + try + { + string fileName = sz == "thumb" ? image.ThumbnailImageStoredFileName : sz == "norm" ? image.NormalSizeImageStoredFileName : image.StoredFileName; + return new RServiceResult(Path.Combine(ImageStoragePath, image.FolderName, fileName)); + } + catch(Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// فرمت تصاویر بندانگشتی و ... + /// + /// image/jpeg + protected string ResizedImageContentType { get { return $"{Configuration.GetSection("PictureFileService")["ResizedImageContentType"]}"; } } + + /// + /// عرض تصویر با اندازه نرمال - جهت نمایش در صفحات + /// + protected int NormalImageMaxWidth { get { return int.Parse($"{Configuration.GetSection("PictureFileService")["NormalImageMaxWidth"]}"); } } + + /// + /// /// طول تصویر با اندازه نرمال - جهت نمایش در صفحات + /// + protected int NormalImageMaxHeight { get { return int.Parse($"{Configuration.GetSection("PictureFileService")["NormalImageMaxHeight"]}"); } } + + /// + /// عرض تصویر بندانگشتی + /// + protected int ThumbnailImageWidth { get { return int.Parse($"{Configuration.GetSection("PictureFileService")["ThumbnailImageWidth"]}"); } } + + /// + /// طول تصویر بندانگشتی + /// + protected int ThumbnailImageMaxHeight { get { return int.Parse($"{Configuration.GetSection("PictureFileService")["ThumbnailMaxHeight"]}"); } } + + + /// + /// Image Storage Path + /// + public string ImageStoragePath { get { return $"{Configuration.GetSection("PictureFileService")["StoragePath"]}"; } } + + private async Task> ProcessImage(IFormFile uploadedImage, RPictureFile pictureFile, Stream stream, string originalFileNameForStreams) + { + if (uploadedImage == null && stream == null) + { + return new RServiceResult(null, "ProcessImage: uploadedImage == null && stream == null"); + } + + pictureFile.ContentType = uploadedImage == null ? "image/jpeg" : uploadedImage.ContentType; + pictureFile.FileSizeInBytes = uploadedImage == null ? stream.Length : uploadedImage.Length; + pictureFile.OriginalFileName = uploadedImage == null ? originalFileNameForStreams : uploadedImage.FileName; + + + + string fullDirStorePath = Path.Combine(ImageStoragePath, pictureFile.FolderName); + string originalStorePath = Path.Combine(fullDirStorePath, "orig"); + string normalStorePath = Path.Combine(fullDirStorePath, "norm"); + string thumbStorePath = Path.Combine(fullDirStorePath, "thumb"); + + + foreach(string path in new string[] + { + fullDirStorePath, + originalStorePath, + normalStorePath, + thumbStorePath + }) + { + if (!Directory.Exists(path)) + { + try + { + Directory.CreateDirectory(path); + } + catch + { + return new RServiceResult(null, $"ProcessImage: create dir failed {path}"); + } + } + } + + + + + string ext = uploadedImage == null ? ".jpg" : Path.GetExtension(uploadedImage.FileName); + pictureFile.StoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + ext; + + string originalFileStorePath = Path.Combine(originalStorePath, pictureFile.StoredFileName); + while (File.Exists(originalFileStorePath)) + { + pictureFile.StoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName)+ "-" + Guid.NewGuid().ToString() + ext; + originalFileStorePath = Path.Combine(originalStorePath, pictureFile.StoredFileName); + } + pictureFile.StoredFileName = $@"orig\{pictureFile.StoredFileName}"; + using (FileStream fsMain = new FileStream(originalFileStorePath, FileMode.Create)) + { + if (uploadedImage != null) + await uploadedImage.CopyToAsync(fsMain); + else + { + stream.Position = 0; + await stream.CopyToAsync(fsMain); + } + } + + using (MemoryStream ms = new MemoryStream()) + { + if (uploadedImage != null) + await uploadedImage.CopyToAsync(ms); + else + { + stream.Position = 0; + await stream.CopyToAsync(ms); + } + + + using (Image img = Bitmap.FromStream(ms)) + { + pictureFile.ImageWidth = img.Width; + pictureFile.ImageHeight = img.Height; + if (img.Width <= NormalImageMaxWidth && img.Height <= NormalImageMaxHeight && ext.Equals(".jpg", StringComparison.InvariantCultureIgnoreCase)) + { + pictureFile.NormalSizeImageStoredFileName = pictureFile.StoredFileName; + pictureFile.NormalSizeImageWidth = img.Width; + pictureFile.NormalSizeImageHeight = img.Height; + } + else + { + pictureFile.NormalSizeImageWidth = NormalImageMaxWidth; + pictureFile.NormalSizeImageHeight = NormalImageMaxWidth * pictureFile.ImageHeight / pictureFile.ImageWidth; + + if (pictureFile.NormalSizeImageHeight > NormalImageMaxHeight) + { + pictureFile.NormalSizeImageHeight = NormalImageMaxHeight; + pictureFile.NormalSizeImageWidth = NormalImageMaxHeight * pictureFile.ImageWidth / pictureFile.ImageHeight; + } + + //روی خود img تأثیر می گذارد + Image resized = new Bitmap(pictureFile.NormalSizeImageWidth, pictureFile.NormalSizeImageHeight); + using(Graphics gResized = Graphics.FromImage(resized)) + { + gResized.DrawImage(img, 0, 0, pictureFile.NormalSizeImageWidth, pictureFile.NormalSizeImageHeight); + } + using (MemoryStream msNormal = new MemoryStream()) + { + resized.Save(msNormal, ImageFormat.Jpeg); + } + + pictureFile.NormalSizeImageStoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + ".jpg"; + string normalFileStorePath = Path.Combine(normalStorePath, pictureFile.NormalSizeImageStoredFileName); + + while (File.Exists(normalFileStorePath)) + { + pictureFile.NormalSizeImageStoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + "-" + Guid.NewGuid().ToString() + ".jpg"; + normalFileStorePath = Path.Combine(normalStorePath, pictureFile.NormalSizeImageStoredFileName); + } + pictureFile.NormalSizeImageStoredFileName = $@"norm\{pictureFile.NormalSizeImageStoredFileName}"; + + using (FileStream fsNormal = new FileStream(normalFileStorePath, FileMode.Create)) + { + resized.Save(fsNormal, ImageFormat.Jpeg); + } + + } + + pictureFile.ThumbnailImageWidth = ThumbnailImageWidth; + pictureFile.ThumbnailImageHeight = ThumbnailImageWidth * pictureFile.ImageHeight / pictureFile.ImageWidth; + + if(pictureFile.ThumbnailImageHeight > ThumbnailImageMaxHeight) + { + pictureFile.ThumbnailImageHeight = ThumbnailImageMaxHeight; + pictureFile.ThumbnailImageWidth = pictureFile.ThumbnailImageHeight * pictureFile.ImageWidth / pictureFile.ImageHeight; + } + + //روی خود img تأثیر می گذارد + // به احتمال قوی داریم روی تصویر resize کار می‌کنیم + Image thumbnail = new Bitmap(pictureFile.ThumbnailImageWidth, pictureFile.ThumbnailImageHeight); + using (Graphics gThumbnail = Graphics.FromImage(thumbnail)) + { + gThumbnail.DrawImage(img, 0, 0, pictureFile.ThumbnailImageWidth, pictureFile.ThumbnailImageHeight); + } + using (MemoryStream msThumbnail = new MemoryStream()) + { + thumbnail.Save(msThumbnail, ImageFormat.Jpeg); + } + + pictureFile.ThumbnailImageStoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + ".jpg"; + string thumbFileStorePath = Path.Combine(thumbStorePath, pictureFile.ThumbnailImageStoredFileName); + + while (File.Exists(thumbFileStorePath)) + { + pictureFile.ThumbnailImageStoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + "-" + Guid.NewGuid().ToString() + ".jpg"; + thumbFileStorePath = Path.Combine(thumbStorePath, pictureFile.ThumbnailImageStoredFileName); + } + + pictureFile.ThumbnailImageStoredFileName = $@"thumb\{pictureFile.ThumbnailImageStoredFileName}"; + + using (FileStream fsThumb = new FileStream(thumbFileStorePath, FileMode.Create)) + { + thumbnail.Save(fsThumb, ImageFormat.Jpeg); + } + + } + } + return new RServiceResult(pictureFile); + } + + /// + /// Generated Cropped Image Based On ThumbnailCoordinates For Notes + /// + /// + /// + /// + /// + /// + /// + public async Task> GenerateCroppedImageBasedOnThumbnailCoordinates(Guid id, decimal left, decimal top, decimal width, decimal height) + { + try + { + RPictureFile rPictureFile = + await _context.PictureFiles + .Where(p => p.Id == id) + .SingleOrDefaultAsync(); + + int adjustedImageWidth = (int)(width * rPictureFile.NormalSizeImageWidth / rPictureFile.ThumbnailImageWidth); + if (adjustedImageWidth > rPictureFile.ThumbnailImageWidth) + adjustedImageWidth = rPictureFile.ThumbnailImageWidth; + + int adjustedImageHeight = (int)(height * adjustedImageWidth / width); + int adjusttedLeft = (int)(left * adjustedImageWidth / width); + int adjusttedTop = (int)(top * adjustedImageHeight / height); + + string normalImagePath = GetImagePath(rPictureFile, "norm").Result; + using (Image targetImage = new Bitmap(adjustedImageWidth, adjustedImageHeight)) + { + using(Graphics g = Graphics.FromImage(targetImage)) + { + using (Image img = Image.FromFile(normalImagePath)) + { + g.DrawImage(img, new Rectangle(0, 0, adjustedImageWidth, adjustedImageHeight), + (int)(left * rPictureFile.NormalSizeImageWidth / rPictureFile.ThumbnailImageWidth), + (int)(top * rPictureFile.NormalSizeImageHeight / rPictureFile.ThumbnailImageHeight), + (int)(width * rPictureFile.NormalSizeImageWidth / rPictureFile.ThumbnailImageWidth), + (int)(height * rPictureFile.NormalSizeImageHeight / rPictureFile.ThumbnailImageHeight), + GraphicsUnit.Pixel, + new ImageAttributes() + ); + } + } + using(MemoryStream ms = new MemoryStream()) + { + targetImage.Save(ms, ImageFormat.Jpeg); + ms.Position = 0; + RServiceResult res = await _simpleImageStorage.Add(null, ms, $"{Path.GetFileNameWithoutExtension(rPictureFile.OriginalFileName)}-cropped-{left}-{top}-{width}-{height}.jpg", "CroppedImages"); + if(string.IsNullOrEmpty(res.ExceptionString)) + { + RImage image = res.Result; + _context.GeneralImages.Add(image); + await _context.SaveChangesAsync(); + return new RServiceResult(image); + } + return res; + } + } + + + + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Database Contetxt + /// + protected readonly RMuseumDbContext _context; + + /// + /// Configuration + /// + protected IConfiguration Configuration { get; } + + /// + /// Simple Image Storage (not storing multiple image sizes) for creating small cropped images + /// + protected readonly IImageFileService _simpleImageStorage; + + + /// + /// constructor + /// + /// + /// + /// + public PictureFileService(RMuseumDbContext context, IConfiguration configuration, IImageFileService simpleImageStorage) + { + _context = context; + Configuration = configuration; + _simpleImageStorage = simpleImageStorage; + } + } +} diff --git a/RMuseum/Services/Implementation/QueuedHostedService.cs b/RMuseum/Services/Implementation/QueuedHostedService.cs new file mode 100644 index 00000000..544d111e --- /dev/null +++ b/RMuseum/Services/Implementation/QueuedHostedService.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + internal class QueuedHostedService : BackgroundService + { + public QueuedHostedService(IBackgroundTaskQueue taskQueue) + { + TaskQueue = taskQueue; + } + + public IBackgroundTaskQueue TaskQueue { get; } + + protected async override Task ExecuteAsync( + CancellationToken cancellationToken) + { + + while (!cancellationToken.IsCancellationRequested) + { + var workItem = await TaskQueue.DequeueAsync(cancellationToken); + + try + { + await workItem(cancellationToken); + } + catch + { + } + } + + } + } +} diff --git a/RMuseum/Services/Implementation/RoleService.cs b/RMuseum/Services/Implementation/RoleService.cs new file mode 100644 index 00000000..8a419ba3 --- /dev/null +++ b/RMuseum/Services/Implementation/RoleService.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Identity; +using RMuseum.Models.Auth.Memory; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Services.Implementation; + +namespace RMuseum.Services.Implementation +{ + /// + /// Role Service Implementation + /// + public class RoleService : RoleServiceBase + { + /// + /// gets list of SecurableItem, should be reimplemented in end user applications + /// + /// + public override SecurableItem[] GetSecurableItems() + { + return RMuseumSecurableItem.Items; + } + + /// + /// constructor + /// + /// + public RoleService( + RoleManager roleManager + ) : base(roleManager) + { + } + + } +} diff --git a/RMuseum/Services/Implementation/TagHandler.cs b/RMuseum/Services/Implementation/TagHandler.cs new file mode 100644 index 00000000..6136f8c3 --- /dev/null +++ b/RMuseum/Services/Implementation/TagHandler.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; +using RMuseum.DbContext; +using RMuseum.Models.Artifact; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + internal class TagHandler + { + public async static Task PrepareAttribute(RMuseumDbContext db, string aName, string aValue, int order) + { + RTag type = await db.Tags.Where(a => a.Name == aName || a.NameInEnglish == aName).SingleOrDefaultAsync(); + if(type == null) + { + int maxOrder = await db.Tags.CountAsync() == 0 ? 0 : await db.Tags.MaxAsync(a => a.Order); + type = new RTag() + { + Name = aName, + NameInEnglish = aName, + PluralName = $"{aName}s", + PluralNameInEnglish = $"{aName}s", + Order = maxOrder + 1, + Status = PublishStatus.Published, + GlobalValue = true + }; + await db.Tags.AddAsync(type); + await db.SaveChangesAsync(); + } + + RTagValue tag = + new RTagValue() + { + Order = order, + Value = aValue, + ValueInEnglish = aValue, + ValueSupplement = "", + RTag = type, + Status = PublishStatus.Published + }; + + if (type.TagType == RTagType.Search || type.TagType == RTagType.LinkSearch) + { + RTagValue similar = await db.TagValues.Where(v => v.RTagId == type.Id && v.ValueInEnglish == aValue && !string.IsNullOrEmpty(v.FriendlyUrl)).FirstOrDefaultAsync(); + if(similar != null) + { + tag.Value = similar.Value; + tag.FriendlyUrl = similar.FriendlyUrl; + } + } + + return tag; + + } + } +} diff --git a/RMuseum/Startup.cs b/RMuseum/Startup.cs new file mode 100644 index 00000000..3618d8de --- /dev/null +++ b/RMuseum/Startup.cs @@ -0,0 +1,301 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Audit.Core; +using Audit.WebApi; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using RMuseum.DbContext; +using RMuseum.Models.Auth.Memory; +using RMuseum.Services; +using RMuseum.Services.Implementation; +using RSecurityBackend.Authorization; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Mail; +using RSecurityBackend.Services; +using RSecurityBackend.Services.Implementation; +using RSecurityBackend.Utilities; +using Swashbuckle.AspNetCore.Filters; + +namespace RMuseum +{ + + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add service and create Policy with options + services.AddCors(options => + { + options.AddPolicy("GanjoorCorsPolicy", + builder => builder.SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("paging-headers") + .AllowCredentials() + ); + }); + + services.AddDbContext(); + + Audit.Core.Configuration.JsonSettings.ContractResolver = AuditNetEnvironmentSkippingContractResolver.Instance; + Audit.Core.Configuration.DataProvider = new RAuditDataProvider(Configuration.GetConnectionString("DefaultConnection")); + + + + services.AddIdentityCore( + options => + { + // Password settings. + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequiredLength = 6; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings. + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = false; + } + ); + + new IdentityBuilder(typeof(RAppUser), typeof(RAppRole), services) + .AddRoleManager>() + .AddSignInManager>() + .AddEntityFrameworkStores(); + ; + + + + services.AddMvc + ( + mvc => + mvc.AddAuditFilter(config => config + .LogRequestIf(r => r.Method != "GET") + .WithEventType("{controller}/{action} ({verb})") + .IncludeHeaders(ctx => !ctx.ModelState.IsValid) + .IncludeRequestBody() + .IncludeModelState() + )).SetCompatibilityVersion(CompatibilityVersion.Latest); + + services.AddMemoryCache(); + + services.Configure(x => + { + x.ValueLengthLimit = int.MaxValue; + x.MultipartBodyLengthLimit = int.MaxValue; // In case of multipart + }); + + services.AddAuthentication(options => + { + options.DefaultScheme = "bearer"; + }).AddJwtBearer("bearer", options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidAudience = "Everyone", + ValidateIssuer = true, + ValidIssuer = "Ganjoor", + + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes($"{Configuration.GetSection("Security")["Secret"]}")), + + ValidateLifetime = true, //validate the expiration and not before values in the token + + ClockSkew = TimeSpan.Zero + }; + + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) + { + context.Response.Headers.Add("Token-Expired", "true"); + } + return Task.CompletedTask; + } + }; + + }); + + services.AddAuthorization(options => + { + foreach (SecurableItem Item in RMuseumSecurableItem.Items) + { + foreach (SecurableItemOperation Operation in Item.Operations) + { + options.AddPolicy($"{Item.ShortName}:{Operation.ShortName}", policy => policy.Requirements.Add(new UserGroupPermissionRequirement(Item.ShortName, Operation.ShortName))); + } + } + }); + + + // Register the Swagger generator, defining 1 or more Swagger documents + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "RMuseum API", + Version = "v1", + Description = "RMuseum API", + TermsOfService = new Uri("https://ganjoor.net/contact"), + Contact = new OpenApiContact + { + Name = "Ganjoor", + Email = "ganjoor@ganjoor.net", + Url = new Uri("https://ganjoor.net") + } + } + ); + c.EnableAnnotations(); + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + + c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "RSecurityBackend.xml")); + + c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme() + { + Description = "format: \"bearer {token}\"", + In = ParameterLocation.Header, + Name = "Authorization", + Type = SecuritySchemeType.ApiKey + + }); + + + c.OperationFilter(); + c.OperationFilter(); // Adds "(Auth)" to the summary so that you can see which endpoints have Authorization + // or use the generic method, e.g. c.OperationFilter>(); + + }); + + //IHttpContextAccessor + services.TryAddSingleton(); + + + //authorization handler + services.AddScoped(); + + + //security context maps to main db context + services.AddTransient, RMuseumDbContext>(); + + //captcha service + services.AddTransient(); + + + //generic image file sercie + services.AddTransient(); + + //app user services + services.AddTransient(); + + //user groups servicess + services.AddTransient(); + + //audit service + services.AddTransient(); + + //user permission checker + services.AddTransient(); + + //secret generator + services.AddTransient(); + + // email service + services.AddTransient(); + services.Configure(Configuration); + + //picture file service + services.AddTransient(); + + //artifact service + services.AddTransient(); + + + services.AddHostedService(); + services.AddSingleton(); + + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + } + + + app.UseStaticFiles(); + + // Enable middleware to serve generated Swagger as a JSON endpoint. + app.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), + // specifying the Swagger JSON endpoint. + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "RMuseum API V1"); + c.RoutePrefix = string.Empty; + }); + + + app.UseAuthentication(); + + // global policy - assign here or on each controller + app.UseCors("GanjoorCorsPolicy"); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + + } + } +} diff --git a/RMuseum/Swashbuckle/SecurityRequirementsOperationFilter.cs b/RMuseum/Swashbuckle/SecurityRequirementsOperationFilter.cs new file mode 100644 index 00000000..45b769bf --- /dev/null +++ b/RMuseum/Swashbuckle/SecurityRequirementsOperationFilter.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; +using Microsoft.OpenApi.Models; + +namespace RMuseum.Swashbuckle +{ + /// + /// //https://mattfrear.com/2018/07/21/add-an-authorization-header-to-your-swagger-ui-with-swashbuckle-revisited/ + /// + public class SecurityRequirementsOperationFilter : IOperationFilter + { + /// + /// + /// + /// + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Policy names map to scopes + var requiredScopes = context.MethodInfo + .GetCustomAttributes(true) + .OfType() + .Select(attr => attr.Policy) + .Distinct(); + + if (requiredScopes.Any()) + { + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + [ oAuthScheme ] = requiredScopes.ToList() + } + }; + } + } + } +} diff --git a/RMuseum/appsettings.Development.json b/RMuseum/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/RMuseum/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/RMuseum/appsettings.json b/RMuseum/appsettings.json new file mode 100644 index 00000000..8e7270f1 --- /dev/null +++ b/RMuseum/appsettings.json @@ -0,0 +1,29 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=museum;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "PictureFileService": { + "StoragePath": "C:\\museum", + "ResizedImageContentType": "image/jpeg", + "NormalImageMaxWidth": "800", + "NormalImageMaxHeight": "1350", + "ThumbnailImageWidth": "300", + "ThumbnailMaxHeight": "500" + }, + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "UserSignupClientCallBack": { + "Url": "https://museum.ganjoor.net/signup" + }, + "ForgotPasswordClientCallBack": { + "Url": "https://museum.ganjoor.net/forgot-password" + }, + "Security": { + "Secret": "k4Fy7pDsc0LgXQ8PCCfZtwmju5Ed8asc", + "DefaultTokenExpirationInSeconds": "2502000" + }, + "AllowedHosts": "*" + } diff --git a/RMuseum/global.json b/RMuseum/global.json new file mode 100644 index 00000000..cc4d115e --- /dev/null +++ b/RMuseum/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.1.200" + } +} \ No newline at end of file diff --git a/RSecurityBackend/Authorization/UserGroupPermissionHandler.cs b/RSecurityBackend/Authorization/UserGroupPermissionHandler.cs new file mode 100644 index 00000000..9b8d453a --- /dev/null +++ b/RSecurityBackend/Authorization/UserGroupPermissionHandler.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authorization; +using RSecurityBackend.Models.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using RSecurityBackend.Services; + +namespace RSecurityBackend.Authorization +{ + /// + /// UserGroupPermissionHandler + /// + public class UserGroupPermissionHandler : AuthorizationHandler + { + /// + /// HandleRequirementAsync + /// + /// + /// + /// + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupPermissionRequirement requirement) + { + + if (!context.User.HasClaim(c => c.Type == "UserId") || !context.User.HasClaim(c => c.Type == "SessionId")) + { + context.Fail(); + return; + } + + RServiceResult result = await _userPermissionChecker.Check + ( + new Guid(context.User.Claims.FirstOrDefault(c => c.Type == "UserId").Value), + new Guid(context.User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + requirement.SecurableItemShortName, + requirement.OperationShortName + ); + + if(result.Result) + { + context.Succeed(requirement); + return; + } + + context.Fail(); + + } + + + /// + /// IUserPermissionChecker instance + /// + private IUserPermissionChecker _userPermissionChecker; + + + /// + /// constructor + /// + /// + public UserGroupPermissionHandler(IUserPermissionChecker userPermissionChecker) : base() + { + _userPermissionChecker = userPermissionChecker; + } + } +} diff --git a/RSecurityBackend/Authorization/UserGroupPermissionRequirement.cs b/RSecurityBackend/Authorization/UserGroupPermissionRequirement.cs new file mode 100644 index 00000000..f8a676be --- /dev/null +++ b/RSecurityBackend/Authorization/UserGroupPermissionRequirement.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; + +namespace RSecurityBackend.Authorization +{ + /// + /// UserGroupPermissionRequirement + /// + public class UserGroupPermissionRequirement : IAuthorizationRequirement + { + /// + /// + /// + /// + /// + /// job + /// + public string SecurableItemShortName { get; set; } + + /// + /// + /// + /// + /// + /// view + /// + public string OperationShortName { get; set; } + + /// + /// constructor + /// + /// + /// + public UserGroupPermissionRequirement(string securableItemShortName, string operationShortName) + { + SecurableItemShortName = securableItemShortName; + OperationShortName = operationShortName; + } + } +} diff --git a/RSecurityBackend/Controllers/AppUserControllerBase.cs b/RSecurityBackend/Controllers/AppUserControllerBase.cs new file mode 100644 index 00000000..f1ff7643 --- /dev/null +++ b/RSecurityBackend/Controllers/AppUserControllerBase.cs @@ -0,0 +1,1040 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Auth.ViewModels; +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using RSecurityBackend.Services; +using Microsoft.AspNetCore.Identity.UI.Services; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Image; +using System.IO; +using Microsoft.Extensions.Configuration; +using Audit.WebApi; + +namespace RSecurityBackend.Controllers +{ + /// + /// User login/logout/register/... + /// + [Produces("application/json")] + [Route("api/users")] + public abstract class AppUserControllerBase : Controller + { + /// + /// login + /// + /// loginViewModel + /// LoggedOnUserModel + [HttpPost] + [AllowAnonymous] + [Route("login")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(LoggedOnUserModel))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Login( + [AuditIgnore] + [FromBody] + LoginViewModel loginViewModel + ) + { + string clientIPAddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString(); + RServiceResult res = await _appUserService.Login(loginViewModel, clientIPAddress); + if(res.Result == null) + { + return BadRequest(res.ExceptionString); + } + + return Ok(res.Result); + } + + /// + /// Logout user (users need user:delothersession to logout other users) + /// + /// + /// User Session Id + /// + [HttpDelete] + [Authorize] + [Route("delsession")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Logout( + Guid userId, + Guid sessionId + ) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + if (loggedOnUserId != userId) + { + RServiceResult canLogoutAllUsers = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.DelOtherUserSessionOperationShortName + ); + + if (!string.IsNullOrEmpty(canLogoutAllUsers.ExceptionString)) + return BadRequest(canLogoutAllUsers.ExceptionString); + if (!canLogoutAllUsers.Result) + return Forbid(); + } + + RServiceResult res = await _appUserService.Logout(userId, sessionId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + + return Ok(res.Result); + } + + + /// + /// Check if my session is valid + /// + /// + [HttpGet] + [Authorize] + [Route("checkmysession")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task IsSessionValid(Guid sessionId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult res = await _appUserService.SessionExists(loggedOnUserId, sessionId); + + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + + + + /// + /// All Users Information (if user does not have user:view permission list only contains him/her information) + /// + /// All Users Information + [HttpGet] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Get() + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.ViewAllOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + if (canViewAllUsersInformation.Result) + { + RServiceResult usersInfo = await _appUserService.GetAllUsersInformation(); + if (usersInfo.Result == null) + { + return BadRequest(usersInfo.ExceptionString); + } + return Ok(usersInfo.Result); + } + else + { + RServiceResult userInfo = await _appUserService.GetUserInformation(loggedOnUserId); + if (userInfo.Result == null) + { + if (string.IsNullOrEmpty(userInfo.ExceptionString)) + return NotFound(); + return BadRequest(userInfo.ExceptionString); + } + return Ok(new PublicRAppUser[] { userInfo.Result }); + } + } + + /// + /// returns user information (if user does not have user:view permission trying to view other users' information fails with a forbidden error) + /// + /// user id + /// user information + [HttpGet("{id}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(PublicRAppUser))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task Get(Guid id) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + if(loggedOnUserId != id) + { + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.ViewAllOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + + if (!canViewAllUsersInformation.Result) + return Forbid(); + } + + + RServiceResult userInfo = await _appUserService.GetUserInformation(id); + if (userInfo.Result == null) + { + if (string.IsNullOrEmpty(userInfo.ExceptionString)) + return NotFound(); + return BadRequest(userInfo.ExceptionString); + } + return Ok(userInfo.Result); + } + + + /// + /// add a new user (if you are trying to add an admin user you yourself should be admin) + /// + /// if passsword is sent empty system genrates one for it which could be retrieved from returned record + /// id/generated password if required could be retrieved from return value + [HttpPost] + [Authorize(Policy = SecurableItem.UserEntityShortName + ":" + SecurableItem.AddOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RegisterRAppUser))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public virtual async Task Post([FromBody]RegisterRAppUser newUserInfo) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + if(newUserInfo.IsAdmin) + { + RServiceResult isAdmin = await _appUserService.IsAdmin(loggedOnUserId); + if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) + return BadRequest(isAdmin.ExceptionString); + if (!isAdmin.Result) + return Forbid();//Only admin users can create admin users + } + RServiceResult result = await _appUserService.AddUser(newUserInfo); + if (result.Result == null) + return BadRequest(result.ExceptionString); + RegisterRAppUser registerRAppUser = new RegisterRAppUser() + { + Email = result.Result.Email, + Status = result.Result.Status, + FirstName = result.Result.FirstName, + SureName = result.Result.SureName, + Id = result.Result.Id, + IsAdmin = newUserInfo.IsAdmin, + PhoneNumber = newUserInfo.PhoneNumber, + RImageId = newUserInfo.RImageId, + Username = newUserInfo.Username + }; + return Ok(registerRAppUser); + } + + /// + /// update existing user (if you are trying to update an admin user you yourself should be admin) (if user does not have user:modify permission trying to modify other users' information fails with a forbidden error) + /// + /// user id + /// existingUserInfo.id could be passed empty and it is ignored completely, if password is sent empty it does not has effect + /// true if succeeds + [HttpPut("{id}")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public virtual async Task Put(Guid id, [FromBody]RegisterRAppUser existingUserInfo) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult isAdmin = await _appUserService.IsAdmin(loggedOnUserId); + if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) + return BadRequest(isAdmin.ExceptionString); + RServiceResult userInfo = await _appUserService.GetUserInformation(id); + if (!isAdmin.Result) + { + + if (!string.IsNullOrEmpty(userInfo.ExceptionString)) + return BadRequest(userInfo.ExceptionString); + + if (existingUserInfo.IsAdmin) + return Forbid();//You should be admin to make other users admin + RServiceResult isEditingUserAdmin = await _appUserService.IsAdmin(id); + if (!string.IsNullOrEmpty(isEditingUserAdmin.ExceptionString)) + return BadRequest(isEditingUserAdmin.ExceptionString); + if(isEditingUserAdmin.Result) + return Forbid();//You can not modify admin users. + + if (loggedOnUserId != id) + { + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.ModifyOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + + if (!canViewAllUsersInformation.Result) + return Forbid(); + } + } + + if (loggedOnUserId == id && userInfo.Result.Username != existingUserInfo.Username) + return BadRequest("You can not change your username!"); + if (loggedOnUserId == id && (existingUserInfo.Status != RAppUserStatus.Active)) + return BadRequest("You can not disable yourself!"); + if (loggedOnUserId == id && !string.IsNullOrEmpty(existingUserInfo.Password)) + return BadRequest("Please use setmypassword method to change your own password."); + + RServiceResult res = await _appUserService.ModifyUser(id, existingUserInfo); + if (!res.Result) + return BadRequest(res.ExceptionString); + + return Ok(true); + + } + + /// + /// set my password + /// + /// + /// + [HttpPost] + [Authorize] + [Route("setmypassword")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public virtual async Task SetMyPassword([AuditIgnore][FromBody]SetPasswordModel model) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + + RServiceResult res = await _appUserService.ChangePassword(loggedOnUserId, model.OldPassword, model.NewPassword); + if (!res.Result) + return BadRequest(res.ExceptionString); + + return Ok(true); + + } + + /// + /// delete user (only admin users can delete other admin users, a user cannot delete himself/herself) + /// + /// user id + /// true if succeeds + [HttpDelete("{id}")] + [Authorize(Policy = SecurableItem.UserEntityShortName + ":" + SecurableItem.DeleteOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task Delete(Guid id) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + RServiceResult isAdmin = await _appUserService.IsAdmin(loggedOnUserId); + if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) + return BadRequest(isAdmin.ExceptionString); + if (!isAdmin.Result) + { + RServiceResult isDeletingUserAdmin = await _appUserService.IsAdmin(id); + if (!string.IsNullOrEmpty(isDeletingUserAdmin.ExceptionString)) + return BadRequest(isDeletingUserAdmin.ExceptionString); + if (isDeletingUserAdmin.Result) + return Forbid();//You can not delete admin users. + } + + if(loggedOnUserId == id) + { + return BadRequest("SOS! Suicide attempt detected!"); + } + + RServiceResult res = await _appUserService.DeleteUser(id); + if (!res.Result) + { + return BadRequest(res.ExceptionString); + } + + return Ok(true); + } + + /// + /// Checks if user is admin (if user does not have user:viewall permission list it will be failed for any user id other than himself/herself) + /// + /// + [HttpGet] + [Authorize] + [Route("isadmin")] + [Authorize(Policy = SecurableItem.UserEntityShortName + ":" + SecurableItem.ViewAllOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task IsAdmin(Guid userId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult res = await _appUserService.IsAdmin(userId); + + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + + /// + /// View User Sessions (user needs user:sessions permission to view other users sessions) + /// + /// + [HttpGet] + [Route("sessions")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task GetUserSessions(Guid? userId) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + if (loggedOnUserId != userId) + { + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.SessionsOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + + if (!canViewAllUsersInformation.Result) + return Forbid(); + } + RServiceResult sessionsInfo = await _appUserService.GetUserSessions(userId); + if (sessionsInfo.Result == null) + { + return BadRequest(sessionsInfo.ExceptionString); + } + return Ok(sessionsInfo.Result); + } + + + /// + /// Set User Image (via FormData, specifying userId as 'id' in formData ) - if Files.count is 0 image would be removed - (users need user:modify to change other users image) + /// + /// new image id + [HttpPost] + [Route("image")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] + public async Task SetUserImage() + { + try + { + if(!Request.Form.TryGetValue("id", out Microsoft.Extensions.Primitives.StringValues tmp)) + { + return BadRequest("id is null"); + } + + Guid userId = new Guid(tmp); + + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + if (loggedOnUserId != userId) + { + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.ModifyOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + + if (!canViewAllUsersInformation.Result) + return Forbid(); + } + + RServiceResult res = await _appUserService.SetUserImage(userId, Request.Form.Files); + + if(!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + + if(res.Result == null) + { + return Ok(""); + } + + return Ok(res.Result.ToString()); + } + catch(Exception exp) + { + return BadRequest(exp.ToString()); + } + } + + /// + /// Get User Image in base 64 + /// + /// + /// + [HttpGet] + [Authorize]//no specific permission + [Route("base64image")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + public async Task GetUserImageBase64(Guid id) + { + try + { + RServiceResult img = await _appUserService.GetUserImage(id); + + if (!string.IsNullOrEmpty(img.ExceptionString)) + { + return BadRequest(img.ExceptionString); + + } + if (img.Result == null) + return new ObjectResult(string.Empty); + + RServiceResult imgPath = _imageFileService.GetImagePath(img.Result); + if (!string.IsNullOrEmpty(imgPath.ExceptionString)) + return BadRequest(imgPath.ExceptionString); + + + return new ObjectResult(Convert.ToBase64String(System.IO.File.ReadAllBytes(imgPath.Result))); + + } + catch (Exception exp) + { + return BadRequest(exp.ToString()); + } + } + + /// + /// Get User Image in base 64 + /// + /// + /// + [HttpGet] + [Authorize]//no specific permission + [Route("image")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + public async Task GetUserImage(Guid id) + { + try + { + RServiceResult img = await _appUserService.GetUserImage(id); + + if(!string.IsNullOrEmpty(img.ExceptionString)) + { + return BadRequest(img.ExceptionString); + + } + if (img.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = img.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= img.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + RServiceResult imgPath = _imageFileService.GetImagePath(img.Result); + if (!string.IsNullOrEmpty(imgPath.ExceptionString)) + return BadRequest(imgPath.ExceptionString); + + + return new FileStreamResult(new FileStream(imgPath.Result, FileMode.Open, FileAccess.Read), img.Result.ContentType); + + + } + catch (Exception exp) + { + return BadRequest(exp.ToString()); + } + } + + /// + /// returns user roles (if user does not have user:view permission trying to view other users' information fails with a forbidden error) + /// + /// user id + /// user roles + [HttpGet("{id}/roles")] + [Authorize] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task GetUserRoles(Guid id) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + if (loggedOnUserId != id) + { + RServiceResult canViewAllUsersInformation = + await _userPermissionChecker.Check + ( + loggedOnUserId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + SecurableItem.UserEntityShortName, + SecurableItem.ViewAllOperationShortName + ); + if (!string.IsNullOrEmpty(canViewAllUsersInformation.ExceptionString)) + return BadRequest(canViewAllUsersInformation.ExceptionString); + + if (!canViewAllUsersInformation.Result) + return Forbid(); + } + + + RServiceResult> roles = await _appUserService.GetUserRoles(id); + if (!string.IsNullOrEmpty(roles.ExceptionString)) + return BadRequest(roles.ExceptionString); + + return Ok(roles.Result.ToArray()); + } + + /// + /// remove user from role + /// + /// user id + /// + /// true if succeeds + [HttpDelete("{id}/roles/{role}")] + [Authorize(Policy = SecurableItem.UserEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task RemoveFromRole(Guid id, string role) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + if(loggedOnUserId == id) + { + return BadRequest("You cannot modify your own roles."); + } + RServiceResult isAdmin = await _appUserService.IsAdmin(loggedOnUserId); + if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) + return BadRequest(isAdmin.ExceptionString); + if (!isAdmin.Result) + { + RServiceResult isDeletingUserAdmin = await _appUserService.IsAdmin(id); + if (!string.IsNullOrEmpty(isDeletingUserAdmin.ExceptionString)) + return BadRequest(isDeletingUserAdmin.ExceptionString); + if (isDeletingUserAdmin.Result) + return Forbid();//You can not delete admin users roles. + } + + + + RServiceResult res = await _appUserService.RemoveFromRole(id, role); + if (!res.Result) + { + return BadRequest(res.ExceptionString); + } + + return Ok(true); + } + /// + /// add user to role + /// + /// user id + /// + /// true if succeeds + [HttpPost("{id}/roles/{role}")] + [Authorize(Policy = SecurableItem.UserEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task AddToRole(Guid id, string role) + { + Guid loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + if (loggedOnUserId == id) + { + return BadRequest("You cannot modify your own roles."); + } + RServiceResult isAdmin = await _appUserService.IsAdmin(loggedOnUserId); + if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) + return BadRequest(isAdmin.ExceptionString); + if (!isAdmin.Result) + { + RServiceResult isDeletingUserAdmin = await _appUserService.IsAdmin(id); + if (!string.IsNullOrEmpty(isDeletingUserAdmin.ExceptionString)) + return BadRequest(isDeletingUserAdmin.ExceptionString); + if (isDeletingUserAdmin.Result) + return Forbid();//You can not delete admin users roles. + } + + + + RServiceResult res = await _appUserService.AddToRole(id, role); + if (!res.Result) + { + return BadRequest(res.ExceptionString); + } + + return Ok(true); + } + + /// + /// get a captcha image for signup or forgot password + /// + /// captchaimageid - display it using api/rimages/captchaimageid.jpg + [HttpGet] + [AllowAnonymous] + [Route("captchaimage")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + + public async Task GenerateCaptchImage() + { + + RServiceResult img = await _captchaService.Generate(); + + if (!string.IsNullOrEmpty(img.ExceptionString)) + { + return BadRequest(img.ExceptionString); + } + + return Ok(img.Result.Id); + } + + + /// + /// signup + /// + /// signUpViewModel + /// result + [HttpPost] + [AllowAnonymous] + [Route("signup")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SignUp( + [FromBody] + UnverifiedSignUpViewModel signUpViewModel + ) + { + if (!IsSignupEnabled()) + return BadRequest("ثبت نام غیرفعال است."); + + RServiceResult captchaRes = await _captchaService.Evaluate(signUpViewModel.CaptchaImageId, signUpViewModel.CaptchaValue); + if (!string.IsNullOrEmpty(captchaRes.ExceptionString)) + return BadRequest(captchaRes.ExceptionString); + + if(!captchaRes.Result) + return BadRequest("مقدار تصویر امنیتی درست وارد نشده است."); + + string clientIPAddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString(); + RServiceResult res = await _appUserService.SignUp(signUpViewModel.Email, clientIPAddress, signUpViewModel.ClientAppName, signUpViewModel.Language); + if (res.Result == null) + { + return BadRequest(res.ExceptionString); + } + + try + { + await _emailSender.SendEmailAsync + ( + signUpViewModel.Email, + GetSignUpEmailSubject(res.Result.Secret), + GetSignUpEmailHtmlContent(res.Result.Secret) + ); + } + catch(Exception exp) + { + return BadRequest("Error sending email: " + exp.ToString()); + } + + return Ok(true); + } + + /// + /// Sign Up Email Subject + /// + /// + /// subject + /// + /// + protected virtual string GetSignUpEmailSubject(string secretCode) + { + return $"Ganjoor SignUp Code:{secretCode}"; + } + + /// + /// Sign Up Email Html Content + /// + /// + /// html content + protected virtual string GetSignUpEmailHtmlContent(string secretCode) + { + return $"{SignupCallbackUrl}?secret={secretCode}"; + } + + /// + /// verify signup / forgot password + /// + /// + /// + /// associated secret email + [HttpGet] + [AllowAnonymous] + [Route("verify")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task VerifySignUp(RVerifyQueueType type, string secret) + { + + RServiceResult res = await _appUserService.RetrieveEmailFromQueueSecret(type, secret); + + if (!string.IsNullOrEmpty(res.ExceptionString)) + { + return BadRequest(res.ExceptionString); + } + if (string.IsNullOrWhiteSpace(res.Result)) + { + return NotFound(); + } + return Ok(res.Result); + } + + /// + /// finalize signup process + /// + /// + /// + [HttpPost] + [AllowAnonymous] + [Route("finalizesignup")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task FinalizeSignUp([AuditIgnore][FromBody]VerifiedSignUpViewModel newUserInfo) + { + RServiceResult result = await _appUserService.FinalizeSignUp(newUserInfo.Email, newUserInfo.Secret, newUserInfo.Password, newUserInfo.FirstName, newUserInfo.SureName); + if (!result.Result) + return BadRequest(result.ExceptionString); + return Ok(true); + } + + /// + /// start forgot password process by email + /// + /// signUpViewModel + /// result + [HttpPost] + [AllowAnonymous] + [Route("forgotpassword")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task ForgotPassword( + [FromBody] + UnverifiedSignUpViewModel fpwdViewModel + ) + { + + RServiceResult captchaRes = await _captchaService.Evaluate(fpwdViewModel.CaptchaImageId, fpwdViewModel.CaptchaValue); + if (!string.IsNullOrEmpty(captchaRes.ExceptionString)) + return BadRequest(captchaRes.ExceptionString); + + if (!captchaRes.Result) + return BadRequest("مقدار تصویر امنیتی درست وارد نشده است."); + + string clientIPAddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString(); + RServiceResult res = await _appUserService.ForgotPassword(fpwdViewModel.Email, clientIPAddress, fpwdViewModel.ClientAppName, fpwdViewModel.Language); + if (res.Result == null) + { + return BadRequest(res.ExceptionString); + } + + try + { + await _emailSender.SendEmailAsync + ( + fpwdViewModel.Email, + GetForgotPasswordEmailSubject( res.Result.Secret), + GetForgotPasswordEmailHtmlContent(res.Result.Secret) + ); + } + catch (Exception exp) + { + return BadRequest("Error sending email: " + exp.ToString()); + } + + return Ok(true); + } + + /// + /// Forgot Password Email Subject + /// + /// + /// subject + /// + /// + protected virtual string GetForgotPasswordEmailSubject(string secretCode) + { + return $"Ganjoor Forgot Password Code:{secretCode}"; + } + + /// + /// Forgot Password Email Html Content + /// + /// + /// html content + protected virtual string GetForgotPasswordEmailHtmlContent(string secretCode) + { + return $"{ForgotPasswordCallbackUrl}?secret={secretCode}"; + } + + /// + /// Is Sign-up enabled? + /// + /// + protected virtual bool IsSignupEnabled() + { + return false; + } + + /// + /// reset password + /// + /// + /// + [HttpPost] + [AllowAnonymous] + [Route("resetpassword")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task ResetPassword([AuditIgnore][FromBody]ResetPasswordViewModel pwd) + { + string clientIPAddress = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString(); + RServiceResult result = await _appUserService.ResetPassword(pwd.Email, pwd.Secret, pwd.Password, clientIPAddress); + if (!result.Result) + return BadRequest(result.ExceptionString); + return Ok(true); + } + + + + + /// + /// signup callback url + /// + protected string SignupCallbackUrl + { + get + { + return $"{Configuration.GetSection("UserSignupClientCallBack")["Url"]}"; + } + } + + /// + /// forgot password call back url + /// + protected string ForgotPasswordCallbackUrl + { + get + { + return $"{Configuration.GetSection("ForgotPasswordClientCallBack")["Url"]}"; + } + } + + + + + /// + /// IAppUserService instance + /// + protected IAppUserService _appUserService; + + /// + /// for client IP resolution + /// + protected IHttpContextAccessor _httpContextAccessor; + + /// + /// IUserPermissionChecker instance + /// + protected IUserPermissionChecker _userPermissionChecker; + + /// + /// IEmailSender instance + /// + protected IEmailSender _emailSender; + + /// + /// Image File Service + /// + protected readonly IImageFileService _imageFileService; + + /// + /// Captcha service + /// + protected readonly ICaptchaService _captchaService; + + /// + /// Configuration + /// + protected IConfiguration Configuration { get; } + + + + /// + /// constructor + /// + /// + /// + /// + /// + /// + /// + /// + public AppUserControllerBase(IConfiguration configuration, IAppUserService appUserService, IHttpContextAccessor httpContextAccessor, IUserPermissionChecker userPermissionChecker, IEmailSender emailSender, IImageFileService imageFileService, ICaptchaService captchaService) + { + Configuration = configuration; + _appUserService = appUserService; + _userPermissionChecker = userPermissionChecker; + _httpContextAccessor = httpContextAccessor; + _emailSender = emailSender; + _imageFileService = imageFileService; + _captchaService = captchaService; + } + } +} diff --git a/RSecurityBackend/Controllers/AuditLogControllerBase.cs b/RSecurityBackend/Controllers/AuditLogControllerBase.cs new file mode 100644 index 00000000..cb497d68 --- /dev/null +++ b/RSecurityBackend/Controllers/AuditLogControllerBase.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using RSecurityBackend.Models.Audit.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Services; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +namespace RSecurityBackend.Controllers +{ + /// + /// Audit Log Controller Base + /// + [Produces("application/json")] + [Route("api/auditlogs")] + public abstract class AuditLogControllerBase : Controller + { + /// + /// get all audit logs ordered by time + /// + /// + /// + /// + /// + + [HttpGet] + [Authorize(Policy = SecurableItem.AuditLogEntityShortName + ":" + SecurableItem.ViewOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + + public async Task Get([FromQuery]PagingParameterModel paging, string userName, bool orderByTimeDescending) + { + RServiceResult<(PaginationMetadata PagingMeta, REvent[] Items)> itemsInfo + = await _auditLogService.GetAll(paging, userName, orderByTimeDescending); + if (!string.IsNullOrEmpty(itemsInfo.ExceptionString)) + { + return BadRequest(itemsInfo.ExceptionString); + } + + + // Paging Header + HttpContext.Response.Headers.Add("paging-headers", JsonConvert.SerializeObject(itemsInfo.Result.PagingMeta)); + + return Ok(itemsInfo.Result.Items); + } + + /// + /// constructor + /// + /// + /// + public AuditLogControllerBase(IAuditLogService auditLogService) + { + _auditLogService = auditLogService; + } + + /// + /// Artifact Service + /// + protected readonly IAuditLogService _auditLogService; + } +} diff --git a/RSecurityBackend/Controllers/RChargableFeatureType.cs b/RSecurityBackend/Controllers/RChargableFeatureType.cs new file mode 100644 index 00000000..f35019b3 --- /dev/null +++ b/RSecurityBackend/Controllers/RChargableFeatureType.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RBilling.Models.Tenant.Db +{ + /// + /// Chargable Feature For Tenant Type + /// + public enum RChargableFeatureType + { + /// + /// yes/no + /// + Boolean = 0, + /// + /// 0, ... + /// + RegularNumber = 1, + /// + /// 1.222 or large numbers + /// + FloatNumber + } +} diff --git a/RSecurityBackend/Controllers/RImageControllerBase.cs b/RSecurityBackend/Controllers/RImageControllerBase.cs new file mode 100644 index 00000000..892057dc --- /dev/null +++ b/RSecurityBackend/Controllers/RImageControllerBase.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using RSecurityBackend.Services; +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +namespace RSecurityBackend.Controllers +{ + /// + /// Generic Image Provider + /// + [Produces("application/json")] + [Route("api/rimages")] + public abstract class RImageControllerBase : Controller + { + /// + /// returns image stream + /// + /// + /// + [HttpGet("{id}.jpg")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(FileStreamResult))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Get(Guid id) + { + RServiceResult img = + await _pictureFileService.GetImage(id); + + if (!string.IsNullOrEmpty(img.ExceptionString)) + return BadRequest(img.ExceptionString); + + if (img.Result == null) + return NotFound(); + + Response.GetTypedHeaders().LastModified = img.Result.LastModified; + + var requestHeaders = Request.GetTypedHeaders(); + if (requestHeaders.IfModifiedSince.HasValue && + requestHeaders.IfModifiedSince.Value >= img.Result.LastModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + + RServiceResult imgPath = _pictureFileService.GetImagePath(img.Result); + if (!string.IsNullOrEmpty(imgPath.ExceptionString)) + return BadRequest(imgPath.ExceptionString); + + + return new FileStreamResult(new FileStream(imgPath.Result, FileMode.Open, FileAccess.Read), "image/jpeg"); + + } + + /// + /// temporary api for uploading temporary images + /// + /// + [HttpPost] + [Authorize(Policy = SecurableItem.AuditLogEntityShortName + ":" + SecurableItem.ViewOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(Guid))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.Forbidden, Type = typeof(string))] + public async Task UploadImage() + { + try + { + if (!Request.Form.TryGetValue("path", out Microsoft.Extensions.Primitives.StringValues path)) + { + return BadRequest("path is null"); + } + + IFormFile file = Request.Form.Files[0]; + + RServiceResult image = await _pictureFileService.Add(file, null, file.FileName, path); + + + if (!string.IsNullOrEmpty(image.ExceptionString)) + { + return BadRequest(image.ExceptionString); + } + + image = await _pictureFileService.Store(image.Result); + + if (!string.IsNullOrEmpty(image.ExceptionString)) + { + return BadRequest(image.ExceptionString); + } + + return Ok(image.Result.Id); + } + catch (Exception exp) + { + return BadRequest(exp.ToString()); + } + } + + + /// + /// constructor + /// + /// + /// + public RImageControllerBase(IImageFileService pictureFileService) + { + _pictureFileService = pictureFileService; + } + + /// + /// Artifact Service + /// + protected readonly IImageFileService _pictureFileService; + + + } +} diff --git a/RSecurityBackend/Controllers/RoleControllerBase.cs b/RSecurityBackend/Controllers/RoleControllerBase.cs new file mode 100644 index 00000000..f82e0763 --- /dev/null +++ b/RSecurityBackend/Controllers/RoleControllerBase.cs @@ -0,0 +1,198 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using RSecurityBackend.Services; + +namespace RSecurityBackend.Controllers +{ + /// + /// User roles + /// + [Produces("application/json")] + [Route("api/roles")] + public abstract class RoleControllerBase : Controller + { + /// + /// All Roles Information + /// + /// All Roles Information + [HttpGet] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.ViewOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task Get() + { + + RServiceResult rolesInfo = await _roleService.GetAllRoles(); + if (rolesInfo.Result == null) + { + return BadRequest(rolesInfo.ExceptionString); + } + return Ok(rolesInfo.Result); + } + + /// + /// returns role information + /// + /// role name + /// role information + [HttpGet("{roleName}")] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.ViewOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RAppRole))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task Get(string roleName) + { + RServiceResult roleInfo = await _roleService.GetRoleInformation(roleName); + if (roleInfo.Result == null) + { + if (string.IsNullOrEmpty(roleInfo.ExceptionString)) + return NotFound(); + return BadRequest(roleInfo.ExceptionString); + } + return Ok(roleInfo.Result); + } + + /// + /// add a new role + /// + /// + /// id if required could be retrieved from return value + [HttpPost] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.AddOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(RAppRole))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task Post([FromBody]RAppRole newGroupInfo) + { + RServiceResult result = await _roleService.AddRole(newGroupInfo); + if (result.Result == null) + return BadRequest(result.ExceptionString); + return Ok(result.Result); + } + + /// + /// update existing role + /// + /// role name + /// existingGroupInfo.id could be passed empty and it is ignored completely + /// true if succeeds + [HttpPut("{roleName}")] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task Put(string roleName, [FromBody]RAppRole existingGroupInfo) + { + + RServiceResult res = await _roleService.ModifyRole(roleName, existingGroupInfo); + if (!res.Result) + return BadRequest(res.ExceptionString); + + return Ok(true); + + } + + /// + /// delete role + /// + /// role name + /// true if succeeds + [HttpDelete("{roleName}")] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.DeleteOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + public async Task Delete(string roleName) + { + + RServiceResult res = await _roleService.DeleteRole(roleName); + if (!res.Result) + { + return BadRequest(res.ExceptionString); + } + + return Ok(true); + } + + /// + /// lists role permissions + /// + /// + [HttpGet] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.ViewOperationShortName)] + [Route("permissions/{roleName}")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetRoleSecurableItemsStatus(string roleName) + { + RServiceResult res = await _roleService.GetRoleSecurableItemsStatus(roleName); + + if (res.Result == null) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + + /// + /// Saves role permissions + /// + /// role name + /// + /// + [HttpPut] + [Authorize(Policy = SecurableItem.RoleEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] + [Route("permissions/{roleName}")] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task SetRoleSecurableItemsStatus(string roleName, [FromBody]SecurableItem[] securableItems) + { + RServiceResult res = await _roleService.SetRoleSecurableItemsStatus(roleName, securableItems); + + if (!res.Result) + { + return BadRequest(res.ExceptionString); + } + return Ok(res.Result); + } + + /// + /// Get All SecurableItems + /// + /// All All SecurableItems + [HttpGet] + [Route("securableitems")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public IActionResult GetSecurableItems() + { + return Ok(_roleService.GetSecurableItems()); + } + + + /// + /// IUserRoleService instance + /// + protected IUserRoleService _roleService; + + + /// + /// constructor + /// + /// + public RoleControllerBase(IUserRoleService roleService) + { + _roleService = roleService; + } + } +} diff --git a/RSecurityBackend/Controllers/UserScopeCheckEnabledControllerBase.cs b/RSecurityBackend/Controllers/UserScopeCheckEnabledControllerBase.cs new file mode 100644 index 00000000..ad51749e --- /dev/null +++ b/RSecurityBackend/Controllers/UserScopeCheckEnabledControllerBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Services; + + +namespace RSecurityBackend.Controllers +{ + /// + /// base class for controllers needing UserScopeCheck + /// + public abstract class UserScopeCheckEnabledControllerBase : Controller + { + /// + /// UserScopeCheck method EntityShortName; + /// + /// + /// RBillingSecurableItem.TenantEntityShortName + /// + protected abstract string UserScopeCheckEntityShortName { get; } + + /// + /// UserScopeCheck method OperationShortName + /// + /// + /// SecurableItem.ViewOperationShortName + /// + protected abstract string UserScopeCheckOperationShortName { get; } + + + /// + /// returns user id if user is a guest, and empty if he/she has access to view all users information + /// + /// + protected async Task> GetUserIdUnlessUserHavePermissionToReadAllUsersDataWhichReturnEmptyUserId() + { + Guid userId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + + RServiceResult canReadAllUsersData = + await _userPermissionChecker.Check + ( + userId, + new Guid(User.Claims.FirstOrDefault(c => c.Type == "SessionId").Value), + UserScopeCheckEntityShortName, + UserScopeCheckOperationShortName + + ); + + if (!string.IsNullOrEmpty(canReadAllUsersData.ExceptionString)) + return new RServiceResult(userId, canReadAllUsersData.ExceptionString); + if (canReadAllUsersData.Result) + { + userId = Guid.Empty; + } + return new RServiceResult(userId); + } + + /// + /// can current user read this userId daya + /// + /// + /// + protected async Task> CanReadUserData(Guid userId) + { + RServiceResult scopeForUserId = await GetUserIdUnlessUserHavePermissionToReadAllUsersDataWhichReturnEmptyUserId(); + if(!string.IsNullOrEmpty(scopeForUserId.ExceptionString)) + { + return new RServiceResult(false, scopeForUserId.ExceptionString); + } + return new RServiceResult(scopeForUserId.Result == Guid.Empty || scopeForUserId.Result == userId); + } + + /// + /// Permission Checker Service + /// + protected readonly IUserPermissionChecker _userPermissionChecker; + + + /// + /// constructor + /// + /// + public UserScopeCheckEnabledControllerBase(IUserPermissionChecker userPermissionChecker) + { + _userPermissionChecker = userPermissionChecker; + } + + + } +} diff --git a/RSecurityBackend/DbContext/RSecurityDbContext.cs b/RSecurityBackend/DbContext/RSecurityDbContext.cs new file mode 100644 index 00000000..f3b5efa8 --- /dev/null +++ b/RSecurityBackend/DbContext/RSecurityDbContext.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using RSecurityBackend.Models.Audit.Db; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Image; +using System; + +namespace RSecurityBackend.DbContext +{ + /// + /// Security EF DbContext + /// + public class RSecurityDbContext : IdentityDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable + { + /// + /// parameterless constructor + /// + public RSecurityDbContext() + : base() + { + + } + /// + /// delete db + /// + public void DeleteDb() + { + Database.EnsureDeleted(); + } + + /// + /// + /// + /// + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); //https://stackoverflow.com/a/34013431/66657 + + builder.Entity() + .HasIndex(u => u.Secret) + .IsUnique(); + } + + #region Auth + /// + /// Permissions + /// + public DbSet Permissions { get; set; } + + /// + /// User Sessions + /// + public DbSet Sessions { get; set; } + + + /// + /// Signup Queue Items + /// + public DbSet VerifyQueueItems { get; set; } + + /// + /// General Images + /// + public DbSet GeneralImages { get; set; } + + /// + /// Captcha Images + /// + public DbSet CaptchaImages { get; set; } + + + #endregion + + #region Audit + + /// + /// Audit Logs Events + /// + public DbSet AuditLogs { get; set; } + #endregion + } +} diff --git a/RSecurityBackend/Models/Audit/Db/REvent.cs b/RSecurityBackend/Models/Audit/Db/REvent.cs new file mode 100644 index 00000000..a08747b9 --- /dev/null +++ b/RSecurityBackend/Models/Audit/Db/REvent.cs @@ -0,0 +1,61 @@ +using System; + +namespace RSecurityBackend.Models.Audit.Db +{ + /// + /// Audit Event + /// + public class REvent + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Event Type + /// + public string EventType { get; set; } + + /// + /// Start Date + /// + public DateTime StartDate { get; set; } + + /// + /// Last Updated Date + /// + public DateTime? EndDate { get; set; } + + /// + /// Duration + /// + public int Duration { get; set; } + + /// + /// User Name + /// + public string UserName { get; set; } + + /// + /// IP Address + /// + public string IpAddress { get; set; } + + /// + /// Response Code + /// + public int ResponseStatusCode { get; set; } + + /// + /// Requset Url + /// + public string RequestUrl { get; set; } + + /// + /// Jason Data + /// + public string JsonData { get; set; } + + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RAppRole.cs b/RSecurityBackend/Models/Auth/Db/RAppRole.cs new file mode 100644 index 00000000..29d28eda --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RAppRole.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Role Permissions (a set of defined permision policies which could be assigned to users) + /// + public class RAppRole : IdentityRole + { + /// + /// default constructor + /// + public RAppRole() : base() + { + + } + + /// + /// constructor + /// + /// + public RAppRole(string roleName) : base(roleName) + { + } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// Permissions + /// + public ICollection Permissions { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RAppUser.cs b/RSecurityBackend/Models/Auth/Db/RAppUser.cs new file mode 100644 index 00000000..f297061a --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RAppUser.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; +using RSecurityBackend.Models.Image; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Ganjoor Application User + /// + public class RAppUser : IdentityUser + { + /// + /// First Name + /// + [MinLength(1)] + public string FirstName { get; set; } + + /// + /// Sure Name + /// + [MinLength(1)] + public string SureName { get; set; } + + /// + /// User Creation Date + /// + [Required] + public DateTime CreateDate { get; set; } + + /// + /// user status + /// + [Required] + public RAppUserStatus Status { get; set; } + + /// + /// user image + /// + public virtual RImage RImage { get; set; } + + /// + /// user image id + /// + public Guid? RImageId { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RAppUserStatus.cs b/RSecurityBackend/Models/Auth/Db/RAppUserStatus.cs new file mode 100644 index 00000000..65531053 --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RAppUserStatus.cs @@ -0,0 +1,17 @@ +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// User Status + /// + public enum RAppUserStatus + { + /// + /// Inactive + /// + Inactive = 0, + /// + /// Active + /// + Active = 1 + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RCaptchaImage.cs b/RSecurityBackend/Models/Auth/Db/RCaptchaImage.cs new file mode 100644 index 00000000..6c79b96b --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RCaptchaImage.cs @@ -0,0 +1,36 @@ +using RSecurityBackend.Models.Image; +using System; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Captcha Image + /// + public class RCaptchaImage + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Captcha Image + /// + public RImage RImage { get; set; } + + /// + /// Captcha Image Id + /// + public Guid RImageId { get; set; } + + /// + /// Captcha Text + /// + public string Value { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RPermission.cs b/RSecurityBackend/Models/Auth/Db/RPermission.cs new file mode 100644 index 00000000..b4fead5f --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RPermission.cs @@ -0,0 +1,32 @@ +using System; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Permission line + /// + /// + public class RPermission + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// SecurableItem short name + /// + /// + /// job + /// + public string SecurableItemShortName { get; set; } + + /// + /// Operation short name + /// + /// + /// view + /// + public string OperationShortName { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RTemporaryUserSession.cs b/RSecurityBackend/Models/Auth/Db/RTemporaryUserSession.cs new file mode 100644 index 00000000..4edfe3e6 --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RTemporaryUserSession.cs @@ -0,0 +1,72 @@ +using System; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Ganjoor User Session (Temporary) + /// + /// + /// Warning: Instances are supposed to delete when user logs out, so do not link anything serious to it + /// + public class RTemporaryUserSession + { + /// + /// Session Id + /// + public Guid Id { get; set; } + + /// + /// User Id + /// + public Guid RAppUserId { get; set; } + + /// + /// User + /// + public RAppUser RAppUser { get; set; } + + /// + /// Client IP Address + /// + public string ClientIPAddress { get; set; } + + /// + /// Client Application Name + /// + /// + /// Ganjoor Angular Client + /// + public string ClientAppName { get; set; } + + /// + /// Client Language + /// + /// + /// fa-IR + /// + public string Language { get; set; } + + /// + /// Login Date + /// + public DateTime LoginTime { get; set; } + + /// + /// Last Renewal + /// + public DateTime LastRenewal { get; set; } + + /// + /// Expiraton Time + /// + public DateTime ValidUntil { get; set; } + + /// + /// User Token + /// + public string Token { get; set; } + + + + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RVerifyQueueItem.cs b/RSecurityBackend/Models/Auth/Db/RVerifyQueueItem.cs new file mode 100644 index 00000000..ccc1d7a7 --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RVerifyQueueItem.cs @@ -0,0 +1,58 @@ +using System; + +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// User SignUp Queue + /// + public class RVerifyQueueItem + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// queue type + /// + public RVerifyQueueType QueueType { get; set; } + + /// + /// Email + /// + public string Email { get; set; } + + /// + /// PhoneNumber + /// + public string PhoneNumber { get; set; } + + /// + /// DateTime + /// + public DateTime DateTime { get; set; } + + /// + /// Secret + /// + public string Secret { get; set; } + + /// + /// client IP address + /// + public string ClientIPAddress { get; set; } + + /// + /// client app name + /// + public string ClientAppName { get; set; } + + /// + /// Client Language + /// + /// + /// fa-IR + /// + public string Language { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Db/RVerifyQueueType.cs b/RSecurityBackend/Models/Auth/Db/RVerifyQueueType.cs new file mode 100644 index 00000000..4c30e962 --- /dev/null +++ b/RSecurityBackend/Models/Auth/Db/RVerifyQueueType.cs @@ -0,0 +1,17 @@ +namespace RSecurityBackend.Models.Auth.Db +{ + /// + /// Verify Queue Type + /// + public enum RVerifyQueueType + { + /// + /// Sign up by email + /// + SignUp = 0, + /// + /// Forgot Password by email + /// + ForgotPassword = 1 + } +} diff --git a/RSecurityBackend/Models/Auth/Memory/SecurableItem.cs b/RSecurityBackend/Models/Auth/Memory/SecurableItem.cs new file mode 100644 index 00000000..6be3e7ba --- /dev/null +++ b/RSecurityBackend/Models/Auth/Memory/SecurableItem.cs @@ -0,0 +1,142 @@ +namespace RSecurityBackend.Models.Auth.Memory +{ + /// + /// securable itmes: forms and ... + /// + public class SecurableItem + { + /// + /// user + /// + public const string UserEntityShortName = "user"; + /// + /// role + /// + public const string RoleEntityShortName = "role"; + + /// + /// audit + /// + public const string AuditLogEntityShortName = "audit"; + + + /// + /// view + /// + public const string ViewOperationShortName = "view"; + /// + /// add + /// + public const string AddOperationShortName = "add"; + /// + /// modify + /// + public const string ModifyOperationShortName = "modify"; + /// + /// delete + /// + public const string DeleteOperationShortName = "delete"; + /// + /// sessions + /// + public const string SessionsOperationShortName = "sessions"; + /// + /// delothersession + /// + public const string DelOtherUserSessionOperationShortName = "delothersession"; + /// + /// view all + /// + public const string ViewAllOperationShortName = "viewall"; + + + + /// + /// list of forms and their permissions + /// + public static SecurableItem[] Items + { + get + { + return new SecurableItem[] + { + new SecurableItem() + { + ShortName = UserEntityShortName, + Description = "کاربران", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(ViewOperationShortName, "مشاهده", false, null ), + new SecurableItemOperation(AddOperationShortName, "ایجاد", false, + new SecurableItemOperationPrerequisite[] + { + new SecurableItemOperationPrerequisite( RoleEntityShortName, ViewOperationShortName) + } + ), + new SecurableItemOperation(ModifyOperationShortName, "اصلاح", false, + new SecurableItemOperationPrerequisite[] + { + new SecurableItemOperationPrerequisite( RoleEntityShortName, ViewOperationShortName) + } + ), + new SecurableItemOperation(DeleteOperationShortName, "حذف", false), + new SecurableItemOperation(SessionsOperationShortName, "مشاهده جلسات همه کاربران", false, null), + new SecurableItemOperation(DelOtherUserSessionOperationShortName, "حذف جلسه سایر کاربران", false), + new SecurableItemOperation(ViewAllOperationShortName, "مشاهده اطلاعات کاربران دیگر", false, null ), + } + }, + new SecurableItem() + { + ShortName = RoleEntityShortName, + Description = "نقش‌ها", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(ViewOperationShortName, "مشاهده", false), + new SecurableItemOperation(AddOperationShortName, "ایجاد", false), + new SecurableItemOperation(ModifyOperationShortName, "اصلاح", false), + new SecurableItemOperation(DeleteOperationShortName, "حذف", false), + } + }, + new SecurableItem() + { + ShortName = AuditLogEntityShortName, + Description = "رویدادها", + Operations = new SecurableItemOperation[] + { + new SecurableItemOperation(ViewOperationShortName, "مشاهده", false), + } + } + }; + } + } + + /// + /// Short Name + /// + /// + /// user + /// + public string ShortName { get; set; } + + /// + /// Descripttion + /// + /// + /// کاربران + /// + public string Description { get; set; } + + /// + /// Operations (short name + description + has permission) + /// + /// + /// [ + /// [view, مشاهده, true], + /// [add, ایجاد, false] + /// ] + /// + public SecurableItemOperation[] Operations { get; set; } + + + } +} diff --git a/RSecurityBackend/Models/Auth/Memory/SecurableItemOperation.cs b/RSecurityBackend/Models/Auth/Memory/SecurableItemOperation.cs new file mode 100644 index 00000000..5c9fbc30 --- /dev/null +++ b/RSecurityBackend/Models/Auth/Memory/SecurableItemOperation.cs @@ -0,0 +1,65 @@ +namespace RSecurityBackend.Models.Auth.Memory +{ + /// + /// operations (rights/permissions) for SecurableItem s + /// + public class SecurableItemOperation + { + /// + /// default constructor + /// + public SecurableItemOperation() : this("", "", false) + { + + } + /// + /// one line constructor + /// + /// + /// + /// + /// + // + public SecurableItemOperation(string shortName, string description, bool status, SecurableItemOperationPrerequisite[] prerequisites = null ) + { + ShortName = shortName; + Description = description; + Status = status; + if(prerequisites == null) + { + Prerequisites = new SecurableItemOperationPrerequisite[] { }; + } + else + { + Prerequisites = prerequisites; + } + + } + /// + /// Short Name + /// + /// + /// view + /// + public string ShortName { get; set; } + + /// + /// Descripttion + /// + /// + /// مشاهده + /// + public string Description { get; set; } + + /// + /// Prerequisites + /// + public SecurableItemOperationPrerequisite[] Prerequisites { get; set; } + + + /// + /// Status (has permission) + /// + public bool Status { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/Memory/SecurableItemOperationPrerequisite.cs b/RSecurityBackend/Models/Auth/Memory/SecurableItemOperationPrerequisite.cs new file mode 100644 index 00000000..9fc5b6df --- /dev/null +++ b/RSecurityBackend/Models/Auth/Memory/SecurableItemOperationPrerequisite.cs @@ -0,0 +1,35 @@ + +namespace RSecurityBackend.Models.Auth.Memory +{ + /// + /// SecurableItemOperation Prerequisite + /// + public class SecurableItemOperationPrerequisite + { + /// + /// + /// + /// + /// + public SecurableItemOperationPrerequisite(string secureItemShortName, string operationShortName) + { + SecureItemShortName = secureItemShortName; + OperationShortName = operationShortName; + } + /// + /// Prerequisite SecureItem ShortName + /// + /// + /// job + /// + public string SecureItemShortName { get; set; } + + /// + /// Prerequisite Operation ShortName + /// + /// + /// view + /// + public string OperationShortName { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/BillingUserSignup.cs b/RSecurityBackend/Models/Auth/ViewModels/BillingUserSignup.cs new file mode 100644 index 00000000..79593023 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/BillingUserSignup.cs @@ -0,0 +1,13 @@ +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// a model suitable for anonymous user sign up by billing users + /// + public class BillingUserSignup : RegisterRAppUser + { + /// + /// secret for securing signup process + /// + public string TenantSecret { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/LoggedOnUserModel.cs b/RSecurityBackend/Models/Auth/ViewModels/LoggedOnUserModel.cs new file mode 100644 index 00000000..d9364dc6 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/LoggedOnUserModel.cs @@ -0,0 +1,32 @@ +using RSecurityBackend.Models.Auth.Memory; +using System; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// Logged On User Model + /// + public class LoggedOnUserModel + { + /// + /// Session Id + /// + public Guid SessionId { get; set; } + + /// + /// Security Token + /// + public string Token { get; set; } + + /// + /// User Information + /// + public PublicRAppUser User { get; set; } + + /// + /// Permissions + /// + public SecurableItem[] SecurableItem { get; set; } + + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/LoginViewModel.cs b/RSecurityBackend/Models/Auth/ViewModels/LoginViewModel.cs new file mode 100644 index 00000000..6ccff5e4 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/LoginViewModel.cs @@ -0,0 +1,42 @@ +using Audit.WebApi; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// AppUserController.Login parameter + /// + public class LoginViewModel + { + /// + /// username + /// + /// + /// admin + /// + public string Username { get; set; } + + /// + /// password + /// + /// + /// Test!123 + /// + public string Password { get; set; } + + /// + /// Client App Name + /// + /// + /// Swashbuckle UI Client + /// + public string ClientAppName { get; set; } + + /// + /// Client Language + /// + /// + /// fa-IR + /// + public string Language { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/PublicRAppUser.cs b/RSecurityBackend/Models/Auth/ViewModels/PublicRAppUser.cs new file mode 100644 index 00000000..39ddc95e --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/PublicRAppUser.cs @@ -0,0 +1,111 @@ +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Image; +using System; +using System.ComponentModel.DataAnnotations; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// user informtaion + /// + /// + /// a safe subset of RAppUser + /// + public class PublicRAppUser + { + /// + /// constructor + /// + /// + public PublicRAppUser(RAppUser dbUserInfo) + { + if (dbUserInfo != null) + { + Id = dbUserInfo.Id; + Username = dbUserInfo.UserName; + FirstName = dbUserInfo.FirstName; + SureName = dbUserInfo.SureName; + Email = dbUserInfo.Email; + PhoneNumber = dbUserInfo.PhoneNumber; + Status = dbUserInfo.Status; + RImageId = dbUserInfo.RImageId; + } + } + /// + /// default constructor + /// + public PublicRAppUser() + { + + } + + /// + /// Id + /// + /// + /// + /// + public Guid? Id { get; set; } + + /// + /// User Name + /// + /// + /// test + /// + [MinLength(4)] + public string Username { get; set; } + + /// + /// User Email + /// + /// + /// test@ganjoor.net + /// + [EmailAddress] + public string Email { get; set; } + + /// + /// User Mobile Phone Number + /// + /// + /// +989121234567 + /// + [Phone] + public string PhoneNumber { get; set; } + + /// + /// First Name + /// + /// + /// Hamid Reza + /// + [MinLength(1)] + public string FirstName { get; set; } + + /// + /// Sure Name + /// + /// + /// Mohammadi + /// + [MinLength(1)] + public string SureName { get; set; } + + /// + /// user status + /// + /// + /// 1 + /// + [Required] + public RAppUserStatus Status { get; set; } + + /// + /// user image + /// + public Guid? RImageId { get; set; } + + + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/PublicRUserSession.cs b/RSecurityBackend/Models/Auth/ViewModels/PublicRUserSession.cs new file mode 100644 index 00000000..f6d0dbc5 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/PublicRUserSession.cs @@ -0,0 +1,68 @@ +using RSecurityBackend.Models.Auth.Db; +using System; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// a safe subset of RUserSession + /// + public class PublicRUserSession + { + + /// + /// constructor + /// + /// + public PublicRUserSession(RTemporaryUserSession src) + { + Id = src.Id; + RAppUser = new PublicRAppUser(src.RAppUser); + ClientIPAddress = src.ClientIPAddress; + ClientAppName = src.ClientAppName; + Language = src.Language; + LoginTime = src.LoginTime; + LastRenewal = src.LastRenewal; + } + + /// + /// Session Id + /// + public Guid Id { get; set; } + + /// + /// User + /// + public PublicRAppUser RAppUser { get; set; } + + /// + /// Client IP Address + /// + public string ClientIPAddress { get; set; } + + /// + /// Client Application Name + /// + /// + /// Ganjoor Angular Client + /// + public string ClientAppName { get; set; } + + /// + /// Client Language + /// + /// + /// fa-IR + /// + public string Language { get; set; } + + /// + /// Login Date + /// + public DateTime LoginTime { get; set; } + + /// + /// Last Renewal + /// + public DateTime LastRenewal { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/RegisterRAppUser.cs b/RSecurityBackend/Models/Auth/ViewModels/RegisterRAppUser.cs new file mode 100644 index 00000000..b307f4bf --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/RegisterRAppUser.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// user infor used for registeration + /// + public class RegisterRAppUser : PublicRAppUser + { + /// + /// desired password, if sent empty system generates one + /// + /// + /// Test!123 + /// + [MinLength(6)] + public string Password { get; set; } + + /// + /// is admin + /// + /// + /// false + /// + public bool IsAdmin { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/ResetPasswordViewModel.cs b/RSecurityBackend/Models/Auth/ViewModels/ResetPasswordViewModel.cs new file mode 100644 index 00000000..7506a5a0 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/ResetPasswordViewModel.cs @@ -0,0 +1,33 @@ +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// reset password view model + /// + public class ResetPasswordViewModel + { + /// + /// Email + /// + /// + /// mohammadi.hr@gmail.com + /// + public string Email { get; set; } + + /// + /// Secret + /// + /// + /// 4ozHJQN0X6CebX0He0/xaznhIjvubfySFnwdoCYLLo8= + /// + public string Secret { get; set; } + + /// + /// password + /// + /// + /// Test!123 + /// + public string Password { get; set; } + + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/SetPasswordModel.cs b/RSecurityBackend/Models/Auth/ViewModels/SetPasswordModel.cs new file mode 100644 index 00000000..0ff653ac --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/SetPasswordModel.cs @@ -0,0 +1,18 @@ +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// set password model + /// + public class SetPasswordModel + { + /// + /// old password + /// + public string OldPassword { get; set; } + + /// + /// new password + /// + public string NewPassword { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/UnverifiedSignUpViewModel.cs b/RSecurityBackend/Models/Auth/ViewModels/UnverifiedSignUpViewModel.cs new file mode 100644 index 00000000..1913d9aa --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/UnverifiedSignUpViewModel.cs @@ -0,0 +1,44 @@ +using System; + +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// SignUp View Model + /// + public class UnverifiedSignUpViewModel + { + /// + /// Email + /// + /// + /// mohammadi.hr@gmail.com + /// + public string Email { get; set; } + + /// + /// Client App Name + /// + /// + /// Swashbuckle UI Client + /// + public string ClientAppName { get; set; } + + /// + /// Client Language + /// + /// + /// fa-IR + /// + public string Language { get; set; } + + /// + /// Captcha Image Id + /// + public Guid CaptchaImageId { get; set; } + + /// + /// Captcha Value + /// + public string CaptchaValue { get; set; } + } +} diff --git a/RSecurityBackend/Models/Auth/ViewModels/VerifiedSignUpViewModel.cs b/RSecurityBackend/Models/Auth/ViewModels/VerifiedSignUpViewModel.cs new file mode 100644 index 00000000..24e8f774 --- /dev/null +++ b/RSecurityBackend/Models/Auth/ViewModels/VerifiedSignUpViewModel.cs @@ -0,0 +1,48 @@ +namespace RSecurityBackend.Models.Auth.ViewModels +{ + /// + /// verified sign up view model + /// + public class VerifiedSignUpViewModel + { + /// + /// Email + /// + /// + /// mohammadi.hr@gmail.com + /// + public string Email { get; set; } + + /// + /// Secret + /// + /// + /// 4ozHJQN0X6CebX0He0/xaznhIjvubfySFnwdoCYLLo8= + /// + public string Secret { get; set; } + + /// + /// password + /// + /// + /// Test!123 + /// + public string Password { get; set; } + + /// + /// First Name + /// + /// + /// Hamid Reza + /// + public string FirstName { get; set; } + + /// + /// Sure Name + /// + /// + /// Mohammadi + /// + public string SureName { get; set; } + } +} diff --git a/RSecurityBackend/Models/Billing/RChargableFeature.cs b/RSecurityBackend/Models/Billing/RChargableFeature.cs new file mode 100644 index 00000000..74d5ffe7 --- /dev/null +++ b/RSecurityBackend/Models/Billing/RChargableFeature.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; + +namespace RSecurityBackend.Models.Billing +{ + /// + /// Chargable Feature For Tenant + /// + public class RChargableFeature + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Code Name + /// + public string CodeName { get; set; } + + /// + /// Feature Name + /// + public string Name { get; set; } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// if it is not optional it cannot be unselected (boolean) or it might be selected at least at minimum value + /// + public bool Optional { get; set; } = true; + + /// + /// Feature Type + /// + public RChargableFeatureType FeatureType { get; set; } = RChargableFeatureType.Boolean; + + /// + /// miniumum amount + /// + public decimal MinimumAmount { get; set; } = 0; + + /// + /// suggested amount + /// + public decimal SuggestedAmount { get; set; } = 0; + + /// + /// has maximum + /// + public bool HasMaximumAmount { get; set; } = true; + + /// + /// maximum value + /// + public decimal MaximumnAmount { get; set; } = 1; + + /// + /// if a module contains features for other modules added their codeNames here (comma separated) + /// + public string Covering { get; set; } = ""; + + /// + /// it must be priced according to period or it is a one time cost + /// + public bool ConstantOverPeriod { get; set; } = false; + + /// + /// is it refundable + /// + public bool Refundable { get; set; } = true; + + /// + /// is it setup cost + /// + public bool InitialSetupCost { get; set; } = false; + + /// + /// is active + /// + public bool Active { get; set; } = true; + + /// + /// prices + /// + public ICollection Prices { get; set; } + } +} diff --git a/RSecurityBackend/Models/Billing/RChargableFeaturePrice.cs b/RSecurityBackend/Models/Billing/RChargableFeaturePrice.cs new file mode 100644 index 00000000..13cf4e3b --- /dev/null +++ b/RSecurityBackend/Models/Billing/RChargableFeaturePrice.cs @@ -0,0 +1,40 @@ +using System; + +namespace RSecurityBackend.Models.Billing +{ + /// + /// Chargable Feature Price Row + /// + public class RChargableFeaturePrice + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Minimum Amount For This Price To be applicable + /// + public decimal MinimumAmount { get; set; } = 0; + + /// + /// base price: (Minimum Amount whole price) + /// + public decimal BasePrice { get; set; } = 0; + + /// + /// unit price for additional items from (Amount-MinimumAmount) + /// + public decimal UnitPrice { get; set; } = 0; + + /// + /// active + /// + public bool Active { get; set; } = true; + + /// + /// is effective from which date/time (useful for maintaining histotical data) + /// + public DateTime EffectiveFrom { get; set; } = DateTime.Now.Date; + } +} diff --git a/RSecurityBackend/Models/Billing/RChargableFeatureType.cs b/RSecurityBackend/Models/Billing/RChargableFeatureType.cs new file mode 100644 index 00000000..45472840 --- /dev/null +++ b/RSecurityBackend/Models/Billing/RChargableFeatureType.cs @@ -0,0 +1,17 @@ +namespace RSecurityBackend.Models.Billing +{ + /// + /// Chargable Feature For Tenant Type + /// + public enum RChargableFeatureType + { + /// + /// yes/no + /// + Boolean = 0, + /// + /// 0, ... + /// + RegularNumber = 1, + } +} diff --git a/RSecurityBackend/Models/Billing/Ravaal/ChargableFeatures.cs b/RSecurityBackend/Models/Billing/Ravaal/ChargableFeatures.cs new file mode 100644 index 00000000..87cbd9d9 --- /dev/null +++ b/RSecurityBackend/Models/Billing/Ravaal/ChargableFeatures.cs @@ -0,0 +1,201 @@ +namespace RSecurityBackend.Models.Billing.Ganjoor +{ + /// + /// Ganjoor Chargable Features + /// + public static class ChargableFeatures + { + /// + /// Setup + /// + public const string Setup = "setup"; + + /// + /// Active Users + /// + public const string ActiveUsers = "activeusers"; + + /// + /// GanjoorDoc View + /// + public const string GanjoorDocView = "ganjoordoc:view"; + + /// + /// GanjoorDoc Full + /// + public const string GanjoorDocFull = "ganjoordoc:full"; + + /// + /// GanjoorRun View + /// + public const string GanjoorRunView = "ganjoorrun:view"; + + /// + /// GanjoorRun Full + /// + public const string GanjoorRunFull = "ganjoorrun:full"; + + /// + /// Features + /// + public static RChargableFeature[] Features + { + get + { + return new RChargableFeature[] + { + new RChargableFeature() + { + CodeName = Setup, + Name = "راه‌اندازی اولیه", + Description = "هزینهٔ اختصاص فضای ذخیره سازی (غیر قابل برگشت)", + Optional = false, + FeatureType = RChargableFeatureType.Boolean, + MinimumAmount = 1, + SuggestedAmount = 1, + HasMaximumAmount = true, + MaximumnAmount = 1, + Covering = "", + Refundable = false, + InitialSetupCost = true, + ConstantOverPeriod = true, + Prices = new RChargableFeaturePrice[] + { + new RChargableFeaturePrice() + { + MinimumAmount = 1, + BasePrice = 10000000, + UnitPrice = 0 + } + } + }, + new RChargableFeature() + { + CodeName = ActiveUsers, + Name = "حداکثر کاربران فعال", + Description = "حداکثر تعداد کاربران فعال در سازمان", + Optional = false, + FeatureType = RChargableFeatureType.RegularNumber, + MinimumAmount = 5, + SuggestedAmount = 5, + HasMaximumAmount = false, + Covering = "", + Refundable = true, + InitialSetupCost = false, + ConstantOverPeriod = false, + Prices = new RChargableFeaturePrice[] + { + new RChargableFeaturePrice() + { + MinimumAmount = 6, + BasePrice = 50, + UnitPrice = 50 + }, + new RChargableFeaturePrice() + { + MinimumAmount = 11, + BasePrice = (10 - 5) * 50 + 25, + UnitPrice = 25 + }, + new RChargableFeaturePrice() + { + MinimumAmount = 21, + BasePrice = ((10 - 5) * 50) + (20 - 10) * 25 + 10, + UnitPrice = 10 + }, + } + }, + new RChargableFeature() + { + CodeName = GanjoorDocView, + Name = "ماژول مشاهده مستندات روالها", + Description = "در صورتی که تمایل دارید عناوین مربوطه در ناوبری ظاهر نشود این ماژول را از حالت انتخاب خارج کنید", + Optional = true, + FeatureType = RChargableFeatureType.Boolean, + MinimumAmount = 0, + SuggestedAmount = 0, + HasMaximumAmount = true, + MaximumnAmount = 1, + Covering = "", + Refundable = true, + InitialSetupCost = false, + ConstantOverPeriod = false, + Prices = new RChargableFeaturePrice[] + { + } + }, + new RChargableFeature() + { + CodeName = GanjoorDocFull, + Name = "ماژول مستندسازی روالها", + Description = "دسترسی کامل به ماژول مستندسازی روالها", + Optional = true, + FeatureType = RChargableFeatureType.Boolean, + MinimumAmount = 0, + SuggestedAmount = 1, + HasMaximumAmount = true, + MaximumnAmount = 1, + Covering = GanjoorDocView, + Refundable = true, + InitialSetupCost = false, + ConstantOverPeriod = false, + Prices = new RChargableFeaturePrice[] + { + new RChargableFeaturePrice() + { + MinimumAmount = 1, + BasePrice = 10000, + UnitPrice = 0 + }, + } + }, + new RChargableFeature() + { + CodeName = GanjoorRunView, + Name = "ماژول مشاهده اطلاعات اجرایی روالها (کارها)", + Description = "در صورتی که تمایل دارید عناوین مربوطه در ناوبری ظاهر نشود این ماژول را از حالت انتخاب خارج کنید", + Optional = true, + FeatureType = RChargableFeatureType.Boolean, + MinimumAmount = 0, + SuggestedAmount = 0, + HasMaximumAmount = true, + MaximumnAmount = 1, + Covering = "", + Refundable = true, + InitialSetupCost = false, + ConstantOverPeriod = false, + Prices = new RChargableFeaturePrice[] + { + } + }, + new RChargableFeature() + { + CodeName = GanjoorRunFull, + Name = "ماژول اجرای روالها (کارها)", + Description = "دسترسی کامل به ماژوال اجرای روالها", + Optional = true, + FeatureType = RChargableFeatureType.Boolean, + MinimumAmount = 0, + SuggestedAmount = 1, + HasMaximumAmount = true, + MaximumnAmount = 1, + Refundable = true, + InitialSetupCost = false, + ConstantOverPeriod = false, + Covering = GanjoorRunView, + Prices = new RChargableFeaturePrice[] + { + new RChargableFeaturePrice() + { + MinimumAmount = 1, + BasePrice = 20000, + UnitPrice = 0 + }, + } + }, + + }; + } + } + } +} diff --git a/RSecurityBackend/Models/ClientBilling/Db/RBillingData.cs b/RSecurityBackend/Models/ClientBilling/Db/RBillingData.cs new file mode 100644 index 00000000..b0910aaf --- /dev/null +++ b/RSecurityBackend/Models/ClientBilling/Db/RBillingData.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace RSecurityBackend.Models.ClientBilling.Db +{ + /// + /// cached billing info + /// + public class RBillingData + { + /// + /// code name + /// + public string CodeName { get; set; } + + /// + /// db name + /// + public string DatabaseName { get; set; } + + /// + /// personal tenants have limitted unmodifyable features and are not listed in tenant management panel + /// + public bool Personal { get; set; } + + /// + /// active + /// + public bool Active { get; set; } + + /// + /// last update (use for caching purposes) + /// + public DateTime LastUpdate { get; set; } + + /// + /// data is valid until this date and should be refreshed after that because of changes of user billing status + /// + public DateTime ValidUntil { get; set; } + + /// + /// auto charge user for tenant validitiy expiration + /// + public bool AutoCharge { get; set; } + + /// + /// charged features + /// + public ICollection Features { get; set; } + } +} diff --git a/RSecurityBackend/Models/ClientBilling/Db/RChargedFeature.cs b/RSecurityBackend/Models/ClientBilling/Db/RChargedFeature.cs new file mode 100644 index 00000000..b9c4c8ac --- /dev/null +++ b/RSecurityBackend/Models/ClientBilling/Db/RChargedFeature.cs @@ -0,0 +1,31 @@ +using System; + + +namespace RSecurityBackend.Models.ClientBilling.Db +{ + /// + /// charged feature + /// + public class RChargedFeature + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Code Name + /// + public string CodeName { get; set; } + + /// + /// Feature Name + /// + public string Name { get; set; } + + /// + /// Amount + /// + public decimal Amount { get; set; } + } +} diff --git a/RSecurityBackend/Models/Generic/PaginationMetadata.cs b/RSecurityBackend/Models/Generic/PaginationMetadata.cs new file mode 100644 index 00000000..7c521e19 --- /dev/null +++ b/RSecurityBackend/Models/Generic/PaginationMetadata.cs @@ -0,0 +1,38 @@ +namespace RSecurityBackend.Models.Generic +{ + /// + /// pagination metadat + /// + public class PaginationMetadata + { + /// + /// total count + /// + public int totalCount { get; set; } + + /// + /// page size + /// + public int pageSize { get; set; } + + /// + /// current page + /// + public int currentPage { get; set; } + + /// + /// total pages + /// + public int totalPages { get; set; } + + /// + /// has previous page + /// + public bool hasPreviousPage { get; set; } + + /// + /// has next page + /// + public bool hasNextPage { get; set; } + } +} diff --git a/RSecurityBackend/Models/Generic/PagingParameterModel.cs b/RSecurityBackend/Models/Generic/PagingParameterModel.cs new file mode 100644 index 00000000..12243baa --- /dev/null +++ b/RSecurityBackend/Models/Generic/PagingParameterModel.cs @@ -0,0 +1,39 @@ +namespace RSecurityBackend.Models.Generic +{ + /// + /// Paging Parameter Model + /// + /// + /// https://www.c-sharpcorner.com/article/how-to-do-paging-with-asp-net-web-api/ + /// + public class PagingParameterModel + { + /// + /// max page size + /// + const int MaxPageSize = 1000; + + /// + /// page number + /// + public int PageNumber { get; set; } = 1; + + /// + /// actual page size + /// + private int _pageSize { get; set; } = -1; + + /// + /// settable page size + /// + public int PageSize + { + + get { return _pageSize; } + set + { + _pageSize = (value > MaxPageSize) ? MaxPageSize : value; + } + } + } +} diff --git a/RSecurityBackend/Models/Generic/RServiceResult.cs b/RSecurityBackend/Models/Generic/RServiceResult.cs new file mode 100644 index 00000000..e2841ace --- /dev/null +++ b/RSecurityBackend/Models/Generic/RServiceResult.cs @@ -0,0 +1,41 @@ +namespace RSecurityBackend.Models.Generic +{ + /// + /// a generic class containing service method return values + an exception string as a more readable replacement for Tuple + /// and also a solution for writing async methods which do not permit using out parameters + /// + public class RServiceResult + { + /// + /// constructor + /// + /// + /// + public RServiceResult(T result, string exceptionString = "") + { + Result = result; + ExceptionString = exceptionString; + } + + /// + /// Actual result + /// + public T Result { get; set; } + + /// + /// Exception String + /// + public string ExceptionString { get; set; } + + /// + /// ToString + /// + /// + public override string ToString() + { + if (!string.IsNullOrEmpty(ExceptionString)) + return ExceptionString; + return Result.ToString(); + } + } +} diff --git a/RSecurityBackend/Models/Image/RImage.cs b/RSecurityBackend/Models/Image/RImage.cs new file mode 100644 index 00000000..b10c9de5 --- /dev/null +++ b/RSecurityBackend/Models/Image/RImage.cs @@ -0,0 +1,70 @@ +using System; + +namespace RSecurityBackend.Models.Image +{ + /// + /// Image Files + /// + public class RImage + { + /// + /// Id + /// + public Guid Id { get; set; } + + /// + /// Original File Name + /// + public string OriginalFileName { get; set; } + + /// + /// content type + /// + public string ContentType { get; set; } + + /// + /// Original Image File Size In Byte + /// + public long FileSizeInBytes { get; set; } + + /// + /// Original Image Width + /// + public int ImageWidth { get; set; } + + /// + /// Original Image Height + /// + public int ImageHeight { get; set; } + + /// + /// پوشه محل ذخیره تصویر + /// + /// + /// 2017-08 + /// + public string FolderName { get; set; } + + /// + /// نام فایل ذخیره شده با بالاترین کیفیت + /// + /// + /// 85007253-09f2-4434-8b7e-1a23db2cd9c9.png + /// + public string StoredFileName { get; set; } + + /// + /// datetime + /// + public DateTime DataTime { get; set; } + + /// + /// Last Modified for caching purposes + /// + public DateTime LastModified { get; set; } + + + + + } +} diff --git a/RSecurityBackend/Models/Mail/SmptConfig.cs b/RSecurityBackend/Models/Mail/SmptConfig.cs new file mode 100644 index 00000000..f86efa51 --- /dev/null +++ b/RSecurityBackend/Models/Mail/SmptConfig.cs @@ -0,0 +1,34 @@ +namespace RSecurityBackend.Models.Mail +{ + /// + /// smtp config + /// + public class SmptConfig + { + /// + /// port + /// + public int port { get; set; } + /// + /// server + /// + public string server { get; set; } + /// + /// username + /// + public string smtpUsername { get; set; } + /// + /// password + /// + public string smtpPassword { get; set; } + /// + /// from + /// + public string from { get; set; } + + /// + /// use ssl + /// + public bool useSsl { get; set; } + } +} diff --git a/RSecurityBackend/RSecurityBackend.csproj b/RSecurityBackend/RSecurityBackend.csproj new file mode 100644 index 00000000..86419890 --- /dev/null +++ b/RSecurityBackend/RSecurityBackend.csproj @@ -0,0 +1,37 @@ + + + + netcoreapp3.1 + + + + RSecurityBackend.xml + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + Always + + + + diff --git a/RSecurityBackend/RSecurityBackend.xml b/RSecurityBackend/RSecurityBackend.xml new file mode 100644 index 00000000..df14a63e --- /dev/null +++ b/RSecurityBackend/RSecurityBackend.xml @@ -0,0 +1,2957 @@ + + + + RSecurityBackend + + + + + UserGroupPermissionHandler + + + + + HandleRequirementAsync + + + + + + + + IUserPermissionChecker instance + + + + + constructor + + + + + + UserGroupPermissionRequirement + + + + + + + + + job + + + + + + + + + view + + + + + constructor + + + + + + + User login/logout/register/... + + + + + login + + loginViewModel + LoggedOnUserModel + + + + Logout user (users need user:delothersession to logout other users) + + + User Session Id + + + + + Check if my session is valid + + + + + + All Users Information (if user does not have user:view permission list only contains him/her information) + + All Users Information + + + + returns user information (if user does not have user:view permission trying to view other users' information fails with a forbidden error) + + user id + user information + + + + add a new user (if you are trying to add an admin user you yourself should be admin) + + if passsword is sent empty system genrates one for it which could be retrieved from returned record + id/generated password if required could be retrieved from return value + + + + update existing user (if you are trying to update an admin user you yourself should be admin) (if user does not have user:modify permission trying to modify other users' information fails with a forbidden error) + + user id + existingUserInfo.id could be passed empty and it is ignored completely, if password is sent empty it does not has effect + true if succeeds + + + + set my password + + + + + + + delete user (only admin users can delete other admin users, a user cannot delete himself/herself) + + user id + true if succeeds + + + + Checks if user is admin (if user does not have user:viewall permission list it will be failed for any user id other than himself/herself) + + + + + + View User Sessions (user needs user:sessions permission to view other users sessions) + + + + + + Set User Image (via FormData, specifying userId as 'id' in formData ) - if Files.count is 0 image would be removed - (users need user:modify to change other users image) + + new image id + + + + Get User Image in base 64 + + + + + + + Get User Image in base 64 + + + + + + + returns user roles (if user does not have user:view permission trying to view other users' information fails with a forbidden error) + + user id + user roles + + + + remove user from role + + user id + + true if succeeds + + + + add user to role + + user id + + true if succeeds + + + + get a captcha image for signup or forgot password + + captchaimageid - display it using api/rimages/captchaimageid.jpg + + + + signup + + signUpViewModel + result + + + + Sign Up Email Subject + + + subject + + + + + + Sign Up Email Html Content + + + html content + + + + verify signup / forgot password + + + + associated secret email + + + + finalize signup process + + + + + + + start forgot password process by email + + signUpViewModel + result + + + + Forgot Password Email Subject + + + subject + + + + + + Forgot Password Email Html Content + + + html content + + + + Is Sign-up enabled? + + + + + + reset password + + + + + + + signup callback url + + + + + forgot password call back url + + + + + IAppUserService instance + + + + + for client IP resolution + + + + + IUserPermissionChecker instance + + + + + IEmailSender instance + + + + + Image File Service + + + + + Captcha service + + + + + Configuration + + + + + constructor + + + + + + + + + + + + Audit Log Controller Base + + + + + get all audit logs ordered by time + + + + + + + + + constructor + + + + + + + Artifact Service + + + + + Generic Image Provider + + + + + returns image stream + + + + + + + temporary api for uploading temporary images + + + + + + constructor + + + + + + + Artifact Service + + + + + User roles + + + + + All Roles Information + + All Roles Information + + + + returns role information + + role name + role information + + + + add a new role + + + id if required could be retrieved from return value + + + + update existing role + + role name + existingGroupInfo.id could be passed empty and it is ignored completely + true if succeeds + + + + delete role + + role name + true if succeeds + + + + lists role permissions + + + + + + Saves role permissions + + role name + + + + + + Get All SecurableItems + + All All SecurableItems + + + + IUserRoleService instance + + + + + constructor + + + + + + base class for controllers needing UserScopeCheck + + + + + UserScopeCheck method EntityShortName; + + + RBillingSecurableItem.TenantEntityShortName + + + + + UserScopeCheck method OperationShortName + + + SecurableItem.ViewOperationShortName + + + + + returns user id if user is a guest, and empty if he/she has access to view all users information + + + + + + can current user read this userId daya + + + + + + + Permission Checker Service + + + + + constructor + + + + + + Security EF DbContext + + + + + parameterless constructor + + + + + delete db + + + + + + + + + + + Permissions + + + + + User Sessions + + + + + Signup Queue Items + + + + + General Images + + + + + Captcha Images + + + + + Audit Logs Events + + + + + Audit Event + + + + + Id + + + + + Event Type + + + + + Start Date + + + + + Last Updated Date + + + + + Duration + + + + + User Name + + + + + IP Address + + + + + Response Code + + + + + Requset Url + + + + + Jason Data + + + + + Role Permissions (a set of defined permision policies which could be assigned to users) + + + + + default constructor + + + + + constructor + + + + + + Description + + + + + Permissions + + + + + Ganjoor Application User + + + + + First Name + + + + + Sure Name + + + + + User Creation Date + + + + + user status + + + + + user image + + + + + user image id + + + + + User Status + + + + + Inactive + + + + + Active + + + + + Captcha Image + + + + + Id + + + + + Captcha Image + + + + + Captcha Image Id + + + + + Captcha Text + + + + + DateTime + + + + + Permission line + + + + + + Id + + + + + SecurableItem short name + + + job + + + + + Operation short name + + + view + + + + + Ganjoor User Session (Temporary) + + + Warning: Instances are supposed to delete when user logs out, so do not link anything serious to it + + + + + Session Id + + + + + User Id + + + + + User + + + + + Client IP Address + + + + + Client Application Name + + + Ganjoor Angular Client + + + + + Client Language + + + fa-IR + + + + + Login Date + + + + + Last Renewal + + + + + Expiraton Time + + + + + User Token + + + + + User SignUp Queue + + + + + Id + + + + + queue type + + + + + Email + + + + + PhoneNumber + + + + + DateTime + + + + + Secret + + + + + client IP address + + + + + client app name + + + + + Client Language + + + fa-IR + + + + + Verify Queue Type + + + + + Sign up by email + + + + + Forgot Password by email + + + + + securable itmes: forms and ... + + + + + user + + + + + role + + + + + audit + + + + + view + + + + + add + + + + + modify + + + + + delete + + + + + sessions + + + + + delothersession + + + + + view all + + + + + list of forms and their permissions + + + + + Short Name + + + user + + + + + Descripttion + + + کاربران + + + + + Operations (short name + description + has permission) + + + [ + [view, مشاهده, true], + [add, ایجاد, false] + ] + + + + + operations (rights/permissions) for SecurableItem s + + + + + default constructor + + + + + one line constructor + + + + + + + + + Short Name + + + view + + + + + Descripttion + + + مشاهده + + + + + Prerequisites + + + + + Status (has permission) + + + + + SecurableItemOperation Prerequisite + + + + + + + + + + + + Prerequisite SecureItem ShortName + + + job + + + + + Prerequisite Operation ShortName + + + view + + + + + a model suitable for anonymous user sign up by billing users + + + + + secret for securing signup process + + + + + Logged On User Model + + + + + Session Id + + + + + Security Token + + + + + User Information + + + + + Permissions + + + + + AppUserController.Login parameter + + + + + username + + + admin + + + + + password + + + Test!123 + + + + + Client App Name + + + Swashbuckle UI Client + + + + + Client Language + + + fa-IR + + + + + user informtaion + + + a safe subset of RAppUser + + + + + constructor + + + + + + default constructor + + + + + Id + + + + + + + + User Name + + + test + + + + + User Email + + + test@ganjoor.net + + + + + User Mobile Phone Number + + + +989121234567 + + + + + First Name + + + Hamid Reza + + + + + Sure Name + + + Mohammadi + + + + + user status + + + 1 + + + + + user image + + + + + a safe subset of RUserSession + + + + + constructor + + + + + + Session Id + + + + + User + + + + + Client IP Address + + + + + Client Application Name + + + Ganjoor Angular Client + + + + + Client Language + + + fa-IR + + + + + Login Date + + + + + Last Renewal + + + + + user infor used for registeration + + + + + desired password, if sent empty system generates one + + + Test!123 + + + + + is admin + + + false + + + + + reset password view model + + + + + Email + + + mohammadi.hr@gmail.com + + + + + Secret + + + 4ozHJQN0X6CebX0He0/xaznhIjvubfySFnwdoCYLLo8= + + + + + password + + + Test!123 + + + + + set password model + + + + + old password + + + + + new password + + + + + SignUp View Model + + + + + Email + + + mohammadi.hr@gmail.com + + + + + Client App Name + + + Swashbuckle UI Client + + + + + Client Language + + + fa-IR + + + + + Captcha Image Id + + + + + Captcha Value + + + + + verified sign up view model + + + + + Email + + + mohammadi.hr@gmail.com + + + + + Secret + + + 4ozHJQN0X6CebX0He0/xaznhIjvubfySFnwdoCYLLo8= + + + + + password + + + Test!123 + + + + + First Name + + + Hamid Reza + + + + + Sure Name + + + Mohammadi + + + + + Ganjoor Chargable Features + + + + + Setup + + + + + Active Users + + + + + GanjoorDoc View + + + + + GanjoorDoc Full + + + + + GanjoorRun View + + + + + GanjoorRun Full + + + + + Features + + + + + Chargable Feature For Tenant + + + + + Id + + + + + Code Name + + + + + Feature Name + + + + + Description + + + + + if it is not optional it cannot be unselected (boolean) or it might be selected at least at minimum value + + + + + Feature Type + + + + + miniumum amount + + + + + suggested amount + + + + + has maximum + + + + + maximum value + + + + + if a module contains features for other modules added their codeNames here (comma separated) + + + + + it must be priced according to period or it is a one time cost + + + + + is it refundable + + + + + is it setup cost + + + + + is active + + + + + prices + + + + + Chargable Feature Price Row + + + + + Id + + + + + Minimum Amount For This Price To be applicable + + + + + base price: (Minimum Amount whole price) + + + + + unit price for additional items from (Amount-MinimumAmount) + + + + + active + + + + + is effective from which date/time (useful for maintaining histotical data) + + + + + Chargable Feature For Tenant Type + + + + + yes/no + + + + + 0, ... + + + + + cached billing info + + + + + code name + + + + + db name + + + + + personal tenants have limitted unmodifyable features and are not listed in tenant management panel + + + + + active + + + + + last update (use for caching purposes) + + + + + data is valid until this date and should be refreshed after that because of changes of user billing status + + + + + auto charge user for tenant validitiy expiration + + + + + charged features + + + + + charged feature + + + + + Id + + + + + Code Name + + + + + Feature Name + + + + + Amount + + + + + pagination metadat + + + + + total count + + + + + page size + + + + + current page + + + + + total pages + + + + + has previous page + + + + + has next page + + + + + Paging Parameter Model + + + https://www.c-sharpcorner.com/article/how-to-do-paging-with-asp-net-web-api/ + + + + + max page size + + + + + page number + + + + + actual page size + + + + + settable page size + + + + + a generic class containing service method return values + an exception string as a more readable replacement for Tuple + and also a solution for writing async methods which do not permit using out parameters + + + + + constructor + + + + + + + Actual result + + + + + Exception String + + + + + ToString + + + + + + Image Files + + + + + Id + + + + + Original File Name + + + + + content type + + + + + Original Image File Size In Byte + + + + + Original Image Width + + + + + Original Image Height + + + + + پوشه محل ذخیره تصویر + + + 2017-08 + + + + + نام فایل ذخیره شده با بالاترین کیفیت + + + 85007253-09f2-4434-8b7e-1a23db2cd9c9.png + + + + + datetime + + + + + Last Modified for caching purposes + + + + + smtp config + + + + + port + + + + + server + + + + + username + + + + + password + + + + + from + + + + + use ssl + + + + + Authentication Service + + + + + Login user, if failed return LoggedOnUserModel is null + + + + + + + + Logout + + + + + + + + Does Session exist? + + + + + + + + is user admin? + + + + + + + is user in either of passed roles? + + + + + + + + Get User Roles + + + + + + + remove user from role + + user id + + + + + + add user to role + + user id + + + + + + Lists user permissions + + + + + + + Has user specified permission + + + + + + + + + returns user information + + + PasswordHash becomes empty + + + + + + + all users informations + + + + + + add a new user + + + + + + + add user to role + + + + + + + + modify existing user + + + + + + + + change user password checking old password + + + + + + + + + delete user + + + true if succeeds + + + + Set User Image + + + + new user image id + + + + Get User Sessions + + if null is passed returns all sessions + + + + + Get User Image + + + + + + + Start signup process using email + + + + + + + + + + verify signup / forgot password + + + + associated email + + + + finalize signup process using email + + + + + + + + + + + Start forgot password process using email + + + + + + + + + + reset password using email + + + + + + + + + + delete tenant + + + + + + Administrator role name + + + + + secret used for generating Jwt token + + + + + JWT Tokens Expiration Time Out + + + + + Audit Log Service + + + + + get all audit logs ordered by time + + + empty means unfiltered + + + + + + Captcha Service + + + + + Generate Captcha + + + + + + evaluate captcha + + + + + + + + Image File Service + + + + + add new picture + + + + + pass empty if you want a generic date based folder + + + + + store added image + + + + + + + returns image info + + + + + + + returns image file stream + + + + + + + delete image from database and file system + + + + + + + Image Storage Path + + + + + Authentication Service + + + + + Login user, if failed return LoggedOnUserModel is null + + + + + + + + add user to role + + + + + + + + Logout + + + + + + + + Does Session exist? + + + + + + + + returns user information + + + PasswordHash becomes empty + + + + + + + all users informations + + + + + + Get User Sessions + + if null is passed returns all sessions + + + + + is user admin? + + + + + + + is user in either of passed roles? + + + + + + + + Get User Roles + + + + + + + remove user from role + + user id + + + + + + add user to role + + user id + + + + + + Lists user permissions + + + + + + + Has user specified permission + + + + + + + + + add a new user + + + + + + modify existing user + + + + + + + + change user password checking old password + + + + + + + + + delete user + + + true if succeeds + + + + Set User Image + + + + + + + + Get User Image + + + + + + + Start signup process using email + + + + + + + + + + verify signup / forgot password + + + + associated email + + + + finalize signup process using email + + + + + + + + + + + Start forgot password process using email + + + + + + + + + + reset password using email + + + + + + + + + + delete tenant + + + + + + EnsureDefaultUserExists + + + + + + Administrator role name + + + + + secret used for generating Jwt token + + + + + JWT Tokens Expiration Time Out + + + + + Token Generation + + + + + + + + + Renew Expired token + + + + + + + Extract Information From Token + + + + + + + + + + + + + + + + convert identity errors to string + + + + + + + Main Database context + + + + + Image File Service + + + + + User Role Service + + + + + Identity User Manageer + + + + + Identity SignIn Manager + + + + + Identity Role Manager + + + + + secret generator + + + + + Configuration + + + + + constructor + + + + + + + + + + + + + Audit Log Service Implementation + + + + + get all audit logs ordered by time + + + empty means unfiltered + + + + + + Database Contetxt + + + + + constuctor + + + + + + Captcha Service + + + + + Generate Captcha + + + + + + evaluate captcha + + + + + + + + Image File Service + + + + + Database Contetxt + + + + + constructor + + + + + + + Image File Service + + + + + Add Image File + + + + + + + + + + store added image + + + + + + + returns image info + + + + + + + delete image (from database and file system) + + + + + + + Get Image Storage Path + + + + + + + Image Storage Path + + + + + Database Contetxt + + + + + Configuration + + + + + constructor + + + + + + + mail sender using MailKit + + + + + constructor + + + + + options + + + + + SmptConfig + + + + + send email + + + + + + + + + + + + + + + + + + + Paginator + + + + + paginate + + + + + + + + Custom Audit Data Provider + + + + + Insert + + + + + + + Insert Async + + + + + + + Update + + + + + + + Update Async + + + + + + + + Get + + + + + + + + Get Async + + + + + + + + is disposed + + + + + dispose + + + + + Protected implementation of Dispose pattern. + + + + + + Connection + + + + + constructor + + + + + + User Roles Service Implementation + + + + + returns all user roles + + + + + + returns user role information + + + + + + + modify existing user role + + + + + + + + delete user role + + + true if succeeds + + + + adds a new user role + + new role info + update user role info (id) + + + + Has role specified permission + + + + + + + + + gets list of SecurableItem, should be reimplemented in end user applications + + + + + + Lists role permissions + + + + + + + Saves role permissions + + + + + + + + Identity Role Manager + + + + + constructor + + + + + + Secret Generator + + + + + Generates a secret + + + + + + Permission checker service implementation + + + + + check to see if user has permission to do operation + + userId + + form + operation + true if has permission + + + + IAppUserService instance + + + + + constructor + + + + + + Secret Generator + + + + + Generates a secret + + + + + + Permission checker service + + + + + check to see if user has permission to do operation + + userId + sessionId + form + operation + true if has permission + + + + User Groups Service + + + + + returns all user roles + + + + + + returns user role information + + + + + + + adds a new user role + + new role info + update user role info (id) + + + + modify existing user role + + + + + + + + delete user role + + + true if succeeds + + + + Has role specified permission + + + + + + + + + gets list of SecurableItem, should be reimplemented in end user applications + + + + + + Lists role permissions + + + + + + + Saves role permissions + + + + + + + + Audit.NET Environment Skipping + https://stackoverflow.com/questions/59627835/dont-include-environment-in-serialised-auditevents + + + + + instance + + + + + create property + + + + + + + + Chargable Feature For Tenant Type + + + + + yes/no + + + + + 0, ... + + + + + 1.222 or large numbers + + + + diff --git a/RSecurityBackend/Services/IAppUserService.cs b/RSecurityBackend/Services/IAppUserService.cs new file mode 100644 index 00000000..70915456 --- /dev/null +++ b/RSecurityBackend/Services/IAppUserService.cs @@ -0,0 +1,252 @@ +using Microsoft.AspNetCore.Http; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Auth.ViewModels; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// Authentication Service + /// + public interface IAppUserService + { + + /// + /// Login user, if failed return LoggedOnUserModel is null + /// + /// + /// + /// + Task> Login(LoginViewModel loginViewModel, string clientIPAddress); + + /// + /// Logout + /// + /// + /// + /// + Task> Logout(Guid userId, Guid sessionId); + + + /// + /// Does Session exist? + /// + /// + /// + /// + Task> SessionExists(Guid userId, Guid sessionId); + + /// + /// is user admin? + /// + /// + /// + Task> IsAdmin(Guid userId); + + /// + /// is user in either of passed roles? + /// + /// + /// + /// + Task> IsInRoles(Guid userId, string[] roleNames); + + /// + /// Get User Roles + /// + /// + /// + Task>> GetUserRoles(Guid userId); + + + /// + /// remove user from role + /// + /// user id + /// + /// + Task> RemoveFromRole(Guid id, string role); + + /// + /// add user to role + /// + /// user id + /// + /// + Task> AddToRole(Guid id, string role); + + /// + /// Lists user permissions + /// + /// + /// + Task> GetUserSecurableItemsStatus(Guid userId); + + /// + /// Has user specified permission + /// + /// + /// + /// + /// + Task> HasPermission(Guid userId, string securableItemShortName, string operationShortName); + + /// + /// returns user information + /// + /// + /// PasswordHash becomes empty + /// + /// + /// + Task> GetUserInformation(Guid userId); + + + /// + /// all users informations + /// + /// + Task> GetAllUsersInformation(); + + /// + /// add a new user + /// + /// + /// + Task> AddUser(RegisterRAppUser newUserInfo); + + /// + /// add user to role + /// + /// + /// + /// + Task> AddUserToRole(Guid userId, string roleName); + + /// + /// modify existing user + /// + /// + /// + /// + Task> ModifyUser(Guid userId, RegisterRAppUser updateUserInfo); + + /// + /// change user password checking old password + /// + /// + /// + /// + /// + Task> ChangePassword(Guid userId, string oldPassword, string newPassword); + + /// + /// delete user + /// + /// + /// true if succeeds + Task> DeleteUser(Guid userId); + + /// + /// Set User Image + /// + /// + /// + /// new user image id + Task> SetUserImage(Guid userId, IFormFileCollection files); + + + /// + /// Get User Sessions + /// + /// if null is passed returns all sessions + /// + Task> GetUserSessions(Guid? userId); + + /// + /// Get User Image + /// + /// + /// + Task> GetUserImage(Guid userId); + + /// + /// Start signup process using email + /// + /// + /// + /// + /// + /// + Task> SignUp(string email, string clientIPAddress, string clientAppName, string langauge); + + /// + /// verify signup / forgot password + /// + /// + /// + /// associated email + Task> RetrieveEmailFromQueueSecret(RVerifyQueueType verifyQueueType, string secret); + + + /// + /// finalize signup process using email + /// + /// + /// + /// + /// + /// + /// + Task> FinalizeSignUp(string email, string secret, string password, string firstName, string sureName); + + /// + /// Start forgot password process using email + /// + /// + /// + /// + /// + /// + Task> ForgotPassword(string email, string clientIPAddress, string clientAppName, string langauge); + + /// + /// reset password using email + /// + /// + /// + /// + /// + /// + Task> ResetPassword(string email, string secret, string password, string clientIPAddress); + + /// + /// delete tenant + /// + /// + RServiceResult DeleteTenant(); + + + /// + /// Administrator role name + /// + string AdministratorRoleName { get; } + + /// + /// secret used for generating Jwt token + /// + string TokenSecret { get; } + + /// + /// JWT Tokens Expiration Time Out + /// + int DefaultTokenExpirationInSeconds { get; } + + + } +} diff --git a/RSecurityBackend/Services/IAuditLogService.cs b/RSecurityBackend/Services/IAuditLogService.cs new file mode 100644 index 00000000..46fc0ba4 --- /dev/null +++ b/RSecurityBackend/Services/IAuditLogService.cs @@ -0,0 +1,21 @@ +using RSecurityBackend.Models.Audit.Db; +using RSecurityBackend.Models.Generic; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// Audit Log Service + /// + public interface IAuditLogService + { + /// + /// get all audit logs ordered by time + /// + /// + /// empty means unfiltered + /// + /// + Task> GetAll(PagingParameterModel paging, string userName, bool orderByTimeDescending); + } +} diff --git a/RSecurityBackend/Services/ICaptchaService.cs b/RSecurityBackend/Services/ICaptchaService.cs new file mode 100644 index 00000000..8082905c --- /dev/null +++ b/RSecurityBackend/Services/ICaptchaService.cs @@ -0,0 +1,31 @@ +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// Captcha Service + /// + public interface ICaptchaService + { + /// + /// Generate Captcha + /// + /// + Task> Generate(); + + /// + /// evaluate captcha + /// + /// + /// + /// + Task> Evaluate(Guid imageId, string value); + + + } +} diff --git a/RSecurityBackend/Services/IImageFileService.cs b/RSecurityBackend/Services/IImageFileService.cs new file mode 100644 index 00000000..68f832f7 --- /dev/null +++ b/RSecurityBackend/Services/IImageFileService.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Http; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// Image File Service + /// + public interface IImageFileService + { + /// + /// add new picture + /// + /// + /// + /// + /// pass empty if you want a generic date based folder + /// + Task> Add + ( + IFormFile file, Stream stream, string originalFileNameForStreams, string imageFolderName + ); + + /// + /// store added image + /// + /// + /// + Task> Store(RImage image); + + + /// + /// returns image info + /// + /// + /// + Task> GetImage(Guid id); + + /// + /// returns image file stream + /// + /// + /// + RServiceResult GetImagePath(RImage image); + + /// + /// delete image from database and file system + /// + /// + /// + Task> DeleteImage(Guid id); + + + /// + /// Image Storage Path + /// + string ImageStoragePath { get; } + + } +} diff --git a/RSecurityBackend/Services/ISecretGenerator.cs b/RSecurityBackend/Services/ISecretGenerator.cs new file mode 100644 index 00000000..345258a6 --- /dev/null +++ b/RSecurityBackend/Services/ISecretGenerator.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RSecurityBackend.Services +{ + /// + /// Secret Generator + /// + public interface ISecretGenerator + { + /// + /// Generates a secret + /// + /// + string Generate(); + } +} diff --git a/RSecurityBackend/Services/IUserPermissionChecker.cs b/RSecurityBackend/Services/IUserPermissionChecker.cs new file mode 100644 index 00000000..3706d15c --- /dev/null +++ b/RSecurityBackend/Services/IUserPermissionChecker.cs @@ -0,0 +1,22 @@ +using RSecurityBackend.Models.Generic; +using System; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// Permission checker service + /// + public interface IUserPermissionChecker + { + /// + /// check to see if user has permission to do operation + /// + /// userId + /// sessionId + /// form + /// operation + /// true if has permission + Task> Check(Guid userId, Guid sessionId, string securableItemShortName, string operationShortName); + } +} diff --git a/RSecurityBackend/Services/IUserRoleService.cs b/RSecurityBackend/Services/IUserRoleService.cs new file mode 100644 index 00000000..8b3f40dd --- /dev/null +++ b/RSecurityBackend/Services/IUserRoleService.cs @@ -0,0 +1,80 @@ +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using System; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services +{ + /// + /// User Groups Service + /// + public interface IUserRoleService + { + /// + /// returns all user roles + /// + /// + Task> GetAllRoles(); + + /// + /// returns user role information + /// + /// + /// + Task> GetRoleInformation(string roleName); + + + /// + /// adds a new user role + /// + /// new role info + /// update user role info (id) + Task> AddRole(RAppRole newRoleInfo); + + /// + /// modify existing user role + /// + /// + /// + /// + Task> ModifyRole(string roleName, RAppRole editedRole); + + /// + /// delete user role + /// + /// + /// true if succeeds + Task> DeleteRole(string roleName); + + /// + /// Has role specified permission + /// + /// + /// + /// + /// + Task> HasPermission(string roleName, string securableItemShortName, string operationShortName); + + /// + /// gets list of SecurableItem, should be reimplemented in end user applications + /// + /// + SecurableItem[] GetSecurableItems(); + + /// + /// Lists role permissions + /// + /// + /// + Task> GetRoleSecurableItemsStatus(string roleName); + + /// + /// Saves role permissions + /// + /// + /// + /// + Task> SetRoleSecurableItemsStatus(string roleName, SecurableItem[] securableItems); + } +} diff --git a/RSecurityBackend/Services/Implementation/AppUserService.cs b/RSecurityBackend/Services/Implementation/AppUserService.cs new file mode 100644 index 00000000..b56e7f96 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/AppUserService.cs @@ -0,0 +1,1487 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.ViewModels; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.DbContext; +using RSecurityBackend.Utilities; +using RSecurityBackend.Models.Image; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Audit.Db; +using Microsoft.Extensions.Configuration; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Authentication Service + /// + public class AppUserService : IAppUserService + { + + /// + /// Login user, if failed return LoggedOnUserModel is null + /// + /// + /// + /// + public async Task> Login(LoginViewModel loginViewModel, string clientIPAddress) + { + try + { + + //we ignore loginViewModel in automatic auditing to prevent loginng password data, so we would add a manual auditing to have enough data on login intrusion and ... + REvent log = new REvent() + { + EventType = "AppUser/Login (POST)(Manual)", + StartDate = DateTime.UtcNow, + UserName = loginViewModel.Username, + IpAddress = clientIPAddress + }; + _context.AuditLogs.Add(log); + await _context.SaveChangesAsync(); + + RServiceResult checkUserExists = await EnsureDefaultUserExists(); + if(!checkUserExists.Result) + { + return new RServiceResult(null, checkUserExists.ExceptionString); + } + + + + RAppUser appUser = await _userManager.FindByNameAsync(loginViewModel.Username); + + if (appUser == null) + { + appUser = await _userManager.FindByEmailAsync(loginViewModel.Username); + if(appUser == null) + { + return new RServiceResult(null, "Invalid username."); + } + } + + var result = await _signInManager.CheckPasswordSignInAsync(appUser, loginViewModel.Password, true); + if (!result.Succeeded) + { + return new RServiceResult(null, "Invalid password or other error. Identity error details says: " + result.ToString()); + } + + if (appUser.Status == RAppUserStatus.Inactive) + { + return new RServiceResult(null, "User is disabled by an admin."); + } + + RServiceResult securableItems = await GetUserSecurableItemsStatus(appUser.Id); + if (!string.IsNullOrEmpty(securableItems.ExceptionString)) + return new RServiceResult(null, securableItems.ExceptionString); + + RTemporaryUserSession userSession = + new RTemporaryUserSession() + { + RAppUserId = appUser.Id, + ClientIPAddress = clientIPAddress, + ClientAppName = loginViewModel.ClientAppName, + Language = loginViewModel.Language, + LoginTime = DateTime.Now, + LastRenewal = DateTime.Now, + ValidUntil = DateTime.Now + TimeSpan.FromSeconds(DefaultTokenExpirationInSeconds), + Token = "" + }; + + + await _context.Sessions.AddAsync(userSession); + + await _context.SaveChangesAsync(); + + RServiceResult userToken = await GenerateToken(loginViewModel.Username, appUser.Id, userSession.Id); + if(userToken.Result == null) + { + return new RServiceResult(null, userToken.ExceptionString); + } + userSession.Token = userToken.Result; + _context.Sessions.Update(userSession); + _context.SaveChanges(); + + return + new RServiceResult( + new LoggedOnUserModel() + { + SessionId = userSession.Id, + User = new PublicRAppUser(appUser), + Token = userToken.Result, + SecurableItem = securableItems.Result + } + ); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// add user to role + /// + /// + /// + /// + public async Task> AddUserToRole(Guid userId, string roleName) + { + try + { + RAppUser dbUserInfo = + await _userManager.Users.Where(u => u.Id == userId).SingleOrDefaultAsync(); + + if (dbUserInfo == null) + { + return new RServiceResult(false, $"User not found"); + } + + if (!await _roleManager.RoleExistsAsync(roleName)) + { + var identityResult = await _roleManager.CreateAsync(new RAppRole(roleName)); + if (!identityResult.Succeeded) + { + return new RServiceResult(false, $"Error creating {roleName} role : " + ErrorsToString(identityResult.Errors)); + } + } + + var addToRoleResult = await _userManager.AddToRoleAsync(dbUserInfo, roleName); + if (!addToRoleResult.Succeeded) + { + return new RServiceResult(false, $"Error adding admin to {roleName} role : " + ErrorsToString(addToRoleResult.Errors)); + } + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// Logout + /// + /// + /// + /// + public async Task> Logout(Guid userId, Guid sessionId) + { + try + { + RTemporaryUserSession session = + await _context.Sessions + .Where(s => s.Id == sessionId && s.RAppUserId == userId) + .FirstOrDefaultAsync(); + if (session == null) + return new RServiceResult(false, "session is invalid"); + _context.Sessions.Remove(session); + await _context.SaveChangesAsync(); + return new RServiceResult(true); + + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// Does Session exist? + /// + /// + /// + /// + public async Task> SessionExists(Guid userId, Guid sessionId) + { + try + { + return new RServiceResult( + await _context.Sessions + .Where(s => s.RAppUserId == userId && s.Id == sessionId) + .FirstOrDefaultAsync() != null + ); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + + /// + /// returns user information + /// + /// + /// PasswordHash becomes empty + /// + /// + /// + public async Task> GetUserInformation(Guid userId) + { + + try + { + RAppUser dbUserInfo = + await _userManager.Users.Where(u => u.Id == userId).SingleOrDefaultAsync(); + return new RServiceResult(new PublicRAppUser(dbUserInfo)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + + } + } + + + /// + /// all users informations + /// + /// + public async Task> GetAllUsersInformation() + { + try + { + + + RAppUser[] usersInfo = await _userManager.Users.ToArrayAsync(); + List lstPublicUsersInfo = new List(); + + + foreach(RAppUser userInfo in usersInfo) + { + lstPublicUsersInfo.Add(new PublicRAppUser(userInfo)); + } + return new RServiceResult(lstPublicUsersInfo.ToArray()); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Get User Sessions + /// + /// if null is passed returns all sessions + /// + public async Task> GetUserSessions(Guid? userId) + { + try + { + List publicRUserSessions = new List(); + + RTemporaryUserSession[] sessions = + userId == null ? + await _context.Sessions.ToArrayAsync() + : + await _context.Sessions.Where(s => s.RAppUserId == userId).ToArrayAsync() + ; + + foreach (RTemporaryUserSession rUserSession in sessions) + publicRUserSessions.Add(new PublicRUserSession(rUserSession)); + + return new RServiceResult(publicRUserSessions.ToArray()); + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// is user admin? + /// + /// + /// + public async Task> IsAdmin(Guid userId) + { + try + { + RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); + if (dbUserInfo == null) + { + return new RServiceResult(false, "user not found"); + } + + bool res = await _userManager.IsInRoleAsync(dbUserInfo, AdministratorRoleName); + return new RServiceResult(res); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// is user in either of passed roles? + /// + /// + /// + /// + public async Task> IsInRoles(Guid userId, string[] roleNames) + { + try + { + RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); + if (dbUserInfo == null) + { + return new RServiceResult(false, "user not found"); + } + + foreach(string roleName in roleNames) + { + if(await _userManager.IsInRoleAsync(dbUserInfo, roleName)) + { + return new RServiceResult(true); + } + } + + return new RServiceResult(false); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Get User Roles + /// + /// + /// + public async Task>> GetUserRoles(Guid userId) + { + + try + { + + RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); + if (dbUserInfo == null) + { + return new RServiceResult>(null, "user not found"); + } + + return new RServiceResult>(await _userManager.GetRolesAsync(dbUserInfo)); + } + catch (Exception exp) + { + return new RServiceResult>(null, exp.ToString()); + } + } + + /// + /// remove user from role + /// + /// user id + /// + /// + public async Task> RemoveFromRole(Guid id, string role) + { + try + { + RAppUser dbUserInfo = await _userManager.FindByIdAsync(id.ToString()); + if (dbUserInfo == null) + { + return new RServiceResult(false, "user not found"); + } + + IdentityResult result = await _userManager.RemoveFromRoleAsync(dbUserInfo, role); + if (!result.Succeeded) + { + return new RServiceResult(false, ErrorsToString(result.Errors)); + } + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// add user to role + /// + /// user id + /// + /// + public async Task> AddToRole(Guid id, string role) + { + try + { + RAppUser dbUserInfo = await _userManager.FindByIdAsync(id.ToString()); + if (dbUserInfo == null) + { + return new RServiceResult(false, "user not found"); + } + + IdentityResult result = await _userManager.AddToRoleAsync(dbUserInfo, role); + if (!result.Succeeded) + { + return new RServiceResult(false, ErrorsToString(result.Errors)); + } + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Lists user permissions + /// + /// + /// + public async Task> GetUserSecurableItemsStatus(Guid userId) + { + try + { + SecurableItem[] securableItems = _userRoleService.GetSecurableItems(); + RServiceResult> roles = await GetUserRoles(userId); + if(!string.IsNullOrEmpty(roles.ExceptionString)) + return new RServiceResult(null, roles.ExceptionString); + + bool isAdmin = (await IsAdmin(userId)).Result; + + foreach(SecurableItem securableItem in securableItems) + { + foreach(SecurableItemOperation operation in securableItem.Operations) + { + foreach(string role in roles.Result) + { + RServiceResult hasPermission = await _userRoleService.HasPermission(role, securableItem.ShortName, operation.ShortName); + if(!string.IsNullOrEmpty(hasPermission.ExceptionString)) + return new RServiceResult(null, hasPermission.ExceptionString); + if (isAdmin || hasPermission.Result) + { + operation.Status = true; + } + } + } + } + return new RServiceResult(securableItems); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Has user specified permission + /// + /// + /// + /// + /// + public async Task> HasPermission(Guid userId, string securableItemShortName, string operationShortName) + { + try + { + RServiceResult> roles = await GetUserRoles(userId); + if (!string.IsNullOrEmpty(roles.ExceptionString)) + return new RServiceResult(false, roles.ExceptionString); + + foreach (string role in roles.Result) + { + RServiceResult hasPermission = await _userRoleService.HasPermission(role, securableItemShortName, operationShortName); + if (!string.IsNullOrEmpty(hasPermission.ExceptionString)) + return new RServiceResult(false, hasPermission.ExceptionString); + if (hasPermission.Result) + { + return new RServiceResult(true); + } + } + + return + new RServiceResult + ( + false + ); + + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// add a new user + /// + /// + public virtual async Task> AddUser(RegisterRAppUser newUserInfo) + { + try + { + if(!newUserInfo.IsAdmin) + { + RServiceResult checkUserExists = await EnsureDefaultUserExists(); + if (!checkUserExists.Result) + { + return new RServiceResult(null, checkUserExists.ExceptionString); + } + } + + RAppUser existingInfo = await _userManager.FindByNameAsync(newUserInfo.Username); + if (existingInfo != null) + { + return new RServiceResult(null, "username is already taken"); + } + + if(string.IsNullOrEmpty(newUserInfo.Password)) + { + newUserInfo.Password = Guid.NewGuid().ToString(); + } + + + RAppUser newDbUser = + new RAppUser() + { + UserName = newUserInfo.Username, + FirstName = newUserInfo.FirstName, + SureName = newUserInfo.SureName, + Email = newUserInfo.Email, + PhoneNumber = newUserInfo.PhoneNumber, + CreateDate = DateTime.Now, + Status = newUserInfo.Status + + }; + + + var result = await _userManager.CreateAsync(newDbUser, newUserInfo.Password); + + if(!result.Succeeded) + { + return new RServiceResult(null, ErrorsToString(result.Errors)); + } + + newUserInfo.Id = newDbUser.Id; + + if(newUserInfo.IsAdmin) + { + + if (!await _roleManager.RoleExistsAsync(AdministratorRoleName)) + { + var roleCheckResult = await _roleManager.CreateAsync(new RAppRole(AdministratorRoleName)); + if (!roleCheckResult.Succeeded) + { + return new RServiceResult(null, "Error creating Administrator role : " + ErrorsToString(roleCheckResult.Errors)); + } + } + + + var addToAdminRoleResult = await _userManager.AddToRoleAsync(newDbUser, AdministratorRoleName); + if (!addToAdminRoleResult.Succeeded) + { + return new RServiceResult(null, $"Error adding {newDbUser.UserName} to Administrator role : " + ErrorsToString(addToAdminRoleResult.Errors)); + } + } + + + return new RServiceResult(newDbUser); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + + /// + /// modify existing user + /// + /// + /// + /// + public async Task> ModifyUser(Guid userId, RegisterRAppUser updateUserInfo) + { + try + { + RAppUser existingInfo = await _userManager.FindByIdAsync(userId.ToString()); + if (existingInfo == null) + { + return new RServiceResult(false, "username not found"); + } + + RServiceResult isAdmin = await IsAdmin(userId); + if(!string.IsNullOrEmpty(isAdmin.ExceptionString)) + { + return new RServiceResult(false, isAdmin.ExceptionString); + } + + if(isAdmin.Result && !updateUserInfo.IsAdmin) + { + List adminUsers = new List(await _userManager.GetUsersInRoleAsync(AdministratorRoleName)); + int nActiveAdminUsers = 0; + foreach(RAppUser adminUser in adminUsers) + { + if (adminUser.Status == RAppUserStatus.Active) + nActiveAdminUsers++; + } + if(nActiveAdminUsers <= 1) + { + return new RServiceResult(false, "You cannot reduce number of active admin users to 0."); + } + } + + + if (existingInfo.UserName != updateUserInfo.Username) + { + + RAppUser anotheruserWithUserName = await _userManager.FindByNameAsync(updateUserInfo.Username); + + if(anotheruserWithUserName != null) + { + return new RServiceResult(false, "duplicated username"); + } + + existingInfo.UserName = updateUserInfo.Username; + } + + + existingInfo.FirstName = updateUserInfo.FirstName; + existingInfo.SureName = updateUserInfo.SureName; + existingInfo.Email = updateUserInfo.Email; + existingInfo.PhoneNumber = updateUserInfo.PhoneNumber; + existingInfo.Status = updateUserInfo.Status; + + if (!string.IsNullOrEmpty(updateUserInfo.Password)) + { + foreach(var passwordValidator in _userManager.PasswordValidators) + { + var resPass = await passwordValidator.ValidateAsync(_userManager, existingInfo, updateUserInfo.Password); + if (!resPass.Succeeded) + { + return new RServiceResult(false, ErrorsToString(resPass.Errors)); + } + } + + existingInfo.PasswordHash = _userManager.PasswordHasher.HashPassword(existingInfo, updateUserInfo.Password); + } + + + var result = await _userManager.UpdateAsync(existingInfo); + if(!result.Succeeded) + { + return new RServiceResult(false, ErrorsToString(result.Errors)); + } + + + + + //updating admin status is not supported + + + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// change user password checking old password + /// + /// + /// + /// + /// + public async Task> ChangePassword(Guid userId, string oldPassword, string newPassword) + { + RAppUser appUser = await _userManager.FindByIdAsync(userId.ToString()); + + if (appUser == null) + { + return new RServiceResult(false, "Invalid user."); + } + + var result = await _userManager.ChangePasswordAsync(appUser, oldPassword, newPassword); + if (!result.Succeeded) + { + return new RServiceResult(false, "Identity error details says: " + result.ToString()); + } + + + + return new RServiceResult(true); + + } + + /// + /// delete user + /// + /// + /// true if succeeds + public async Task> DeleteUser(Guid userId) + { + try + { + RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); + if (dbUserInfo != null) + { + var result = await _userManager.DeleteAsync(dbUserInfo); + if(!result.Succeeded) + { + return new RServiceResult(false, ErrorsToString(result.Errors)); + } + return new RServiceResult(true); + } + return new RServiceResult(false, "user not found."); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Set User Image + /// + /// + /// + /// + public async Task> SetUserImage(Guid userId, IFormFileCollection files) + { + try + { + + RAppUser user = await _userManager.FindByIdAsync(userId.ToString()); + + if(files.Count == 0) + { + user.RImageId = null; + } + else + { + if (files.Count != 1) + { + return new RServiceResult(null, "files.Count != 1"); + } + + IFormFile file = files[0]; + + + int nImageWidth = 192; + using (Stream stream = file.OpenReadStream()) + { + using (Image img = Image.FromStream(stream)) + { + if (img.Width > nImageWidth) + { + using (Bitmap bmpPhase1 = new Bitmap(nImageWidth, nImageWidth)) + { + using (Graphics g = Graphics.FromImage(bmpPhase1)) + { + g.DrawImage(img, new Rectangle(0, 0, nImageWidth, img.Height * nImageWidth / img.Height)); + } + + using (Brush brush = new TextureBrush(bmpPhase1)) + { + using (Bitmap bmpPhase2 = new Bitmap(nImageWidth, nImageWidth)) + { + using (Graphics g = Graphics.FromImage(bmpPhase2)) + { + g.FillEllipse(brush, new Rectangle(0, 0, nImageWidth, nImageWidth)); + } + bmpPhase2.MakeTransparent(); + + using (MemoryStream ms = new MemoryStream()) + { + bmpPhase2.Save(ms, ImageFormat.Png); + + ms.Position = 0; + RServiceResult image = await _imageFileService.Add(null, ms, file.FileName, "UserProfiles"); + + if (!string.IsNullOrEmpty(image.ExceptionString)) + { + return new RServiceResult(null, image.ExceptionString); + } + user.RImage = image.Result; + + } + } + } + } + } + else + { + RServiceResult image = await _imageFileService.Add(null, stream, file.FileName, "UserProfiles"); + + if (!string.IsNullOrEmpty(image.ExceptionString)) + { + return new RServiceResult(null, image.ExceptionString); + } + user.RImage = image.Result; + } + } + + } + } + + var result = await _userManager.UpdateAsync(user); + if(!result.Succeeded) + { + return new RServiceResult(null, ErrorsToString(result.Errors)); + } + + return new RServiceResult((Guid?)user.RImageId); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Get User Image + /// + /// + /// + public async Task> GetUserImage(Guid userId) + { + try + { + + RAppUser user = await _userManager.FindByIdAsync(userId.ToString()); + + if(user.RImageId != null) + { + return await _imageFileService.GetImage((Guid)user.RImageId); + } + + return new RServiceResult(null); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Start signup process using email + /// + /// + /// + /// + /// + /// + public async Task> SignUp(string email, string clientIPAddress, string clientAppName, string langauge) + { + try + { + if(string.IsNullOrEmpty(clientIPAddress)) + { + return new RServiceResult(null, "client ip address is empty"); + } + + if (string.IsNullOrEmpty(clientAppName)) + { + return new RServiceResult(null, "client app name is empty"); + } + + RAppUser existingUser = await _userManager.FindByEmailAsync(email); + if (existingUser != null) + { + return new RServiceResult(null, "user already registered using same email"); + } + + existingUser = await _userManager.FindByNameAsync(email); + if (existingUser != null) + { + return new RServiceResult(null, "user already registered using same username"); + } + + //checking this queue for previous signup attempts is unnecessary and is not done intentionally + RVerifyQueueItem item = new RVerifyQueueItem() + { + QueueType = RVerifyQueueType.SignUp, + Email = email, + DateTime = DateTime.Now, + ClientIPAddress = clientIPAddress, + ClientAppName = clientAppName, + Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'), + Language = langauge + }; + + while( + null != + await _context.VerifyQueueItems.Where(i => i.Secret == item.Secret).SingleOrDefaultAsync() + ) + { + item.Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'); + } + + + await _context.VerifyQueueItems.AddAsync + ( + item + ); + await _context.SaveChangesAsync(); + return new RServiceResult(item); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// verify signup / forgot password + /// + /// + /// + /// associated email + public async Task> RetrieveEmailFromQueueSecret(RVerifyQueueType verifyQueueType, string secret) + { + try + { + RVerifyQueueItem item = await _context.VerifyQueueItems.Where(i => i.QueueType == verifyQueueType && i.Secret == secret).SingleOrDefaultAsync(); + if(item == null) + { + return new RServiceResult(""); + } + + return new RServiceResult(item.Email); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// finalize signup process using email + /// + /// + /// + /// + /// + /// + /// + public async Task> FinalizeSignUp(string email, string secret, string password, string firstName, string sureName) + { + try + { + + RAppUser existingUser = await _userManager.FindByEmailAsync(email); + if (existingUser != null) + { + return new RServiceResult(false, "user already registered using same email"); + } + + existingUser = await _userManager.FindByNameAsync(email); + if (existingUser != null) + { + return new RServiceResult(false, "user already registered using same username"); + } + + if( + email + != + (await RetrieveEmailFromQueueSecret(RVerifyQueueType.SignUp, secret)).Result + ) + { + return new RServiceResult(false, "secret mismatch"); + } + + secret = secret.Replace(" ", "");//TODO: check this + + RegisterRAppUser newUserInfo = new RegisterRAppUser() + { + Username = email, + Email = email, + Password = password, + Status = RAppUserStatus.Active, + IsAdmin = false, + FirstName = firstName, + SureName = sureName + }; + + RServiceResult userAddResult = await AddUser(newUserInfo); + + if(userAddResult.Result == null) + { + return new RServiceResult(false, userAddResult.ExceptionString); + } + + userAddResult.Result.EmailConfirmed = true; + await _userManager.UpdateAsync(userAddResult.Result); + + RVerifyQueueItem[] failedQueue = await _context.VerifyQueueItems.Where(i => i.Email == email && i.Secret != secret && i.QueueType == RVerifyQueueType.SignUp).ToArrayAsync(); + if(failedQueue.Length != 0) + { + _context.VerifyQueueItems.RemoveRange(failedQueue); + } + + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// Start forgot password process using email + /// + /// + /// + /// + /// + /// + public async Task> ForgotPassword(string email, string clientIPAddress, string clientAppName, string langauge) + { + try + { + if (string.IsNullOrEmpty(clientIPAddress)) + { + return new RServiceResult(null, "client ip address is empty"); + } + + if (string.IsNullOrEmpty(clientAppName)) + { + return new RServiceResult(null, "client app name is empty"); + } + + RAppUser rAppUser = await _userManager.FindByEmailAsync(email); + if (rAppUser == null) + { + return new RServiceResult(null, "user not found"); + } + + //checking this queue for previous signup attempts is unnecessary and is not done intentionally + RVerifyQueueItem item = new RVerifyQueueItem() + { + QueueType = RVerifyQueueType.ForgotPassword, + Email = email, + DateTime = DateTime.Now, + ClientIPAddress = clientIPAddress, + ClientAppName = clientAppName, + Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'), + Language = langauge + }; + + while ( + null != + await _context.VerifyQueueItems.Where(i => i.Secret == item.Secret).SingleOrDefaultAsync() + ) + { + item.Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'); + } + + + await _context.VerifyQueueItems.AddAsync + ( + item + ); + await _context.SaveChangesAsync(); + return new RServiceResult(item); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// reset password using email + /// + /// + /// + /// + /// + /// + public async Task> ResetPassword(string email, string secret, string password, string clientIPAddress) + { + try + { + //we ignore input model in automatic auditing to prevent loginng password data, so we would add a manual auditing to have enough data on login intrusion and ... + REvent log = new REvent() + { + EventType = "AppUser/ResetPassword (POST)(Manual)", + StartDate = DateTime.UtcNow, + UserName = email, + IpAddress = clientIPAddress + }; + _context.AuditLogs.Add(log); + await _context.SaveChangesAsync(); + + RAppUser existingUser = await _userManager.FindByEmailAsync(email); + if (existingUser == null) + { + return new RServiceResult(false, "user not found"); + } + + + if ( + email + != + (await RetrieveEmailFromQueueSecret(RVerifyQueueType.ForgotPassword, secret)).Result + ) + { + return new RServiceResult(false, "secret mismatch"); + } + + foreach (var passwordValidator in _userManager.PasswordValidators) + { + var resPass = await passwordValidator.ValidateAsync(_userManager, existingUser, password); + if (!resPass.Succeeded) + { + return new RServiceResult(false, ErrorsToString(resPass.Errors)); + } + } + + existingUser.PasswordHash = _userManager.PasswordHasher.HashPassword(existingUser, password); + + await _userManager.UpdateAsync(existingUser); + + RVerifyQueueItem[] failedQueue = await _context.VerifyQueueItems.Where(i => i.Email == email && i.QueueType == RVerifyQueueType.ForgotPassword).ToArrayAsync(); + if (failedQueue.Length != 0) + { + _context.VerifyQueueItems.RemoveRange(failedQueue); + } + + + + await _context.SaveChangesAsync(); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// delete tenant + /// + /// + public RServiceResult DeleteTenant() + { + try + { + _context.DeleteDb(); + return new RServiceResult(true); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// EnsureDefaultUserExists + /// + /// + private async Task> EnsureDefaultUserExists() + { + try + { + //If no user exists create default one + if (_userManager.Users.Count() == 0) + { + RAppUser admin = new RAppUser() + { + UserName = "admin", + FirstName = "راهبر", + SureName = "سیستم", + Email = "admin@ganjoor.net", + PhoneNumber = "00989123456789", + CreateDate = DateTime.Now, + Status = RAppUserStatus.Active + }; + + var identityResult = await _userManager.CreateAsync( + admin, "Test!123" + ); + if (!identityResult.Succeeded) + { + return new RServiceResult(false, "Error creating default user : " + ErrorsToString(identityResult.Errors)); + } + + if (!await _roleManager.RoleExistsAsync(AdministratorRoleName)) + { + identityResult = await _roleManager.CreateAsync(new RAppRole(AdministratorRoleName)); + if (!identityResult.Succeeded) + { + return new RServiceResult(false, "Error creating Administrator role : " + ErrorsToString(identityResult.Errors)); + } + } + + identityResult = await _userManager.AddToRoleAsync(admin, AdministratorRoleName); + if (!identityResult.Succeeded) + { + return new RServiceResult(false, "Error adding admin to Administrator role : " + ErrorsToString(identityResult.Errors)); + } + } + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// Administrator role name + /// + public string AdministratorRoleName { get { return "Administrator"; } } + + + /// + /// secret used for generating Jwt token + /// + public string TokenSecret { get { return $"{Configuration.GetSection("Security")["Secret"]}"; } } + + /// + /// JWT Tokens Expiration Time Out + /// + public int DefaultTokenExpirationInSeconds { get { return int.Parse( $"{Configuration.GetSection("Security")["DefaultTokenExpirationInSeconds"]}"); } } + + + + #region Internals + + #region Token Generation + + /// + /// Token Generation + /// + /// + /// + /// + /// + private async Task> GenerateToken(string username, Guid userId, Guid sessionId) + { + var user = await _userManager.FindByIdAsync(userId.ToString()); + if (user == null) + return new RServiceResult(null, "user not found"); + + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim("UserId", userId.ToString()), + new Claim("SessionId", sessionId.ToString()), + }; + + var userClaims = await _userManager.GetClaimsAsync(user); + claims.AddRange(userClaims); + + var userRoles = await _userManager.GetRolesAsync(user); + foreach (var userRole in userRoles) + { + claims.Add(new Claim(ClaimTypes.Role, userRole)); + var role = await _roleManager.FindByNameAsync(userRole); + if (role != null) + { + var roleClaims = await _roleManager.GetClaimsAsync(role); + foreach (Claim roleClaim in roleClaims) + { + claims.Add(roleClaim); + } + } + } + + var token = new JwtSecurityToken( + issuer: "Ganjoor", + audience: "Everyone", + claims: claims, + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow.AddSeconds(DefaultTokenExpirationInSeconds), + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenSecret)), SecurityAlgorithms.HmacSha256) + ); + + return new RServiceResult(new JwtSecurityTokenHandler().WriteToken(token)); + + } + + /// + /// Renew Expired token + /// + /// + /// + private async Task> RegenerateToken(string token) + { + try + { + var principal = GetPrincipalFromExpiredToken(token); + + //check refreshToken here to see if it is valid or not + //check user here, if is not valid return BadRequest(); + ExtractTokenInfo(token, out string username, out Guid userId, out Guid sessionId); + + return await GenerateToken(username, userId, sessionId); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + + } + + + /// + /// Extract Information From Token + /// + /// + /// + /// + /// + public void ExtractTokenInfo(string token, out string username, out Guid userId, out Guid sessionId) + { + + var principal = GetPrincipalFromExpiredToken(token); + username = principal.Identity.Name; + userId = new Guid(principal.Claims.FirstOrDefault(c => c.Type == "UserId").Value); + sessionId = new Guid(principal.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); + } + + /// + /// + /// + /// + /// + private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidAudience = "Everyone", + ValidateIssuer = true, + ValidIssuer = "Ganjoor", + + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenSecret)), + + ValidateLifetime = false, //important + + ClockSkew = TimeSpan.Zero + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + SecurityToken securityToken; + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); + var jwtSecurityToken = securityToken as JwtSecurityToken; + if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + throw new SecurityTokenException("Invalid token"); + + return principal; + } + + #endregion + + #region Identity Errors conversion + /// + /// convert identity errors to string + /// + /// + /// + protected static string ErrorsToString(IEnumerable errors) + { + StringBuilder sb = new StringBuilder(); + foreach(IdentityError error in errors) + { + sb.AppendLine(error.Description); + } + return sb.ToString(); + } + #endregion + #endregion + + /// + /// Main Database context + /// + protected readonly RSecurityDbContext _context; + + /// + /// Image File Service + /// + protected readonly IImageFileService _imageFileService; + /// + /// User Role Service + /// + protected readonly IUserRoleService _userRoleService; + /// + /// Identity User Manageer + /// + protected UserManager _userManager = null; + /// + /// Identity SignIn Manager + /// + protected SignInManager _signInManager = null; + /// + /// Identity Role Manager + /// + protected RoleManager _roleManager = null; + + /// + /// secret generator + /// + protected readonly ISecretGenerator _secretGenerator; + + /// + /// Configuration + /// + protected IConfiguration Configuration { get; } + + + + /// + /// constructor + /// + /// + /// + /// + /// + /// + /// + /// + /// + public AppUserService( + RSecurityDbContext context, + UserManager userManager, + SignInManager signInManager, + RoleManager roleManager, + ISecretGenerator secretGenerator, + IImageFileService imageFileService, + IUserRoleService userRoleService, + IConfiguration configuration) + { + _context = context; + _userManager = userManager; + _signInManager = signInManager; + _roleManager = roleManager; + _secretGenerator = secretGenerator; + _imageFileService = imageFileService; + _userRoleService = userRoleService; + Configuration = configuration; + } + } +} diff --git a/RSecurityBackend/Services/Implementation/AuditLogServiceEF.cs b/RSecurityBackend/Services/Implementation/AuditLogServiceEF.cs new file mode 100644 index 00000000..ab843f01 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/AuditLogServiceEF.cs @@ -0,0 +1,68 @@ +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Audit.Db; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Audit Log Service Implementation + /// + public class AuditLogServiceEF : IAuditLogService + { + /// + /// get all audit logs ordered by time + /// + /// + /// empty means unfiltered + /// + /// + public async Task> GetAll(PagingParameterModel paging, string userName, bool orderByTimeDescending) + { + try + { + userName = userName == null ? "" : userName.Trim(); + var source = + orderByTimeDescending ? + _context.AuditLogs + .Where(l => string.IsNullOrEmpty(userName) || l.UserName == userName) + .OrderByDescending(l => l.StartDate) + .AsQueryable() + : + _context.AuditLogs + .Where(l => string.IsNullOrEmpty(userName) || l.UserName == userName) + .OrderBy(l => l.StartDate) + .AsQueryable(); + + + (PaginationMetadata PagingMeta, REvent[] Items) paginatedResult = + await QueryablePaginator.Paginate(source, paging); + + + + return new RServiceResult<(PaginationMetadata PagingMeta, REvent[] Items)>(paginatedResult); + } + catch (Exception exp) + { + return new RServiceResult<(PaginationMetadata PagingMeta, REvent[] Items)>((PagingMeta: null, Items: null), exp.ToString()); + } + } + + /// + /// Database Contetxt + /// + protected readonly RSecurityDbContext _context; + + /// + /// constuctor + /// + /// + public AuditLogServiceEF(RSecurityDbContext context) + { + _context = context; + } + } +} diff --git a/RSecurityBackend/Services/Implementation/CaptchaServiceEF.cs b/RSecurityBackend/Services/Implementation/CaptchaServiceEF.cs new file mode 100644 index 00000000..ab56117c --- /dev/null +++ b/RSecurityBackend/Services/Implementation/CaptchaServiceEF.cs @@ -0,0 +1,136 @@ +using Microsoft.EntityFrameworkCore; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Captcha Service + /// + + public class CaptchaServiceEF : ICaptchaService + { + + private const int _CaptchaWidth = 50; + + private const int _CaptchaHeight = 30; + + private const string _FontName = "Tahoma"; + + private const float _FontSize = 12.0f; + + /// + /// Generate Captcha + /// + /// + public async Task> Generate() + { + try + { + string value = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(5, '0'); + using (Image img = new Bitmap(_CaptchaWidth, _CaptchaHeight)) + { + using (Graphics g = Graphics.FromImage(img)) + { + g.FillRectangle(Brushes.White, 0, 0, _CaptchaWidth, _CaptchaHeight); + using (Font fnt = new Font(_FontName, _FontSize)) + { + SizeF sz = g.MeasureString(value, fnt); + + g.DrawString(value, fnt, Brushes.Black, new PointF((_CaptchaWidth - sz.Width) / 2, (_CaptchaHeight - sz.Height) / 2)); + } + } + + using (MemoryStream ms = new MemoryStream()) + { + img.Save(ms, ImageFormat.Png); + + ms.Position = 0; + RServiceResult image = await _imageFileService.Add(null, ms, $"{$"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(5, '0')}-{Guid.NewGuid()}.png", "Captcha"); + + if (!string.IsNullOrEmpty(image.ExceptionString)) + { + return new RServiceResult(null, image.ExceptionString); + } + + RCaptchaImage captcha = new RCaptchaImage() + { + Value = value, + DateTime = DateTime.Now, + RImage = image.Result + }; + + await _context.CaptchaImages.AddAsync(captcha); + await _context.SaveChangesAsync(); + + return image; + } + } + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// evaluate captcha + /// + /// + /// + /// + public async Task> Evaluate(Guid imageId, string value) + { + try + { + RCaptchaImage captcha = await _context.CaptchaImages.Where(c => c.RImageId == imageId).FirstOrDefaultAsync(); + if(captcha == null) + return new RServiceResult(false, "Captcha not found"); + bool bRes = captcha.Value.ToLower() == value.ToLower(); + if(bRes) + { + _context.CaptchaImages.Remove(captcha); + await _context.SaveChangesAsync(); + + //we can clean up OLD captcha images using a background service later + //await _imageFileService.DeleteImage(imageId);//might fail, does not matter + } + return new RServiceResult(bRes); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + + } + + /// + /// Image File Service + /// + protected readonly IImageFileService _imageFileService; + + /// + /// Database Contetxt + /// + protected readonly RSecurityDbContext _context; + + /// + /// constructor + /// + /// + /// + public CaptchaServiceEF(RSecurityDbContext context, IImageFileService imageFileService) + { + _context = context; + _imageFileService = imageFileService; + } + } +} diff --git a/RSecurityBackend/Services/Implementation/ImageFileServiceEF.cs b/RSecurityBackend/Services/Implementation/ImageFileServiceEF.cs new file mode 100644 index 00000000..2516e818 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/ImageFileServiceEF.cs @@ -0,0 +1,242 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Generic; +using RSecurityBackend.Models.Image; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Image File Service + /// + public class ImageFileServiceEF : IImageFileService + { + /// + /// Add Image File + /// + /// + /// + /// + /// + /// + public async Task> Add(IFormFile file, Stream stream, string originalFileNameForStreams, string imageFolderName) + { + try + { + + RServiceResult + pictureFile = + await ProcessImage + ( + file, + new RImage() + { + DataTime = DateTime.Now, + LastModified = DateTime.Now, + FolderName = string.IsNullOrEmpty(imageFolderName) ? DateTime.Now.ToString("yyyy-MM") : imageFolderName + }, + stream, + originalFileNameForStreams + ); + if (pictureFile == null) + return new RServiceResult(null, pictureFile.ExceptionString); + + return new RServiceResult(pictureFile.Result); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// store added image + /// + /// + /// + public async Task> Store(RImage image) + { + try + { + + _context.GeneralImages.Add(image); + await _context.SaveChangesAsync(); + + return new RServiceResult(image); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + private async Task> ProcessImage(IFormFile uploadedImage, RImage pictureFile, Stream stream, string originalFileNameForStreams) + { + if (uploadedImage == null && stream == null) + { + return new RServiceResult(null, "ProcessImage: uploadedImage == null && stream == null"); + } + + pictureFile.ContentType = uploadedImage == null ? "image/jpeg" : uploadedImage.ContentType; + pictureFile.FileSizeInBytes = uploadedImage == null ? stream.Length : uploadedImage.Length; + pictureFile.OriginalFileName = uploadedImage == null ? originalFileNameForStreams : uploadedImage.FileName; + + + + string fullDirStorePath = Path.Combine(ImageStoragePath, pictureFile.FolderName); + + + if (!Directory.Exists(fullDirStorePath)) + { + try + { + Directory.CreateDirectory(fullDirStorePath); + } + catch + { + return new RServiceResult(null, $"ProcessImage: create dir failed {fullDirStorePath}"); + } + } + + + + + string ext = uploadedImage == null ? ".jpg" : Path.GetExtension(uploadedImage.FileName); + pictureFile.StoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + ext; + + string originalFileStorePath = Path.Combine(fullDirStorePath, pictureFile.StoredFileName); + while (File.Exists(originalFileStorePath)) + { + pictureFile.StoredFileName = Path.GetFileNameWithoutExtension(pictureFile.OriginalFileName) + "-" + Guid.NewGuid().ToString() + ext; + originalFileStorePath = Path.Combine(fullDirStorePath, pictureFile.StoredFileName); + } + using (FileStream fsMain = new FileStream(originalFileStorePath, FileMode.Create)) + { + if (uploadedImage != null) + await uploadedImage.CopyToAsync(fsMain); + else + await stream.CopyToAsync(fsMain); + } + + using (MemoryStream ms = new MemoryStream()) + { + if (uploadedImage != null) + await uploadedImage.CopyToAsync(ms); + else + { + stream.Position = 0; + await stream.CopyToAsync(ms); + } + + + using (Image img = Bitmap.FromStream(ms)) + { + pictureFile.ImageWidth = img.Width; + pictureFile.ImageHeight = img.Height; + } + } + return new RServiceResult(pictureFile); + } + + + /// + /// returns image info + /// + /// + /// + public async Task> GetImage(Guid id) + { + try + { + return new RServiceResult( + await _context.GeneralImages + .Where(p => p.Id == id) + .SingleOrDefaultAsync() + ); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// delete image (from database and file system) + /// + /// + /// + public async Task> DeleteImage(Guid id) + { + try + { + RServiceResult img = await GetImage(id); + if(!string.IsNullOrEmpty(img.ExceptionString)) + return new RServiceResult(false, img.ExceptionString); + if(img.Result == null) + { + return new RServiceResult(false, "image not found"); + } + File.Delete(GetImagePath(img.Result).Result); + _context.GeneralImages.Remove(img.Result); + await _context.SaveChangesAsync(); + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// Get Image Storage Path + /// + /// + /// + public RServiceResult GetImagePath(RImage image) + { + try + { + return new RServiceResult(Path.Combine(ImageStoragePath, image.FolderName, image.StoredFileName)); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Image Storage Path + /// + public string ImageStoragePath { get { return $"{Configuration.GetSection("PictureFileService")["StoragePath"]}"; } } + + /// + /// Database Contetxt + /// + protected readonly RSecurityDbContext _context; + + /// + /// Configuration + /// + protected IConfiguration Configuration { get; } + + /// + /// constructor + /// + /// + /// + public ImageFileServiceEF(RSecurityDbContext context, IConfiguration configuration) + { + _context = context; + Configuration = configuration; + } + + } +} diff --git a/RSecurityBackend/Services/Implementation/MailKitEmailSenderBase.cs b/RSecurityBackend/Services/Implementation/MailKitEmailSenderBase.cs new file mode 100644 index 00000000..3c97f607 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/MailKitEmailSenderBase.cs @@ -0,0 +1,98 @@ +using MailKit.Net.Smtp; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Options; +using MimeKit; +using RSecurityBackend.Models.Mail; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// mail sender using MailKit + /// + public class MailKitEmailSenderBase : IEmailSender + { + /// + /// constructor + /// + public MailKitEmailSenderBase() + { + Options = SmptConfig; + } + + /// + /// options + /// + public SmptConfig Options { get; } //set only via Secret Manager + + /// + /// SmptConfig + /// + public virtual SmptConfig SmptConfig + { + get + { + return + new SmptConfig() + { + port = 465, + server = "smtp.gmail.com", + smtpUsername = "nonexistingganjoor@gmail.com", + smtpPassword = "APasswordHere", + from = "nonexistingganjoor@gmail.com", + useSsl = true + }; + } + } + + /// + /// send email + /// + /// + /// + /// + /// + public Task SendEmailAsync(string email, string subject, string message) + { + return Execute(Options, subject, message, email); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public Task Execute(SmptConfig options, string subject, string message, string email) + { + var mimeMessage = new MimeMessage(); + mimeMessage.From.Add(new MailboxAddress(options.from, options.from)); + mimeMessage.To.Add(new MailboxAddress(email, email)); + mimeMessage.Subject = subject; + + mimeMessage.Body = new TextPart("html") + { + Text = message + }; + + + SmtpClient client = new SmtpClient(); + + // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) + client.ServerCertificateValidationCallback = (s, c, h, e) => true; + + client.Connect(options.server, options.port, options.useSsl); + + // Note: only needed if the SMTP server requires authentication + client.Authenticate(options.smtpUsername, options.smtpPassword); + + client.Send(mimeMessage); + client.Disconnect(true); + client.Dispose(); + + return Task.FromResult(0); + } + } +} diff --git a/RSecurityBackend/Services/Implementation/QueryablePaginator.cs b/RSecurityBackend/Services/Implementation/QueryablePaginator.cs new file mode 100644 index 00000000..f66104e8 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/QueryablePaginator.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Paginator + /// + public static class QueryablePaginator + { + /// + /// paginate + /// + /// + /// + /// + public static async Task<(PaginationMetadata PagingMeta, ArrayType[] Items)> Paginate(IQueryable source, PagingParameterModel paging) + { + // Get's No of Rows Count + int count = source.Count(); + + // Parameter is passed from Query string if it is null then it default Value will be pageSize:20 + int PageSize = paging.PageSize; + + // Parameter is passed from Query string if it is null then it default Value will be pageNumber:1 + int CurrentPage = paging.PageNumber; + + // Display TotalCount to Records to User + int TotalCount = count; + + // Calculating Totalpage by Dividing (No of Records / Pagesize) + int TotalPages = PageSize <= 0 ? 1 : (int)Math.Ceiling(count / (double)PageSize); + + // Returns List of Customer after applying Paging + var items = PageSize <= 0 ? source.ToList() : source.Skip((CurrentPage - 1) * PageSize).Take(PageSize).ToList(); + + // if CurrentPage is greater than 1 means it has previousPage + bool previousPage = CurrentPage > 1; + + // if TotalPages is greater than CurrentPage means it has nextPage + bool nextPage = CurrentPage < TotalPages; + + // Object which we are going to send in header + PaginationMetadata paginationMetadata = new PaginationMetadata() + { + totalCount = TotalCount, + pageSize = PageSize, + currentPage = CurrentPage, + totalPages = TotalPages, + hasPreviousPage = previousPage, + hasNextPage = nextPage + }; + + ArrayType[] result = + PageSize <= 0 ? + await source.ToArrayAsync() + : + await source.Skip((CurrentPage - 1) * PageSize).Take(PageSize).ToArrayAsync(); + + return (paginationMetadata, result); + + } + } +} diff --git a/RSecurityBackend/Services/Implementation/RAuditDataProvider.cs b/RSecurityBackend/Services/Implementation/RAuditDataProvider.cs new file mode 100644 index 00000000..a9c13936 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/RAuditDataProvider.cs @@ -0,0 +1,169 @@ +using Audit.Core; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; +using Dapper; +using System.Data; +using Audit.WebApi; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Custom Audit Data Provider + /// + public class RAuditDataProvider : AuditDataProvider, IDisposable + { + /// + /// Insert + /// + /// + /// + public override object InsertEvent(AuditEvent auditEvent) + { + if (_connection == null) + return null; + AuditEventWebApi apiEvent = (AuditEventWebApi)auditEvent; + Guid Id = new Guid(apiEvent.Action.TraceId); + string sql = $"INSERT INTO AuditLogs (Id, EventType, StartDate, EndDate, Duration, UserName, IpAddress, ResponseStatusCode, RequestUrl, JsonData) VALUES (@Id, @EventType, @StartDate, @EndDate, @Duration, @UserName, @IpAddress, @ResponseStatusCode, @RequestUrl, @JsonData);"; + _connection.Execute(sql, new { Id, apiEvent.EventType, apiEvent.StartDate, apiEvent.EndDate, apiEvent.Duration, apiEvent.Action.UserName, apiEvent.Action.IpAddress, apiEvent.Action.ResponseStatusCode, apiEvent.Action.RequestUrl, JsonData = apiEvent.ToJson() }); + return Id; + } + + /// + /// Insert Async + /// + /// + /// + public override async Task InsertEventAsync(AuditEvent auditEvent) + { + if (_connection == null) + return null; + AuditEventWebApi apiEvent = (AuditEventWebApi)auditEvent; + Guid Id = new Guid(apiEvent.Action.TraceId); + string sql = $"INSERT INTO AuditLogs (Id, EventType, StartDate, EndDate, Duration, UserName, IpAddress, ResponseStatusCode, RequestUrl, JsonData) VALUES (@Id, @EventType, @StartDate, @EndDate, @Duration, @UserName, @IpAddress, @ResponseStatusCode, @RequestUrl, @JsonData);"; + await _connection.ExecuteAsync(sql, new { Id, apiEvent.EventType, apiEvent.StartDate, apiEvent.EndDate, apiEvent.Duration, apiEvent.Action.UserName, apiEvent.Action.IpAddress, apiEvent.Action.ResponseStatusCode, apiEvent.Action.RequestUrl, JsonData = apiEvent.ToJson() }); + return Id; + } + + /// + /// Update + /// + /// + /// + public override void ReplaceEvent(object eventId, AuditEvent auditEvent) + { + if (_connection == null) + return; + Guid Id = new Guid(eventId.ToString()); + IDbConnection dapper = _connection; + dapper.Execute($"UPDATE AuditLogs SET JsonData = @JsonData, EndDate = @EndDate WHERE Id = @Id", new { JsonData = auditEvent.ToJson(), auditEvent.EndDate, Id }); + } + + /// + /// Update Async + /// + /// + /// + /// + public override async Task ReplaceEventAsync(object eventId, AuditEvent auditEvent) + { + if (_connection == null) + return; + Guid Id = new Guid(eventId.ToString()); + IDbConnection dapper = _connection; + await dapper.ExecuteAsync($"UPDATE AuditLogs SET JsonData = @JsonData, EndDate = @EndDate WHERE Id = @Id", new { JsonData = auditEvent.ToJson(), auditEvent.EndDate, Id }); + } + + /// + /// Get + /// + /// + /// + /// + public override T GetEvent(object eventId) + { + if (_connection == null) + return null; + Guid id = new Guid(eventId.ToString()); + IDbConnection dapper = _connection; + return JsonConvert.DeserializeObject(dapper.QueryFirstOrDefault($"SELECT JsonData FROM AuditLogs WHERE Id = '{id}'")); + } + + + + + + /// + /// Get Async + /// + /// + /// + /// + public override async Task GetEventAsync(object eventId) + { + if (_connection == null) + return null; + Guid id = new Guid(eventId.ToString()); + IDbConnection dapper = _connection; + return JsonConvert.DeserializeObject((await dapper.QueryFirstOrDefaultAsync($"SELECT JsonData FROM AuditLogs WHERE Id = '{id}'"))); + } + + /// + /// is disposed + /// + private bool _disposed = false; + + /// + /// dispose + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + if(_connection != null) + _connection.Dispose(); + _connection = null; + } + + _disposed = true; + } + + + /// + /// Connection + /// + private SqlConnection _connection; + + /// + /// constructor + /// + /// + public RAuditDataProvider(string connectionString) + { + try + { + _connection = new SqlConnection(connectionString); + _connection.Open(); + } + catch //this happens when database does not exits, it is only the first run of the app + { + _connection = null; + } + } + } +} diff --git a/RSecurityBackend/Services/Implementation/RoleServiceBase.cs b/RSecurityBackend/Services/Implementation/RoleServiceBase.cs new file mode 100644 index 00000000..550a4c40 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/RoleServiceBase.cs @@ -0,0 +1,318 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using RSecurityBackend.DbContext; +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// User Roles Service Implementation + /// + public class RoleServiceBase : IUserRoleService + { + /// + /// returns all user roles + /// + /// + public async Task> GetAllRoles() + { + try + { + RAppRole[] rolesInfo = await _roleManager.Roles.Include(r => r.Permissions).ToArrayAsync(); + return new RServiceResult(rolesInfo); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// returns user role information + /// + /// + /// + public async Task> GetRoleInformation(string roleName) + { + + try + { + RAppRole dbUserRoleInfo = + await _roleManager.FindByNameAsync(roleName); + return new RServiceResult(dbUserRoleInfo); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + + } + } + + + /// + /// modify existing user role + /// + /// + /// + /// + public async Task> ModifyRole(string roleName, RAppRole updateRoleInfo) + { + try + { + RAppRole existingInfo = await _roleManager.FindByNameAsync(roleName); + if (existingInfo == null) + { + return new RServiceResult(false, "role not found"); + } + + if (existingInfo.Name != updateRoleInfo.Name) + { + + RAppRole anotherWithSameName = await _roleManager.Roles.Where(g => g.Name == updateRoleInfo.Name && g.Id != existingInfo.Id).SingleOrDefaultAsync(); + + if (anotherWithSameName != null) + { + return new RServiceResult(false, "duplicated role name"); + } + + existingInfo.Name = updateRoleInfo.Name; + } + + + existingInfo.Description = updateRoleInfo.Description; + + + await _roleManager.UpdateAsync(existingInfo); + + return new RServiceResult(true); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// delete user role + /// + /// + /// true if succeeds + public async Task> DeleteRole(string roleName) + { + try + { + RAppRole existingInfo = await _roleManager.FindByNameAsync(roleName); + if (existingInfo != null) + { + await _roleManager.DeleteAsync(existingInfo); + + return new RServiceResult(true); + } + + + return new RServiceResult(false, "role not found."); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// adds a new user role + /// + /// new role info + /// update user role info (id) + public async Task> AddRole(RAppRole newRoleInfo) + { + try + { + RAppRole existingRole = await _roleManager.Roles.Where(g => g.Name == newRoleInfo.Name).SingleOrDefaultAsync(); + + if(existingRole != null) + { + return new RServiceResult(null, "Role name is in use"); + } + + await _roleManager.CreateAsync(newRoleInfo); + return new RServiceResult(newRoleInfo); + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Has role specified permission + /// + /// + /// + /// + /// + public async Task> HasPermission(string roleName, string securableItemShortName, string operationShortName) + { + try + { + RAppRole roleByName = await _roleManager.FindByNameAsync(roleName); + if (roleByName == null) + { + + return new RServiceResult(false, "role not found"); + } + + RAppRole role = await _roleManager.Roles.Include(g => g.Permissions) + .Where(g => g.Id == roleByName.Id) + .SingleOrDefaultAsync(); + + return + new RServiceResult( + role.Permissions.Where(p => p.SecurableItemShortName == securableItemShortName && p.OperationShortName == operationShortName) + .SingleOrDefault() != null + ); + + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// gets list of SecurableItem, should be reimplemented in end user applications + /// + /// + public virtual SecurableItem[] GetSecurableItems() + { + return SecurableItem.Items; + } + + /// + /// Lists role permissions + /// + /// + /// + public async Task> GetRoleSecurableItemsStatus(string roleName) + { + try + { + RAppRole roleByName = await _roleManager.FindByNameAsync(roleName); + if (roleByName == null) + { + + return new RServiceResult(null, "role not found"); + } + RAppRole role = await _roleManager.Roles.Include(g => g.Permissions).Where(g => g.Id == roleByName.Id).SingleOrDefaultAsync(); + List securableItems = new List(); + foreach(SecurableItem templateItem in GetSecurableItems()) + { + SecurableItem item = new SecurableItem() + { + ShortName = templateItem.ShortName, + Description = templateItem.Description + }; + + List operations = new List(); + + foreach(SecurableItemOperation operation in templateItem.Operations) + { + operations.Add( + new SecurableItemOperation() + { + ShortName = operation.ShortName, + Description = operation.Description, + Prerequisites = operation.Prerequisites, + Status = role.Permissions.Where(p => p.SecurableItemShortName == templateItem.ShortName && p.OperationShortName == operation.ShortName).SingleOrDefault() != null + } + ); + } + + item.Operations = operations.ToArray(); + + securableItems.Add(item); + } + + return new RServiceResult(securableItems.ToArray()); + + } + catch (Exception exp) + { + return new RServiceResult(null, exp.ToString()); + } + } + + /// + /// Saves role permissions + /// + /// + /// + /// + public async Task> SetRoleSecurableItemsStatus(string roleName, SecurableItem[] securableItems) + { + try + { + RAppRole roleByName = await _roleManager.FindByNameAsync(roleName); + if (roleByName == null) + { + + return new RServiceResult(false, "role not found"); + } + + RAppRole role = await _roleManager.Roles.Include(g => g.Permissions).Where(g => g.Id == roleByName.Id).SingleOrDefaultAsync(); + + role.Permissions.Clear(); + await _roleManager.UpdateAsync(role); + + List newPermissionSet = new List(); + foreach(SecurableItem securableItem in securableItems) + { + foreach (SecurableItemOperation operation in securableItem.Operations) + { + if(operation.Status) + { + newPermissionSet.Add(new RPermission() + { + SecurableItemShortName = securableItem.ShortName, + OperationShortName = operation.ShortName + }); + } + } + } + + role.Permissions = newPermissionSet; + await _roleManager.UpdateAsync(role); + + return new RServiceResult(true); + + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + + /// + /// Identity Role Manager + /// + protected RoleManager _roleManager = null; + + + /// + /// constructor + /// + /// + public RoleServiceBase( + RoleManager roleManager + ) + { + _roleManager = roleManager; + } + } +} diff --git a/RSecurityBackend/Services/Implementation/SecretGenerator.cs b/RSecurityBackend/Services/Implementation/SecretGenerator.cs new file mode 100644 index 00000000..dbbae633 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/SecretGenerator.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Secret Generator + /// + public class SecretGenerator : ISecretGenerator + { + /// + /// Generates a secret + /// + /// + public string Generate() + { + Random random = new Random(DateTime.Now.Millisecond); + + //a 32 letter string + string randomString = ""; + for (int i = 0; i < 32; i++) + { + randomString += (random.Next(1, 3) == 1 ? random.Next('0', '9') : random.Next(2, 3) == 2 ? random.Next('A', 'Z') : random.Next('a', 'z')); + } + + // generate a 128-bit salt using a secure PRNG + byte[] salt = new byte[128 / 8]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + + // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations) + string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: randomString, + salt: salt, + prf: KeyDerivationPrf.HMACSHA1, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + hashed = hashed.Replace(" ", "").Replace("+", ""); + return hashed; + + } + } +} diff --git a/RSecurityBackend/Services/Implementation/UserPermissionChecker.cs b/RSecurityBackend/Services/Implementation/UserPermissionChecker.cs new file mode 100644 index 00000000..5762c439 --- /dev/null +++ b/RSecurityBackend/Services/Implementation/UserPermissionChecker.cs @@ -0,0 +1,101 @@ +using RSecurityBackend.Models.Auth.Db; +using RSecurityBackend.Models.Auth.Memory; +using RSecurityBackend.Models.Auth.ViewModels; +using RSecurityBackend.Models.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace RSecurityBackend.Services.Implementation +{ + /// + /// Permission checker service implementation + /// + public class UserPermissionChecker : IUserPermissionChecker + { + /// + /// check to see if user has permission to do operation + /// + /// userId + /// + /// form + /// operation + /// true if has permission + public virtual async Task> Check(Guid userId, Guid sessionId, string securableItemShortName, string operationShortName) + { + try + { + + RServiceResult userInfo = await _appUserService.GetUserInformation(userId); + + if (userInfo.Result == null) + { + return new RServiceResult(false); + } + + if (userInfo.Result.Status != RAppUserStatus.Active) + { + return new RServiceResult(false); + } + + RServiceResult sessionCheckResult = await _appUserService.SessionExists(userId, sessionId); + if(!string.IsNullOrEmpty(sessionCheckResult.ExceptionString)) + { + return new RServiceResult(false, sessionCheckResult.ExceptionString); + } + + if(!sessionCheckResult.Result) + { + return new RServiceResult(false); + } + + + + RServiceResult isAdminResult = await _appUserService.IsAdmin(userId); + if(!string.IsNullOrEmpty(isAdminResult.ExceptionString)) + { + return new RServiceResult(false, isAdminResult.ExceptionString); + } + + if (isAdminResult.Result) + { + return new RServiceResult(true); + } + + + + + RServiceResult hasPermission = + await _appUserService.HasPermission(userId, securableItemShortName, operationShortName); + + if (hasPermission.Result) + { + return new RServiceResult(true); + } + + return new RServiceResult(false); + } + catch(Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + } + + /// + /// IAppUserService instance + /// + protected IAppUserService _appUserService; + + + + + /// + /// constructor + /// + /// + public UserPermissionChecker(IAppUserService appUserService) + { + _appUserService = appUserService; + } + } +} diff --git a/RSecurityBackend/Utilities/AuditNetEnvironmentSkippingContractResolver.cs b/RSecurityBackend/Utilities/AuditNetEnvironmentSkippingContractResolver.cs new file mode 100644 index 00000000..b3364f0b --- /dev/null +++ b/RSecurityBackend/Utilities/AuditNetEnvironmentSkippingContractResolver.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Reflection; + +namespace RSecurityBackend.Utilities +{ + /// + /// Audit.NET Environment Skipping + /// https://stackoverflow.com/questions/59627835/dont-include-environment-in-serialised-auditevents + /// + public class AuditNetEnvironmentSkippingContractResolver : DefaultContractResolver + { + /// + /// instance + /// + public static readonly AuditNetEnvironmentSkippingContractResolver Instance = new AuditNetEnvironmentSkippingContractResolver(); + + /// + /// create property + /// + /// + /// + /// + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + property.ShouldSerialize = instance => member.Name != "Environment"; + return property; + } + } +}