mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-08-15 21:10:21 +02:00
open source Notesnook API
This commit is contained in:
+265
@@ -0,0 +1,265 @@
|
|||||||
|
## 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
|
||||||
|
|
||||||
|
keys/
|
||||||
|
dist/
|
||||||
|
appsettings.json
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Notesnook.API.Repositories;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Accessors
|
||||||
|
{
|
||||||
|
public class SyncItemsRepositoryAccessor : ISyncItemsRepositoryAccessor
|
||||||
|
{
|
||||||
|
public SyncItemsRepository<Note> Notes { get; }
|
||||||
|
public SyncItemsRepository<Notebook> Notebooks { get; }
|
||||||
|
public SyncItemsRepository<Shortcut> Shortcuts { get; }
|
||||||
|
public SyncItemsRepository<Relation> Relations { get; }
|
||||||
|
public SyncItemsRepository<Reminder> Reminders { get; }
|
||||||
|
public SyncItemsRepository<Content> Contents { get; }
|
||||||
|
public SyncItemsRepository<Setting> Settings { get; }
|
||||||
|
public SyncItemsRepository<Attachment> Attachments { get; }
|
||||||
|
public Repository<UserSettings> UsersSettings { get; }
|
||||||
|
public Repository<Monograph> Monographs { get; }
|
||||||
|
|
||||||
|
public SyncItemsRepositoryAccessor(SyncItemsRepository<Note> _notes,
|
||||||
|
SyncItemsRepository<Notebook> _notebooks,
|
||||||
|
SyncItemsRepository<Content> _content,
|
||||||
|
SyncItemsRepository<Setting> _settings,
|
||||||
|
SyncItemsRepository<Attachment> _attachments,
|
||||||
|
SyncItemsRepository<Shortcut> _shortcuts,
|
||||||
|
SyncItemsRepository<Relation> _relations,
|
||||||
|
SyncItemsRepository<Reminder> _reminders,
|
||||||
|
Repository<UserSettings> _usersSettings,
|
||||||
|
Repository<Monograph> _monographs)
|
||||||
|
{
|
||||||
|
Notebooks = _notebooks;
|
||||||
|
Notes = _notes;
|
||||||
|
Contents = _content;
|
||||||
|
Settings = _settings;
|
||||||
|
Attachments = _attachments;
|
||||||
|
UsersSettings = _usersSettings;
|
||||||
|
Monographs = _monographs;
|
||||||
|
Shortcuts = _shortcuts;
|
||||||
|
Reminders = _reminders;
|
||||||
|
Relations = _relations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Authorization
|
||||||
|
{
|
||||||
|
public class EmailVerifiedRequirement : AuthorizationHandler<EmailVerifiedRequirement>, IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EmailVerifiedRequirement requirement)
|
||||||
|
{
|
||||||
|
var isEmailVerified = context.User.HasClaim("verified", "true");
|
||||||
|
var isUserBasic = context.User.HasClaim("notesnook:status", "basic") || context.User.HasClaim("notesnook:status", "premium_expired");
|
||||||
|
if (!isUserBasic || isEmailVerified)
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Authorization
|
||||||
|
{
|
||||||
|
public class NotesnookUserRequirement : AuthorizationHandler<NotesnookUserRequirement>, IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotesnookUserRequirement requirement)
|
||||||
|
{
|
||||||
|
var isInAudience = context.User.HasClaim("aud", "notesnook");
|
||||||
|
var hasRole = context.User.HasClaim("role", "notesnook");
|
||||||
|
if (isInAudience && hasRole)
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Authorization
|
||||||
|
{
|
||||||
|
public class ProUserRequirement : AuthorizationHandler<ProUserRequirement>, IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
private string[] allowedClaims = { "trial", "premium", "premium_canceled" };
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProUserRequirement requirement)
|
||||||
|
{
|
||||||
|
var isProOrTrial = context.User.HasClaim((c) => c.Type == "notesnook:status" && allowedClaims.Contains(c.Value));
|
||||||
|
if (isProOrTrial)
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Authorization
|
||||||
|
{
|
||||||
|
public class SyncRequirement : AuthorizationHandler<SyncRequirement>, IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> pathErrorPhraseMap = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["/sync/attachments"] = "use attachments",
|
||||||
|
["/sync"] = "sync your notes",
|
||||||
|
["/hubs/sync"] = "sync your notes",
|
||||||
|
["/monographs"] = "publish monographs"
|
||||||
|
};
|
||||||
|
|
||||||
|
private string[] allowedClaims = { "trial", "premium", "premium_canceled" };
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement)
|
||||||
|
{
|
||||||
|
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
||||||
|
var result = this.IsAuthorized(context.User, path);
|
||||||
|
if (result.Succeeded) context.Succeed(requirement);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var hasReason = result.AuthorizationFailure.FailureReasons.Count() > 0;
|
||||||
|
if (hasReason)
|
||||||
|
context.Fail(result.AuthorizationFailure.FailureReasons.First());
|
||||||
|
else context.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal User, PathString requestPath)
|
||||||
|
{
|
||||||
|
var id = User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
var reason = new AuthorizationFailureReason[]
|
||||||
|
{
|
||||||
|
new AuthorizationFailureReason(this, "Invalid token.")
|
||||||
|
};
|
||||||
|
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
|
||||||
|
var isInAudience = User.HasClaim("aud", "notesnook");
|
||||||
|
var hasRole = User.HasClaim("role", "notesnook");
|
||||||
|
|
||||||
|
var isEmailVerified = User.HasClaim("verified", "true");
|
||||||
|
|
||||||
|
if (!isEmailVerified)
|
||||||
|
{
|
||||||
|
var phrase = "continue";
|
||||||
|
|
||||||
|
foreach (var item in pathErrorPhraseMap)
|
||||||
|
{
|
||||||
|
if (requestPath != null && requestPath.StartsWithSegments(item.Key))
|
||||||
|
phrase = item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = $"Please confirm your email to {phrase}.";
|
||||||
|
var reason = new AuthorizationFailureReason[]
|
||||||
|
{
|
||||||
|
new AuthorizationFailureReason(this, error)
|
||||||
|
};
|
||||||
|
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||||
|
// context.Fail(new AuthorizationFailureReason(this, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
var isProOrTrial = User.HasClaim((c) => c.Type == "notesnook:status" && allowedClaims.Contains(c.Value));
|
||||||
|
if (hasSyncScope && isInAudience && hasRole && isEmailVerified)
|
||||||
|
return PolicyAuthorizationResult.Success(); //(requirement);
|
||||||
|
return PolicyAuthorizationResult.Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task HandleAsync(AuthorizationHandlerContext context)
|
||||||
|
{
|
||||||
|
return this.HandleRequirementAsync(context, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Controllers
|
||||||
|
{
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("announcements")]
|
||||||
|
public class AnnouncementController : ControllerBase
|
||||||
|
{
|
||||||
|
private Repository<Announcement> Announcements { get; set; }
|
||||||
|
public AnnouncementController(Repository<Announcement> announcements)
|
||||||
|
{
|
||||||
|
Announcements = announcements;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("active")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string userId)
|
||||||
|
{
|
||||||
|
var announcements = await Announcements.FindAsync((a) => a.IsActive);
|
||||||
|
return Ok(announcements.Where((a) => a.UserIds != null && a.UserIds.Length > 0
|
||||||
|
? a.UserIds.Contains(userId)
|
||||||
|
: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("monographs")]
|
||||||
|
[Authorize("Sync")]
|
||||||
|
public class MonographsController : ControllerBase
|
||||||
|
{
|
||||||
|
private Repository<Monograph> Monographs { get; set; }
|
||||||
|
private readonly IUnitOfWork unit;
|
||||||
|
private const int MAX_DOC_SIZE = 15 * 1024 * 1024;
|
||||||
|
public MonographsController(Repository<Monograph> monographs, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
Monographs = monographs;
|
||||||
|
unit = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> PublishAsync([FromBody] Monograph monograph)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
|
if (await Monographs.GetAsync(monograph.Id) != null) return base.Conflict("This monograph is already published.");
|
||||||
|
|
||||||
|
if (monograph.EncryptedContent == null)
|
||||||
|
monograph.CompressedContent = monograph.Content.CompressBrotli();
|
||||||
|
monograph.UserId = userId;
|
||||||
|
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
|
||||||
|
if (monograph.EncryptedContent?.Cipher.Length > MAX_DOC_SIZE || monograph.CompressedContent?.Length > MAX_DOC_SIZE)
|
||||||
|
return base.BadRequest("Monograph is too big. Max allowed size is 15mb.");
|
||||||
|
|
||||||
|
Monographs.Insert(monograph);
|
||||||
|
|
||||||
|
if (!await unit.Commit()) return BadRequest();
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
id = monograph.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch]
|
||||||
|
public async Task<IActionResult> UpdateAsync([FromBody] Monograph monograph)
|
||||||
|
{
|
||||||
|
if (await Monographs.GetAsync(monograph.Id) == null) return NotFound();
|
||||||
|
|
||||||
|
if (monograph.EncryptedContent == null)
|
||||||
|
monograph.CompressedContent = monograph.Content.CompressBrotli();
|
||||||
|
else
|
||||||
|
monograph.Content = null;
|
||||||
|
|
||||||
|
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
Monographs.Update(monograph.Id, monograph);
|
||||||
|
|
||||||
|
if (!await unit.Commit()) return BadRequest();
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
id = monograph.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetUserMonographsAsync()
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
|
var userMonographs = await Monographs.FindAsync((m) => m.UserId == userId);
|
||||||
|
return Ok(userMonographs.Select((m) => m.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
|
||||||
|
{
|
||||||
|
var monograph = await Monographs.FindOneAsync((m) => m.Id == id);
|
||||||
|
if (monograph == null)
|
||||||
|
{
|
||||||
|
return NotFound(new
|
||||||
|
{
|
||||||
|
error = "invalid_id",
|
||||||
|
error_description = $"No such monograph found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monograph.SelfDestruct)
|
||||||
|
await Monographs.DeleteByIdAsync(monograph.Id);
|
||||||
|
|
||||||
|
if (monograph.EncryptedContent == null)
|
||||||
|
monograph.Content = monograph.CompressedContent.DecompressBrotli();
|
||||||
|
return Ok(monograph);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] string id)
|
||||||
|
{
|
||||||
|
Monographs.DeleteById(id);
|
||||||
|
if (!await unit.Commit()) return BadRequest();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Amazon.S3;
|
||||||
|
using Amazon.Runtime;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Linq;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("s3")]
|
||||||
|
[Authorize("Sync")]
|
||||||
|
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||||
|
public class S3Controller : ControllerBase
|
||||||
|
{
|
||||||
|
private IS3Service S3Service { get; set; }
|
||||||
|
public S3Controller(IS3Service s3Service)
|
||||||
|
{
|
||||||
|
S3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public IActionResult Upload([FromQuery] string name)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
var url = S3Service.GetUploadObjectUrl(userId, name);
|
||||||
|
if (url == null) return BadRequest("Could not create signed url.");
|
||||||
|
return Ok(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("multipart")]
|
||||||
|
public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string uploadId)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
|
||||||
|
return Ok(meta);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("multipart")]
|
||||||
|
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await S3Service.AbortMultipartUploadAsync(userId, name, uploadId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("multipart")]
|
||||||
|
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequest uploadRequest)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await S3Service.CompleteMultipartUploadAsync(userId, uploadRequest);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult Download([FromQuery] string name)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
var url = S3Service.GetDownloadObjectUrl(userId, name);
|
||||||
|
if (url == null) return BadRequest("Could not create signed url.");
|
||||||
|
return Ok(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpHead]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> Info([FromQuery] string name)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
var size = await S3Service.GetObjectSizeAsync(userId, name);
|
||||||
|
if (size == null) return BadRequest();
|
||||||
|
|
||||||
|
HttpContext.Response.Headers.ContentLength = size;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromQuery] string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
await S3Service.DeleteObjectAsync(userId, name);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models.Responses;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
using Streetwriters.Common.Extensions;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("users")]
|
||||||
|
public class UsersController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private readonly IHttpContextAccessor HttpContextAccessor;
|
||||||
|
private IUserService UserService { get; set; }
|
||||||
|
public UsersController(IUserService userService, IHttpContextAccessor accessor)
|
||||||
|
{
|
||||||
|
httpClient = new HttpClient();
|
||||||
|
HttpContextAccessor = accessor;
|
||||||
|
UserService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Signup()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UserService.CreateUserAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Slogger<UsersController>.Error(nameof(Signup), "Couldn't sign up.", ex.ToString());
|
||||||
|
return BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetUser()
|
||||||
|
{
|
||||||
|
UserResponse response = await UserService.GetUserAsync();
|
||||||
|
if (!response.Success) return BadRequest(response);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch]
|
||||||
|
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
|
||||||
|
{
|
||||||
|
UserResponse response = await UserService.GetUserAsync(false);
|
||||||
|
|
||||||
|
if (user.AttachmentsKey != null)
|
||||||
|
await UserService.SetUserAttachmentsKeyAsync(response.UserId, user.AttachmentsKey);
|
||||||
|
else return BadRequest();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reset")]
|
||||||
|
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
if (await UserService.ResetUserAsync(userId, removeAttachments))
|
||||||
|
return Ok();
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("delete")]
|
||||||
|
public async Task<IActionResult> Delete()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
Response response = await this.httpClient.ForwardAsync<Response>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account/unregister", HttpMethod.Post);
|
||||||
|
if (!response.Success) return BadRequest();
|
||||||
|
|
||||||
|
if (await UserService.DeleteUserAsync(userId, User.FindFirstValue("jti")))
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Notesnook.API.Authorization;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Extensions
|
||||||
|
{
|
||||||
|
public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
|
||||||
|
{
|
||||||
|
private readonly IAuthorizationMiddlewareResultHandler _handler;
|
||||||
|
|
||||||
|
public AuthorizationResultTransformer()
|
||||||
|
{
|
||||||
|
_handler = new AuthorizationMiddlewareResultHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleAsync(
|
||||||
|
RequestDelegate requestDelegate,
|
||||||
|
HttpContext httpContext,
|
||||||
|
AuthorizationPolicy authorizationPolicy,
|
||||||
|
PolicyAuthorizationResult policyAuthorizationResult)
|
||||||
|
{
|
||||||
|
var isWebsocket = httpContext.Request.Headers.Upgrade == "websocket";
|
||||||
|
|
||||||
|
if (!isWebsocket && policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
|
||||||
|
{
|
||||||
|
var error = string.Join("\n", policyAuthorizationResult.AuthorizationFailure.FailureReasons.Select((r) => r.Message));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(error) && !isWebsocket)
|
||||||
|
{
|
||||||
|
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
|
httpContext.Response.ContentType = "application/json";
|
||||||
|
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
||||||
|
}
|
||||||
|
else if (isWebsocket)
|
||||||
|
{
|
||||||
|
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, PolicyAuthorizationResult.Success());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MongoDB.Driver
|
||||||
|
{
|
||||||
|
public static class TransactionHelper
|
||||||
|
{
|
||||||
|
public static async Task StartTransaction(this IMongoClient client, Action<CancellationToken> operate, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using (var session = await client.StartSessionAsync())
|
||||||
|
{
|
||||||
|
var transactionOptions = new TransactionOptions(readPreference: ReadPreference.Nearest, readConcern: ReadConcern.Local, writeConcern: WriteConcern.WMajority);
|
||||||
|
await session.WithTransactionAsync((handle, token) =>
|
||||||
|
{
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
operate(token);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, transactionOptions, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Notesnook.API.Authorization;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Hubs
|
||||||
|
{
|
||||||
|
public interface ISyncHubClient
|
||||||
|
{
|
||||||
|
Task SyncItem(SyncTransferItem transferItem);
|
||||||
|
Task RemoteSyncCompleted(long lastSynced);
|
||||||
|
Task SyncCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize("Sync")]
|
||||||
|
public class SyncHub : Hub<ISyncHubClient>
|
||||||
|
{
|
||||||
|
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||||
|
private readonly IUnitOfWork unit;
|
||||||
|
|
||||||
|
public SyncHub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
Repositories = syncItemsRepositoryAccessor;
|
||||||
|
unit = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync"));
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault();
|
||||||
|
throw new HubException(reason?.Message ?? "Unauthorized");
|
||||||
|
}
|
||||||
|
var id = Context.User.FindFirstValue("sub");
|
||||||
|
Groups.AddToGroupAsync(Context.ConnectionId, id);
|
||||||
|
return base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task OnDisconnectedAsync(Exception exception)
|
||||||
|
{
|
||||||
|
var id = Context.User.FindFirstValue("sub");
|
||||||
|
Groups.RemoveFromGroupAsync(Context.ConnectionId, id);
|
||||||
|
return base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SyncItem(BatchedSyncTransferItem transferItem)
|
||||||
|
{
|
||||||
|
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
if (string.IsNullOrEmpty(userId)) return 0;
|
||||||
|
|
||||||
|
var others = Clients.OthersInGroup(userId);
|
||||||
|
|
||||||
|
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
|
||||||
|
long dateSynced = transferItem.LastSynced > userSettings.LastSynced ? transferItem.LastSynced : userSettings.LastSynced;
|
||||||
|
|
||||||
|
for (int i = 0; i < transferItem.Items.Length; ++i)
|
||||||
|
{
|
||||||
|
var data = transferItem.Items[i];
|
||||||
|
var type = transferItem.Types[i];
|
||||||
|
|
||||||
|
others.SyncItem(
|
||||||
|
new SyncTransferItem
|
||||||
|
{
|
||||||
|
Item = data,
|
||||||
|
ItemType = type,
|
||||||
|
LastSynced = dateSynced,
|
||||||
|
Total = transferItem.Total,
|
||||||
|
Current = transferItem.Current + i
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "content":
|
||||||
|
Repositories.Contents.Upsert(JsonSerializer.Deserialize<Content>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "attachment":
|
||||||
|
Repositories.Attachments.Upsert(JsonSerializer.Deserialize<Attachment>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "note":
|
||||||
|
Repositories.Notes.Upsert(JsonSerializer.Deserialize<Note>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "notebook":
|
||||||
|
Repositories.Notebooks.Upsert(JsonSerializer.Deserialize<Notebook>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "shortcut":
|
||||||
|
Repositories.Shortcuts.Upsert(JsonSerializer.Deserialize<Shortcut>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "reminder":
|
||||||
|
Repositories.Reminders.Upsert(JsonSerializer.Deserialize<Reminder>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "relation":
|
||||||
|
Repositories.Relations.Upsert(JsonSerializer.Deserialize<Relation>(data), userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "settings":
|
||||||
|
var settings = JsonSerializer.Deserialize<Setting>(data);
|
||||||
|
settings.Id = MongoDB.Bson.ObjectId.Parse(userId);
|
||||||
|
settings.ItemId = userId;
|
||||||
|
Repositories.Settings.Upsert(settings, userId, dateSynced);
|
||||||
|
break;
|
||||||
|
case "vaultKey":
|
||||||
|
userSettings.VaultKey = JsonSerializer.Deserialize<EncryptedData>(data);
|
||||||
|
Repositories.UsersSettings.Upsert(userSettings, (u) => u.UserId == userId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new HubException("Invalid item type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await unit.Commit() ? 1 : 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncCompleted(long dateSynced)
|
||||||
|
{
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
|
||||||
|
long lastSynced = dateSynced > userSettings.LastSynced ? dateSynced : userSettings.LastSynced;
|
||||||
|
|
||||||
|
userSettings.LastSynced = lastSynced;
|
||||||
|
|
||||||
|
await this.Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
|
||||||
|
|
||||||
|
await Clients.OthersInGroup(userId).RemoteSyncCompleted(lastSynced);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<SyncTransferItem> FetchItems(long lastSyncedTimestamp, [EnumeratorCancellation]
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
if (userSettings.LastSynced > 0 && lastSyncedTimestamp > userSettings.LastSynced)
|
||||||
|
throw new HubException($"Provided timestamp value is too large. Server timestamp: {userSettings.LastSynced} Sent timestamp: {lastSyncedTimestamp}");
|
||||||
|
|
||||||
|
// var client = Clients.Caller;
|
||||||
|
|
||||||
|
if (lastSyncedTimestamp > 0 && userSettings.LastSynced == lastSyncedTimestamp)
|
||||||
|
{
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
LastSynced = userSettings.LastSynced,
|
||||||
|
Synced = true
|
||||||
|
};
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var attachments = await Repositories.Attachments.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var notes = await Repositories.Notes.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var notebooks = await Repositories.Notebooks.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var contents = await Repositories.Contents.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var settings = await Repositories.Settings.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var shortcuts = await Repositories.Shortcuts.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var reminders = await Repositories.Reminders.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var relations = await Repositories.Relations.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var collections = new Dictionary<string, IEnumerable<object>>
|
||||||
|
{
|
||||||
|
["attachment"] = attachments,
|
||||||
|
["note"] = notes,
|
||||||
|
["notebook"] = notebooks,
|
||||||
|
["content"] = contents,
|
||||||
|
["shortcut"] = shortcuts,
|
||||||
|
["reminder"] = reminders,
|
||||||
|
["relation"] = relations,
|
||||||
|
["settings"] = settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userSettings.VaultKey != null)
|
||||||
|
{
|
||||||
|
collections.Add("vaultKey", new object[] { userSettings.VaultKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = collections.Values.Sum((a) => a.Count());
|
||||||
|
if (total == 0)
|
||||||
|
{
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
Synced = true,
|
||||||
|
LastSynced = userSettings.LastSynced
|
||||||
|
};
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
foreach (var item in collection.Value)
|
||||||
|
{
|
||||||
|
if (item == null) continue;
|
||||||
|
// Check the cancellation token regularly so that the server will stop producing items if the client disconnects.
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
LastSynced = userSettings.LastSynced,
|
||||||
|
Synced = false,
|
||||||
|
Item = JsonSerializer.Serialize(item),
|
||||||
|
ItemType = collection.Key,
|
||||||
|
Total = total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePack.MessagePackObject]
|
||||||
|
public struct BatchedSyncTransferItem
|
||||||
|
{
|
||||||
|
[MessagePack.Key("lastSynced")]
|
||||||
|
public long LastSynced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("items")]
|
||||||
|
public string[] Items { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("types")]
|
||||||
|
public string[] Types { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("current")]
|
||||||
|
public int Current { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePack.MessagePackObject]
|
||||||
|
public struct SyncTransferItem
|
||||||
|
{
|
||||||
|
[MessagePack.Key("synced")]
|
||||||
|
public bool Synced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("lastSynced")]
|
||||||
|
public long LastSynced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("item")]
|
||||||
|
public string Item { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("itemType")]
|
||||||
|
public string ItemType { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("current")]
|
||||||
|
public int Current { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IEncrypted
|
||||||
|
{
|
||||||
|
string Cipher { get; set; }
|
||||||
|
string IV { get; set; }
|
||||||
|
long Length { get; set; }
|
||||||
|
string Salt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IMonograph : IDocument
|
||||||
|
{
|
||||||
|
string Title { get; set; }
|
||||||
|
string UserId { get; set; }
|
||||||
|
byte[] CompressedContent { get; set; }
|
||||||
|
EncryptedData EncryptedContent { get; set; }
|
||||||
|
long DatePublished { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Notesnook.API.Models.Responses;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IS3Service
|
||||||
|
{
|
||||||
|
Task DeleteObjectAsync(string userId, string name);
|
||||||
|
Task DeleteDirectoryAsync(string userId);
|
||||||
|
Task<long?> GetObjectSizeAsync(string userId, string name);
|
||||||
|
string GetUploadObjectUrl(string userId, string name);
|
||||||
|
string GetDownloadObjectUrl(string userId, string name);
|
||||||
|
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null);
|
||||||
|
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
|
||||||
|
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using MongoDB.Bson.Serialization.Serializers;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Attributes;
|
||||||
|
using Streetwriters.Common.Converters;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<ISyncItem, SyncItem>))]
|
||||||
|
[JsonInterfaceConverter(typeof(InterfaceConverter<ISyncItem, SyncItem>))]
|
||||||
|
public interface ISyncItem
|
||||||
|
{
|
||||||
|
long DateSynced
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
string UserId { get; set; }
|
||||||
|
string Algorithm { get; set; }
|
||||||
|
string IV { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Notesnook.API.Models;
|
||||||
|
using Notesnook.API.Repositories;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISyncItemsRepositoryAccessor
|
||||||
|
{
|
||||||
|
SyncItemsRepository<Note> Notes { get; }
|
||||||
|
SyncItemsRepository<Notebook> Notebooks { get; }
|
||||||
|
SyncItemsRepository<Shortcut> Shortcuts { get; }
|
||||||
|
SyncItemsRepository<Reminder> Reminders { get; }
|
||||||
|
SyncItemsRepository<Relation> Relations { get; }
|
||||||
|
SyncItemsRepository<Content> Contents { get; }
|
||||||
|
SyncItemsRepository<Setting> Settings { get; }
|
||||||
|
SyncItemsRepository<Attachment> Attachments { get; }
|
||||||
|
Repository<UserSettings> UsersSettings { get; }
|
||||||
|
Repository<Monograph> Monographs { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Notesnook.API.Models.Responses;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
Task CreateUserAsync();
|
||||||
|
Task<bool> DeleteUserAsync(string userId, string jti);
|
||||||
|
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
|
||||||
|
Task<UserResponse> GetUserAsync(bool repair = true);
|
||||||
|
Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUserSettings : IDocument
|
||||||
|
{
|
||||||
|
string UserId { get; set; }
|
||||||
|
|
||||||
|
long LastSynced
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
EncryptedData VaultKey
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
string Salt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
public class Algorithms
|
||||||
|
{
|
||||||
|
public static string Default => "xcha-argon2i13-7";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("notesnook", "announcements")]
|
||||||
|
public class Announcement
|
||||||
|
{
|
||||||
|
public Announcement()
|
||||||
|
{
|
||||||
|
this.Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
[BsonElement("id")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timestamp")]
|
||||||
|
[BsonElement("timestamp")]
|
||||||
|
public long Timestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
|
public string[] Platforms { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isActive")]
|
||||||
|
[BsonElement("isActive")]
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userTypes")]
|
||||||
|
[BsonElement("userTypes")]
|
||||||
|
public string[] UserTypes { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("appVersion")]
|
||||||
|
[BsonElement("appVersion")]
|
||||||
|
public int AppVersion { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("body")]
|
||||||
|
[BsonElement("body")]
|
||||||
|
public BodyComponent[] Body { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[BsonElement("userIds")]
|
||||||
|
public string[] UserIds { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
[DataMember(Name = "title")]
|
||||||
|
[BsonElement("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
[BsonElement("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
|
[JsonPropertyName("callToActions")]
|
||||||
|
[BsonElement("callToActions")]
|
||||||
|
public CallToAction[] CallToActions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BodyComponent
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
|
public string[] Platforms { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("style")]
|
||||||
|
[BsonElement("style")]
|
||||||
|
public Style Style { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("src")]
|
||||||
|
[BsonElement("src")]
|
||||||
|
public string Src { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("text")]
|
||||||
|
[BsonElement("text")]
|
||||||
|
public string Text { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
[BsonElement("value")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
[BsonElement("items")]
|
||||||
|
public BodyComponent[] Items { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("actions")]
|
||||||
|
[BsonElement("actions")]
|
||||||
|
public CallToAction[] Actions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Style
|
||||||
|
{
|
||||||
|
[JsonPropertyName("marginTop")]
|
||||||
|
[BsonElement("marginTop")]
|
||||||
|
public int MarginTop { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("marginBottom")]
|
||||||
|
[BsonElement("marginBottom")]
|
||||||
|
public int MarginBottom { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("textAlign")]
|
||||||
|
[BsonElement("textAlign")]
|
||||||
|
public string TextAlign { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CallToAction
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
|
public string[] Platforms { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
[BsonElement("data")]
|
||||||
|
public string Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
[BsonElement("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
public class EncryptedData : IEncrypted
|
||||||
|
{
|
||||||
|
[JsonPropertyName("iv")]
|
||||||
|
[BsonElement("iv")]
|
||||||
|
[DataMember(Name = "iv")]
|
||||||
|
public string IV
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("cipher")]
|
||||||
|
[BsonElement("cipher")]
|
||||||
|
[DataMember(Name = "cipher")]
|
||||||
|
public string Cipher
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("length")]
|
||||||
|
[BsonElement("length")]
|
||||||
|
[DataMember(Name = "length")]
|
||||||
|
public long Length { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("salt")]
|
||||||
|
[BsonElement("salt")]
|
||||||
|
[DataMember(Name = "salt")]
|
||||||
|
public string Salt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("notesnook", "monographs")]
|
||||||
|
public class Monograph : IMonograph
|
||||||
|
{
|
||||||
|
public Monograph()
|
||||||
|
{
|
||||||
|
Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("selfDestruct")]
|
||||||
|
public bool SelfDestruct { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("encryptedContent")]
|
||||||
|
public EncryptedData EncryptedContent { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("datePublished")]
|
||||||
|
public long DatePublished { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
[BsonIgnore]
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] CompressedContent { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
public class MultipartUploadMeta
|
||||||
|
{
|
||||||
|
public string UploadId { get; set; }
|
||||||
|
public string[] Parts { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models.Responses
|
||||||
|
{
|
||||||
|
public class SignupResponse : Response
|
||||||
|
{
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
public string[] Errors { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models.Responses
|
||||||
|
{
|
||||||
|
public class UserResponse : UserModel, IResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("salt")]
|
||||||
|
public string Salt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("attachmentsKey")]
|
||||||
|
public EncryptedData AttachmentsKey { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("subscription")]
|
||||||
|
public ISubscription Subscription { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
public class S3Options
|
||||||
|
{
|
||||||
|
public string ServiceUrl { get; set; }
|
||||||
|
public string Region { get; set; }
|
||||||
|
public string AccessKeyId { get; set; }
|
||||||
|
public string SecretAccessKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
public class SyncItem : ISyncItem
|
||||||
|
{
|
||||||
|
[IgnoreDataMember]
|
||||||
|
[JsonPropertyName("dateSynced")]
|
||||||
|
public long DateSynced
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "userId")]
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("iv")]
|
||||||
|
[DataMember(Name = "iv")]
|
||||||
|
[Required]
|
||||||
|
public string IV
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[JsonPropertyName("cipher")]
|
||||||
|
[DataMember(Name = "cipher")]
|
||||||
|
[Required]
|
||||||
|
public string Cipher
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "id")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string ItemId
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonIgnoreIfDefault]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public ObjectId Id
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("length")]
|
||||||
|
[DataMember(Name = "length")]
|
||||||
|
[Required]
|
||||||
|
public long Length
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("v")]
|
||||||
|
[DataMember(Name = "v")]
|
||||||
|
[Required]
|
||||||
|
public double Version
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("alg")]
|
||||||
|
[DataMember(Name = "alg")]
|
||||||
|
[Required]
|
||||||
|
public string Algorithm
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = Algorithms.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "attachments")]
|
||||||
|
public class Attachment : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "content")]
|
||||||
|
public class Content : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "notes")]
|
||||||
|
public class Note : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "notebooks")]
|
||||||
|
public class Notebook : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "relations")]
|
||||||
|
public class Relation : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "reminders")]
|
||||||
|
public class Reminder : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "settings")]
|
||||||
|
public class Setting : SyncItem { }
|
||||||
|
|
||||||
|
[BsonCollection("notesnook", "shortcuts")]
|
||||||
|
public class Shortcut : SyncItem { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("notesnook", "user_settings")]
|
||||||
|
public class UserSettings : IUserSettings
|
||||||
|
{
|
||||||
|
public UserSettings()
|
||||||
|
{
|
||||||
|
this.Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public long LastSynced { get; set; }
|
||||||
|
public string Salt { get; set; }
|
||||||
|
public EncryptedData VaultKey { get; set; }
|
||||||
|
public EncryptedData AttachmentsKey { get; set; }
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<StartupObject>Notesnook.API.Program</StartupObject>
|
||||||
|
<LangVersion>10.0</LangVersion>
|
||||||
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.Core" Version="3.7.12.5" />
|
||||||
|
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="6.0.1-rc2.2" />
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="3.7.9.21" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Streetwriters.Data\Streetwriters.Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.S3;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
|
||||||
|
namespace Notesnook.API
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
IHost host = CreateHostBuilder(args).Build();
|
||||||
|
await host.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.UseKestrel((options) =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = long.MaxValue;
|
||||||
|
#if DEBUG
|
||||||
|
options.ListenAnyIP(int.Parse(Servers.NotesnookAPI.Port));
|
||||||
|
#else
|
||||||
|
options.ListenAnyIP(443, listenerOptions =>
|
||||||
|
{
|
||||||
|
listenerOptions.UseHttps(Servers.OriginSSLCertificate);
|
||||||
|
});
|
||||||
|
options.ListenAnyIP(80);
|
||||||
|
options.Listen(IPAddress.Parse(Servers.NotesnookAPI.Hostname), int.Parse(Servers.NotesnookAPI.Port));
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:1740",
|
||||||
|
"sslPort": 44313
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "weatherforecast",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Notesnook.API": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "weatherforecast",
|
||||||
|
"applicationUrl": "http://localhost:6000",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.VisualBasic;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Repositories
|
||||||
|
{
|
||||||
|
public class SyncItemsRepository<T> : Repository<T> where T : SyncItem
|
||||||
|
{
|
||||||
|
public SyncItemsRepository(IDbContext dbContext) : base(dbContext)
|
||||||
|
{
|
||||||
|
Collection.Indexes.CreateOne(new CreateIndexModel<T>(Builders<T>.IndexKeys.Descending(i => i.DateSynced).Ascending(i => i.UserId)));
|
||||||
|
Collection.Indexes.CreateOne(new CreateIndexModel<T>(Builders<T>.IndexKeys.Ascending((i) => i.ItemId).Ascending(i => i.UserId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<string> ALGORITHMS = new List<string> { Algorithms.Default };
|
||||||
|
private bool IsValidAlgorithm(string algorithm)
|
||||||
|
{
|
||||||
|
return ALGORITHMS.Contains(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<T>> GetItemsSyncedAfterAsync(string userId, long timestamp)
|
||||||
|
{
|
||||||
|
var cursor = await Collection.FindAsync(n => (n.DateSynced > timestamp) && n.UserId.Equals(userId));
|
||||||
|
return cursor.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// public async Task DeleteIdsAsync(string[] ids, string userId, CancellationToken token = default(CancellationToken))
|
||||||
|
// {
|
||||||
|
// await Collection.DeleteManyAsync<T>((i) => ids.Contains(i.Id) && i.UserId == userId, token);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public void DeleteByUserId(string userId)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.DeleteManyAsync<T>(handle, (i) => i.UserId == userId, cancellationToken: ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(T item, string userId, long dateSynced)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (item.Length > 15 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
throw new Exception($"Size of item \"{item.ItemId}\" is too large. Maximum allowed size is 15 MB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidAlgorithm(item.Algorithm))
|
||||||
|
{
|
||||||
|
throw new Exception($"Invalid alg identifier {item.Algorithm}");
|
||||||
|
}
|
||||||
|
|
||||||
|
item.DateSynced = dateSynced;
|
||||||
|
item.UserId = userId;
|
||||||
|
|
||||||
|
await base.UpsertAsync(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Upsert(T item, string userId, long dateSynced)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (item.Length > 15 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
throw new Exception($"Size of item \"{item.ItemId}\" is too large. Maximum allowed size is 15 MB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidAlgorithm(item.Algorithm))
|
||||||
|
{
|
||||||
|
throw new Exception($"Invalid alg identifier {item.Algorithm}");
|
||||||
|
}
|
||||||
|
|
||||||
|
item.DateSynced = dateSynced;
|
||||||
|
item.UserId = userId;
|
||||||
|
|
||||||
|
// await base.UpsertAsync(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId);
|
||||||
|
base.Upsert(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon;
|
||||||
|
using Amazon.Runtime;
|
||||||
|
using Amazon.S3;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Services
|
||||||
|
{
|
||||||
|
public class S3Service : IS3Service
|
||||||
|
{
|
||||||
|
private readonly string BUCKET_NAME = "nn-attachments";
|
||||||
|
private AmazonS3Client S3Client { get; }
|
||||||
|
private HttpClient httpClient = new HttpClient();
|
||||||
|
|
||||||
|
public S3Service(IOptions<S3Options> s3Options)
|
||||||
|
{
|
||||||
|
var config = new AmazonS3Config
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
ServiceURL = Servers.S3Server.ToString(),
|
||||||
|
#else
|
||||||
|
ServiceURL = s3Options.Value.ServiceUrl,
|
||||||
|
AuthenticationRegion = s3Options.Value.Region,
|
||||||
|
#endif
|
||||||
|
ForcePathStyle = true,
|
||||||
|
SignatureMethod = SigningAlgorithm.HmacSHA256,
|
||||||
|
SignatureVersion = "4"
|
||||||
|
};
|
||||||
|
#if DEBUG
|
||||||
|
S3Client = new AmazonS3Client("S3RVER", "S3RVER", config);
|
||||||
|
#else
|
||||||
|
S3Client = new AmazonS3Client(s3Options.Value.AccessKeyId, s3Options.Value.SecretAccessKey, config);
|
||||||
|
#endif
|
||||||
|
AWSConfigsS3.UseSignatureVersion4 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteObjectAsync(string userId, string name)
|
||||||
|
{
|
||||||
|
var objectName = GetFullObjectName(userId, name);
|
||||||
|
if (objectName == null) throw new Exception("Invalid object name."); ;
|
||||||
|
|
||||||
|
var response = await S3Client.DeleteObjectAsync(BUCKET_NAME, objectName);
|
||||||
|
|
||||||
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode)))
|
||||||
|
throw new Exception("Could not delete object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDirectoryAsync(string userId)
|
||||||
|
{
|
||||||
|
var request = new ListObjectsV2Request
|
||||||
|
{
|
||||||
|
BucketName = BUCKET_NAME,
|
||||||
|
Prefix = userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = new ListObjectsV2Response();
|
||||||
|
var keys = new List<KeyVersion>();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
response = await S3Client.ListObjectsV2Async(request);
|
||||||
|
response.S3Objects.ForEach(obj => keys.Add(new KeyVersion
|
||||||
|
{
|
||||||
|
Key = obj.Key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
request.ContinuationToken = response.NextContinuationToken;
|
||||||
|
}
|
||||||
|
while (response.IsTruncated);
|
||||||
|
|
||||||
|
if (keys.Count <= 0) return;
|
||||||
|
|
||||||
|
var deleteObjectsResponse = await S3Client
|
||||||
|
.DeleteObjectsAsync(new DeleteObjectsRequest
|
||||||
|
{
|
||||||
|
BucketName = BUCKET_NAME,
|
||||||
|
Objects = keys,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!IsSuccessStatusCode((int)deleteObjectsResponse.HttpStatusCode))
|
||||||
|
throw new Exception("Could not delete directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long?> GetObjectSizeAsync(string userId, string name)
|
||||||
|
{
|
||||||
|
var url = this.GetPresignedURL(userId, name, HttpVerb.HEAD);
|
||||||
|
if (url == null) return null;
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||||
|
var response = await httpClient.SendAsync(request);
|
||||||
|
return response.Content.Headers.ContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string GetUploadObjectUrl(string userId, string name)
|
||||||
|
{
|
||||||
|
var url = this.GetPresignedURL(userId, name, HttpVerb.PUT);
|
||||||
|
if (url == null) return null;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDownloadObjectUrl(string userId, string name)
|
||||||
|
{
|
||||||
|
var url = this.GetPresignedURL(userId, name, HttpVerb.GET);
|
||||||
|
if (url == null) return null;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null)
|
||||||
|
{
|
||||||
|
var objectName = GetFullObjectName(userId, name);
|
||||||
|
if (userId == null || objectName == null) throw new Exception("Could not initiate multipart upload.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(uploadId))
|
||||||
|
{
|
||||||
|
var response = await S3Client.InitiateMultipartUploadAsync(BUCKET_NAME, objectName);
|
||||||
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to initiate multipart upload.");
|
||||||
|
|
||||||
|
uploadId = response.UploadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signedUrls = new string[parts];
|
||||||
|
for (var i = 0; i < parts; ++i)
|
||||||
|
{
|
||||||
|
signedUrls[i] = GetPresignedURLForUploadPart(objectName, uploadId, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MultipartUploadMeta
|
||||||
|
{
|
||||||
|
UploadId = uploadId,
|
||||||
|
Parts = signedUrls
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AbortMultipartUploadAsync(string userId, string name, string uploadId)
|
||||||
|
{
|
||||||
|
var objectName = GetFullObjectName(userId, name);
|
||||||
|
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
||||||
|
|
||||||
|
var response = await S3Client.AbortMultipartUploadAsync(BUCKET_NAME, objectName, uploadId);
|
||||||
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to abort multipart upload.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest)
|
||||||
|
{
|
||||||
|
var objectName = GetFullObjectName(userId, uploadRequest.Key);
|
||||||
|
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
||||||
|
|
||||||
|
uploadRequest.Key = objectName;
|
||||||
|
uploadRequest.BucketName = BUCKET_NAME;
|
||||||
|
var response = await S3Client.CompleteMultipartUploadAsync(uploadRequest);
|
||||||
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPresignedURL(string userId, string name, HttpVerb httpVerb)
|
||||||
|
{
|
||||||
|
var objectName = GetFullObjectName(userId, name);
|
||||||
|
if (userId == null || objectName == null) return null;
|
||||||
|
|
||||||
|
var request = new GetPreSignedUrlRequest
|
||||||
|
{
|
||||||
|
BucketName = BUCKET_NAME,
|
||||||
|
Expires = System.DateTime.Now.AddHours(1),
|
||||||
|
Verb = httpVerb,
|
||||||
|
Key = objectName,
|
||||||
|
#if DEBUG
|
||||||
|
Protocol = Protocol.HTTP,
|
||||||
|
#else
|
||||||
|
Protocol = Protocol.HTTPS,
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
return S3Client.GetPreSignedURL(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber)
|
||||||
|
{
|
||||||
|
return S3Client.GetPreSignedURL(new GetPreSignedUrlRequest
|
||||||
|
{
|
||||||
|
BucketName = BUCKET_NAME,
|
||||||
|
Expires = System.DateTime.Now.AddHours(1),
|
||||||
|
Verb = HttpVerb.PUT,
|
||||||
|
Key = objectName,
|
||||||
|
PartNumber = partNumber,
|
||||||
|
UploadId = uploadId,
|
||||||
|
#if DEBUG
|
||||||
|
Protocol = Protocol.HTTP,
|
||||||
|
#else
|
||||||
|
Protocol = Protocol.HTTPS,
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFullObjectName(string userId, string name)
|
||||||
|
{
|
||||||
|
if (userId == null || !Regex.IsMatch(name, "[0-9a-zA-Z!" + Regex.Escape("-") + "_.*'()]")) return null;
|
||||||
|
return $"{userId}/{name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsSuccessStatusCode(int statusCode)
|
||||||
|
{
|
||||||
|
return ((int)statusCode >= 200) && ((int)statusCode <= 299);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Notesnook.API.Models.Responses;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Extensions;
|
||||||
|
using Streetwriters.Common.Messages;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Services
|
||||||
|
{
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
private static readonly System.Security.Cryptography.RandomNumberGenerator Rng = System.Security.Cryptography.RandomNumberGenerator.Create();
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private IHttpContextAccessor HttpContextAccessor { get; }
|
||||||
|
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||||
|
private IS3Service S3Service { get; set; }
|
||||||
|
private readonly IUnitOfWork unit;
|
||||||
|
|
||||||
|
public UserService(IHttpContextAccessor accessor,
|
||||||
|
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor,
|
||||||
|
IUnitOfWork unitOfWork, IS3Service s3Service)
|
||||||
|
{
|
||||||
|
httpClient = new HttpClient();
|
||||||
|
|
||||||
|
Repositories = syncItemsRepositoryAccessor;
|
||||||
|
HttpContextAccessor = accessor;
|
||||||
|
unit = unitOfWork;
|
||||||
|
S3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateUserAsync()
|
||||||
|
{
|
||||||
|
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
|
||||||
|
if (!response.Success || (response.Errors != null && response.Errors.Length > 0))
|
||||||
|
{
|
||||||
|
await Slogger<UserService>.Error(nameof(CreateUserAsync), "Couldn't sign up.", JsonSerializer.Serialize(response));
|
||||||
|
if (response.Errors != null && response.Errors.Length > 0) throw new Exception(string.Join(" ", response.Errors));
|
||||||
|
else throw new Exception("Could not create a new account. Error code: " + response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Repositories.UsersSettings.InsertAsync(new UserSettings
|
||||||
|
{
|
||||||
|
UserId = response.UserId,
|
||||||
|
LastSynced = 0,
|
||||||
|
Salt = GetSalt()
|
||||||
|
});
|
||||||
|
|
||||||
|
await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.CreateSubscriptionTopic, new CreateSubscriptionMessage
|
||||||
|
{
|
||||||
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
|
Provider = SubscriptionProvider.STREETWRITERS,
|
||||||
|
Type = SubscriptionType.BASIC,
|
||||||
|
UserId = response.UserId,
|
||||||
|
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await Slogger<UserService>.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserResponse> GetUserAsync(bool repair = true)
|
||||||
|
{
|
||||||
|
UserResponse response = await httpClient.ForwardAsync<UserResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account", HttpMethod.Get);
|
||||||
|
if (!response.Success) return response;
|
||||||
|
|
||||||
|
SubscriptionResponse subscriptionResponse = await httpClient.ForwardAsync<SubscriptionResponse>(this.HttpContextAccessor, $"{Servers.SubscriptionServer}/subscriptions", HttpMethod.Get);
|
||||||
|
if (repair && subscriptionResponse.StatusCode == 404)
|
||||||
|
{
|
||||||
|
await Slogger<UserService>.Error(nameof(GetUserAsync), "Repairing user subscription.", JsonSerializer.Serialize(response));
|
||||||
|
// user was partially created. We should continue the process here.
|
||||||
|
await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.CreateSubscriptionTopic, new CreateSubscriptionMessage
|
||||||
|
{
|
||||||
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
|
Provider = SubscriptionProvider.STREETWRITERS,
|
||||||
|
Type = SubscriptionType.TRIAL,
|
||||||
|
UserId = response.UserId,
|
||||||
|
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
ExpiryTime = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds()
|
||||||
|
});
|
||||||
|
// just a dummy object
|
||||||
|
subscriptionResponse.Subscription = new Subscription
|
||||||
|
{
|
||||||
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
|
Provider = SubscriptionProvider.STREETWRITERS,
|
||||||
|
Type = SubscriptionType.TRIAL,
|
||||||
|
UserId = response.UserId,
|
||||||
|
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
ExpiryDate = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == response.UserId);
|
||||||
|
if (repair && userSettings == null)
|
||||||
|
{
|
||||||
|
await Slogger<UserService>.Error(nameof(GetUserAsync), "Repairing user settings.", JsonSerializer.Serialize(response));
|
||||||
|
userSettings = new UserSettings
|
||||||
|
{
|
||||||
|
UserId = response.UserId,
|
||||||
|
LastSynced = 0,
|
||||||
|
Salt = GetSalt()
|
||||||
|
};
|
||||||
|
await Repositories.UsersSettings.InsertAsync(userSettings);
|
||||||
|
}
|
||||||
|
response.AttachmentsKey = userSettings.AttachmentsKey;
|
||||||
|
response.Salt = userSettings.Salt;
|
||||||
|
response.Subscription = subscriptionResponse.Subscription;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key)
|
||||||
|
{
|
||||||
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
userSettings.AttachmentsKey = (EncryptedData)key;
|
||||||
|
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUserAsync(string userId, string jti)
|
||||||
|
{
|
||||||
|
var cc = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Repositories.Notes.DeleteByUserId(userId);
|
||||||
|
Repositories.Notebooks.DeleteByUserId(userId);
|
||||||
|
Repositories.Shortcuts.DeleteByUserId(userId);
|
||||||
|
Repositories.Contents.DeleteByUserId(userId);
|
||||||
|
Repositories.Settings.DeleteByUserId(userId);
|
||||||
|
Repositories.Attachments.DeleteByUserId(userId);
|
||||||
|
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
|
||||||
|
|
||||||
|
await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
|
||||||
|
{
|
||||||
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
|
||||||
|
await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
|
||||||
|
{
|
||||||
|
SendToAll = false,
|
||||||
|
OriginTokenId = jti,
|
||||||
|
UserId = userId,
|
||||||
|
Message = new Message
|
||||||
|
{
|
||||||
|
Type = "userDeleted",
|
||||||
|
Data = JsonSerializer.Serialize(new { reason = "accountDeleted" })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await S3Service.DeleteDirectoryAsync(userId);
|
||||||
|
|
||||||
|
return await unit.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
|
||||||
|
{
|
||||||
|
var cc = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Repositories.Notes.DeleteByUserId(userId);
|
||||||
|
Repositories.Notebooks.DeleteByUserId(userId);
|
||||||
|
Repositories.Shortcuts.DeleteByUserId(userId);
|
||||||
|
Repositories.Contents.DeleteByUserId(userId);
|
||||||
|
Repositories.Settings.DeleteByUserId(userId);
|
||||||
|
Repositories.Attachments.DeleteByUserId(userId);
|
||||||
|
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||||
|
if (!await unit.Commit()) return false;
|
||||||
|
|
||||||
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
|
||||||
|
|
||||||
|
userSettings.AttachmentsKey = null;
|
||||||
|
userSettings.VaultKey = null;
|
||||||
|
userSettings.LastSynced = 0;
|
||||||
|
|
||||||
|
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
|
||||||
|
|
||||||
|
if (removeAttachments)
|
||||||
|
await S3Service.DeleteDirectoryAsync(userId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSalt()
|
||||||
|
{
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
Rng.GetNonZeroBytes(salt);
|
||||||
|
return Convert.ToBase64String(salt).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using IdentityModel.AspNetCore.OAuth2Introspection;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
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.Connections;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MongoDB.Bson.Serialization;
|
||||||
|
using Notesnook.API.Accessors;
|
||||||
|
using Notesnook.API.Authorization;
|
||||||
|
using Notesnook.API.Extensions;
|
||||||
|
using Notesnook.API.Hubs;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Notesnook.API.Repositories;
|
||||||
|
using Notesnook.API.Services;
|
||||||
|
using Streetwriters.Common;
|
||||||
|
using Streetwriters.Common.Extensions;
|
||||||
|
using Streetwriters.Common.Messages;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data;
|
||||||
|
using Streetwriters.Data.DbContexts;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
|
namespace Notesnook.API
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var dbSettings = Configuration.GetSection("MongoDbSettings").Get<DbSettings>();
|
||||||
|
services.AddSingleton<IDbSettings>(dbSettings);
|
||||||
|
|
||||||
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
|
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
|
||||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||||
|
|
||||||
|
services.AddCors();
|
||||||
|
|
||||||
|
services.AddDistributedMemoryCache(delegate (MemoryDistributedCacheOptions cacheOptions)
|
||||||
|
{
|
||||||
|
cacheOptions.SizeLimit = 262144000L;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("Notesnook", policy =>
|
||||||
|
{
|
||||||
|
policy.AuthenticationSchemes.Add("introspection");
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.Requirements.Add(new NotesnookUserRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy("Sync", policy =>
|
||||||
|
{
|
||||||
|
policy.AuthenticationSchemes.Add("introspection");
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.Requirements.Add(new SyncRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy("Verified", policy =>
|
||||||
|
{
|
||||||
|
policy.AuthenticationSchemes.Add("introspection");
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.Requirements.Add(new EmailVerifiedRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy("Pro", policy =>
|
||||||
|
{
|
||||||
|
policy.AuthenticationSchemes.Add("introspection");
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.Requirements.Add(new ProUserRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy("BasicAdmin", policy =>
|
||||||
|
{
|
||||||
|
policy.AuthenticationSchemes.Add("BasicAuthentication");
|
||||||
|
policy.RequireClaim(ClaimTypes.Role, "Admin");
|
||||||
|
});
|
||||||
|
|
||||||
|
options.DefaultPolicy = options.GetPolicy("Notesnook");
|
||||||
|
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
|
||||||
|
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddOAuth2Introspection("introspection", options =>
|
||||||
|
{
|
||||||
|
options.Authority = Servers.IdentityServer.ToString();
|
||||||
|
options.ClientSecret = Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET");
|
||||||
|
options.ClientId = "notesnook";
|
||||||
|
options.DiscoveryPolicy.RequireHttps = false;
|
||||||
|
options.TokenRetriever = new Func<HttpRequest, string>(req =>
|
||||||
|
{
|
||||||
|
var fromHeader = TokenRetrieval.FromAuthorizationHeader();
|
||||||
|
var fromQuery = TokenRetrieval.FromQueryString(); //needed for signalr and ws/wss conections to be authed via jwt
|
||||||
|
return fromHeader(req) ?? fromQuery(req);
|
||||||
|
});
|
||||||
|
|
||||||
|
options.Events.OnTokenValidated = (context) =>
|
||||||
|
{
|
||||||
|
if (long.TryParse(context.Principal.FindFirst("exp")?.Value, out long expiryTime))
|
||||||
|
{
|
||||||
|
context.Properties.ExpiresUtc = DateTimeOffset.FromUnixTimeSeconds(expiryTime);
|
||||||
|
}
|
||||||
|
context.Properties.AllowRefresh = true;
|
||||||
|
context.Properties.IsPersistent = true;
|
||||||
|
context.HttpContext.User = context.Principal;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
options.SaveToken = true;
|
||||||
|
options.EnableCaching = true;
|
||||||
|
options.CacheDuration = TimeSpan.FromMinutes(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
|
||||||
|
{
|
||||||
|
BsonClassMap.RegisterClassMap<UserSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BsonClassMap.IsClassMapRegistered(typeof(EncryptedData)))
|
||||||
|
{
|
||||||
|
BsonClassMap.RegisterClassMap<EncryptedData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
|
||||||
|
{
|
||||||
|
BsonClassMap.RegisterClassMap<CallToAction>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BsonClassMap.IsClassMapRegistered(typeof(Announcement)))
|
||||||
|
{
|
||||||
|
BsonClassMap.RegisterClassMap<Announcement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddScoped<IDbContext, MongoDbContext>();
|
||||||
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
services.AddScoped(typeof(Repository<>));
|
||||||
|
services.AddScoped(typeof(SyncItemsRepository<>));
|
||||||
|
|
||||||
|
services.TryAddTransient<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||||
|
services.TryAddTransient<IUserService, UserService>();
|
||||||
|
services.TryAddTransient<IS3Service, S3Service>();
|
||||||
|
|
||||||
|
services.AddControllers();
|
||||||
|
|
||||||
|
services.AddHealthChecks().AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
|
||||||
|
services.AddSignalR((hub) =>
|
||||||
|
{
|
||||||
|
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
|
||||||
|
hub.EnableDetailedErrors = true;
|
||||||
|
}).AddMessagePackProtocol();
|
||||||
|
|
||||||
|
services.AddResponseCompression(options =>
|
||||||
|
{
|
||||||
|
options.EnableForHttps = true;
|
||||||
|
options.Providers.Add<BrotliCompressionProvider>();
|
||||||
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.Configure<BrotliCompressionProviderOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Level = CompressionLevel.Fastest;
|
||||||
|
});
|
||||||
|
services.Configure<GzipCompressionProviderOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Level = CompressionLevel.Fastest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
app.UseCors("notesnook");
|
||||||
|
|
||||||
|
app.UseWamp(WampServers.NotesnookServer, (realm, server) =>
|
||||||
|
{
|
||||||
|
IUserService service = app.GetScopedService<IUserService>();
|
||||||
|
realm.Subscribe<DeleteUserMessage>(server.Topics.DeleteUserTopic, async (ev) =>
|
||||||
|
{
|
||||||
|
await service.DeleteUserAsync(ev.UserId, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapHealthChecks("/health");
|
||||||
|
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
|
||||||
|
{
|
||||||
|
options.CloseOnAuthenticationExpiration = false;
|
||||||
|
options.Transports = HttpTransportType.WebSockets;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MongoDbSettings": {
|
||||||
|
"ConnectionString": "mongodb://localhost:27017/notesnook",
|
||||||
|
"DatabaseName": "notesnook"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notesnook.API", "Notesnook.API\Notesnook.API.csproj", "{05F79941-F8EF-4C56-A763-CDDFCA6645A3}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Common", "Streetwriters.Common\Streetwriters.Common.csproj", "{0606F6B6-118F-42C0-A1E2-EBEF406F7DD9}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Data", "Streetwriters.Data\Streetwriters.Data.csproj", "{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{05F79941-F8EF-4C56-A763-CDDFCA6645A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{05F79941-F8EF-4C56-A763-CDDFCA6645A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{05F79941-F8EF-4C56-A763-CDDFCA6645A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{05F79941-F8EF-4C56-A763-CDDFCA6645A3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0606F6B6-118F-42C0-A1E2-EBEF406F7DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0606F6B6-118F-42C0-A1E2-EBEF406F7DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0606F6B6-118F-42C0-A1E2-EBEF406F7DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0606F6B6-118F-42C0-A1E2-EBEF406F7DD9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
|
||||||
|
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
|
||||||
|
{
|
||||||
|
public JsonInterfaceConverterAttribute(Type converterType)
|
||||||
|
: base(converterType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common
|
||||||
|
{
|
||||||
|
public class Clients
|
||||||
|
{
|
||||||
|
private static IClient Notesnook = new Client
|
||||||
|
{
|
||||||
|
Id = "notesnook",
|
||||||
|
Name = "Notesnook",
|
||||||
|
ProductIds = new string[]
|
||||||
|
{
|
||||||
|
"com.streetwriters.notesnook",
|
||||||
|
"org.streetwriters.notesnook",
|
||||||
|
"com.streetwriters.notesnook.sub.mo",
|
||||||
|
"com.streetwriters.notesnook.sub.yr",
|
||||||
|
"com.streetwriters.notesnook.sub.mo.15",
|
||||||
|
"com.streetwriters.notesnook.sub.yr.15",
|
||||||
|
"com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||||
|
"com.streetwriters.notesnook.sub.mo.trialoffer",
|
||||||
|
"com.streetwriters.notesnook.sub.mo.tier1",
|
||||||
|
"com.streetwriters.notesnook.sub.yr.tier1",
|
||||||
|
"com.streetwriters.notesnook.sub.mo.tier2",
|
||||||
|
"com.streetwriters.notesnook.sub.yr.tier2",
|
||||||
|
"com.streetwriters.notesnook.sub.mo.tier3",
|
||||||
|
"com.streetwriters.notesnook.sub.yr.tier3",
|
||||||
|
"9822", // dev
|
||||||
|
"648884", // monthly tier 1
|
||||||
|
"658759", // yearly tier 1
|
||||||
|
"763942", // monthly tier 2
|
||||||
|
"763945", // yearly tier 2
|
||||||
|
"763943", // monthly tier 3
|
||||||
|
"763944", // yearly tier 3
|
||||||
|
},
|
||||||
|
SenderEmail = "support@notesnook.com",
|
||||||
|
SenderName = "Notesnook",
|
||||||
|
Type = ApplicationType.NOTESNOOK,
|
||||||
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
|
WelcomeEmailTemplateId = "d-87768b3ee17d41fdbe4bcf0eb2583682"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Dictionary<string, IClient> ClientsMap = new Dictionary<string, IClient>
|
||||||
|
{
|
||||||
|
{ "notesnook", Notesnook }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IClient FindClientById(string id)
|
||||||
|
{
|
||||||
|
if (!IsValidClient(id)) return null;
|
||||||
|
return ClientsMap[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IClient FindClientByAppId(ApplicationType appId)
|
||||||
|
{
|
||||||
|
switch (appId)
|
||||||
|
{
|
||||||
|
case ApplicationType.NOTESNOOK:
|
||||||
|
return ClientsMap["notesnook"];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IClient FindClientByProductId(string productId)
|
||||||
|
{
|
||||||
|
foreach (var client in ClientsMap)
|
||||||
|
{
|
||||||
|
if (client.Value.ProductIds.Contains(productId)) return client.Value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidClient(string id)
|
||||||
|
{
|
||||||
|
return ClientsMap.ContainsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SubscriptionProvider? PlatformToSubscriptionProvider(string platform)
|
||||||
|
{
|
||||||
|
return platform switch
|
||||||
|
{
|
||||||
|
"ios" => SubscriptionProvider.APPLE,
|
||||||
|
"android" => SubscriptionProvider.GOOGLE,
|
||||||
|
"web" => SubscriptionProvider.PADDLE,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts simple interface into an object (assumes that there is only one class of TInterface)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInterface">Interface type</typeparam>
|
||||||
|
/// <typeparam name="TClass">Class type</typeparam>
|
||||||
|
public class InterfaceConverter<TInterface, TClass> : JsonConverter<TInterface> where TClass : TInterface
|
||||||
|
{
|
||||||
|
public override TInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<TClass>(ref reader, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
JsonSerializer.Serialize(writer, null, options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var type = value.GetType();
|
||||||
|
JsonSerializer.Serialize(writer, value, type, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Streetwriters.Common.Enums
|
||||||
|
{
|
||||||
|
public enum ApplicationType
|
||||||
|
{
|
||||||
|
NOTESNOOK = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Streetwriters.Common.Enums
|
||||||
|
{
|
||||||
|
public class MFAMethods
|
||||||
|
{
|
||||||
|
public static string Email => "email";
|
||||||
|
public static string SMS => "sms";
|
||||||
|
public static string App => "app";
|
||||||
|
public static string RecoveryCode => "recoveryCode";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Streetwriters.Common.Enums
|
||||||
|
{
|
||||||
|
public enum SubscriptionProvider
|
||||||
|
{
|
||||||
|
STREETWRITERS = 0,
|
||||||
|
APPLE = 1,
|
||||||
|
GOOGLE = 2,
|
||||||
|
PADDLE = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Streetwriters.Common.Enums
|
||||||
|
{
|
||||||
|
public enum SubscriptionType
|
||||||
|
{
|
||||||
|
BASIC = 0,
|
||||||
|
TRIAL = 1,
|
||||||
|
BETA = 2,
|
||||||
|
PREMIUM = 5,
|
||||||
|
PREMIUM_EXPIRED = 6,
|
||||||
|
PREMIUM_CANCELED = 7
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using WampSharp.AspNetCore.WebSockets.Server;
|
||||||
|
using WampSharp.Binding;
|
||||||
|
using WampSharp.V2;
|
||||||
|
using WampSharp.V2.Realm;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Extensions
|
||||||
|
{
|
||||||
|
public static class AppBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseWamp<T>(this IApplicationBuilder app, WampServer<T> server, Action<IWampHostedRealm, WampServer<T>> action) where T : new()
|
||||||
|
{
|
||||||
|
WampHost host = new WampHost();
|
||||||
|
|
||||||
|
app.Map(server.Endpoint, builder =>
|
||||||
|
{
|
||||||
|
builder.UseWebSockets();
|
||||||
|
host.RegisterTransport(new AspNetCoreWebSocketTransport(builder),
|
||||||
|
new JTokenJsonBinding(),
|
||||||
|
new JTokenMsgpackBinding());
|
||||||
|
});
|
||||||
|
|
||||||
|
host.Open();
|
||||||
|
|
||||||
|
action.Invoke(host.RealmContainer.GetRealmByName(server.Realm), server);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T GetService<T>(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.ApplicationServices.GetRequiredService<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T GetScopedService<T>(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
using (var scope = app.ApplicationServices.CreateScope())
|
||||||
|
{
|
||||||
|
return scope.ServiceProvider.GetRequiredService<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Extensions
|
||||||
|
{
|
||||||
|
public static class HttpClientExtensions
|
||||||
|
{
|
||||||
|
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary headers, HttpMethod method, HttpContent content = null) where T : IResponse, new()
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(method, url);
|
||||||
|
|
||||||
|
if (method != HttpMethod.Get && method != HttpMethod.Delete)
|
||||||
|
{
|
||||||
|
request.Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
if (header.Key == "Content-Type" || header.Key == "Content-Length")
|
||||||
|
{
|
||||||
|
if (request.Content != null)
|
||||||
|
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await httpClient.SendAsync(request);
|
||||||
|
if (response.Content.Headers.ContentLength > 0)
|
||||||
|
{
|
||||||
|
var res = await response.Content.ReadFromJsonAsync<T>();
|
||||||
|
res.Success = response.IsSuccessStatusCode;
|
||||||
|
res.StatusCode = (int)response.StatusCode;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
|
||||||
|
{
|
||||||
|
var httpContext = accessor.HttpContext;
|
||||||
|
var content = new StreamContent(httpContext.Request.BodyReader.AsStream());
|
||||||
|
return httpClient.SendRequestAsync<T>(url, httpContext.Request.Headers, method, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Microsoft.Extensions.DependencyInjection.CorsServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static class ServiceCollectionServiceExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddCors(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("notesnook", (b) =>
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
b.AllowAnyOrigin();
|
||||||
|
#else
|
||||||
|
b.WithOrigins("http://localhost:3000", "http://192.168.10.29:3000", "https://app.notesnook.com", "https://beta.notesnook.com", "https://budi.streetwriters.co", "http://localhost:9876");
|
||||||
|
#endif
|
||||||
|
b.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace System
|
||||||
|
{
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string ToSha256(this string rawData, int maxLength = 12)
|
||||||
|
{
|
||||||
|
// Create a SHA256
|
||||||
|
using (SHA256 sha256Hash = SHA256.Create())
|
||||||
|
{
|
||||||
|
// ComputeHash - returns byte array
|
||||||
|
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
|
||||||
|
return ToHex(bytes, 0, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] CompressBrotli(this string input)
|
||||||
|
{
|
||||||
|
var raw = Encoding.Default.GetBytes(input);
|
||||||
|
using (MemoryStream memory = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (BrotliStream brotli = new BrotliStream(memory, CompressionLevel.Optimal))
|
||||||
|
{
|
||||||
|
brotli.Write(raw, 0, raw.Length);
|
||||||
|
}
|
||||||
|
return memory.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecompressBrotli(this byte[] compressed)
|
||||||
|
{
|
||||||
|
using (BrotliStream stream = new BrotliStream(new MemoryStream(compressed), CompressionMode.Decompress))
|
||||||
|
{
|
||||||
|
const int size = 4096;
|
||||||
|
byte[] buffer = new byte[size];
|
||||||
|
using (MemoryStream memory = new MemoryStream())
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
count = stream.Read(buffer, 0, size);
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
memory.Write(buffer, 0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (count > 0);
|
||||||
|
return Encoding.Default.GetString(memory.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToHex(byte[] bytes, int startIndex, int length)
|
||||||
|
{
|
||||||
|
char[] c = new char[length * 2];
|
||||||
|
byte b;
|
||||||
|
for (int bx = startIndex, cx = startIndex; bx < length; ++bx, ++cx)
|
||||||
|
{
|
||||||
|
b = ((byte)(bytes[bx] >> 4));
|
||||||
|
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
||||||
|
|
||||||
|
b = ((byte)(bytes[bx] & 0x0F));
|
||||||
|
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
||||||
|
}
|
||||||
|
return new string(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using WampSharp.AspNetCore.WebSockets.Server;
|
||||||
|
using WampSharp.Binding;
|
||||||
|
using WampSharp.V2;
|
||||||
|
using WampSharp.V2.Realm;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Extensions
|
||||||
|
{
|
||||||
|
public static class WampRealmExtensions
|
||||||
|
{
|
||||||
|
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, Action<T> onNext)
|
||||||
|
{
|
||||||
|
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(onNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
|
||||||
|
{
|
||||||
|
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Reactive.Subjects;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Streetwriters.Common.Messages;
|
||||||
|
using WampSharp.V2;
|
||||||
|
using WampSharp.V2.Client;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Helpers
|
||||||
|
{
|
||||||
|
public class WampHelper
|
||||||
|
{
|
||||||
|
public static async Task<IWampRealmProxy> OpenWampChannelAsync<T>(string server, string realmName)
|
||||||
|
{
|
||||||
|
DefaultWampChannelFactory channelFactory = new DefaultWampChannelFactory();
|
||||||
|
|
||||||
|
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
|
||||||
|
|
||||||
|
await channel.Open();
|
||||||
|
|
||||||
|
return channel.RealmProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
|
||||||
|
{
|
||||||
|
var subject = realm.Services.GetSubject<T>(topicName);
|
||||||
|
subject.OnNext(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
public interface IClient
|
||||||
|
{
|
||||||
|
string Id { get; set; }
|
||||||
|
string Name { get; set; }
|
||||||
|
string[] ProductIds { get; set; }
|
||||||
|
ApplicationType Type { get; set; }
|
||||||
|
ApplicationType AppId { get; set; }
|
||||||
|
string SenderEmail { get; set; }
|
||||||
|
string SenderName { get; set; }
|
||||||
|
string WelcomeEmailTemplateId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
public interface IDocument
|
||||||
|
{
|
||||||
|
string Id
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
public interface IMessageHandler<T>
|
||||||
|
{
|
||||||
|
Task Process(T message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
public interface IOffer : IDocument
|
||||||
|
{
|
||||||
|
ApplicationType AppId { get; set; }
|
||||||
|
string PromoCode { get; set; }
|
||||||
|
PromoCode[] Codes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
public interface IResponse
|
||||||
|
{
|
||||||
|
bool Success { get; set; }
|
||||||
|
int StatusCode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Streetwriters.Common.Attributes;
|
||||||
|
using Streetwriters.Common.Converters;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
|
[JsonInterfaceConverter(typeof(InterfaceConverter<ISubscription, Subscription>))]
|
||||||
|
public interface ISubscription : IDocument
|
||||||
|
{
|
||||||
|
string UserId { get; set; }
|
||||||
|
ApplicationType AppId { get; set; }
|
||||||
|
SubscriptionProvider Provider { get; set; }
|
||||||
|
long StartDate { get; set; }
|
||||||
|
long ExpiryDate { get; set; }
|
||||||
|
SubscriptionType Type { get; set; }
|
||||||
|
string OrderId { get; set; }
|
||||||
|
string SubscriptionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common
|
||||||
|
{
|
||||||
|
public class Slogger<T>
|
||||||
|
{
|
||||||
|
public static Task Info(string scope, params string[] messages)
|
||||||
|
{
|
||||||
|
return Write(Format("info", scope, messages));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task Error(string scope, params string[] messages)
|
||||||
|
{
|
||||||
|
return Write(Format("error", scope, messages));
|
||||||
|
}
|
||||||
|
private static string Format(string level, string scope, params string[] messages)
|
||||||
|
{
|
||||||
|
var date = DateTime.UtcNow.ToString("MM-dd-yyyy HH:mm:ss");
|
||||||
|
var messageText = string.Join(" ", messages);
|
||||||
|
return $"[{date}] | {level} | <{scope}> {messageText}";
|
||||||
|
}
|
||||||
|
private static Task Write(string line)
|
||||||
|
{
|
||||||
|
var logDirectory = Path.GetFullPath("./logs");
|
||||||
|
if (!Directory.Exists(logDirectory))
|
||||||
|
Directory.CreateDirectory(logDirectory);
|
||||||
|
var path = Path.Join(logDirectory, typeof(T).FullName + "-" + DateTime.UtcNow.ToString("MM-dd-yyyy") + ".log");
|
||||||
|
return File.AppendAllLinesAsync(path, new string[1] { line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Messages
|
||||||
|
{
|
||||||
|
public class CreateSubscriptionMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("provider")]
|
||||||
|
public SubscriptionProvider Provider { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("appId")]
|
||||||
|
public ApplicationType AppId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public SubscriptionType Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("start")]
|
||||||
|
public long StartTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expiry")]
|
||||||
|
public long ExpiryTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("orderId")]
|
||||||
|
public string OrderId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("updateURL")]
|
||||||
|
public string UpdateURL { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("cancelURL")]
|
||||||
|
public string CancelURL { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("subscriptionId")]
|
||||||
|
public string SubscriptionId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("productId")]
|
||||||
|
public string ProductId { get; set; }
|
||||||
|
public bool Extend { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Messages
|
||||||
|
{
|
||||||
|
public class DeleteSubscriptionMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("appId")]
|
||||||
|
public ApplicationType AppId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Messages
|
||||||
|
{
|
||||||
|
public class DeleteUserMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Messages
|
||||||
|
{
|
||||||
|
public class Message
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public string Data { get; set; }
|
||||||
|
}
|
||||||
|
public class SendSSEMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("sendToAll")]
|
||||||
|
public bool SendToAll { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public Message Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("originTokenId")]
|
||||||
|
public string OriginTokenId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class Client : IClient
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string[] ProductIds { get; set; }
|
||||||
|
public ApplicationType Type { get; set; }
|
||||||
|
public ApplicationType AppId { get; set; }
|
||||||
|
public string SenderEmail { get; set; }
|
||||||
|
public string SenderName { get; set; }
|
||||||
|
public string WelcomeEmailTemplateId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class SubscriptionResponse : Response
|
||||||
|
{
|
||||||
|
[JsonPropertyName("subscription")]
|
||||||
|
public ISubscription Subscription { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class MFAConfig
|
||||||
|
{
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public string PrimaryMethod { get; set; }
|
||||||
|
public string SecondaryMethod { get; set; }
|
||||||
|
public int RemainingValidCodes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// Streetwriters.Common.Models.Offer
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("subscriptions", "offers")]
|
||||||
|
public class Offer : IOffer
|
||||||
|
{
|
||||||
|
public Offer()
|
||||||
|
{
|
||||||
|
Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("appId")]
|
||||||
|
public ApplicationType AppId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("promoCode")]
|
||||||
|
public string PromoCode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("codes")]
|
||||||
|
public PromoCode[] Codes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// Streetwriters.Common.Models.Offer
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class PromoCode
|
||||||
|
{
|
||||||
|
[JsonPropertyName("provider")]
|
||||||
|
public SubscriptionProvider Provider { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public string Code { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class Response : IResponse
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
|
||||||
|
|
||||||
|
using AspNetCore.Identity.Mongo.Model;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("identity", "roles")]
|
||||||
|
public class Role : MongoRole
|
||||||
|
{
|
||||||
|
// [DataMember(Name = "email")]
|
||||||
|
// [BsonElement("email")]
|
||||||
|
// public string Email
|
||||||
|
// {
|
||||||
|
// get; set;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [DataMember(Name = "isEmailConfirmed")]
|
||||||
|
// [BsonElement("isEmailConfirmed")]
|
||||||
|
// public bool IsEmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
// [DataMember(Name = "username")]
|
||||||
|
// [BsonElement("username")]
|
||||||
|
// public string Username
|
||||||
|
// {
|
||||||
|
// get; set;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [BsonId]
|
||||||
|
// [BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
// public string Id
|
||||||
|
// {
|
||||||
|
// get; set;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [IgnoreDataMember]
|
||||||
|
// [BsonElement("passwordHash")]
|
||||||
|
// public string PasswordHash
|
||||||
|
// {
|
||||||
|
// get; set;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [DataMember(Name = "salt")]
|
||||||
|
// public string Salt
|
||||||
|
// {
|
||||||
|
// get; set;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
public class Picture
|
||||||
|
{
|
||||||
|
[DataMember(Name = "thumbnail")]
|
||||||
|
public string Thumbnail
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
[DataMember(Name = "full")]
|
||||||
|
public string Full
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Streetwriters
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
[DataMember(Name = "fullName")]
|
||||||
|
public string FullName
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "biography")]
|
||||||
|
[StringLength(240)]
|
||||||
|
public string Biography
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "favoriteWords")]
|
||||||
|
public string FavoriteWords
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "profilePicture")]
|
||||||
|
public Picture ProfilePicture
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "followers")]
|
||||||
|
public string[] Followers
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "following")]
|
||||||
|
public string[] Following
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "website")]
|
||||||
|
[Url]
|
||||||
|
public string Website
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "instagram")]
|
||||||
|
public string Instagram
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "twitter")]
|
||||||
|
public string Twitter
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Common.Enums;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("subscriptions", "subscriptions")]
|
||||||
|
public class Subscription : ISubscription
|
||||||
|
{
|
||||||
|
public Subscription()
|
||||||
|
{
|
||||||
|
Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string OrderId { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public string SubscriptionId { get; set; }
|
||||||
|
|
||||||
|
[BsonRepresentation(BsonType.Int32)]
|
||||||
|
[JsonPropertyName("appId")]
|
||||||
|
public ApplicationType AppId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("start")]
|
||||||
|
public long StartDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expiry")]
|
||||||
|
public long ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
[BsonRepresentation(BsonType.Int32)]
|
||||||
|
[JsonPropertyName("provider")]
|
||||||
|
public SubscriptionProvider Provider { get; set; }
|
||||||
|
|
||||||
|
[BsonRepresentation(BsonType.Int32)]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public SubscriptionType Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("cancelURL")]
|
||||||
|
public string CancelURL { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("updateURL")]
|
||||||
|
public string UpdateURL { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("productId")]
|
||||||
|
public string ProductId { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public int TrialExtensionCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
using AspNetCore.Identity.Mongo.Model;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
[BsonCollection("identity", "users")]
|
||||||
|
public class User : MongoUser
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Models
|
||||||
|
{
|
||||||
|
public class UserModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("phoneNumber")]
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isEmailConfirmed")]
|
||||||
|
public bool IsEmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mfa")]
|
||||||
|
public MFAConfig MFA { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common
|
||||||
|
{
|
||||||
|
public class Server
|
||||||
|
{
|
||||||
|
public string Port { get; set; }
|
||||||
|
public bool IsSecure { get; set; }
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
public string Domain { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var url = "";
|
||||||
|
url += IsSecure ? "https" : "http";
|
||||||
|
url += $"://{Hostname}";
|
||||||
|
url += IsSecure ? "" : $":{Port}";
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WS()
|
||||||
|
{
|
||||||
|
var url = "";
|
||||||
|
url += IsSecure ? "ws" : "ws";
|
||||||
|
url += $"://{Hostname}";
|
||||||
|
url += $":{Port}";
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Servers
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
public static string GetLocalIPv4(NetworkInterfaceType _type)
|
||||||
|
{
|
||||||
|
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||||
|
string output = "";
|
||||||
|
foreach (NetworkInterface item in interfaces)
|
||||||
|
{
|
||||||
|
if (item.NetworkInterfaceType == _type && item.OperationalStatus == OperationalStatus.Up)
|
||||||
|
{
|
||||||
|
foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses)
|
||||||
|
{
|
||||||
|
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||||
|
{
|
||||||
|
output = ip.Address.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
public readonly static string HOST = GetLocalIPv4(NetworkInterfaceType.Ethernet);
|
||||||
|
public static Server S3Server { get; } = new()
|
||||||
|
{
|
||||||
|
Port = "4568",
|
||||||
|
Hostname = HOST,
|
||||||
|
IsSecure = false,
|
||||||
|
Domain = HOST
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
private readonly static string HOST = "localhost";
|
||||||
|
public readonly static X509Certificate2 OriginSSLCertificate = X509Certificate2.CreateFromPemFile("/home/notesnook/.ssl/CF_Origin_Streetwriters.pem", "/home/notesnook/.ssl/CF_Origin_Streetwriters.key");
|
||||||
|
#endif
|
||||||
|
public static Server NotesnookAPI { get; } = new()
|
||||||
|
{
|
||||||
|
Domain = "api.notesnook.com",
|
||||||
|
Port = "5264",
|
||||||
|
#if DEBUG
|
||||||
|
IsSecure = false,
|
||||||
|
Hostname = HOST,
|
||||||
|
#else
|
||||||
|
IsSecure = true,
|
||||||
|
Hostname = "10.0.0.5",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Server MessengerServer { get; } = new()
|
||||||
|
{
|
||||||
|
Domain = "events.streetwriters.co",
|
||||||
|
Port = "7264",
|
||||||
|
#if DEBUG
|
||||||
|
IsSecure = false,
|
||||||
|
Hostname = HOST,
|
||||||
|
#else
|
||||||
|
IsSecure = true,
|
||||||
|
Hostname = "10.0.0.6",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Server IdentityServer { get; } = new()
|
||||||
|
{
|
||||||
|
Domain = "auth.streetwriters.co",
|
||||||
|
IsSecure = false,
|
||||||
|
Port = "8264",
|
||||||
|
#if DEBUG
|
||||||
|
Hostname = HOST,
|
||||||
|
#else
|
||||||
|
Hostname = "10.0.0.4",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Server SubscriptionServer { get; } = new()
|
||||||
|
{
|
||||||
|
Domain = "subscriptions.streetwriters.co",
|
||||||
|
IsSecure = false,
|
||||||
|
Port = "9264",
|
||||||
|
#if DEBUG
|
||||||
|
Hostname = HOST,
|
||||||
|
#else
|
||||||
|
Hostname = "10.0.0.4",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
public static Server PaymentsServer { get; } = new()
|
||||||
|
{
|
||||||
|
Domain = "payments.streetwriters.co",
|
||||||
|
IsSecure = false,
|
||||||
|
Port = "6264",
|
||||||
|
#if DEBUG
|
||||||
|
Hostname = HOST,
|
||||||
|
#else
|
||||||
|
Hostname = "10.0.0.4",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
|
||||||
|
<PackageReference Include="WampSharp.Default" Version="20.1.1" />
|
||||||
|
<PackageReference Include="WampSharp.AspNetCore.WebSockets.Server" Version="20.1.1" />
|
||||||
|
<PackageReference Include="WampSharp.NewtonsoftMsgpack" Version="20.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||||
|
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Streetwriters.Data\Streetwriters.Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Streetwriters.Common.Helpers;
|
||||||
|
using WampSharp.V2.Client;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common
|
||||||
|
{
|
||||||
|
public class WampServer<T> where T : new()
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, IWampRealmProxy> Channels = new();
|
||||||
|
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
public string Address { get; set; }
|
||||||
|
public T Topics { get; set; } = new T();
|
||||||
|
public string Realm { get; set; }
|
||||||
|
|
||||||
|
public async Task PublishMessageAsync<V>(string topic, V message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IWampRealmProxy channel;
|
||||||
|
if (Channels.ContainsKey(topic))
|
||||||
|
channel = Channels[topic];
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channel = await WampHelper.OpenWampChannelAsync<V>(this.Address, this.Realm);
|
||||||
|
Channels.TryAdd(topic, channel);
|
||||||
|
}
|
||||||
|
if (!channel.Monitor.IsConnected)
|
||||||
|
{
|
||||||
|
Channels.TryRemove(topic, out IWampRealmProxy value);
|
||||||
|
await PublishMessageAsync<V>(topic, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WampHelper.PublishMessage<V>(channel, topic, message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Slogger<WampServer<T>>.Error(nameof(PublishMessageAsync), ex.ToString());
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WampServers
|
||||||
|
{
|
||||||
|
public static WampServer<MessengerServerTopics> MessengerServer { get; } = new WampServer<MessengerServerTopics>
|
||||||
|
{
|
||||||
|
Endpoint = "/wamp",
|
||||||
|
Address = $"{Servers.MessengerServer.WS()}/wamp",
|
||||||
|
Realm = "messages",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static WampServer<SubscriptionServerTopics> SubscriptionServer { get; } = new WampServer<SubscriptionServerTopics>
|
||||||
|
{
|
||||||
|
Endpoint = "/wamp",
|
||||||
|
Address = $"{Servers.SubscriptionServer.WS()}/wamp",
|
||||||
|
Realm = "messages",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static WampServer<IdentityServerTopics> IdentityServer { get; } = new WampServer<IdentityServerTopics>
|
||||||
|
{
|
||||||
|
Endpoint = "/wamp",
|
||||||
|
Address = $"{Servers.IdentityServer.WS()}/wamp",
|
||||||
|
Realm = "messages",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static WampServer<NotesnookServerTopics> NotesnookServer { get; } = new WampServer<NotesnookServerTopics>
|
||||||
|
{
|
||||||
|
Endpoint = "/wamp",
|
||||||
|
Address = $"{Servers.NotesnookAPI.WS()}/wamp",
|
||||||
|
Realm = "messages",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessengerServerTopics
|
||||||
|
{
|
||||||
|
public string SendSSETopic => "com.streetwriters.sse.send";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscriptionServerTopics
|
||||||
|
{
|
||||||
|
public string CreateSubscriptionTopic => "com.streetwriters.subscriptions.create";
|
||||||
|
public string DeleteSubscriptionTopic => "com.streetwriters.subscriptions.delete";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IdentityServerTopics
|
||||||
|
{
|
||||||
|
public string CreateSubscriptionTopic => "com.streetwriters.subscriptions.create";
|
||||||
|
public string DeleteSubscriptionTopic => "com.streetwriters.subscriptions.delete";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotesnookServerTopics
|
||||||
|
{
|
||||||
|
public string DeleteUserTopic => "com.streetwriters.notesnook.user.delete";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class BsonCollectionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string CollectionName { get; }
|
||||||
|
public string DatabaseName { get; }
|
||||||
|
|
||||||
|
public BsonCollectionAttribute(string databaseName, string collectionName)
|
||||||
|
{
|
||||||
|
CollectionName = collectionName;
|
||||||
|
DatabaseName = databaseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data.DbContexts
|
||||||
|
{
|
||||||
|
public class MongoDbContext : IDbContext
|
||||||
|
{
|
||||||
|
private IMongoDatabase Database { get; set; }
|
||||||
|
private MongoClient MongoClient { get; set; }
|
||||||
|
private readonly List<Func<IClientSessionHandle, CancellationToken, Task>> _commands;
|
||||||
|
private IDbSettings DbSettings { get; set; }
|
||||||
|
public MongoDbContext(IDbSettings dbSettings)
|
||||||
|
{
|
||||||
|
DbSettings = dbSettings;
|
||||||
|
Configure();
|
||||||
|
// Every command will be stored and it'll be processed at SaveChanges
|
||||||
|
_commands = new List<Func<IClientSessionHandle, CancellationToken, Task>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveChanges()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var count = _commands.Count;
|
||||||
|
|
||||||
|
using (IClientSessionHandle session = await MongoClient.StartSessionAsync())
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
await Task.WhenAll(_commands.Select(c => c(session, default(CancellationToken))));
|
||||||
|
#else
|
||||||
|
await session.WithTransactionAsync(async (handle, token) =>
|
||||||
|
{
|
||||||
|
await Task.WhenAll(_commands.Select(c => c(handle, token)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// TODO use Slogger here.
|
||||||
|
await Console.Error.WriteLineAsync(ex.ToString());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Configure()
|
||||||
|
{
|
||||||
|
if (MongoClient != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var settings = MongoClientSettings.FromConnectionString(DbSettings.ConnectionString);
|
||||||
|
settings.MaxConnectionPoolSize = 5000;
|
||||||
|
settings.MinConnectionPoolSize = 300;
|
||||||
|
MongoClient = new MongoClient(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMongoCollection<T> GetCollection<T>(string databaseName, string collectionName)
|
||||||
|
{
|
||||||
|
return MongoClient.GetDatabase(databaseName).GetCollection<T>(collectionName, new MongoCollectionSettings()
|
||||||
|
{
|
||||||
|
AssignIdOnInsert = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCommand(Func<IClientSessionHandle, CancellationToken, Task> func)
|
||||||
|
{
|
||||||
|
_commands.Add(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DropDatabaseAsync()
|
||||||
|
{
|
||||||
|
return MongoClient.DropDatabaseAsync(DbSettings.DatabaseName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data
|
||||||
|
{
|
||||||
|
public class DbSettings : IDbSettings
|
||||||
|
{
|
||||||
|
public string DatabaseName { get; set; }
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data.Interfaces
|
||||||
|
{
|
||||||
|
public interface IDbContext : IDisposable
|
||||||
|
{
|
||||||
|
void AddCommand(Func<IClientSessionHandle, CancellationToken, Task> func);
|
||||||
|
Task<int> SaveChanges();
|
||||||
|
IMongoCollection<T> GetCollection<T>(string databaseName, string collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Streetwriters.Data.Interfaces
|
||||||
|
{
|
||||||
|
public interface IDbSettings
|
||||||
|
{
|
||||||
|
string DatabaseName { get; set; }
|
||||||
|
string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUnitOfWork : IDisposable
|
||||||
|
{
|
||||||
|
Task<bool> Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data.Repositories
|
||||||
|
{
|
||||||
|
public class Repository<TEntity> where TEntity : class
|
||||||
|
{
|
||||||
|
protected readonly IDbContext dbContext;
|
||||||
|
protected IMongoCollection<TEntity> Collection { get; set; }
|
||||||
|
|
||||||
|
public Repository(IDbContext _dbContext)
|
||||||
|
{
|
||||||
|
dbContext = _dbContext;
|
||||||
|
Collection = GetCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private protected IMongoCollection<TEntity> GetCollection()
|
||||||
|
{
|
||||||
|
var attribute = (BsonCollectionAttribute)typeof(TEntity).GetCustomAttributes(
|
||||||
|
typeof(BsonCollectionAttribute),
|
||||||
|
true).FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(attribute.CollectionName) || string.IsNullOrEmpty(attribute.DatabaseName)) throw new Exception("Could not get a valid collection or database name.");
|
||||||
|
return dbContext.GetCollection<TEntity>(attribute.DatabaseName, attribute.CollectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual void Insert(TEntity obj)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.InsertOneAsync(handle, obj, null, ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual Task InsertAsync(TEntity obj)
|
||||||
|
{
|
||||||
|
return Collection.InsertOneAsync(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Upsert(TEntity obj, Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, filterExpression, obj, new ReplaceOptions { IsUpsert = true }, ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task UpsertAsync(TEntity obj, Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
return Collection.ReplaceOneAsync(filterExpression, obj, new ReplaceOptions { IsUpsert = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TEntity> FindOneAsync(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
var data = await Collection.FindAsync(filterExpression);
|
||||||
|
return data.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TEntity> GetAsync(string id)
|
||||||
|
{
|
||||||
|
var data = await Collection.FindAsync(Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)));
|
||||||
|
return data.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
var data = await Collection.FindAsync(filterExpression);
|
||||||
|
return data.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<IEnumerable<TEntity>> GetAllAsync()
|
||||||
|
{
|
||||||
|
var all = await Collection.FindAsync(Builders<TEntity>.Filter.Empty);
|
||||||
|
return all.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Update(string id, TEntity obj)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)), obj, cancellationToken: ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task UpdateAsync(string id, TEntity obj)
|
||||||
|
{
|
||||||
|
return Collection.ReplaceOneAsync(Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)), obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void DeleteById(string id)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.DeleteOneAsync(handle, Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)), cancellationToken: ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task DeleteByIdAsync(string id)
|
||||||
|
{
|
||||||
|
return Collection.DeleteOneAsync(Builders<TEntity>.Filter.Eq("_id", ObjectId.Parse(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Delete(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.DeleteOneAsync(handle, filterExpression, cancellationToken: ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void DeleteMany(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
dbContext.AddCommand((handle, ct) => Collection.DeleteManyAsync(handle, filterExpression, cancellationToken: ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task DeleteAsync(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
return Collection.DeleteOneAsync(filterExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task DeleteManyAsync(Expression<Func<TEntity, bool>> filterExpression)
|
||||||
|
{
|
||||||
|
return Collection.DeleteManyAsync(filterExpression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="2.13.2" />
|
||||||
|
<PackageReference Include="MongoDB.Driver.Core" Version="2.13.2" />
|
||||||
|
<PackageReference Include="MongoDB.Bson" Version="2.13.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Streetwriters.Data
|
||||||
|
{
|
||||||
|
public class UnitOfWork : IUnitOfWork
|
||||||
|
{
|
||||||
|
private readonly IDbContext dbContext;
|
||||||
|
|
||||||
|
public UnitOfWork(IDbContext _dbContext)
|
||||||
|
{
|
||||||
|
dbContext = _dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Commit()
|
||||||
|
{
|
||||||
|
var changeAmount = await dbContext.SaveChanges();
|
||||||
|
return changeAmount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.dbContext.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user