mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
open source Notesnook API
This commit is contained in:
265
.gitignore
vendored
Normal file
265
.gitignore
vendored
Normal file
@@ -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
|
||||
45
Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs
Normal file
45
Notesnook.API/Accessors/SyncItemsRepositoryAccessor.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Notesnook.API/Authorization/EmailVerifiedRequirement.cs
Normal file
17
Notesnook.API/Authorization/EmailVerifiedRequirement.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Notesnook.API/Authorization/NotesnookUserRequirement.cs
Normal file
18
Notesnook.API/Authorization/NotesnookUserRequirement.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Notesnook.API/Authorization/ProUserRequirement.cs
Normal file
18
Notesnook.API/Authorization/ProUserRequirement.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Notesnook.API/Authorization/SyncRequirement.cs
Normal file
89
Notesnook.API/Authorization/SyncRequirement.cs
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
32
Notesnook.API/Controllers/AnnouncementController.cs
Normal file
32
Notesnook.API/Controllers/AnnouncementController.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Notesnook.API/Controllers/MonographsController.cs
Normal file
116
Notesnook.API/Controllers/MonographsController.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Notesnook.API/Controllers/S3Controller.cs
Normal file
111
Notesnook.API/Controllers/S3Controller.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Notesnook.API/Controllers/UsersController.cs
Normal file
98
Notesnook.API/Controllers/UsersController.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Notesnook.API/Extensions/AuthorizationResultTransformer.cs
Normal file
54
Notesnook.API/Extensions/AuthorizationResultTransformer.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Notesnook.API/Extensions/TransactionHelper.cs
Normal file
25
Notesnook.API/Extensions/TransactionHelper.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
272
Notesnook.API/Hubs/SyncHub.cs
Normal file
272
Notesnook.API/Hubs/SyncHub.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
10
Notesnook.API/Interfaces/IEncrypted.cs
Normal file
10
Notesnook.API/Interfaces/IEncrypted.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
14
Notesnook.API/Interfaces/IMonograph.cs
Normal file
14
Notesnook.API/Interfaces/IMonograph.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
21
Notesnook.API/Interfaces/IS3Service.cs
Normal file
21
Notesnook.API/Interfaces/IS3Service.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
24
Notesnook.API/Interfaces/ISyncItem.cs
Normal file
24
Notesnook.API/Interfaces/ISyncItem.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
21
Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs
Normal file
21
Notesnook.API/Interfaces/ISyncItemsRepositoryAccessor.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
16
Notesnook.API/Interfaces/IUserService.cs
Normal file
16
Notesnook.API/Interfaces/IUserService.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
23
Notesnook.API/Interfaces/IUserSettings.cs
Normal file
23
Notesnook.API/Interfaces/IUserSettings.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
7
Notesnook.API/Models/Algorithms.cs
Normal file
7
Notesnook.API/Models/Algorithms.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class Algorithms
|
||||
{
|
||||
public static string Default => "xcha-argon2i13-7";
|
||||
}
|
||||
}
|
||||
142
Notesnook.API/Models/Announcement.cs
Normal file
142
Notesnook.API/Models/Announcement.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
37
Notesnook.API/Models/EncryptedData.cs
Normal file
37
Notesnook.API/Models/EncryptedData.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
43
Notesnook.API/Models/Monograph.cs
Normal file
43
Notesnook.API/Models/Monograph.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
8
Notesnook.API/Models/MultipartUploadMeta.cs
Normal file
8
Notesnook.API/Models/MultipartUploadMeta.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class MultipartUploadMeta
|
||||
{
|
||||
public string UploadId { get; set; }
|
||||
public string[] Parts { get; set; }
|
||||
}
|
||||
}
|
||||
16
Notesnook.API/Models/Responses/SignupResponse.cs
Normal file
16
Notesnook.API/Models/Responses/SignupResponse.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
24
Notesnook.API/Models/Responses/UserResponse.cs
Normal file
24
Notesnook.API/Models/Responses/UserResponse.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
14
Notesnook.API/Models/S3Options.cs
Normal file
14
Notesnook.API/Models/S3Options.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
108
Notesnook.API/Models/SyncItem.cs
Normal file
108
Notesnook.API/Models/SyncItem.cs
Normal file
@@ -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 { }
|
||||
}
|
||||
25
Notesnook.API/Models/UserSettings.cs
Normal file
25
Notesnook.API/Models/UserSettings.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
28
Notesnook.API/Notesnook.API.csproj
Normal file
28
Notesnook.API/Notesnook.API.csproj
Normal file
@@ -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>
|
||||
49
Notesnook.API/Program.cs
Normal file
49
Notesnook.API/Program.cs
Normal file
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
30
Notesnook.API/Properties/launchSettings.json
Normal file
30
Notesnook.API/Properties/launchSettings.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Notesnook.API/Repositories/SyncItemsRepository.cs
Normal file
88
Notesnook.API/Repositories/SyncItemsRepository.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
211
Notesnook.API/Services/S3Service.cs
Normal file
211
Notesnook.API/Services/S3Service.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
194
Notesnook.API/Services/UserService.cs
Normal file
194
Notesnook.API/Services/UserService.cs
Normal file
@@ -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('/', '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
233
Notesnook.API/Startup.cs
Normal file
233
Notesnook.API/Startup.cs
Normal file
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Notesnook.API/appsettings.Development.json
Normal file
13
Notesnook.API/appsettings.Development.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"MongoDbSettings": {
|
||||
"ConnectionString": "mongodb://localhost:27017/notesnook",
|
||||
"DatabaseName": "notesnook"
|
||||
}
|
||||
}
|
||||
34
Notesnook.sln
Normal file
34
Notesnook.sln
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Streetwriters.Common/Clients.cs
Normal file
93
Streetwriters.Common/Clients.cs
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Streetwriters.Common/Converters/InterfaceConverter.cs
Normal file
35
Streetwriters.Common/Converters/InterfaceConverter.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Streetwriters.Common/Enums/ApplicationType.cs
Normal file
7
Streetwriters.Common/Enums/ApplicationType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum ApplicationType
|
||||
{
|
||||
NOTESNOOK = 0
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Common/Enums/MFAMethods.cs
Normal file
10
Streetwriters.Common/Enums/MFAMethods.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Common/Enums/SubscriptionProvider.cs
Normal file
10
Streetwriters.Common/Enums/SubscriptionProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Streetwriters.Common.Enums
|
||||
{
|
||||
public enum SubscriptionProvider
|
||||
{
|
||||
STREETWRITERS = 0,
|
||||
APPLE = 1,
|
||||
GOOGLE = 2,
|
||||
PADDLE = 3
|
||||
}
|
||||
}
|
||||
12
Streetwriters.Common/Enums/SubscriptionType.cs
Normal file
12
Streetwriters.Common/Enums/SubscriptionType.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
45
Streetwriters.Common/Extensions/AppBuilderExtensions.cs
Normal file
45
Streetwriters.Common/Extensions/AppBuilderExtensions.cs
Normal file
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Streetwriters.Common/Extensions/HttpClientExtensions.cs
Normal file
53
Streetwriters.Common/Extensions/HttpClientExtensions.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Streetwriters.Common/Extensions/StringExtensions.cs
Normal file
72
Streetwriters.Common/Extensions/StringExtensions.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Streetwriters.Common/Extensions/WampRealmExtensions.cs
Normal file
23
Streetwriters.Common/Extensions/WampRealmExtensions.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Streetwriters.Common/Helpers/WampHelper.cs
Normal file
28
Streetwriters.Common/Helpers/WampHelper.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Streetwriters.Common/Interfaces/IClient.cs
Normal file
16
Streetwriters.Common/Interfaces/IClient.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Common/Interfaces/IDocument.cs
Normal file
10
Streetwriters.Common/Interfaces/IDocument.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
{
|
||||
public interface IDocument
|
||||
{
|
||||
string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Common/Interfaces/IMessageHandler.cs
Normal file
10
Streetwriters.Common/Interfaces/IMessageHandler.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
{
|
||||
public interface IMessageHandler<T>
|
||||
{
|
||||
Task Process(T message);
|
||||
}
|
||||
}
|
||||
13
Streetwriters.Common/Interfaces/IOffer.cs
Normal file
13
Streetwriters.Common/Interfaces/IOffer.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
8
Streetwriters.Common/Interfaces/IResponse.cs
Normal file
8
Streetwriters.Common/Interfaces/IResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Streetwriters.Common.Interfaces
|
||||
{
|
||||
public interface IResponse
|
||||
{
|
||||
bool Success { get; set; }
|
||||
int StatusCode { get; set; }
|
||||
}
|
||||
}
|
||||
22
Streetwriters.Common/Interfaces/ISubscription.cs
Normal file
22
Streetwriters.Common/Interfaces/ISubscription.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
38
Streetwriters.Common/Logger.cs
Normal file
38
Streetwriters.Common/Logger.cs
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Streetwriters.Common/Messages/CreateSubscriptionMessage.cs
Normal file
44
Streetwriters.Common/Messages/CreateSubscriptionMessage.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
16
Streetwriters.Common/Messages/DeleteSubscriptionMessage.cs
Normal file
16
Streetwriters.Common/Messages/DeleteSubscriptionMessage.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
13
Streetwriters.Common/Messages/DeleteUserMessage.cs
Normal file
13
Streetwriters.Common/Messages/DeleteUserMessage.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
29
Streetwriters.Common/Messages/SendSSEMessage.cs
Normal file
29
Streetwriters.Common/Messages/SendSSEMessage.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
22
Streetwriters.Common/Models/Client.cs
Normal file
22
Streetwriters.Common/Models/Client.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
12
Streetwriters.Common/Models/GetSubscriptionResponse.cs
Normal file
12
Streetwriters.Common/Models/GetSubscriptionResponse.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Common/Models/MFAConfig.cs
Normal file
10
Streetwriters.Common/Models/MFAConfig.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
36
Streetwriters.Common/Models/Offer.cs
Normal file
36
Streetwriters.Common/Models/Offer.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
21
Streetwriters.Common/Models/PromoCode.cs
Normal file
21
Streetwriters.Common/Models/PromoCode.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
13
Streetwriters.Common/Models/Response.cs
Normal file
13
Streetwriters.Common/Models/Response.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
124
Streetwriters.Common/Models/Role.cs
Normal file
124
Streetwriters.Common/Models/Role.cs
Normal file
@@ -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;
|
||||
}
|
||||
} */
|
||||
}
|
||||
63
Streetwriters.Common/Models/Subscription.cs
Normal file
63
Streetwriters.Common/Models/Subscription.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
12
Streetwriters.Common/Models/User.cs
Normal file
12
Streetwriters.Common/Models/User.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using Streetwriters.Data.Attributes;
|
||||
|
||||
namespace Streetwriters.Common.Models
|
||||
{
|
||||
[BsonCollection("identity", "users")]
|
||||
public class User : MongoUser
|
||||
{
|
||||
}
|
||||
}
|
||||
23
Streetwriters.Common/Models/UserModel.cs
Normal file
23
Streetwriters.Common/Models/UserModel.cs
Normal file
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
130
Streetwriters.Common/Servers.cs
Normal file
130
Streetwriters.Common/Servers.cs
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
22
Streetwriters.Common/Streetwriters.Common.csproj
Normal file
22
Streetwriters.Common/Streetwriters.Common.csproj
Normal file
@@ -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>
|
||||
100
Streetwriters.Common/WampServers.cs
Normal file
100
Streetwriters.Common/WampServers.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
17
Streetwriters.Data/Attributes/BsonCollection.cs
Normal file
17
Streetwriters.Data/Attributes/BsonCollection.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Streetwriters.Data/DbContexts/MongoDbContext.cs
Normal file
90
Streetwriters.Data/DbContexts/MongoDbContext.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Data/DbSettings.cs
Normal file
10
Streetwriters.Data/DbSettings.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
15
Streetwriters.Data/Interfaces/IDbContext.cs
Normal file
15
Streetwriters.Data/Interfaces/IDbContext.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
8
Streetwriters.Data/Interfaces/IDbSettings.cs
Normal file
8
Streetwriters.Data/Interfaces/IDbSettings.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Streetwriters.Data.Interfaces
|
||||
{
|
||||
public interface IDbSettings
|
||||
{
|
||||
string DatabaseName { get; set; }
|
||||
string ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
10
Streetwriters.Data/Interfaces/IUnitOfWork.cs
Normal file
10
Streetwriters.Data/Interfaces/IUnitOfWork.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Streetwriters.Data.Interfaces
|
||||
{
|
||||
public interface IUnitOfWork : IDisposable
|
||||
{
|
||||
Task<bool> Commit();
|
||||
}
|
||||
}
|
||||
119
Streetwriters.Data/Repositories/Repository.cs
Normal file
119
Streetwriters.Data/Repositories/Repository.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Streetwriters.Data/Streetwriters.Data.csproj
Normal file
15
Streetwriters.Data/Streetwriters.Data.csproj
Normal file
@@ -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>
|
||||
27
Streetwriters.Data/UnitOfWork.cs
Normal file
27
Streetwriters.Data/UnitOfWork.cs
Normal file
@@ -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