Compare commits

..
Author SHA1 Message Date
Abdullah Atta 8d20a9cff0 sync: replace mongodb with file system based repository 2023-04-06 01:57:39 +05:00
223 changed files with 11502 additions and 16264 deletions
+22 -63
View File
@@ -1,72 +1,31 @@
# Description: Name of your self hosted instance. Used in the client apps for identification purposes
# Required: yes
# Example: notesnook-instance-sg
INSTANCE_NAME=self-hosted-notesnook-instance
# Required variables
NOTESNOOK_API_SECRET= # This should be a randomly generated secret
# Description: This secret is used for generating, validating, and introspecting auth tokens. It must be a randomly generated token (preferably >32 characters).
# Required: yes
NOTESNOOK_API_SECRET=
# Description: Use this flag to disable creation of new accounts on your instance (i.e. in case it is exposed to the Internet).
# Required: yes
# Possible values: true/false
DISABLE_SIGNUPS=false
### SMTP Configuration ###
# SMTP Configuration is required for sending emails for password reset, 2FA emails etc. You can get SMTP settings from your email provider.
# Description: Username for the SMTP connection (most time it is the email address of your account). Check your email provider's documentation to get the appropriate value.
# Required: yes
# SMTP settings required for delivering emails
SMTP_USERNAME=
# Description: Password for the SMTP connection. Check your email provider's documentation to get the appropriate value.
# Required: yes
SMTP_PASSWORD=
# Description: Host on which the the SMTP connection is running. Check your email provider's documentation to get the appropriate value.
# Required: yes
# Example: smtp.gmail.com
SMTP_HOST=
# Description: Port on which the the SMTP connection is running. Check your email provider's documentation to get the appropriate value.
# Required: yes
# Example: 465
SMTP_PORT=
NOTESNOOK_SENDER_EMAIL=
NOTESNOOK_SENDER_NAME=
SMTP_REPLYTO_NAME= # optional
SMTP_REPLYTO_EMAIL= # optional
# Description: Twilio account SID is required for sending SMS with 2FA codes. Learn more here: https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-
# Required: no
TWILIO_ACCOUNT_SID=
# Description: Twilio account auth is required for sending SMS with 2FA codes. Learn more here: https://help.twilio.com/articles/223136027-Auth-Tokens-and-How-to-Change-Them
# Required: no
TWILIO_AUTH_TOKEN=
# Description: The unique string that we created to identify the Service resource.
# Required: no
# Example: VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
TWILIO_SERVICE_SID=
# MessageBird is used for 2FA via SMS
MESSAGEBIRD_ACCESS_KEY=
# Description: Add the origins for which you want to allow CORS. Leave it empty to allow all origins to access your server. If you want to allow multiple origins, seperate each origin with a comma.
# Required: no
# Example: https://app.notesnook.com,http://localhost:3000
NOTESNOOK_CORS_ORIGINS=
# Server discovery settings
# The domain must be without protocol
# e.g. example.org NOT http://example.org
NOTESNOOK_SERVER_DOMAIN=
IDENTITY_SERVER_DOMAIN=
SSE_SERVER_DOMAIN=
# Description: This is the public URL for the web app, and is used by the backend for creating redirect URLs (e.g. after email confirmation etc).
# Note: the URL has no slashes at the end
# Required: yes
# Example: https://app.notesnook.com
NOTESNOOK_APP_PUBLIC_URL=https://app.notesnook.com
# Description: This is the public URL for the monograph frontend.
# Required: yes
# Example: https://monogr.ph
MONOGRAPH_PUBLIC_URL=http://localhost:6264
# Description: This is the public URL for the Authentication server. Used for generating email confirmation & password reset URLs.
# Required: yes
# Example: https://auth.streetwriters.co
AUTH_SERVER_PUBLIC_URL=http://localhost:8264
# Description: This is the public URL for the S3 attachments server (minio). It'll be used by the Notesnook clients for uploading/downloading attachments.
# Required: yes
# Example: https://attachments.notesnook.com
ATTACHMENTS_SERVER_PUBLIC_URL=http://localhost:9000
# url of the web app instance you want to use
# e.g. http://localhost:3000
# Note: no slashes at the end
NOTESNOOK_APP_HOST=
# Description: Custom username for the root Minio account. Minio is used for storing your attachments. This must be greater than 3 characters in length.
# Required: no
MINIO_ROOT_USER=
# Description: Custom password for the root Minio account. Minio is used for storing your attachments. This must be greater than 8 characters in length.
# Required: no
MINIO_ROOT_PASSWORD=
# Minio is used for S3 storage
MINIO_ROOT_USER= # aka. AccessKeyId (must be > 3 characters)
MINIO_ROOT_PASSWORD= # aka. AccessKey (must be > 8 characters)
-86
View File
@@ -1,86 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Publish Docker images
on:
push:
tags:
- "v*"
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
strategy:
matrix:
repos:
- image: streetwriters/notesnook-sync
file: ./Notesnook.API/Dockerfile
- image: streetwriters/identity
file: ./Streetwriters.Identity/Dockerfile
- image: streetwriters/sse
file: ./Streetwriters.Messenger/Dockerfile
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
ecr: auto
logout: true
# Pull previous image from docker hub to use it as cache to improve the image build time.
- name: docker pull cache image
continue-on-error: true
run: docker pull ${{ matrix.repos.image }}:latest
# Setup QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ matrix.repos.image }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.repos.file }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: ${{ steps.meta.outputs.tags }}
cache-from: ${{ matrix.repos.image }}:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/${{ matrix.repos.image }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
+1 -1
View File
@@ -262,6 +262,6 @@ __pycache__/
keys/
dist/
appsettings.json
keystore/
.env.local
Notesnook.API/sync/
+6 -3
View File
@@ -9,7 +9,8 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-notesnook",
"program": "bin/Debug/net8.0/Notesnook.API.dll",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Notesnook.API/bin/Debug/net7.0/linux-x64/Notesnook.API.dll",
"args": [],
"cwd": "${workspaceFolder}/Notesnook.API",
"stopAtEntry": false,
@@ -24,7 +25,8 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-identity",
"program": "bin/Debug/net8.0/Streetwriters.Identity.dll",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Streetwriters.Identity/bin/Debug/net7.0/linux-x64/Streetwriters.Identity.dll",
"args": [],
"cwd": "${workspaceFolder}/Streetwriters.Identity",
"stopAtEntry": false,
@@ -39,7 +41,8 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-messenger",
"program": "bin/Debug/net8.0/Streetwriters.Messenger.dll",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Streetwriters.Messenger/bin/Debug/net7.0/linux-x64/Streetwriters.Messenger.dll",
"args": [],
"cwd": "${workspaceFolder}/Streetwriters.Messenger",
"stopAtEntry": false,
@@ -17,81 +17,47 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Repositories;
using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Accessors
{
public class SyncItemsRepositoryAccessor : ISyncItemsRepositoryAccessor
{
public SyncItemsRepository Notes { get; }
public SyncItemsRepository Notebooks { get; }
public SyncItemsRepository Shortcuts { get; }
public SyncItemsRepository Relations { get; }
public SyncItemsRepository Reminders { get; }
public SyncItemsRepository Contents { get; }
public SyncItemsRepository LegacySettings { get; }
public SyncItemsRepository Settings { get; }
public SyncItemsRepository Attachments { get; }
public SyncItemsRepository Colors { get; }
public SyncItemsRepository Vaults { get; }
public SyncItemsRepository Tags { get; }
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 Repository<InboxApiKey> InboxApiKey { get; }
public Repository<InboxSyncItem> InboxItems { get; }
public SyncItemsRepositoryAccessor(IDbContext dbContext,
[FromKeyedServices(Collections.NotebooksKey)]
IMongoCollection<SyncItem> notebooks,
[FromKeyedServices(Collections.NotesKey)]
IMongoCollection<SyncItem> notes,
[FromKeyedServices(Collections.ContentKey)]
IMongoCollection<SyncItem> content,
[FromKeyedServices(Collections.SettingsKey)]
IMongoCollection<SyncItem> settings,
[FromKeyedServices(Collections.LegacySettingsKey)]
IMongoCollection<SyncItem> legacySettings,
[FromKeyedServices(Collections.AttachmentsKey)]
IMongoCollection<SyncItem> attachments,
[FromKeyedServices(Collections.ShortcutsKey)]
IMongoCollection<SyncItem> shortcuts,
[FromKeyedServices(Collections.RemindersKey)]
IMongoCollection<SyncItem> reminders,
[FromKeyedServices(Collections.RelationsKey)]
IMongoCollection<SyncItem> relations,
[FromKeyedServices(Collections.ColorsKey)]
IMongoCollection<SyncItem> colors,
[FromKeyedServices(Collections.VaultsKey)]
IMongoCollection<SyncItem> vaults,
[FromKeyedServices(Collections.TagsKey)]
IMongoCollection<SyncItem> tags,
Repository<UserSettings> usersSettings, Repository<Monograph> monographs,
Repository<InboxApiKey> inboxApiKey, Repository<InboxSyncItem> inboxItems)
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)
{
UsersSettings = usersSettings;
Monographs = monographs;
InboxApiKey = inboxApiKey;
InboxItems = inboxItems;
Notebooks = new SyncItemsRepository(dbContext, notebooks);
Notes = new SyncItemsRepository(dbContext, notes);
Contents = new SyncItemsRepository(dbContext, content);
Settings = new SyncItemsRepository(dbContext, settings);
LegacySettings = new SyncItemsRepository(dbContext, legacySettings);
Attachments = new SyncItemsRepository(dbContext, attachments);
Shortcuts = new SyncItemsRepository(dbContext, shortcuts);
Reminders = new SyncItemsRepository(dbContext, reminders);
Relations = new SyncItemsRepository(dbContext, relations);
Colors = new SyncItemsRepository(dbContext, colors);
Vaults = new SyncItemsRepository(dbContext, vaults);
Tags = new SyncItemsRepository(dbContext, tags);
Notebooks = _notebooks;
Notes = _notes;
Contents = _content;
Settings = _settings;
Attachments = _attachments;
UsersSettings = _usersSettings;
Monographs = _monographs;
Shortcuts = _shortcuts;
Reminders = _reminders;
Relations = _relations;
}
}
}
@@ -0,0 +1,36 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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;
}
}
}
@@ -1,101 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Notesnook.API.Models;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Authorization
{
public static class InboxApiKeyAuthenticationDefaults
{
public const string AuthenticationScheme = "InboxApiKey";
}
public class InboxApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
public class InboxApiKeyAuthenticationHandler : AuthenticationHandler<InboxApiKeyAuthenticationSchemeOptions>
{
private readonly Repository<InboxApiKey> _inboxApiKeyRepository;
public InboxApiKeyAuthenticationHandler(
IOptionsMonitor<InboxApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
Repository<InboxApiKey> inboxApiKeyRepository)
: base(options, logger, encoder)
{
_inboxApiKeyRepository = inboxApiKeyRepository;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return AuthenticateResult.Fail("Missing Authorization header");
}
var apiKey = Request.Headers["Authorization"].ToString().Trim();
if (string.IsNullOrEmpty(apiKey))
{
return AuthenticateResult.Fail("Missing API key");
}
try
{
var inboxApiKey = await _inboxApiKeyRepository.FindOneAsync(k => k.Key == apiKey);
if (inboxApiKey == null)
{
return AuthenticateResult.Fail("Invalid API key");
}
if (inboxApiKey.ExpiryDate > 0 && DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() > inboxApiKey.ExpiryDate)
{
return AuthenticateResult.Fail("API key has expired");
}
inboxApiKey.LastUsedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
await _inboxApiKeyRepository.UpsertAsync(inboxApiKey, k => k.Key == apiKey);
var claims = new[]
{
new Claim("sub", inboxApiKey.UserId),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error validating inbox API key");
return AuthenticateResult.Fail("Error validating API key");
}
}
}
}
@@ -0,0 +1,37 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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;
}
}
}
+14 -9
View File
@@ -29,34 +29,38 @@ namespace Notesnook.API.Authorization
{
public class SyncRequirement : AuthorizationHandler<SyncRequirement>, IAuthorizationRequirement
{
private readonly Dictionary<string, string> pathErrorPhraseMap = new()
private Dictionary<string, string> pathErrorPhraseMap = new Dictionary<string, string>
{
["/sync/attachments"] = "use attachments",
["/sync"] = "sync your notes",
["/hubs/sync"] = "sync your notes",
["/hubs/sync/v2"] = "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 if (result.AuthorizationFailure.FailureReasons.Any())
context.Fail(result.AuthorizationFailure.FailureReasons.First());
else context.Fail();
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)
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal User, PathString requestPath)
{
var id = User?.FindFirstValue("sub");
var id = User.FindFirstValue("sub");
if (string.IsNullOrEmpty(id))
{
var reason = new[]
var reason = new AuthorizationFailureReason[]
{
new AuthorizationFailureReason(this, "Invalid token.")
};
@@ -80,7 +84,7 @@ namespace Notesnook.API.Authorization
}
var error = $"Please confirm your email to {phrase}.";
var reason = new[]
var reason = new AuthorizationFailureReason[]
{
new AuthorizationFailureReason(this, error)
};
@@ -88,6 +92,7 @@ namespace Notesnook.API.Authorization
// 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();
-20
View File
@@ -1,20 +0,0 @@
namespace Notesnook.API
{
public class Collections
{
public const string SettingsKey = "settingsv2";
public const string AttachmentsKey = "attachments";
public const string ContentKey = "content";
public const string NotesKey = "notes";
public const string NotebooksKey = "notebooks";
public const string RelationsKey = "relations";
public const string RemindersKey = "reminders";
public const string LegacySettingsKey = "settings";
public const string ShortcutsKey = "shortcuts";
public const string TagsKey = "tags";
public const string ColorsKey = "colors";
public const string VaultsKey = "vaults";
public const string InboxItems = "inbox_items";
public const string InboxApiKeysKey = "inbox_api_keys";
}
}
@@ -18,12 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using Notesnook.API.Models;
using Streetwriters.Data.Repositories;
@@ -42,28 +40,12 @@ namespace Notesnook.API.Controllers
[HttpGet("active")]
[AllowAnonymous]
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string? userId)
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string userId)
{
var totalActive = await Announcements.Collection.CountDocumentsAsync(Builders<Announcement>.Filter.Eq("IsActive", true));
if (totalActive <= 0) return Ok(Array.Empty<Announcement>());
var announcements = (await Announcements.FindAsync((a) => a.IsActive)).Where((a) => a.UserIds == null || a.UserIds.Length == 0 || a.UserIds.Contains(userId));
foreach (var announcement in announcements)
{
if (announcement.UserIds != null && !announcement.UserIds.Contains(userId)) continue;
foreach (var item in announcement.Body)
{
if (item.Type != "callToActions") continue;
foreach (var action in item.Actions)
{
if (action.Type != "link") continue;
action.Data = action.Data.Replace("{{UserId}}", userId ?? "0");
}
}
}
return Ok(announcements);
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));
}
}
}
@@ -1,214 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
using Notesnook.API.Authorization;
using Notesnook.API.Models;
using Notesnook.API.Services;
using Streetwriters.Common;
using Streetwriters.Common.Messages;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Controllers
{
[ApiController]
[Route("inbox")]
public class InboxController : ControllerBase
{
private readonly Repository<InboxApiKey> InboxApiKey;
private readonly Repository<UserSettings> UserSetting;
private Repository<InboxSyncItem> InboxItems;
public InboxController(
Repository<InboxApiKey> inboxApiKeysRepository,
Repository<UserSettings> userSettingsRepository,
Repository<InboxSyncItem> inboxItemsRepository)
{
InboxApiKey = inboxApiKeysRepository;
UserSetting = userSettingsRepository;
InboxItems = inboxItemsRepository;
}
[HttpGet("api-keys")]
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> GetApiKeysAsync()
{
var userId = User.FindFirstValue("sub");
try
{
var apiKeys = await InboxApiKey.FindAsync(t => t.UserId == userId);
return Ok(apiKeys);
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(GetApiKeysAsync), "Couldn't get inbox api keys.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("api-keys")]
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
{
var userId = User.FindFirstValue("sub");
try
{
if (string.IsNullOrWhiteSpace(request.Name))
{
return BadRequest(new { error = "Api key name is required." });
}
if (request.ExpiryDate <= -1)
{
return BadRequest(new { error = "Valid expiry date is required." });
}
var count = await InboxApiKey.CountAsync(t => t.UserId == userId);
if (count >= 10)
{
return BadRequest(new { error = "Maximum of 10 inbox api keys allowed." });
}
var inboxApiKey = new InboxApiKey
{
UserId = userId,
Name = request.Name,
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = request.ExpiryDate,
LastUsedAt = 0
};
await InboxApiKey.InsertAsync(inboxApiKey);
return Ok(inboxApiKey);
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(CreateApiKeyAsync), "Couldn't create inbox api key.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpDelete("api-keys/{apiKey}")]
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
{
var userId = User.FindFirstValue("sub");
try
{
if (string.IsNullOrWhiteSpace(apiKey))
{
return BadRequest(new { error = "Api key is required." });
}
await InboxApiKey.DeleteAsync(t => t.UserId == userId && t.Key == apiKey);
return Ok(new { message = "Api key deleted successfully." });
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(DeleteApiKeyAsync), "Couldn't delete inbox api key.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpGet("public-encryption-key")]
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetPublicKeyAsync()
{
var userId = User.FindFirstValue("sub");
try
{
var userSetting = await UserSetting.FindOneAsync(u => u.UserId == userId);
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
{
return BadRequest(new { error = "Inbox public key is not configured." });
}
return Ok(new { key = userSetting.InboxKeys.Public });
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(GetPublicKeyAsync), "Couldn't get user's inbox's public key.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("items")]
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> CreateInboxItemAsync([FromBody] InboxSyncItem request)
{
var userId = User.FindFirstValue("sub");
try
{
if (request.Key.Algorithm != Algorithms.XSAL_X25519_7)
{
return BadRequest(new { error = $"Only {Algorithms.XSAL_X25519_7} is supported for inbox item password." });
}
if (string.IsNullOrWhiteSpace(request.Key.Cipher))
{
return BadRequest(new { error = "Inbox item password cipher is required." });
}
if (request.Key.Length <= 0)
{
return BadRequest(new { error = "Valid inbox item password length is required." });
}
if (request.Algorithm != Algorithms.Default)
{
return BadRequest(new { error = $"Only {Algorithms.Default} is supported for inbox item." });
}
if (request.Version <= 0)
{
return BadRequest(new { error = "Valid inbox item version is required." });
}
if (string.IsNullOrWhiteSpace(request.Cipher) || string.IsNullOrWhiteSpace(request.IV))
{
return BadRequest(new { error = "Inbox item cipher and iv is required." });
}
if (request.Length <= 0)
{
return BadRequest(new { error = "Valid inbox item length is required." });
}
request.UserId = userId;
request.ItemId = ObjectId.GenerateNewId().ToString();
await InboxItems.InsertAsync(request);
new SyncDeviceService(new SyncDevice(userId, string.Empty))
.AddIdsToAllDevices([$"{request.ItemId}:inboxItems"]);
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
OriginTokenId = null,
UserId = userId,
Message = new Message
{
Type = "triggerSync",
Data = JsonSerializer.Serialize(new { reason = "Inbox items updated." })
}
});
return Ok();
}
catch (Exception ex)
{
await Slogger<InboxController>.Error(nameof(CreateInboxItemAsync), "Couldn't create inbox item.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
}
}
+54 -283
View File
@@ -18,23 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
using MongoDB.Driver;
using Notesnook.API.Authorization;
using Notesnook.API.Models;
using Notesnook.API.Services;
using Streetwriters.Common;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories;
@@ -43,150 +32,61 @@ namespace Notesnook.API.Controllers
[ApiController]
[Route("monographs")]
[Authorize("Sync")]
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer) : ControllerBase
public class MonographsController : ControllerBase
{
const string SVG_PIXEL = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><circle r='9'/></svg>";
private Repository<Monograph> Monographs { get; set; }
private readonly IUnitOfWork unit;
private const int MAX_DOC_SIZE = 15 * 1024 * 1024;
private static FilterDefinition<Monograph> CreateMonographFilter(string userId, Monograph monograph)
public MonographsController(Repository<Monograph> monographs, IUnitOfWork unitOfWork)
{
var userIdFilter = Builders<Monograph>.Filter.Eq("UserId", userId);
monograph.ItemId ??= monograph.Id;
return ObjectId.TryParse(monograph.ItemId, out ObjectId id)
? Builders<Monograph>.Filter
.And(userIdFilter,
Builders<Monograph>.Filter.Or(
Builders<Monograph>.Filter.Eq("_id", id), Builders<Monograph>.Filter.Eq("ItemId", monograph.ItemId)
)
)
: Builders<Monograph>.Filter
.And(userIdFilter,
Builders<Monograph>.Filter.Eq("ItemId", monograph.ItemId)
);
}
private static FilterDefinition<Monograph> CreateMonographFilter(string itemId)
{
return ObjectId.TryParse(itemId, out ObjectId id)
? Builders<Monograph>.Filter.Or(
Builders<Monograph>.Filter.Eq("_id", id),
Builders<Monograph>.Filter.Eq("ItemId", itemId))
: Builders<Monograph>.Filter.Eq("ItemId", itemId);
}
private async Task<Monograph> FindMonographAsync(string userId, Monograph monograph)
{
var result = await monographs.Collection.FindAsync(CreateMonographFilter(userId, monograph), new FindOptions<Monograph>
{
Limit = 1
});
return await result.FirstOrDefaultAsync();
}
private async Task<Monograph> FindMonographAsync(string itemId)
{
var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions<Monograph>
{
Limit = 1
});
return await result.FirstOrDefaultAsync();
Monographs = monographs;
unit = unitOfWork;
}
[HttpPost]
public async Task<IActionResult> PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
public async Task<IActionResult> PublishAsync([FromBody] Monograph monograph)
{
try
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
{
var userId = this.User.FindFirstValue("sub");
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph != null && !existingMonograph.Deleted) return await UpdateAsync(deviceId, monograph);
if (monograph.EncryptedContent == null)
monograph.CompressedContent = (await CleanupContentAsync(User, 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.");
if (existingMonograph != null)
{
monograph.Id = existingMonograph.Id;
}
monograph.Deleted = false;
await monographs.Collection.ReplaceOneAsync(
CreateMonographFilter(userId, monograph),
monograph,
new ReplaceOptions { IsUpsert = true }
);
await MarkMonographForSyncAsync(userId, monograph.ItemId ?? monograph.Id, deviceId, jti);
return Ok(new
{
id = monograph.ItemId,
datePublished = monograph.DatePublished,
});
}
catch (Exception e)
{
await Slogger<MonographsController>.Error(nameof(PublishAsync), e.ToString());
return BadRequest();
}
id = monograph.Id
});
}
[HttpPatch]
public async Task<IActionResult> UpdateAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
public async Task<IActionResult> UpdateAsync([FromBody] Monograph monograph)
{
try
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
{
var userId = this.User.FindFirstValue("sub");
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph == null || existingMonograph.Deleted)
{
return NotFound();
}
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.");
if (monograph.EncryptedContent == null)
monograph.CompressedContent = (await CleanupContentAsync(User, monograph.Content)).CompressBrotli();
else
monograph.Content = null;
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var result = await monographs.Collection.UpdateOneAsync(
CreateMonographFilter(userId, monograph),
Builders<Monograph>.Update
.Set(m => m.DatePublished, monograph.DatePublished)
.Set(m => m.CompressedContent, monograph.CompressedContent)
.Set(m => m.EncryptedContent, monograph.EncryptedContent)
.Set(m => m.SelfDestruct, monograph.SelfDestruct)
.Set(m => m.Title, monograph.Title)
.Set(m => m.Password, monograph.Password)
);
if (!result.IsAcknowledged) return BadRequest();
await MarkMonographForSyncAsync(userId, monograph.ItemId ?? monograph.Id, deviceId, jti);
return Ok(new
{
id = monograph.ItemId,
datePublished = monograph.DatePublished,
});
}
catch (Exception e)
{
await Slogger<MonographsController>.Error(nameof(UpdateAsync), e.ToString());
return BadRequest();
}
id = monograph.Id
});
}
[HttpGet]
@@ -195,24 +95,17 @@ namespace Notesnook.API.Controllers
var userId = this.User.FindFirstValue("sub");
if (userId == null) return Unauthorized();
var userMonographs = (await monographs.Collection.FindAsync(
Builders<Monograph>.Filter.And(
Builders<Monograph>.Filter.Eq("UserId", userId),
Builders<Monograph>.Filter.Ne("Deleted", true)
)
, new FindOptions<Monograph, ObjectWithId>
{
Projection = Builders<Monograph>.Projection.Include("_id").Include("ItemId"),
})).ToEnumerable();
return Ok(userMonographs.Select((m) => m.ItemId ?? m.Id));
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 FindMonographAsync(id);
if (monograph == null || monograph.Deleted)
var monograph = await Monographs.FindOneAsync((m) => m.Id == id);
if (monograph == null)
{
return NotFound(new
{
@@ -221,143 +114,21 @@ namespace Notesnook.API.Controllers
});
}
if (monograph.SelfDestruct)
await Monographs.DeleteByIdAsync(monograph.Id);
if (monograph.EncryptedContent == null)
monograph.Content = monograph.CompressedContent?.DecompressBrotli();
monograph.ItemId ??= monograph.Id;
monograph.Content = monograph.CompressedContent.DecompressBrotli();
return Ok(monograph);
}
[HttpGet("{id}/view")]
[AllowAnonymous]
public async Task<IActionResult> TrackView([FromRoute] string id)
{
var monograph = await FindMonographAsync(id);
if (monograph == null || monograph.Deleted) return Content(SVG_PIXEL, "image/svg+xml");
if (monograph.SelfDestruct)
{
await monographs.Collection.ReplaceOneAsync(
CreateMonographFilter(monograph.UserId, monograph),
new Monograph
{
ItemId = id,
Id = monograph.Id,
Deleted = true,
UserId = monograph.UserId
}
);
await MarkMonographForSyncAsync(monograph.UserId, id);
}
return Content(SVG_PIXEL, "image/svg+xml");
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync([FromQuery] string? deviceId, [FromRoute] string id)
public async Task<IActionResult> DeleteAsync([FromRoute] string id)
{
var userId = this.User.FindFirstValue("sub");
if (userId is null) return Unauthorized();
var monograph = await FindMonographAsync(id);
if (monograph == null || monograph.Deleted)
return Ok();
var jti = this.User.FindFirstValue("jti");
await monographs.Collection.ReplaceOneAsync(
CreateMonographFilter(userId, monograph),
new Monograph
{
ItemId = id,
Id = monograph.Id,
Deleted = true,
UserId = monograph.UserId
}
);
await MarkMonographForSyncAsync(userId, id, deviceId, jti);
Monographs.DeleteById(id);
if (!await unit.Commit()) return BadRequest();
return Ok();
}
private static async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
{
if (deviceId == null) return;
new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices([$"{monographId}:monograph"]);
await SendTriggerSyncEventAsync(userId, jti);
}
private static async Task MarkMonographForSyncAsync(string userId, string monographId)
{
new SyncDeviceService(new SyncDevice(userId, string.Empty)).AddIdsToAllDevices([$"{monographId}:monograph"]);
await SendTriggerSyncEventAsync(userId, sendToAllDevices: true);
}
private static async Task SendTriggerSyncEventAsync(string userId, string? jti = null, bool sendToAllDevices = false)
{
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
OriginTokenId = sendToAllDevices ? null : jti,
UserId = userId,
Message = new Message
{
Type = "triggerSync",
Data = JsonSerializer.Serialize(new { reason = "Monographs updated." })
}
});
}
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string content)
{
if (Constants.IS_SELF_HOSTED) return content;
try
{
var json = JsonSerializer.Deserialize<MonographContent>(content);
var html = json.Data;
if (user.IsUserSubscribed())
{
var config = Configuration.Default.WithDefaultLoader();
var context = BrowsingContext.New(config);
var document = await context.OpenAsync(r => r.Content(html));
foreach (var element in document.QuerySelectorAll("a"))
{
var href = element.GetAttribute("href");
if (string.IsNullOrEmpty(href)) continue;
if (!await analyzer.IsURLSafeAsync(href))
{
await Slogger<MonographsController>.Info("CleanupContentAsync", "Malicious URL detected: " + href);
element.RemoveAttribute("href");
}
}
html = document.ToHtml();
}
else
{
var config = Configuration.Default.WithDefaultLoader();
var context = BrowsingContext.New(config);
var document = await context.OpenAsync(r => r.Content(html));
foreach (var element in document.QuerySelectorAll("a,iframe,img,object,svg,button,link"))
{
foreach (var attr in element.Attributes)
element.RemoveAttribute(attr.Name);
}
html = document.ToHtml();
}
return JsonSerializer.Serialize<MonographContent>(new MonographContent
{
Type = json.Type,
Data = html
});
}
catch (Exception ex)
{
await Slogger<MonographsController>.Error("CleanupContentAsync", ex.ToString());
return content;
}
}
}
}
+18 -69
View File
@@ -24,84 +24,33 @@ using System.Threading.Tasks;
using System.Security.Claims;
using Notesnook.API.Interfaces;
using System;
using System.Net.Http;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Models;
using Notesnook.API.Helpers;
using Streetwriters.Common;
using Streetwriters.Common.Interfaces;
using Notesnook.API.Models;
namespace Notesnook.API.Controllers
{
[ApiController]
[Route("s3")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
[Authorize("Sync")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public class S3Controller : ControllerBase
{
private ISyncItemsRepositoryAccessor Repositories { get; }
private IS3Service S3Service { get; set; }
public S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
public S3Controller(IS3Service s3Service)
{
S3Service = s3Service;
Repositories = syncItemsRepositoryAccessor;
}
[HttpPut]
public async Task<IActionResult> Upload([FromQuery] string name)
public IActionResult Upload([FromQuery] string name)
{
var userId = this.User.FindFirstValue("sub");
if (!HttpContext.Request.Headers.ContentLength.HasValue) return BadRequest(new { error = "No Content-Length header found." });
long fileSize = HttpContext.Request.Headers.ContentLength.Value;
if (fileSize == 0)
{
var uploadUrl = S3Service.GetUploadObjectUrl(userId, name);
if (uploadUrl == null) return BadRequest(new { error = "Could not create signed url." });
return Ok(uploadUrl);
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
if (!Constants.IS_SELF_HOSTED)
{
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
if (subscription is null) return BadRequest(new { error = "User subscription not found." });
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
{
return BadRequest(new { error = "Max file size exceeded." });
}
userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 };
userSettings.StorageLimit.Value += fileSize;
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit))
return BadRequest(new { error = "Storage limit exceeded." });
}
var url = S3Service.GetInternalUploadObjectUrl(userId, name);
if (url == null) return BadRequest(new { error = "Could not create signed url." });
var httpClient = new HttpClient();
var content = new StreamContent(HttpContext.Request.BodyReader.AsStream());
content.Headers.ContentLength = Request.ContentLength;
var response = await httpClient.SendRequestAsync<Response>(url, null, HttpMethod.Put, content);
if (!response.Success) return BadRequest(await response.Content.ReadAsStringAsync());
if (!Constants.IS_SELF_HOSTED)
{
userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
}
return Ok(response);
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)
public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string uploadId)
{
var userId = this.User.FindFirstValue("sub");
try
@@ -125,35 +74,35 @@ namespace Notesnook.API.Controllers
}
[HttpPost("multipart")]
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequestWrapper uploadRequestWrapper)
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequest uploadRequest)
{
var userId = this.User.FindFirstValue("sub");
try
{
await S3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest());
await S3Service.CompleteMultipartUploadAsync(userId, uploadRequest);
return Ok();
}
catch (Exception ex) { return BadRequest(ex.Message); }
}
[HttpGet]
public async Task<IActionResult> Download([FromQuery] string name)
[Authorize]
public IActionResult Download([FromQuery] string name)
{
try
{
var userId = this.User.FindFirstValue("sub");
var url = await S3Service.GetDownloadObjectUrl(userId, name);
if (url == null) return BadRequest("Could not create signed url.");
return Ok(url);
}
catch (Exception ex) { return BadRequest(ex.Message); }
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();
}
@@ -1,74 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 Notesnook.API.Services;
using Streetwriters.Common;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Models;
namespace Notesnook.API.Controllers
{
[ApiController]
[Authorize]
[Route("devices")]
public class SyncDeviceController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> RegisterDevice([FromQuery] string deviceId)
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
new SyncDeviceService(new SyncDevice(userId, deviceId)).RegisterDevice();
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't register device.", ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpDelete]
public async Task<IActionResult> UnregisterDevice([FromQuery] string deviceId)
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
new SyncDeviceService(new SyncDevice(userId, deviceId)).UnregisterDevice();
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't unregister device.", ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
}
}
+38 -35
View File
@@ -18,23 +18,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
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(IUserService UserService) : ControllerBase
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()
@@ -54,34 +66,21 @@ namespace Notesnook.API.Controllers
[HttpGet]
public async Task<IActionResult> GetUser()
{
var userId = User.FindFirstValue("sub");
try
{
UserResponse response = await UserService.GetUserAsync(userId);
if (!response.Success) return BadRequest();
return Ok(response);
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't get user for id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
UserResponse response = await UserService.GetUserAsync();
if (!response.Success) return BadRequest(response);
return Ok(response);
}
[HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserKeys keys)
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
{
var userId = User.FindFirstValue("sub");
try
{
await UserService.SetUserKeysAsync(userId, keys);
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't update user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
UserResponse response = await UserService.GetUserAsync(false);
if (user.AttachmentsKey != null)
await UserService.SetUserAttachmentsKeyAsync(response.UserId, user.AttachmentsKey);
else return BadRequest();
return Ok();
}
[HttpPost("reset")]
@@ -95,20 +94,24 @@ namespace Notesnook.API.Controllers
}
[HttpPost("delete")]
[RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
public async Task<IActionResult> Delete()
{
var userId = this.User.FindFirstValue("sub");
var jti = User.FindFirstValue("jti");
try
{
await UserService.DeleteUserAsync(userId, jti, form.Password);
return Ok();
var userId = this.User.FindFirstValue("sub");
if (await UserService.DeleteUserAsync(userId, User.FindFirstValue("jti")))
{
Response response = await this.httpClient.ForwardAsync<Response>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account/unregister", HttpMethod.Post);
if (!response.Success) return BadRequest();
return Ok();
}
return BadRequest();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't delete user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
return BadRequest(ex.Message);
}
}
}
+16 -38
View File
@@ -1,50 +1,28 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS base
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /app
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG TARGETARCH
ARG BUILDPLATFORM
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
WORKDIR /src
# restore all project dependencies
COPY Streetwriters.Data/*.csproj ./Streetwriters.Data/
RUN dotnet restore /app/Streetwriters.Data/Streetwriters.Data.csproj --use-current-runtime
COPY Streetwriters.Common/*.csproj ./Streetwriters.Common/
RUN dotnet restore /app/Streetwriters.Common/Streetwriters.Common.csproj --use-current-runtime
COPY Notesnook.API/*.csproj ./Notesnook.API/
RUN dotnet restore /app/Notesnook.API/Notesnook.API.csproj --use-current-runtime
# restore dependencies
RUN dotnet restore -v d /src/Notesnook.API/Notesnook.API.csproj --use-current-runtime
# copy everything else
COPY Streetwriters.Data/ ./Streetwriters.Data/
COPY Streetwriters.Common/ ./Streetwriters.Common/
COPY Notesnook.API/ ./Notesnook.API/
WORKDIR /src/Notesnook.API/
# build
WORKDIR /app/Notesnook.API/
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
RUN dotnet publish -c Release -o /app/out --use-current-runtime --self-contained false --no-restore
RUN dotnet build -c Release -o /app/build -a $TARGETARCH
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish \
#--runtime alpine-x64 \
--self-contained true \
/p:TrimMode=partial \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true \
/p:JsonSerializerIsReflectionEnabledByDefault=true \
-a $TARGETARCH
FROM --platform=$BUILDPLATFORM base AS final
ARG TARGETARCH
ARG BUILDPLATFORM
# create a new user and change directory ownership
RUN adduser --disabled-password \
--home /app \
--gecos '' dotnetuser && chown -R dotnetuser /app
# impersonate into the new user
USER dotnetuser
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["./Notesnook.API"]
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "Notesnook.API.dll"]
@@ -1,49 +0,0 @@
using System;
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
[EventSource(Name = "Notesnook.API.EventCounter.Sync")]
public sealed class SyncEventCounterSource : EventSource
{
public static readonly SyncEventCounterSource Log = new();
private Meter meter = new("Notesnook.API.Metrics.Sync", "1.0.0");
private Counter<int> fetchCounter;
private Counter<int> pushCounter;
private Counter<int> legacyFetchCounter;
private Counter<int> pushV2Counter;
private Counter<int> fetchV2Counter;
private Histogram<long> fetchV2Duration;
private Histogram<long> pushV2Duration;
private SyncEventCounterSource()
{
fetchCounter = meter.CreateCounter<int>("sync.fetches", "fetches", "Total fetches");
pushCounter = meter.CreateCounter<int>("sync.pushes", "pushes", "Total pushes");
legacyFetchCounter = meter.CreateCounter<int>("sync.legacy-fetches", "fetches", "Total legacy fetches");
fetchV2Counter = meter.CreateCounter<int>("sync.v2.fetches", "fetches", "Total v2 fetches");
pushV2Counter = meter.CreateCounter<int>("sync.v2.pushes", "pushes", "Total v2 pushes");
fetchV2Duration = meter.CreateHistogram<long>("sync.v2.fetch_duration");
pushV2Duration = meter.CreateHistogram<long>("sync.v2.push_duration");
}
public void Fetch() => fetchCounter.Add(1);
public void LegacyFetch() => legacyFetchCounter.Add(1);
public void FetchV2() => fetchV2Counter.Add(1);
public void PushV2() => pushV2Counter.Add(1);
public void Push() => pushCounter.Add(1);
public void RecordFetchDuration(long durationMs) => fetchV2Duration.Record(durationMs);
public void RecordPushDuration(long durationMs) => pushV2Duration.Record(durationMs);
protected override void Dispose(bool disposing)
{
legacyFetchCounter = null;
fetchV2Counter = null;
pushV2Counter = null;
pushCounter = null;
fetchCounter = null;
meter.Dispose();
meter = null;
base.Dispose(disposing);
}
}
@@ -17,11 +17,8 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
@@ -45,18 +42,30 @@ namespace Notesnook.API.Extensions
AuthorizationPolicy authorizationPolicy,
PolicyAuthorizationResult policyAuthorizationResult)
{
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
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))
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);
}
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
}
@@ -1,14 +0,0 @@
using System.Threading;
using System;
using System.Threading.Tasks;
using System.Linq;
namespace System.Security.Claims
{
public static class ClaimsPrincipalExtensions
{
private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "legacy_pro"];
public static bool IsUserSubscribed(this ClaimsPrincipal user)
=> user.Claims.Any((c) => c.Type == "notesnook:status" && SUBSCRIBED_CLAIMS.Contains(c.Value));
}
}
-68
View File
@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using Notesnook.API.Models;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Models;
namespace Notesnook.API.Helpers
{
class StorageHelper
{
const long MB = 1024 * 1024;
const long GB = 1024 * MB;
public readonly static Dictionary<SubscriptionPlan, long> MAX_STORAGE_PER_MONTH = new()
{
{ SubscriptionPlan.FREE, 50L * MB },
{ SubscriptionPlan.ESSENTIAL, GB },
{ SubscriptionPlan.PRO, 10L * GB },
{ SubscriptionPlan.EDUCATION, 10L * GB },
{ SubscriptionPlan.BELIEVER, 25L * GB },
{ SubscriptionPlan.LEGACY_PRO, -1 }
};
public readonly static Dictionary<SubscriptionPlan, long> MAX_FILE_SIZE = new()
{
{ SubscriptionPlan.FREE, 10 * MB },
{ SubscriptionPlan.ESSENTIAL, 100 * MB },
{ SubscriptionPlan.PRO, 1L * GB },
{ SubscriptionPlan.EDUCATION, 1L * GB },
{ SubscriptionPlan.BELIEVER, 5L * GB },
{ SubscriptionPlan.LEGACY_PRO, 512 * MB }
};
public static long GetStorageLimitForPlan(Subscription subscription)
{
return MAX_STORAGE_PER_MONTH[subscription.Plan];
}
public static long GetFileSizeLimitForPlan(Subscription subscription)
{
return MAX_FILE_SIZE[subscription.Plan];
}
public static bool IsStorageLimitReached(Subscription subscription, Limit limit)
{
var storageLimit = GetStorageLimitForPlan(subscription);
if (storageLimit == -1) return false;
return limit.Value > storageLimit;
}
public static bool IsFileSizeExceeded(Subscription subscription, long fileSize)
{
var maxFileSize = MAX_FILE_SIZE[subscription.Plan];
return fileSize > maxFileSize;
}
private static readonly string[] sizes = ["B", "KB", "MB", "GB", "TB"];
public static string FormatBytes(long size)
{
int order = 0;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size = size / 1024;
}
return String.Format("{0:0.##} {1}", size, sizes[order]);
}
}
}
+141 -322
View File
@@ -23,114 +23,24 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using MongoDB.Driver;
using Notesnook.API.Authorization;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Repositories;
using Streetwriters.Common.Models;
using Streetwriters.Data.Interfaces;
namespace Notesnook.API.Hubs
{
public struct RunningPush
{
public long Timestamp { get; set; }
public long Validity { get; set; }
public string ConnectionId { get; set; }
}
public interface ISyncHubClient
{
Task PushItems(SyncTransferItemV2 transferItem);
Task<bool> SendItems(SyncTransferItemV2 transferItem);
Task PushCompleted(long lastSynced);
}
public class GlobalSync
{
private const long PUSH_VALIDITY_EXTENSION_PERIOD = 16 * 1000; // 16 second
private const int PUSH_VALIDITY_PERIOD_PER_ITEM = 5 * 100; // 0.5 second
private const long BASE_PUSH_VALIDITY_PERIOD = 5 * 1000; // 5 seconds
private const long BASE_PUSH_VALIDITY_PERIOD_NEW = 16 * 1000; // 16 seconds
private readonly static Dictionary<string, List<RunningPush>> PushOperations = new();
public static void ClearPushOperations(string userId, string connectionId)
{
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations.ToArray())
if (push.ConnectionId == connectionId || !IsPushValid(push, now))
operations.Remove(push);
}
}
public static bool IsPushing(string userId, string connectionId)
{
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations)
if (push.ConnectionId == connectionId && IsPushValid(push, now)) return true;
}
return false;
}
public static bool IsUserPushing(string userId)
{
var count = 0;
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations)
if (IsPushValid(push, now)) ++count;
}
return count > 0;
}
public static void StartPush(string userId, string connectionId, long? totalItems = null)
{
if (IsPushing(userId, connectionId)) return;
if (!PushOperations.ContainsKey(userId))
PushOperations[userId] = new List<RunningPush>();
PushOperations[userId].Add(new RunningPush
{
ConnectionId = connectionId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Validity = totalItems.HasValue ? BASE_PUSH_VALIDITY_PERIOD + (totalItems.Value * PUSH_VALIDITY_PERIOD_PER_ITEM) : BASE_PUSH_VALIDITY_PERIOD_NEW
});
}
public static void ExtendPush(string userId, string connectionId)
{
if (!IsPushing(userId, connectionId) || !PushOperations.ContainsKey(userId))
{
StartPush(userId, connectionId);
return;
}
var index = PushOperations[userId].FindIndex((push) => push.ConnectionId == connectionId);
if (index < 0)
{
StartPush(userId, connectionId);
return;
}
var pushOperation = PushOperations[userId][index];
pushOperation.Validity += PUSH_VALIDITY_EXTENSION_PERIOD;
}
private static bool IsPushValid(RunningPush push, long now)
{
return now < push.Timestamp + push.Validity;
}
Task SyncItem(SyncTransferItem transferItem);
Task RemoteSyncCompleted(long lastSynced);
Task SyncCompleted();
}
[Authorize("Sync")]
@@ -138,16 +48,6 @@ namespace Notesnook.API.Hubs
{
private ISyncItemsRepositoryAccessor Repositories { get; }
private readonly IUnitOfWork unit;
private readonly string[] CollectionKeys = new[] {
"settings",
"attachment",
"note",
"notebook",
"content",
"shortcut",
"reminder",
"relation", // relations must sync at the end to prevent invalid state
};
public SyncHub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
{
@@ -170,235 +70,181 @@ namespace Notesnook.API.Hubs
public override async Task OnDisconnectedAsync(Exception exception)
{
try
{
await base.OnDisconnectedAsync(exception);
}
finally
{
var id = Context.User.FindFirstValue("sub");
GlobalSync.ClearPushOperations(id, Context.ConnectionId);
}
var id = Context.User.FindFirstValue("sub");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, id);
await base.OnDisconnectedAsync(exception);
}
private Action<SyncItem, string, long> MapTypeToUpsertAction(string type)
public async Task<int> SyncItem(BatchedSyncTransferItem transferItem)
{
return type switch
{
"attachment" => Repositories.Attachments.Upsert,
"note" => Repositories.Notes.Upsert,
"notebook" => Repositories.Notebooks.Upsert,
"content" => Repositories.Contents.Upsert,
"shortcut" => Repositories.Shortcuts.Upsert,
"reminder" => Repositories.Reminders.Upsert,
"relation" => Repositories.Relations.Upsert,
_ => null,
};
}
public async Task<long> InitializePush(SyncMetadata syncMetadata)
{
if (syncMetadata.LastSynced <= 0) throw new HubException("Last synced time cannot be zero or less than zero.");
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) return 0;
UserSettings userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
long dateSynced = Math.Max(syncMetadata.LastSynced, userSettings.LastSynced);
var others = Clients.OthersInGroup(userId);
GlobalSync.StartPush(userId, Context.ConnectionId);
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
long dateSynced = transferItem.LastSynced > userSettings.LastSynced ? transferItem.LastSynced : userSettings.LastSynced;
if (
(userSettings.VaultKey != null &&
syncMetadata.VaultKey != null &&
!userSettings.VaultKey.Equals(syncMetadata.VaultKey) &&
!syncMetadata.VaultKey.IsEmpty()) ||
(userSettings.VaultKey == null &&
syncMetadata.VaultKey != null &&
!syncMetadata.VaultKey.IsEmpty()))
Parallel.For(0, transferItem.Items.Length, async (i) =>
{
userSettings.VaultKey = syncMetadata.VaultKey;
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
}
var data = transferItem.Items[i];
var type = transferItem.Types[i];
var id = transferItem.Ids[i];
return dateSynced;
}
// We intentionally don't await here to speed up the sync. Fire and forget
// suits here because we don't really care if the item reaches the other
// devices.
others.SyncItem(
new SyncTransferItem
{
Item = data,
ItemType = type,
LastSynced = dateSynced,
Total = transferItem.Total,
Current = transferItem.Current + i
});
public async Task<int> PushItems(SyncTransferItemV2 pushItem, long dateSynced)
{
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) return 0;
SyncEventCounterSource.Log.Push();
try
{
var others = Clients.OthersInGroup(userId);
others.PushItems(pushItem);
GlobalSync.ExtendPush(userId, Context.ConnectionId);
if (pushItem.Type == "settings")
switch (type)
{
var settings = pushItem.Items.First();
if (settings == null) return 0;
settings.Id = MongoDB.Bson.ObjectId.Parse(userId);
settings.ItemId = userId;
Repositories.LegacySettings.Upsert(settings, userId, dateSynced);
}
else
{
var UpsertItem = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception("Invalid item type.");
foreach (var item in pushItem.Items)
{
UpsertItem(item, userId, dateSynced);
}
case "content":
await Repositories.Contents.UpsertAsync(id, data, userId, dateSynced);
break;
case "attachment":
await Repositories.Attachments.UpsertAsync(id, data, userId, dateSynced);
break;
case "note":
await Repositories.Notes.UpsertAsync(id, data, userId, dateSynced);
break;
case "notebook":
await Repositories.Notebooks.UpsertAsync(id, data, userId, dateSynced);
break;
case "shortcut":
await Repositories.Shortcuts.UpsertAsync(id, data, userId, dateSynced);
break;
case "reminder":
await Repositories.Reminders.UpsertAsync(id, data, userId, dateSynced);
break;
case "relation":
await Repositories.Relations.UpsertAsync(id, data, userId, dateSynced);
break;
case "settings":
await Repositories.Settings.UpsertAsync(userId, data, userId, dateSynced);
break;
case "vaultKey":
userSettings.VaultKey = JsonSerializer.Deserialize<EncryptedData>(data);
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
break;
default:
throw new HubException("Invalid item type.");
}
});
return await unit.Commit() ? 1 : 0;
}
catch (Exception ex)
{
GlobalSync.ClearPushOperations(userId, Context.ConnectionId);
throw ex;
}
return 1;
}
public async Task<bool> SyncCompleted(long dateSynced)
{
var userId = Context.User.FindFirstValue("sub");
try
{
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
long lastSynced = Math.Max(dateSynced, userSettings.LastSynced);
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
userSettings.LastSynced = lastSynced;
long lastSynced = dateSynced > userSettings.LastSynced ? dateSynced : userSettings.LastSynced;
await this.Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
userSettings.LastSynced = lastSynced;
await Clients.OthersInGroup(userId).PushCompleted(lastSynced);
await this.Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
return true;
}
finally
{
GlobalSync.ClearPushOperations(userId, Context.ConnectionId);
}
await Clients.OthersInGroup(userId).RemoteSyncCompleted(lastSynced);
return true;
}
private static async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(Func<string, long, int, Task<IAsyncCursor<SyncItem>>>[] collections, string[] types, string userId, long lastSyncedTimestamp, int size, long maxBytes, int skipChunks)
public async IAsyncEnumerable<SyncTransferItem> FetchItems(long lastSyncedTimestamp, [EnumeratorCancellation]
CancellationToken cancellationToken)
{
var chunksProcessed = 0;
for (int i = 0; i < collections.Length; i++)
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)
{
var type = types[i];
using var cursor = await collections[i](userId, lastSyncedTimestamp, size);
var chunk = new List<SyncItem>();
long totalBytes = 0;
long METADATA_BYTES = 5 * 1024;
while (await cursor.MoveNextAsync())
yield return new SyncTransferItem
{
if (chunksProcessed++ < skipChunks) continue;
foreach (var item in cursor.Current)
{
chunk.Add(item);
totalBytes += item.Length + METADATA_BYTES;
if (totalBytes >= maxBytes)
{
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = chunksProcessed
};
LastSynced = userSettings.LastSynced,
Synced = true
};
yield break;
}
totalBytes = 0;
chunk.Clear();
}
}
}
if (chunk.Count > 0)
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
{
if (chunksProcessed++ < skipChunks) continue;
yield return new SyncTransferItemV2
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
{
Items = chunk,
Type = type,
Count = chunksProcessed
LastSynced = userSettings.LastSynced,
Synced = false,
Item = JsonSerializer.Serialize(item),
ItemType = collection.Key,
Total = total,
};
}
}
}
public Task<SyncMetadata> RequestFetch(long lastSyncedTimestamp)
{
return RequestResumableFetch(lastSyncedTimestamp);
}
public async Task<SyncMetadata> RequestResumableFetch(long lastSyncedTimestamp, int cursor = 0)
{
var userId = Context.User.FindFirstValue("sub");
if (GlobalSync.IsUserPushing(userId))
{
throw new HubException("Cannot fetch data while another sync is in progress. Please try again later.");
}
SyncEventCounterSource.Log.Fetch();
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}. Please run a Force Sync to fix this issue.");
}
// var client = Clients.Caller;
if (lastSyncedTimestamp > 0 && userSettings.LastSynced == lastSyncedTimestamp)
{
return new SyncMetadata
{
LastSynced = userSettings.LastSynced,
};
}
var isResumable = lastSyncedTimestamp == 0;
if (!isResumable) cursor = 0;
var chunks = PrepareChunks(
collections: new[] {
Repositories.LegacySettings.FindItemsSyncedAfter,
Repositories.Attachments.FindItemsSyncedAfter,
Repositories.Notes.FindItemsSyncedAfter,
Repositories.Notebooks.FindItemsSyncedAfter,
Repositories.Contents.FindItemsSyncedAfter,
Repositories.Shortcuts.FindItemsSyncedAfter,
Repositories.Reminders.FindItemsSyncedAfter,
Repositories.Relations.FindItemsSyncedAfter,
},
types: CollectionKeys,
userId,
lastSyncedTimestamp,
size: 1000,
maxBytes: 7 * 1024 * 1024,
skipChunks: cursor
);
await foreach (var chunk in chunks)
{
_ = await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10));
}
return new SyncMetadata
{
VaultKey = userSettings.VaultKey,
LastSynced = userSettings.LastSynced,
};
}
}
[MessagePack.MessagePackObject]
@@ -412,6 +258,8 @@ namespace Notesnook.API.Hubs
[MessagePack.Key("types")]
public string[] Types { get; set; }
[MessagePack.Key("ids")]
public string[] Ids { get; set; }
[MessagePack.Key("total")]
public int Total { get; set; }
@@ -441,33 +289,4 @@ namespace Notesnook.API.Hubs
[MessagePack.Key("current")]
public int Current { get; set; }
}
[MessagePack.MessagePackObject]
public struct SyncTransferItemV2
{
[MessagePack.Key("items")]
[JsonPropertyName("items")]
public IEnumerable<SyncItem> Items { get; set; }
[MessagePack.Key("type")]
[JsonPropertyName("type")]
public string Type { get; set; }
[MessagePack.Key("count")]
[JsonPropertyName("count")]
public int Count { get; set; }
}
[MessagePack.MessagePackObject]
public struct SyncMetadata
{
[MessagePack.Key("vaultKey")]
[JsonPropertyName("vaultKey")]
public EncryptedData VaultKey { get; set; }
[MessagePack.Key("lastSynced")]
[JsonPropertyName("lastSynced")]
public long LastSynced { get; set; }
// [MessagePack.Key("total")]
// public long TotalItems { get; set; }
}
}
-328
View File
@@ -1,328 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using MongoDB.Driver;
using Notesnook.API.Authorization;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Services;
using Streetwriters.Data.Interfaces;
namespace Notesnook.API.Hubs
{
public interface ISyncV2HubClient
{
Task<bool> SendItems(SyncTransferItemV2 transferItem);
Task<bool> SendVaultKey(EncryptedData vaultKey);
Task<bool> SendMonographs(IEnumerable<MonographMetadata> monographs);
Task<bool> SendInboxItems(IEnumerable<InboxSyncItem> inboxItems);
Task PushCompleted();
}
[Authorize]
public class SyncV2Hub : Hub<ISyncV2HubClient>
{
private ISyncItemsRepositoryAccessor Repositories { get; }
private readonly IUnitOfWork unit;
private static readonly string[] CollectionKeys = [
"settingitem",
"attachment",
"note",
"notebook",
"content",
"shortcut",
"reminder",
"color",
"tag",
"vault",
"relation", // relations must sync at the end to prevent invalid state
];
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
private readonly Func<string, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
{
Repositories = syncItemsRepositoryAccessor;
unit = unitOfWork;
Collections = [
Repositories.Settings.FindItemsById,
Repositories.Attachments.FindItemsById,
Repositories.Notes.FindItemsById,
Repositories.Notebooks.FindItemsById,
Repositories.Contents.FindItemsById,
Repositories.Shortcuts.FindItemsById,
Repositories.Reminders.FindItemsById,
Repositories.Colors.FindItemsById,
Repositories.Tags.FindItemsById,
Repositories.Vaults.FindItemsById,
Repositories.Relations.FindItemsById,
];
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
{ "settingitem", Repositories.Settings.UpsertMany },
{ "attachment", Repositories.Attachments.UpsertMany },
{ "note", Repositories.Notes.UpsertMany },
{ "notebook", Repositories.Notebooks.UpsertMany },
{ "content", Repositories.Contents.UpsertMany },
{ "shortcut", Repositories.Shortcuts.UpsertMany },
{ "reminder", Repositories.Reminders.UpsertMany },
{ "relation", Repositories.Relations.UpsertMany },
{ "color", Repositories.Colors.UpsertMany },
{ "vault", Repositories.Vaults.UpsertMany },
{ "tag", Repositories.Tags.UpsertMany },
}.ToFrozenDictionary();
}
public override async Task OnConnectedAsync()
{
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2"));
if (!result.Succeeded)
{
var reason = result.AuthorizationFailure?.FailureReasons.FirstOrDefault();
throw new HubException(reason?.Message ?? "Unauthorized");
}
var id = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
await Groups.AddToGroupAsync(Context.ConnectionId, id);
await base.OnConnectedAsync();
}
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
{
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
SyncEventCounterSource.Log.PushV2();
var stopwatch = Stopwatch.StartNew();
try
{
var UpsertItems = UpsertActionsMap[pushItem.Type] ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
UpsertItems(pushItem.Items, userId, 1);
if (!await unit.Commit()) return 0;
new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices(pushItem.Items.Select((i) => $"{i.ItemId}:{pushItem.Type}").ToList());
return 1;
}
finally
{
SyncEventCounterSource.Log.RecordPushDuration(stopwatch.ElapsedMilliseconds);
}
}
public async Task<bool> PushCompleted()
{
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
await Clients.OthersInGroup(userId).PushCompleted();
return true;
}
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, string[] ids, int size, bool resetSync, long maxBytes)
{
var itemsProcessed = 0;
for (int i = 0; i < Collections.Length; i++)
{
var type = CollectionKeys[i];
var filteredIds = ids.Where((id) => id.EndsWith($":{type}")).Select((id) => id.Split(":")[0]).ToArray();
if (!resetSync && filteredIds.Length == 0) continue;
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
var chunk = new List<SyncItem>();
long totalBytes = 0;
long METADATA_BYTES = 5 * 1024;
while (await cursor.MoveNextAsync())
{
foreach (var item in cursor.Current)
{
chunk.Add(item);
totalBytes += item.Length + METADATA_BYTES;
if (totalBytes >= maxBytes)
{
itemsProcessed += chunk.Count;
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = itemsProcessed
};
totalBytes = 0;
chunk.Clear();
}
}
}
if (chunk.Count > 0)
{
itemsProcessed += chunk.Count;
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = itemsProcessed
};
}
}
}
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
{
return await HandleRequestFetch(deviceId, false, false);
}
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
{
return await HandleRequestFetch(deviceId, true, false);
}
public async Task<SyncV2Metadata> RequestFetchV3(string deviceId)
{
return await HandleRequestFetch(deviceId, true, true);
}
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems)
{
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
SyncEventCounterSource.Log.FetchV2();
var device = new SyncDevice(userId, deviceId);
var deviceService = new SyncDeviceService(device);
if (!deviceService.IsDeviceRegistered()) deviceService.RegisterDevice();
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var isResetSync = deviceService.IsSyncReset();
if (!deviceService.IsUnsynced() &&
!deviceService.IsSyncPending() &&
!isResetSync)
return new SyncV2Metadata { Synced = true };
var stopwatch = Stopwatch.StartNew();
try
{
string[] ids = deviceService.FetchUnsyncedIds();
var chunks = PrepareChunks(
userId,
ids,
size: 1000,
resetSync: isResetSync,
maxBytes: 7 * 1024 * 1024
);
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId.Equals(userId));
if (userSettings.VaultKey != null)
{
if (!await Clients.Caller.SendVaultKey(userSettings.VaultKey).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected vault key.");
}
await foreach (var chunk in chunks)
{
if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items.");
if (!isResetSync)
{
var syncedIds = chunk.Items.Select((i) => $"{i.ItemId}:{chunk.Type}").ToHashSet();
ids = ids.Where((id) => !syncedIds.Contains(id)).ToArray();
deviceService.WritePendingIds(ids);
}
}
if (includeMonographs)
{
var isSyncingMonographsForFirstTime = !device.HasInitialMonographsSync;
var unsyncedMonographs = ids.Where((id) => id.EndsWith(":monograph")).ToHashSet();
var unsyncedMonographIds = unsyncedMonographs.Select((id) => id.Split(":")[0]).ToArray();
FilterDefinition<Monograph> filter = isResetSync || isSyncingMonographsForFirstTime
? Builders<Monograph>.Filter.Eq("UserId", userId)
: Builders<Monograph>.Filter.And(
Builders<Monograph>.Filter.Eq("UserId", userId),
Builders<Monograph>.Filter.Or(
Builders<Monograph>.Filter.In("ItemId", unsyncedMonographIds),
Builders<Monograph>.Filter.In("_id", unsyncedMonographIds)
)
);
var userMonographs = await Repositories.Monographs.Collection.Find(filter).Project((m) => new MonographMetadata
{
DatePublished = m.DatePublished,
Deleted = m.Deleted,
Password = m.Password,
SelfDestruct = m.SelfDestruct,
Title = m.Title,
ItemId = m.ItemId ?? m.Id.ToString(),
}).ToListAsync();
if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
throw new HubException("Client rejected monographs.");
device.HasInitialMonographsSync = true;
}
if (includeInboxItems)
{
var unsyncedInboxItems = ids.Where((id) => id.EndsWith(":inboxItems")).ToHashSet();
var unsyncedInboxItemIds = unsyncedInboxItems.Select((id) => id.Split(":")[0]).ToArray();
var userInboxItems = isResetSync
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId));
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
{
throw new HubException("Client rejected inbox items.");
}
}
deviceService.Reset();
return new SyncV2Metadata
{
Synced = true,
};
}
finally
{
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
}
}
}
[MessagePack.MessagePackObject]
public struct SyncV2Metadata
{
[MessagePack.Key("synced")]
[JsonPropertyName("synced")]
public bool Synced { get; set; }
}
}
+4 -5
View File
@@ -30,11 +30,10 @@ namespace Notesnook.API.Interfaces
{
Task DeleteObjectAsync(string userId, string name);
Task DeleteDirectoryAsync(string userId);
Task<long> GetObjectSizeAsync(string userId, string name);
string? GetUploadObjectUrl(string userId, string name);
string? GetInternalUploadObjectUrl(string userId, string name);
Task<string?> GetDownloadObjectUrl(string userId, string name);
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null);
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);
}
@@ -18,18 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Runtime.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Notesnook.API.Models;
using Streetwriters.Common.Attributes;
using Streetwriters.Common.Converters;
using Streetwriters.Common.Interfaces;
namespace Notesnook.API.Models
namespace Notesnook.API.Interfaces
{
public class MonographContent
[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<ISyncItem, SyncItem>))]
[JsonInterfaceConverter(typeof(InterfaceConverter<ISyncItem, SyncItem>))]
public interface ISyncItem
{
[JsonPropertyName("data")]
public string Data { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
long DateSynced
{
get; set;
}
string UserId { get; set; }
string Algorithm { get; set; }
string IV { get; set; }
}
}
@@ -26,21 +26,15 @@ namespace Notesnook.API.Interfaces
{
public interface ISyncItemsRepositoryAccessor
{
SyncItemsRepository Notes { get; }
SyncItemsRepository Notebooks { get; }
SyncItemsRepository Shortcuts { get; }
SyncItemsRepository Reminders { get; }
SyncItemsRepository Relations { get; }
SyncItemsRepository Contents { get; }
SyncItemsRepository LegacySettings { get; }
SyncItemsRepository Attachments { get; }
SyncItemsRepository Settings { get; }
SyncItemsRepository Colors { get; }
SyncItemsRepository Vaults { get; }
SyncItemsRepository Tags { get; }
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; }
Repository<InboxApiKey> InboxApiKey { get; }
Repository<InboxSyncItem> InboxItems { get; }
}
}
+5 -5
View File
@@ -17,19 +17,19 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Threading;
using System.Threading.Tasks;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Streetwriters.Common.Interfaces;
namespace Notesnook.API.Interfaces
{
public interface IUserService
{
Task CreateUserAsync();
Task DeleteUserAsync(string userId);
Task DeleteUserAsync(string userId, string jti, string password);
Task<bool> DeleteUserAsync(string userId, string jti);
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
Task<UserResponse> GetUserAsync(string userId);
Task SetUserKeysAsync(string userId, UserKeys keys);
Task<UserResponse> GetUserAsync(bool repair = true);
Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key);
}
}
-64
View File
@@ -1,64 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Quartz;
namespace Notesnook.API.Jobs
{
public class DeviceCleanupJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = 100,
CancellationToken = context.CancellationToken,
};
Parallel.ForEach(Directory.EnumerateDirectories("sync"), parallelOptions, (userDir, ct) =>
{
foreach (var device in Directory.EnumerateDirectories(userDir))
{
string lastAccessFile = Path.Combine(device, "LastAccessTime");
try
{
if (!File.Exists(lastAccessFile))
{
Directory.Delete(device, true);
continue;
}
string content = File.ReadAllText(lastAccessFile);
if (!long.TryParse(content, out long lastAccessTime) || lastAccessTime <= 0)
{
Directory.Delete(device, true);
continue;
}
DateTimeOffset accessTime;
try
{
accessTime = DateTimeOffset.FromUnixTimeMilliseconds(lastAccessTime);
}
catch (Exception)
{
Directory.Delete(device, true);
continue;
}
// If the device hasn't been accessed for more than one month, delete it.
if (accessTime.AddMonths(1) < DateTimeOffset.UtcNow)
{
Directory.Delete(device, true);
}
}
catch (Exception ex)
{
// Log the error and continue processing other directories.
Console.Error.WriteLine($"Error processing device '{device}': {ex.Message}");
}
}
});
}
}
}
+8 -2
View File
@@ -17,11 +17,17 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
namespace Notesnook.API.Models
{
public class Algorithms
{
public static string Default => "xcha-argon2i13-7";
public static string XSAL_X25519_7 => "xsal-x25519-7";
public const string Default = "xcha-argon2i13-7";
static readonly List<string> ALGORITHMS = new List<string> { Algorithms.Default };
public static bool IsValidAlgorithm(string algorithm)
{
return ALGORITHMS.Contains(algorithm);
}
}
}
+2
View File
@@ -22,9 +22,11 @@ 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()
@@ -1,30 +0,0 @@
using System.Collections.Generic;
using Amazon.S3.Model;
namespace Notesnook.API.Models;
public class CompleteMultipartUploadRequestWrapper
{
public string Key { get; set; }
public List<PartETagWrapper> PartETags { get; set; }
public string UploadId { get; set; }
public CompleteMultipartUploadRequest ToRequest()
{
CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest();
completeMultipartUploadRequest.Key = Key;
completeMultipartUploadRequest.UploadId = UploadId;
completeMultipartUploadRequest.PartETags = [];
foreach (var partETagWrapper in PartETags)
{
var partETag = new PartETag
{
PartNumber = partETagWrapper.PartNumber,
ETag = partETagWrapper.ETag
};
completeMultipartUploadRequest.PartETags.Add(partETag);
}
return completeMultipartUploadRequest;
}
}
-13
View File
@@ -1,13 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Notesnook.API.Models
{
public class DeleteAccountForm
{
[Required]
public string Password
{
get; set;
}
}
}
-19
View File
@@ -25,10 +25,8 @@ using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class EncryptedData : IEncrypted
{
[MessagePack.Key("iv")]
[JsonPropertyName("iv")]
[BsonElement("iv")]
[DataMember(Name = "iv")]
@@ -37,7 +35,6 @@ namespace Notesnook.API.Models
get; set;
}
[MessagePack.Key("cipher")]
[JsonPropertyName("cipher")]
[BsonElement("cipher")]
[DataMember(Name = "cipher")]
@@ -46,30 +43,14 @@ namespace Notesnook.API.Models
get; set;
}
[MessagePack.Key("length")]
[JsonPropertyName("length")]
[BsonElement("length")]
[DataMember(Name = "length")]
public long Length { get; set; }
[MessagePack.Key("salt")]
[JsonPropertyName("salt")]
[BsonElement("salt")]
[DataMember(Name = "salt")]
public string Salt { get; set; }
public override bool Equals(object obj)
{
if (obj is EncryptedData encryptedData)
{
return IV == encryptedData.IV && Salt == encryptedData.Salt && Cipher == encryptedData.Cipher && Length == encryptedData.Length;
}
return base.Equals(obj);
}
public bool IsEmpty()
{
return this.Cipher == null && this.IV == null && this.Length == 0 && this.Salt == null;
}
}
}
-60
View File
@@ -1,60 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using NanoidDotNet;
namespace Notesnook.API.Models
{
public class InboxApiKey
{
public InboxApiKey()
{
var random = Nanoid.Generate(size: 64);
Key = "nn__" + random;
}
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id { get; set; }
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("key")]
public string Key { get; set; }
[JsonPropertyName("dateCreated")]
public long DateCreated { get; set; }
[JsonPropertyName("expiryDate")]
public long ExpiryDate { get; set; }
[JsonPropertyName("lastUsedAt")]
public long LastUsedAt { get; set; }
}
}
-70
View File
@@ -1,70 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class InboxSyncItem : SyncItem
{
[DataMember(Name = "key")]
[JsonPropertyName("key")]
[MessagePack.Key("key")]
[Required]
public EncryptedKey Key
{
get; set;
}
}
[MessagePack.MessagePackObject]
public class EncryptedKey
{
[DataMember(Name = "alg")]
[JsonPropertyName("alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
[DataMember(Name = "cipher")]
[JsonPropertyName("cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
[JsonPropertyName("length")]
[DataMember(Name = "length")]
[MessagePack.Key("length")]
[Required]
public long Length
{
get; set;
}
}
}
+9 -43
View File
@@ -20,77 +20,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Runtime.Serialization;
using Notesnook.API.Interfaces;
using Streetwriters.Data.Attributes;
namespace Notesnook.API.Models
{
public class ObjectWithId
{
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id
{
get; set;
}
public string ItemId
{
get; set;
}
}
public class Monograph
[BsonCollection("notesnook", "monographs")]
public class Monograph : IMonograph
{
public Monograph()
{
Id = ObjectId.GenerateNewId().ToString();
}
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string ItemId
{
get; set;
}
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id
{
get; set;
}
public string Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
public string UserId { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
[JsonPropertyName("encryptedContent")]
public EncryptedData? EncryptedContent { get; set; }
public EncryptedData EncryptedContent { get; set; }
[JsonPropertyName("datePublished")]
public long DatePublished { get; set; }
[JsonPropertyName("content")]
[BsonIgnore]
public string? Content { get; set; }
public string Content { get; set; }
[JsonIgnore]
public byte[]? CompressedContent { get; set; }
[JsonPropertyName("password")]
public EncryptedData? Password { get; set; }
[JsonPropertyName("deleted")]
public bool Deleted { get; set; }
public byte[] CompressedContent { get; set; }
}
}
-52
View File
@@ -1,52 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Runtime.Serialization;
namespace Notesnook.API.Models
{
public class MonographMetadata
{
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public required string ItemId
{
get; set;
}
[JsonPropertyName("title")]
public required string Title { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
[JsonPropertyName("datePublished")]
public long DatePublished { get; set; }
[JsonPropertyName("password")]
public EncryptedData? Password { get; set; }
[JsonPropertyName("deleted")]
public bool Deleted { get; set; }
}
}
-7
View File
@@ -1,7 +0,0 @@
namespace Notesnook.API.Models;
public class PartETagWrapper
{
public int PartNumber { get; set; }
public string ETag { get; set; }
}
+3 -18
View File
@@ -1,4 +1,3 @@
using System.Net.Http;
using System.Text.Json.Serialization;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
@@ -8,30 +7,16 @@ namespace Notesnook.API.Models.Responses
public class UserResponse : UserModel, IResponse
{
[JsonPropertyName("salt")]
public string? Salt { get; set; }
public string Salt { get; set; }
[JsonPropertyName("attachmentsKey")]
public EncryptedData? AttachmentsKey { get; set; }
[JsonPropertyName("monographPasswordsKey")]
public EncryptedData? MonographPasswordsKey { get; set; }
[JsonPropertyName("inboxKeys")]
public InboxKeys? InboxKeys { get; set; }
public EncryptedData AttachmentsKey { get; set; }
[JsonPropertyName("subscription")]
public Subscription? Subscription { get; set; }
[JsonPropertyName("storageUsed")]
public long StorageUsed { get; set; }
[JsonPropertyName("totalStorage")]
public long TotalStorage { get; set; }
public ISubscription Subscription { get; set; }
[JsonIgnore]
public bool Success { get; set; }
public int StatusCode { get; set; }
[JsonIgnore]
public HttpContent? Content { get; set; }
}
}
+21 -99
View File
@@ -17,24 +17,19 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;
using Notesnook.API.Interfaces;
using Streetwriters.Data.Attributes;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class SyncItem
public class SyncItem : ISyncItem
{
[IgnoreDataMember]
[MessagePack.IgnoreMember]
[JsonPropertyName("dateSynced")]
public long DateSynced
{
@@ -43,15 +38,13 @@ namespace Notesnook.API.Models
[DataMember(Name = "userId")]
[JsonPropertyName("userId")]
[MessagePack.Key("userId")]
public string? UserId
public string UserId
{
get; set;
}
[JsonPropertyName("iv")]
[DataMember(Name = "iv")]
[MessagePack.Key("iv")]
[Required]
public string IV
{
@@ -61,7 +54,6 @@ namespace Notesnook.API.Models
[JsonPropertyName("cipher")]
[DataMember(Name = "cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
@@ -70,8 +62,7 @@ namespace Notesnook.API.Models
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string? ItemId
public string ItemId
{
get; set;
}
@@ -80,7 +71,6 @@ namespace Notesnook.API.Models
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public ObjectId Id
{
get; set;
@@ -88,7 +78,6 @@ namespace Notesnook.API.Models
[JsonPropertyName("length")]
[DataMember(Name = "length")]
[MessagePack.Key("length")]
[Required]
public long Length
{
@@ -97,7 +86,6 @@ namespace Notesnook.API.Models
[JsonPropertyName("v")]
[DataMember(Name = "v")]
[MessagePack.Key("v")]
[Required]
public double Version
{
@@ -106,100 +94,34 @@ namespace Notesnook.API.Models
[JsonPropertyName("alg")]
[DataMember(Name = "alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
} = Algorithms.Default;
}
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
{
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, SyncItem value)
{
var writer = context.Writer;
writer.WriteStartDocument();
[BsonCollection("notesnook", "attachments")]
public class Attachment : SyncItem { }
if (value.Id != ObjectId.Empty)
{
writer.WriteName("_id");
writer.WriteObjectId(value.Id);
}
[BsonCollection("notesnook", "content")]
public class Content : SyncItem { }
writer.WriteName("DateSynced");
writer.WriteInt64(value.DateSynced);
[BsonCollection("notesnook", "notes")]
public class Note : SyncItem { }
writer.WriteName("UserId");
writer.WriteString(value.UserId);
[BsonCollection("notesnook", "notebooks")]
public class Notebook : SyncItem { }
writer.WriteName("IV");
writer.WriteString(value.IV);
[BsonCollection("notesnook", "relations")]
public class Relation : SyncItem { }
writer.WriteName("Cipher");
writer.WriteString(value.Cipher);
[BsonCollection("notesnook", "reminders")]
public class Reminder : SyncItem { }
writer.WriteName("ItemId");
writer.WriteString(value.ItemId);
[BsonCollection("notesnook", "settings")]
public class Setting : SyncItem { }
writer.WriteName("Length");
writer.WriteInt64(value.Length);
writer.WriteName("Version");
writer.WriteDouble(value.Version);
writer.WriteName("Algorithm");
writer.WriteString(value.Algorithm);
writer.WriteEndDocument();
}
public override SyncItem Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var syncItem = new SyncItem();
var bsonReader = context.Reader;
bsonReader.ReadStartDocument();
while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
{
var fieldName = bsonReader.ReadName();
switch (fieldName)
{
case "DateSynced":
syncItem.DateSynced = bsonReader.ReadInt64();
break;
case "UserId":
syncItem.UserId = bsonReader.ReadString();
break;
case "IV":
syncItem.IV = bsonReader.ReadString();
break;
case "Cipher":
syncItem.Cipher = bsonReader.ReadString();
break;
case "ItemId":
syncItem.ItemId = bsonReader.ReadString();
break;
case "_id":
syncItem.Id = bsonReader.ReadObjectId();
break;
case "Length":
syncItem.Length = bsonReader.ReadInt64();
break;
case "Version":
syncItem.Version = bsonReader.ReadDouble();
break;
case "Algorithm":
syncItem.Algorithm = bsonReader.ReadString();
break;
default:
bsonReader.SkipValue();
break;
}
}
bsonReader.ReadEndDocument();
return syncItem;
}
}
[BsonCollection("notesnook", "shortcuts")]
public class Shortcut : SyncItem { }
}
+4 -11
View File
@@ -20,15 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Notesnook.API.Interfaces;
using Streetwriters.Data.Attributes;
namespace Notesnook.API.Models
{
public class Limit
{
public long Value { get; set; }
public long UpdatedAt { get; set; }
}
[BsonCollection("notesnook", "user_settings")]
public class UserSettings : IUserSettings
{
public UserSettings()
@@ -38,11 +34,8 @@ namespace Notesnook.API.Models
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; }
public EncryptedData? MonographPasswordsKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
public Limit StorageLimit { get; set; }
public EncryptedData VaultKey { get; set; }
public EncryptedData AttachmentsKey { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
+8 -12
View File
@@ -1,28 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<StartupObject>Notesnook.API.Program</StartupObject>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
<PackageReference Include="AWSSDK.Core" Version="3.7.12.5" />
<PackageReference Include="DotNetEnv" Version="2.3.0" />
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
<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.310.8" />
<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" />
<PackageReference Include="Nanoid" Version="3.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-alpha.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="Quartz" Version="3.5.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
+5 -4
View File
@@ -17,13 +17,15 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if !DEBUG
using System.Net;
#endif
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Streetwriters.Common;
using System.Net;
using System.Linq;
namespace Notesnook.API
{
@@ -31,7 +33,7 @@ namespace Notesnook.API
{
public static async Task Main(string[] args)
{
#if (DEBUG || STAGING)
#if DEBUG
DotNetEnv.Env.TraversePath().Load(".env.local");
#else
DotNetEnv.Env.TraversePath().Load(".env");
@@ -57,7 +59,6 @@ namespace Notesnook.API
listenerOptions.UseHttps(Servers.NotesnookAPI.SSLCertificate);
});
}
options.Listen(IPAddress.Parse("127.0.0.1"), 5067);
});
});
}
+48 -152
View File
@@ -19,186 +19,82 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.VisualBasic;
using MongoDB.Bson;
using MongoDB.Driver;
using Notesnook.API.Hubs;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories;
using Streetwriters.Data.Attributes;
namespace Notesnook.API.Repositories
{
public class SyncItemsRepository : Repository<SyncItem>
public class SyncItemsRepository<T> where T : SyncItem
{
private readonly string collectionName;
public SyncItemsRepository(IDbContext dbContext, IMongoCollection<SyncItem> collection) : base(dbContext, collection)
const string BASE_DATA_DIR = "data";
private string GetCollectionName()
{
this.collectionName = collection.CollectionNamespace.CollectionName;
var attribute = (BsonCollectionAttribute)typeof(T).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 attribute.CollectionName;
}
private readonly List<string> ALGORITHMS = [Algorithms.Default, Algorithms.XSAL_X25519_7];
private bool IsValidAlgorithm(string algorithm)
private string GetUserDirectoryPath(string userId)
{
return ALGORITHMS.Contains(algorithm);
return System.IO.Path.Join(BASE_DATA_DIR, userId, GetCollectionName());
}
public Task<long> CountItemsSyncedAfterAsync(string userId, long timestamp)
private IEnumerable<string> EnumerateItems(string userId, string searchPattern = "*")
{
var filter = Builders<SyncItem>.Filter.And(Builders<SyncItem>.Filter.Gt("DateSynced", timestamp), Builders<SyncItem>.Filter.Eq("UserId", userId));
return Collection.CountDocumentsAsync(filter);
}
public Task<IAsyncCursor<SyncItem>> FindItemsSyncedAfter(string userId, long timestamp, int batchSize)
{
var filter = Builders<SyncItem>.Filter.And(Builders<SyncItem>.Filter.Gt("DateSynced", timestamp), Builders<SyncItem>.Filter.Eq("UserId", userId));
return Collection.FindAsync(filter, new FindOptions<SyncItem>
try
{
BatchSize = batchSize,
AllowDiskUse = true,
AllowPartialResults = false,
NoCursorTimeout = true,
Sort = new SortDefinitionBuilder<SyncItem>().Ascending("_id")
});
return System.IO.Directory.EnumerateFiles(GetUserDirectoryPath(userId), searchPattern, System.IO.SearchOption.TopDirectoryOnly);
}
catch
{
return new string[] { };
}
}
public Task<IAsyncCursor<SyncItem>> FindItemsById(string userId, IEnumerable<string> ids, bool all, int batchSize)
private string FindItemById(string userId, string id)
{
var filters = new List<FilterDefinition<SyncItem>>(new[] { Builders<SyncItem>.Filter.Eq("UserId", userId) });
if (!all) filters.Add(Builders<SyncItem>.Filter.In("ItemId", ids));
return Collection.FindAsync(Builders<SyncItem>.Filter.And(filters), new FindOptions<SyncItem>
try
{
BatchSize = batchSize,
AllowDiskUse = true,
AllowPartialResults = false,
NoCursorTimeout = true
var files = Directory.GetFiles(GetUserDirectoryPath(userId), $"{id}-*", System.IO.SearchOption.TopDirectoryOnly);
return files.Length > 0 ? files[0] : null;
}
catch
{
return null;
}
}
public async Task<IEnumerable<string>> GetItemsSyncedAfterAsync(string userId, long timestamp)
{
var items = new List<string>();
await Parallel.ForEachAsync(EnumerateItems(userId), async (file, ct) =>
{
var parts = file.Split("-");
var id = parts[0];
var dateSynced = long.Parse(parts[1]);
if (dateSynced > timestamp) items.Add(await File.ReadAllTextAsync(file));
});
return items;
}
public void DeleteByUserId(string userId)
{
var filter = Builders<SyncItem>.Filter.Eq("UserId", userId);
var writes = new List<WriteModel<SyncItem>>
{
new DeleteManyModel<SyncItem>(filter)
};
dbContext.AddCommand((handle, ct) => Collection.BulkWriteAsync(handle, writes, options: null, ct));
Directory.Delete(GetUserDirectoryPath(userId), true);
}
public void Upsert(SyncItem item, string userId, long dateSynced)
public async Task UpsertAsync(string id, string 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}");
}
// Handle case where the cipher is corrupted.
if (!IsBase64String(item.Cipher))
{
Slogger<SyncHub>.Error("Upsert", "Corrupted", item.ItemId, item.Length.ToString(), item.Cipher);
throw new Exception($"Corrupted item \"{item.ItemId}\" in collection \"{this.collectionName}\". Please report this error to support@streetwriters.co.");
}
if (item.ItemId == null)
throw new Exception($"Item does not have an ItemId.");
item.DateSynced = dateSynced;
item.UserId = userId;
var filter = Builders<SyncItem>.Filter.And(
Builders<SyncItem>.Filter.Eq("UserId", userId),
Builders<SyncItem>.Filter.Eq("ItemId", item.ItemId)
);
dbContext.AddCommand((handle, ct) => Collection.ReplaceOneAsync(handle, filter, item, new ReplaceOptions { IsUpsert = true }, ct));
}
public void UpsertMany(IEnumerable<SyncItem> items, string userId, long dateSynced)
{
var userIdFilter = Builders<SyncItem>.Filter.Eq("UserId", userId);
var writes = new List<WriteModel<SyncItem>>();
foreach (var item in items)
{
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}");
}
// Handle case where the cipher is corrupted.
if (!IsBase64String(item.Cipher))
{
Slogger<SyncHub>.Error("Upsert", "Corrupted", item.ItemId, item.Length.ToString(), item.Cipher);
throw new Exception($"Corrupted item \"{item.ItemId}\" in collection \"{this.collectionName}\". Please report this error to support@streetwriters.co.");
}
if (item.ItemId == null)
throw new Exception($"Item does not have an ItemId.");
var filter = Builders<SyncItem>.Filter.And(
userIdFilter,
Builders<SyncItem>.Filter.Eq("ItemId", item.ItemId)
);
item.DateSynced = dateSynced;
item.UserId = userId;
writes.Add(new ReplaceOneModel<SyncItem>(filter, item)
{
IsUpsert = true
});
}
dbContext.AddCommand((handle, ct) => Collection.BulkWriteAsync(handle, writes, options: new BulkWriteOptions { IsOrdered = false }, ct));
}
private static bool IsBase64String(string value)
{
if (value == null || value.Length == 0 || value.Contains(' ') || value.Contains('\t') || value.Contains('\r') || value.Contains('\n'))
return false;
var index = value.Length - 1;
if (value[index] == '=')
index--;
if (value[index] == '=')
index--;
for (var i = 0; i <= index; i++)
if (IsInvalidBase64Char(value[i]))
return false;
return true;
}
private static bool IsInvalidBase64Char(char value)
{
var code = (int)value;
// 1 - 9
if (code >= 48 && code <= 57)
return false;
// A - Z
if (code >= 65 && code <= 90)
return false;
// a - z
if (code >= 97 && code <= 122)
return false;
// - & _
return code != 45 && code != 95;
Directory.CreateDirectory(GetUserDirectoryPath(userId));
var oldPath = FindItemById(userId, id);
var newPath = Path.Join(GetUserDirectoryPath(userId), $"{id}-{dateSynced}");
await File.WriteAllTextAsync(newPath, item);
if (oldPath != null) File.Delete(oldPath);
}
}
}
+30 -104
View File
@@ -28,13 +28,9 @@ using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Services
{
@@ -46,10 +42,8 @@ namespace Notesnook.API.Services
public class S3Service : IS3Service
{
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME ?? "";
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? "";
private readonly string BUCKET_NAME = "nn-attachments";
private AmazonS3Client S3Client { get; }
private ISyncItemsRepositoryAccessor Repositories { get; }
// When running in a dockerized environment the sync server doesn't have access
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
@@ -62,12 +56,11 @@ namespace Notesnook.API.Services
private AmazonS3Client S3InternalClient { get; }
private HttpClient httpClient = new HttpClient();
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
public S3Service()
{
Repositories = syncItemsRepositoryAccessor;
var config = new AmazonS3Config
{
#if (DEBUG || STAGING)
#if DEBUG
ServiceURL = Servers.S3Server.ToString(),
#else
ServiceURL = Constants.S3_SERVICE_URL,
@@ -77,7 +70,7 @@ namespace Notesnook.API.Services
SignatureMethod = SigningAlgorithm.HmacSHA256,
SignatureVersion = "4"
};
#if (DEBUG || STAGING)
#if DEBUG
S3Client = new AmazonS3Client("S3RVER", "S3RVER", config);
#else
S3Client = new AmazonS3Client(Constants.S3_ACCESS_KEY_ID, Constants.S3_ACCESS_KEY, config);
@@ -103,7 +96,7 @@ namespace Notesnook.API.Services
var objectName = GetFullObjectName(userId, name);
if (objectName == null) throw new Exception("Invalid object name."); ;
var response = await GetS3Client(S3ClientMode.INTERNAL).DeleteObjectAsync(GetBucketName(S3ClientMode.INTERNAL), objectName);
var response = await GetS3Client(S3ClientMode.INTERNAL).DeleteObjectAsync(BUCKET_NAME, objectName);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode)))
throw new Exception("Could not delete object.");
@@ -113,7 +106,7 @@ namespace Notesnook.API.Services
{
var request = new ListObjectsV2Request
{
BucketName = GetBucketName(S3ClientMode.INTERNAL),
BucketName = BUCKET_NAME,
Prefix = userId,
};
@@ -133,10 +126,10 @@ namespace Notesnook.API.Services
if (keys.Count <= 0) return;
var deleteObjectsResponse = await GetS3Client(S3ClientMode.INTERNAL)
var deleteObjectsResponse = await S3Client
.DeleteObjectsAsync(new DeleteObjectsRequest
{
BucketName = GetBucketName(S3ClientMode.INTERNAL),
BucketName = BUCKET_NAME,
Objects = keys,
});
@@ -144,52 +137,39 @@ namespace Notesnook.API.Services
throw new Exception("Could not delete directory.");
}
public async Task<long> GetObjectSizeAsync(string userId, string name)
public async Task<long?> GetObjectSizeAsync(string userId, string name)
{
var url = this.GetPresignedURL(userId, name, HttpVerb.HEAD, S3ClientMode.INTERNAL);
if (url == null) return 0;
if (url == null) return null;
var request = new HttpRequestMessage(HttpMethod.Head, url);
var response = await httpClient.SendAsync(request);
return response.Content.Headers.ContentLength ?? 0;
return response.Content.Headers.ContentLength;
}
public string? GetUploadObjectUrl(string userId, string name)
public string GetUploadObjectUrl(string userId, string name)
{
return this.GetPresignedURL(userId, name, HttpVerb.PUT);
var url = this.GetPresignedURL(userId, name, HttpVerb.PUT);
if (url == null) return null;
return url;
}
public string? GetInternalUploadObjectUrl(string userId, string name)
public string GetDownloadObjectUrl(string userId, string name)
{
return this.GetPresignedURL(userId, name, HttpVerb.PUT, S3ClientMode.INTERNAL);
}
public async Task<string?> GetDownloadObjectUrl(string userId, string name)
{
// var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
// var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
// var size = await GetObjectSizeAsync(userId, name);
// if (StorageHelper.IsFileSizeExceeded(subscription, size))
// {
// var fileSizeLimit = StorageHelper.GetFileSizeLimitForPlan(subscription);
// throw new Exception($"You cannot download files larger than {StorageHelper.FormatBytes(fileSizeLimit)} on this plan.");
// }
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)
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 GetS3Client(S3ClientMode.INTERNAL).InitiateMultipartUploadAsync(GetBucketName(S3ClientMode.INTERNAL), objectName);
var response = await GetS3Client(S3ClientMode.INTERNAL).InitiateMultipartUploadAsync(BUCKET_NAME, objectName);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to initiate multipart upload.");
uploadId = response.UploadId;
@@ -213,104 +193,56 @@ namespace Notesnook.API.Services
var objectName = GetFullObjectName(userId, name);
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
var response = await GetS3Client(S3ClientMode.INTERNAL).AbortMultipartUploadAsync(GetBucketName(S3ClientMode.INTERNAL), objectName, uploadId);
var response = await GetS3Client(S3ClientMode.INTERNAL).AbortMultipartUploadAsync(BUCKET_NAME, objectName, uploadId);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to abort multipart upload.");
}
private async Task<long> GetMultipartUploadSizeAsync(string userId, string key, string uploadId)
{
var objectName = GetFullObjectName(userId, key);
var parts = await GetS3Client(S3ClientMode.INTERNAL).ListPartsAsync(GetBucketName(S3ClientMode.INTERNAL), objectName, uploadId);
long totalSize = 0;
foreach (var part in parts.Parts)
{
totalSize += part.Size;
}
return totalSize;
}
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.");
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
if (userSettings == null)
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("User settings not found.");
}
if (!Constants.IS_SELF_HOSTED)
{
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
long fileSize = await GetMultipartUploadSizeAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("Max file size exceeded.");
}
userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 };
userSettings.StorageLimit.Value += fileSize;
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit))
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("Storage limit reached.");
}
}
uploadRequest.Key = objectName;
uploadRequest.BucketName = GetBucketName(S3ClientMode.INTERNAL);
uploadRequest.BucketName = BUCKET_NAME;
var response = await GetS3Client(S3ClientMode.INTERNAL).CompleteMultipartUploadAsync(uploadRequest);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
if (!Constants.IS_SELF_HOSTED)
{
userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
}
}
private string? GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
private string GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
{
var objectName = GetFullObjectName(userId, name);
if (userId == null || objectName == null) return null;
var client = GetS3Client(mode);
var request = new GetPreSignedUrlRequest
{
BucketName = GetBucketName(mode),
BucketName = BUCKET_NAME,
Expires = System.DateTime.Now.AddHours(1),
Verb = httpVerb,
Key = objectName,
#if (DEBUG || STAGING)
#if DEBUG
Protocol = Protocol.HTTP,
#else
Protocol = client.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
#endif
};
return client.GetPreSignedURL(request);
return GetS3Client(mode).GetPreSignedURL(request);
}
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber, S3ClientMode mode = S3ClientMode.EXTERNAL)
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber)
{
var client = GetS3Client(mode);
return client.GetPreSignedURL(new GetPreSignedUrlRequest
return GetS3Client().GetPreSignedURL(new GetPreSignedUrlRequest
{
BucketName = GetBucketName(mode),
BucketName = BUCKET_NAME,
Expires = System.DateTime.Now.AddHours(1),
Verb = HttpVerb.PUT,
Key = objectName,
PartNumber = partNumber,
UploadId = uploadId,
#if (DEBUG || STAGING)
#if DEBUG
Protocol = Protocol.HTTP,
#else
Protocol = client.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
#endif
});
}
@@ -331,11 +263,5 @@ namespace Notesnook.API.Services
if (mode == S3ClientMode.INTERNAL && S3InternalClient != null) return S3InternalClient;
return S3Client;
}
string GetBucketName(S3ClientMode mode = S3ClientMode.EXTERNAL)
{
if (mode == S3ClientMode.INTERNAL && S3InternalClient != null) return INTERNAL_BUCKET_NAME;
return BUCKET_NAME;
}
}
}
-243
View File
@@ -1,243 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Notesnook.API.Services
{
public struct SyncDevice(string userId, string deviceId)
{
public readonly string DeviceId => deviceId;
public readonly string UserId => userId;
public string UserSyncDirectoryPath = CreateFilePath(userId);
public string UserDeviceDirectoryPath = CreateFilePath(userId, deviceId);
public string PendingIdsFilePath = CreateFilePath(userId, deviceId, "pending");
public string UnsyncedIdsFilePath = CreateFilePath(userId, deviceId, "unsynced");
public string ResetSyncFilePath = CreateFilePath(userId, deviceId, "reset-sync");
public readonly long LastAccessTime
{
get => long.Parse(GetMetadata("LastAccessTime") ?? "0");
set => SetMetadata("LastAccessTime", value.ToString());
}
/// <summary>
/// Indicates if the monographs have been synced for the first time
/// ever on a device.
/// </summary>
public readonly bool HasInitialMonographsSync
{
get => !string.IsNullOrEmpty(GetMetadata("HasInitialMonographsSync"));
set => SetMetadata("HasInitialMonographsSync", value.ToString());
}
private static string CreateFilePath(string userId, string? deviceId = null, string? metadataKey = null)
{
return Path.Join("sync", userId, deviceId, metadataKey);
}
private readonly string? GetMetadata(string metadataKey)
{
var path = CreateFilePath(userId, deviceId, metadataKey);
if (!File.Exists(path)) return null;
return File.ReadAllText(path);
}
private readonly void SetMetadata(string metadataKey, string value)
{
try
{
var path = CreateFilePath(userId, deviceId, metadataKey);
File.WriteAllText(path, value);
}
catch (DirectoryNotFoundException) { }
}
}
public class SyncDeviceService(SyncDevice device)
{
public string[] GetUnsyncedIds()
{
try
{
return File.ReadAllLines(device.UnsyncedIdsFilePath);
}
catch { return []; }
}
public string[] GetUnsyncedIds(string deviceId)
{
try
{
return File.ReadAllLines(Path.Join(device.UserSyncDirectoryPath, deviceId, "unsynced"));
}
catch { return []; }
}
public string[] FetchUnsyncedIds()
{
if (IsSyncReset()) return [];
try
{
var unsyncedIds = GetUnsyncedIds();
lock (device.DeviceId)
{
if (IsSyncPending())
{
unsyncedIds = unsyncedIds.Union(File.ReadAllLines(device.PendingIdsFilePath)).ToArray();
}
if (unsyncedIds.Length == 0) return [];
File.Delete(device.UnsyncedIdsFilePath);
File.WriteAllLines(device.PendingIdsFilePath, unsyncedIds);
}
return unsyncedIds;
}
catch
{
return [];
}
}
public void WritePendingIds(IEnumerable<string> ids)
{
lock (device.DeviceId)
{
File.WriteAllLines(device.PendingIdsFilePath, ids);
}
}
public bool IsSyncReset()
{
return File.Exists(device.ResetSyncFilePath);
}
public bool IsSyncReset(string deviceId)
{
return File.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId, "reset-sync"));
}
public bool IsSyncPending()
{
return File.Exists(device.PendingIdsFilePath);
}
public bool IsUnsynced()
{
return File.Exists(device.UnsyncedIdsFilePath);
}
public void Reset()
{
try
{
lock (device.UserId)
{
File.Delete(device.ResetSyncFilePath);
File.Delete(device.PendingIdsFilePath);
}
}
catch (FileNotFoundException) { }
catch (DirectoryNotFoundException) { }
}
public bool IsDeviceRegistered()
{
return Directory.Exists(device.UserDeviceDirectoryPath);
}
public bool IsDeviceRegistered(string deviceId)
{
return Directory.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId));
}
public string[] ListDevices()
{
return Directory.GetDirectories(device.UserSyncDirectoryPath).Select((path) => path[(path.LastIndexOf(Path.DirectorySeparatorChar) + 1)..]).ToArray();
}
public void ResetDevices()
{
lock (device.UserId)
{
if (File.Exists(device.UserSyncDirectoryPath)) File.Delete(device.UserSyncDirectoryPath);
Directory.CreateDirectory(device.UserSyncDirectoryPath);
}
}
public void AddIdsToOtherDevices(List<string> ids)
{
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (string id in ListDevices())
{
if (id == device.DeviceId || IsSyncReset(id)) continue;
lock (id)
{
if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id));
var oldIds = GetUnsyncedIds(id);
File.WriteAllLines(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds));
}
}
}
public void AddIdsToAllDevices(List<string> ids)
{
foreach (var id in ListDevices())
{
if (IsSyncReset(id)) return;
lock (id)
{
if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id));
var oldIds = GetUnsyncedIds(id);
File.WriteAllLinesAsync(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds));
}
}
}
public void RegisterDevice()
{
lock (device.UserId)
{
if (Directory.Exists(device.UserDeviceDirectoryPath))
Directory.Delete(device.UserDeviceDirectoryPath, true);
Directory.CreateDirectory(device.UserDeviceDirectoryPath);
File.Create(device.ResetSyncFilePath).Close();
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
public void UnregisterDevice()
{
lock (device.UserId)
{
if (!Path.Exists(device.UserDeviceDirectoryPath)) return;
Directory.Delete(device.UserDeviceDirectoryPath, true);
}
}
}
}
+61 -114
View File
@@ -23,7 +23,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
@@ -64,52 +63,46 @@ namespace Notesnook.API.Services
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));
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,
StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 },
LastSynced = 0,
Salt = GetSalt()
});
if (!Constants.IS_SELF_HOSTED)
{
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionV2Topic, new CreateSubscriptionMessageV2
await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.CreateSubscriptionTopic, new CreateSubscriptionMessage
{
AppId = ApplicationType.NOTESNOOK,
Provider = SubscriptionProvider.STREETWRITERS,
Status = SubscriptionStatus.ACTIVE,
Plan = SubscriptionPlan.FREE,
Type = SubscriptionType.BASIC,
UserId = response.UserId,
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});
}
await Slogger<UserService>.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response));
}
public async Task<UserResponse> GetUserAsync(string userId)
public async Task<UserResponse> GetUserAsync(bool repair = true)
{
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
UserResponse response = await httpClient.ForwardAsync<UserResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account", HttpMethod.Get);
if (!response.Success) return response;
var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
Subscription? subscription = null;
ISubscription subscription = null;
if (Constants.IS_SELF_HOSTED)
{
subscription = new Subscription
{
AppId = ApplicationType.NOTESNOOK,
Provider = SubscriptionProvider.STREETWRITERS,
Plan = SubscriptionPlan.BELIEVER,
Status = SubscriptionStatus.ACTIVE,
UserId = user.UserId,
Type = SubscriptionType.PREMIUM,
UserId = response.UserId,
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
// this date doesn't matter as the subscription is static.
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds()
@@ -117,80 +110,61 @@ namespace Notesnook.API.Services
}
else
{
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
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()
};
}
subscription = subscriptionResponse.Subscription;
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
// reset user's attachment limit every month
if (userSettings.StorageLimit == null || DateTimeOffset.UtcNow.Month > DateTimeOffset.FromUnixTimeMilliseconds(userSettings.StorageLimit.UpdatedAt).Month)
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == response.UserId);
if (repair && userSettings == null)
{
userSettings.StorageLimit ??= new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 };
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == user.UserId);
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);
}
return new UserResponse
{
UserId = user.UserId,
Email = user.Email,
IsEmailConfirmed = user.IsEmailConfirmed,
MarketingConsent = user.MarketingConsent,
MFA = user.MFA,
PhoneNumber = user.PhoneNumber,
AttachmentsKey = userSettings.AttachmentsKey,
MonographPasswordsKey = userSettings.MonographPasswordsKey,
InboxKeys = userSettings.InboxKeys,
Salt = userSettings.Salt,
Subscription = subscription,
StorageUsed = userSettings.StorageLimit.Value,
TotalStorage = StorageHelper.GetStorageLimitForPlan(subscription),
Success = true,
StatusCode = 200
};
response.AttachmentsKey = userSettings.AttachmentsKey;
response.Salt = userSettings.Salt;
response.Subscription = subscription;
return response;
}
public async Task SetUserKeysAsync(string userId, UserKeys keys)
public async Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key)
{
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId) ?? throw new Exception("User not found.");
if (keys.AttachmentsKey != null)
{
userSettings.AttachmentsKey = keys.AttachmentsKey;
}
if (keys.MonographPasswordsKey != null)
{
userSettings.MonographPasswordsKey = keys.MonographPasswordsKey;
}
if (keys.InboxKeys != null)
{
if (keys.InboxKeys.Public == null || keys.InboxKeys.Private == null)
{
userSettings.InboxKeys = null;
await Repositories.InboxApiKey.DeleteManyAsync(t => t.UserId == userId);
}
else
{
userSettings.InboxKeys = keys.InboxKeys;
var defaultInboxKey = new InboxApiKey
{
UserId = userId,
Name = "Default",
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
LastUsedAt = 0
};
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
}
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
userSettings.AttachmentsKey = (EncryptedData)key;
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
}
public async Task DeleteUserAsync(string userId)
public async Task<bool> DeleteUserAsync(string userId, string jti)
{
new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices();
var cc = new CancellationTokenSource();
Repositories.Notes.DeleteByUserId(userId);
@@ -198,60 +172,40 @@ namespace Notesnook.API.Services
Repositories.Shortcuts.DeleteByUserId(userId);
Repositories.Contents.DeleteByUserId(userId);
Repositories.Settings.DeleteByUserId(userId);
Repositories.LegacySettings.DeleteByUserId(userId);
Repositories.Attachments.DeleteByUserId(userId);
Repositories.Reminders.DeleteByUserId(userId);
Repositories.Relations.DeleteByUserId(userId);
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
var result = await unit.Commit();
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString());
if (!result) throw new Exception("Could not delete user data.");
if (!Constants.IS_SELF_HOSTED)
{
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
{
AppId = ApplicationType.NOTESNOOK,
UserId = userId
});
}
await S3Service.DeleteDirectoryAsync(userId);
}
public async Task DeleteUserAsync(string userId, string jti, string password)
{
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "Deleting user account", userId);
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
await DeleteUserAsync(userId);
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
{
SendToAll = false,
OriginTokenId = jti,
UserId = userId,
Message = new Message
{
Type = "logout",
Data = JsonSerializer.Serialize(new { reason = "Account deleted." })
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)
{
new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices();
var cc = new CancellationTokenSource();
Repositories.Notes.DeleteByUserId(userId);
@@ -259,23 +213,16 @@ namespace Notesnook.API.Services
Repositories.Shortcuts.DeleteByUserId(userId);
Repositories.Contents.DeleteByUserId(userId);
Repositories.Settings.DeleteByUserId(userId);
Repositories.LegacySettings.DeleteByUserId(userId);
Repositories.Attachments.DeleteByUserId(userId);
Repositories.Reminders.DeleteByUserId(userId);
Repositories.Relations.DeleteByUserId(userId);
Repositories.Colors.DeleteByUserId(userId);
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
if (!await unit.Commit()) return false;
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
userSettings.AttachmentsKey = null;
userSettings.MonographPasswordsKey = null;
userSettings.VaultKey = null;
userSettings.InboxKeys = null;
userSettings.LastSynced = 0;
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
@@ -286,7 +233,7 @@ namespace Notesnook.API.Services
return true;
}
private static string GetSalt()
private string GetSalt()
{
byte[] salt = new byte[16];
Rng.GetNonZeroBytes(salt);
+40 -97
View File
@@ -34,7 +34,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -48,19 +47,13 @@ using Notesnook.API.Authorization;
using Notesnook.API.Extensions;
using Notesnook.API.Hubs;
using Notesnook.API.Interfaces;
using Notesnook.API.Jobs;
using Notesnook.API.Models;
using Notesnook.API.Repositories;
using Notesnook.API.Services;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using Quartz;
using Streetwriters.Common;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
using Streetwriters.Common.Services;
using Streetwriters.Data;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Interfaces;
@@ -80,11 +73,12 @@ namespace Notesnook.API
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(MongoDbContext.CreateMongoDbClient(new DbSettings
var dbSettings = new DbSettings
{
ConnectionString = Constants.MONGODB_CONNECTION_STRING,
DatabaseName = Constants.MONGODB_DATABASE_NAME
}));
};
services.AddSingleton<IDbSettings>(dbSettings);
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
@@ -112,11 +106,22 @@ namespace Notesnook.API
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new SyncRequirement());
});
options.AddPolicy(InboxApiKeyAuthenticationDefaults.AuthenticationScheme, policy =>
options.AddPolicy("Verified", policy =>
{
policy.AuthenticationSchemes.Add(InboxApiKeyAuthenticationDefaults.AuthenticationScheme);
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");
@@ -147,64 +152,48 @@ namespace Notesnook.API
context.HttpContext.User = context.Principal;
return Task.CompletedTask;
};
options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256();
options.SaveToken = true;
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(30);
})
.AddScheme<InboxApiKeyAuthenticationSchemeOptions, InboxApiKeyAuthenticationHandler>(
InboxApiKeyAuthenticationDefaults.AuthenticationScheme,
options => { }
);
});
// Serializer.RegisterSerializer(new SyncItemBsonSerializer());
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.AddRepository<UserSettings>("user_settings", "notesnook")
.AddRepository<Monograph>("monographs", "notesnook")
.AddRepository<Announcement>("announcements", "notesnook")
.AddRepository<InboxApiKey>(Collections.InboxApiKeysKey, "notesnook")
.AddRepository<InboxSyncItem>(Collections.InboxItems, "notesnook");
services.AddMongoCollection(Collections.SettingsKey)
.AddMongoCollection(Collections.AttachmentsKey)
.AddMongoCollection(Collections.ContentKey)
.AddMongoCollection(Collections.NotesKey)
.AddMongoCollection(Collections.NotebooksKey)
.AddMongoCollection(Collections.RelationsKey)
.AddMongoCollection(Collections.RemindersKey)
.AddMongoCollection(Collections.LegacySettingsKey)
.AddMongoCollection(Collections.ShortcutsKey)
.AddMongoCollection(Collections.TagsKey)
.AddMongoCollection(Collections.ColorsKey)
.AddMongoCollection(Collections.VaultsKey)
.AddMongoCollection(Collections.InboxItems)
.AddMongoCollection(Collections.InboxApiKeysKey);
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IS3Service, S3Service>();
services.AddScoped<IURLAnalyzer, URLAnalyzer>();
services.TryAddTransient<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
services.TryAddTransient<IUserService, UserService>();
services.TryAddTransient<IS3Service, S3Service>();
services.AddControllers();
services.AddHealthChecks(); // .AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
services.AddHealthChecks().AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
services.AddSignalR((hub) =>
{
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
hub.ClientTimeoutInterval = TimeSpan.FromMinutes(10);
hub.EnableDetailedErrors = true;
}).AddMessagePackProtocol().AddJsonProtocol();
}).AddMessagePackProtocol();
services.AddResponseCompression(options =>
{
@@ -221,31 +210,6 @@ namespace Notesnook.API
{
options.Level = CompressionLevel.Fastest;
});
services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName: "Notesnook.API"))
.WithMetrics((builder) => builder
.AddMeter("Notesnook.API.Metrics.Sync")
.AddPrometheusExporter());
services.AddQuartzHostedService(q =>
{
q.WaitForJobsToComplete = false;
q.AwaitApplicationStarted = true;
q.StartDelay = TimeSpan.FromMinutes(1);
}).AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
var jobKey = new JobKey("DeviceCleanupJob");
q.AddJob<DeviceCleanupJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DeviceCleanup-trigger")
// first of every month
.WithCronSchedule("0 0 0 1 * ? *"));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -259,24 +223,17 @@ namespace Notesnook.API
});
}
app.UseOpenTelemetryPrometheusScrapingEndpoint((context) => context.Request.Path == "/metrics" && context.Connection.LocalPort == 5067);
app.UseResponseCompression();
app.UseCors("notesnook");
app.UseVersion(Servers.NotesnookAPI);
app.UseVersion();
app.UseWamp(WampServers.NotesnookServer, (realm, server) =>
{
realm.Subscribe<DeleteUserMessage>(IdentityServerTopics.DeleteUserTopic, async (ev) =>
IUserService service = app.GetScopedService<IUserService>();
realm.Subscribe<DeleteUserMessage>(server.Topics.DeleteUserTopic, async (ev) =>
{
IUserService service = app.GetScopedService<IUserService>();
await service.DeleteUserAsync(ev.UserId);
});
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) =>
{
IDistributedCache cache = app.GetScopedService<IDistributedCache>();
ev.Keys.ForEach((key) => cache.Remove(key));
await service.DeleteUserAsync(ev.UserId, null);
});
});
@@ -294,21 +251,7 @@ namespace Notesnook.API
options.CloseOnAuthenticationExpiration = false;
options.Transports = HttpTransportType.WebSockets;
});
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
{
options.CloseOnAuthenticationExpiration = false;
options.Transports = HttpTransportType.WebSockets;
});
});
}
}
public static class ServiceCollectionMongoCollectionExtensions
{
public static IServiceCollection AddMongoCollection(this IServiceCollection services, string collectionName, string database = "notesnook")
{
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
return services;
}
}
}
+1 -3
View File
@@ -3,9 +3,7 @@
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Trace",
"Microsoft.AspNetCore.Http.Connections": "Trace"
"Microsoft.Hosting.Lifetime": "Information"
}
},
"MongoDbSettings": {
-7
View File
@@ -1,7 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
-2
View File
@@ -1,2 +0,0 @@
PORT=5181
NOTESNOOK_API_SERVER_URL=http://localhost:5264/
-3
View File
@@ -1,3 +0,0 @@
node_modules/
dist/
.env
-19
View File
@@ -1,19 +0,0 @@
FROM oven/bun:1.2.21-slim
RUN mkdir -p /home/bun/app && chown -R bun:bun /home/bun/app
WORKDIR /home/bun/app
USER bun
COPY --chown=bun:bun package.json bun.lock .
RUN bun install --frozen-lockfile
COPY --chown=bun:bun . .
RUN bun run build
EXPOSE 5181
CMD ["bun", "run", "start"]
-192
View File
@@ -1,192 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "notesnook-inbox-api",
"dependencies": {
"express": "^5.1.0",
"libsodium-wrappers-sumo": "^0.7.15",
"zod": "^4.1.9",
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/libsodium-wrappers-sumo": "^0.7.8",
"@types/node": "^24.5.2",
"typescript": "^5.9.2",
},
},
},
"packages": {
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
"@types/libsodium-wrappers-sumo": ["@types/libsodium-wrappers-sumo@0.7.8", "", { "dependencies": { "@types/libsodium-wrappers": "*" } }, "sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"libsodium-sumo": ["libsodium-sumo@0.7.15", "", {}, "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="],
"libsodium-wrappers-sumo": ["libsodium-wrappers-sumo@0.7.15", "", { "dependencies": { "libsodium-sumo": "^0.7.15" } }, "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
}
}
-33
View File
@@ -1,33 +0,0 @@
{
"name": "notesnook-inbox-api",
"version": "1.0.0",
"description": "Notesnook Inbox API server",
"main": "dist/index.js",
"scripts": {
"build": "bun build src/index.ts --outdir dist --target bun",
"start": "bun run dist/index.js",
"dev": "bun --watch src/index.ts"
},
"keywords": [
"notesnook",
"inbox",
"api"
],
"license": "GPL-3.0-or-later",
"author": {
"name": "Streetwriters (Private) Limited",
"email": "support@streetwriters.co",
"url": "https://streetwriters.co"
},
"dependencies": {
"express": "^5.1.0",
"libsodium-wrappers-sumo": "^0.7.15",
"zod": "^4.1.9"
},
"devDependencies": {
"@types/libsodium-wrappers-sumo": "^0.7.8",
"@types/express": "^5.0.3",
"@types/node": "^24.5.2",
"typescript": "^5.9.2"
}
}
-173
View File
@@ -1,173 +0,0 @@
import express from "express";
import _sodium, { base64_variants } from "libsodium-wrappers-sumo";
import { z } from "zod";
const NOTESNOOK_API_SERVER_URL = process.env.NOTESNOOK_API_SERVER_URL;
if (!NOTESNOOK_API_SERVER_URL) {
throw new Error("NOTESNOOK_API_SERVER_URL is not defined");
}
let sodium: typeof _sodium;
const RawInboxItemSchema = z.object({
title: z.string().min(1, "Title is required"),
pinned: z.boolean().optional(),
favorite: z.boolean().optional(),
readonly: z.boolean().optional(),
archived: z.boolean().optional(),
notebookIds: z.array(z.string()).optional(),
tagIds: z.array(z.string()).optional(),
type: z.enum(["note"]),
source: z.string(),
version: z.literal(1),
content: z
.object({
type: z.enum(["html"]),
data: z.string(),
})
.optional(),
});
interface EncryptedInboxItem {
v: 1;
key: Omit<EncryptedInboxItem, "key" | "iv" | "v">;
iv: string;
alg: string;
cipher: string;
length: number;
}
function encrypt(rawData: string, publicKey: string): EncryptedInboxItem {
try {
const password = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
const nonce = sodium.randombytes_buf(
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
);
const data = sodium.from_string(rawData);
const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
data,
null,
null,
nonce,
password
);
const inboxPublicKey = sodium.from_base64(
publicKey,
base64_variants.URLSAFE_NO_PADDING
);
const encryptedPassword = sodium.crypto_box_seal(password, inboxPublicKey);
return {
v: 1,
key: {
cipher: sodium.to_base64(
encryptedPassword,
base64_variants.URLSAFE_NO_PADDING
),
alg: `xsal-x25519-${base64_variants.URLSAFE_NO_PADDING}`,
length: password.length,
},
iv: sodium.to_base64(nonce, base64_variants.URLSAFE_NO_PADDING),
alg: `xcha-argon2i13-${base64_variants.URLSAFE_NO_PADDING}`,
cipher: sodium.to_base64(cipher, base64_variants.URLSAFE_NO_PADDING),
length: data.length,
};
} catch (error) {
throw new Error(`encryption failed: ${error}`);
}
}
async function getInboxPublicEncryptionKey(apiKey: string) {
const response = await fetch(
`${NOTESNOOK_API_SERVER_URL}inbox/public-encryption-key`,
{
headers: {
Authorization: apiKey,
},
}
);
if (!response.ok) {
throw new Error(
`failed to fetch inbox public encryption key: ${await response.text()}`
);
}
const data = (await response.json()) as unknown as any;
return (data?.key as string) || null;
}
async function postEncryptedInboxItem(
apiKey: string,
item: EncryptedInboxItem
) {
const response = await fetch(`${NOTESNOOK_API_SERVER_URL}inbox/items`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: apiKey,
},
body: JSON.stringify({ ...item }),
});
if (!response.ok) {
throw new Error(`failed to post inbox item: ${await response.text()}`);
}
}
const app = express();
app.use(express.json({ limit: "10mb" }));
app.post("/inbox", async (req, res) => {
try {
const apiKey = req.headers["authorization"];
if (!apiKey) {
return res.status(401).json({ error: "unauthorized" });
}
if (!req.body.item) {
return res.status(400).json({ error: "item is required" });
}
const validationResult = RawInboxItemSchema.safeParse(req.body.item);
if (!validationResult.success) {
return res.status(400).json({
error: "invalid item",
details: validationResult.error.issues,
});
}
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
if (!inboxPublicKey) {
return res.status(403).json({ error: "inbox public key not found" });
}
console.log("[info] fetched inbox public key:", inboxPublicKey);
const item = validationResult.data;
const encryptedItem = encrypt(JSON.stringify(item), inboxPublicKey);
console.log("[info] encrypted item:", encryptedItem);
await postEncryptedInboxItem(apiKey, encryptedItem);
return res.status(200).json({ message: "inbox item posted" });
} catch (error) {
if (error instanceof Error) {
console.log("[error]", error.message);
return res
.status(500)
.json({ error: "internal server error", description: error.message });
} else {
console.log("[error] unknown error occured:", error);
return res.status(500).json({
error: "internal server error",
description: `unknown error occured: ${error}`,
});
}
}
});
(async () => {
await _sodium.ready;
sodium = _sodium;
const PORT = Number(process.env.PORT || "5181");
app.listen(PORT, () => {
console.log(`📫 notesnook inbox api server running on port ${PORT}`);
});
})();
export default app;
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"outDir": "./dist",
"strict": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+11 -6
View File
@@ -8,7 +8,7 @@ This repo contains the full source code of the Notesnook Sync Server licensed un
Requirements:
1. [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
1. [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
2. [git](https://git-scm.com/downloads)
The first step is to `clone` the repository:
@@ -55,30 +55,35 @@ dotnet run --project Streetwriters.Identity/Streetwriters.Identity.csproj
The sync server can easily be started using Docker.
The first step is to `clone` the repository:
```bash
wget https://raw.githubusercontent.com/streetwriters/notesnook-sync-server/master/docker-compose.yml
git clone https://github.com/streetwriters/notesnook-sync-server.git
# change directory
cd notesnook-sync-server
```
And then use Docker Compose to start the servers:
```bash
docker compose up
docker-compose up
```
This takes care of setting up everything including MongoDB, Minio etc.
## TODO Self-hosting
**Note: Self-hosting the Notesnook Sync Server is now possible, but without support. Documentation will be provided at a later date. We are working to enable full on-premise self-hosting, so stay tuned!**
**Note: Self-hosting the Notesnook Sync Server is not yet possible. We are working to enable full on-premise self hosting so stay tuned!**
- [x] Open source the Sync server
- [x] Open source the Identity server
- [x] Open source the SSE Messaging infrastructure
- [x] Fully Dockerize all services
- [x] Use self-hosted Minio for S3 storage
- [x] Publish on DockerHub
- [x] Add settings to change server URLs in Notesnook client apps (starting from v3.0.18)
- [ ] Publish on DockerHub
- [ ] Write self hosting docs
- [ ] Add settings to change server URLs in Notesnook client apps
## License
+4 -4
View File
@@ -29,19 +29,19 @@ namespace Streetwriters.Common
{
public class Clients
{
public static readonly Client Notesnook = new()
private static Client Notesnook = new Client
{
Id = "notesnook",
Name = "Notesnook",
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL,
SenderName = "Notesnook",
SenderName = Constants.NOTESNOOK_SENDER_NAME,
Type = ApplicationType.NOTESNOOK,
AppId = ApplicationType.NOTESNOOK,
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified",
OnEmailConfirmed = async (userId) =>
{
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
{
UserId = userId,
Message = new Message
@@ -53,7 +53,7 @@ namespace Streetwriters.Common
}
};
public static Dictionary<string, Client> ClientsMap = new()
public static Dictionary<string, Client> ClientsMap = new Dictionary<string, Client>
{
{ "notesnook", Notesnook }
};
+14 -16
View File
@@ -23,58 +23,56 @@ namespace Streetwriters.Common
{
public class Constants
{
public static int COMPATIBILITY_VERSION = 1;
public static bool IS_SELF_HOSTED => Environment.GetEnvironmentVariable("SELF_HOSTED") == "1";
public static bool DISABLE_SIGNUPS => Environment.GetEnvironmentVariable("DISABLE_SIGNUPS") == "true";
public static string INSTANCE_NAME => Environment.GetEnvironmentVariable("INSTANCE_NAME") ?? "default";
// S3 related
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
public static string S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
public static string S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
// SMTP settings
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
public static string SMTP_REPLYTO_NAME => Environment.GetEnvironmentVariable("SMTP_REPLYTO_NAME");
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL") ?? Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL");
public static string NOTESNOOK_SENDER_NAME => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_NAME");
public static string NOTESNOOK_APP_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_APP_HOST");
public static string NOTESNOOK_API_SECRET => Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET");
// MessageBird is used for SMS sending
public static string TWILIO_ACCOUNT_SID => Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
public static string TWILIO_AUTH_TOKEN => Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
public static string TWILIO_SERVICE_SID => Environment.GetEnvironmentVariable("TWILIO_SERVICE_SID");
public static string MESSAGEBIRD_ACCESS_KEY => Environment.GetEnvironmentVariable("MESSAGEBIRD_ACCESS_KEY");
// Server discovery
public static int NOTESNOOK_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_PORT") ?? "80");
public static int NOTESNOOK_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_PORT"));
public static string NOTESNOOK_SERVER_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_HOST");
public static string NOTESNOOK_SERVER_DOMAIN => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_DOMAIN");
public static string NOTESNOOK_CERT_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_PATH");
public static string NOTESNOOK_CERT_KEY_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_KEY_PATH");
public static int IDENTITY_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("IDENTITY_SERVER_PORT") ?? "80");
public static int IDENTITY_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("IDENTITY_SERVER_PORT"));
public static string IDENTITY_SERVER_HOST => Environment.GetEnvironmentVariable("IDENTITY_SERVER_HOST");
public static Uri IDENTITY_SERVER_URL => new(Environment.GetEnvironmentVariable("IDENTITY_SERVER_URL"));
public static string IDENTITY_SERVER_DOMAIN => Environment.GetEnvironmentVariable("IDENTITY_SERVER_DOMAIN");
public static string IDENTITY_CERT_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_PATH");
public static string IDENTITY_CERT_KEY_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_KEY_PATH");
public static int SSE_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SSE_SERVER_PORT") ?? "80");
public static int SSE_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SSE_SERVER_PORT"));
public static string SSE_SERVER_HOST => Environment.GetEnvironmentVariable("SSE_SERVER_HOST");
public static string SSE_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SSE_SERVER_DOMAIN");
public static string SSE_CERT_PATH => Environment.GetEnvironmentVariable("SSE_CERT_PATH");
public static string SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
// internal
public static string WEBRISK_API_URI => Environment.GetEnvironmentVariable("WEBRISK_API_URI");
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME");
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT"));
public static string SUBSCRIPTIONS_SERVER_HOST => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_HOST");
public static string SUBSCRIPTIONS_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_DOMAIN");
public static string SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH");
public static string SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH");
public static string[] NOTESNOOK_CORS_ORIGINS => Environment.GetEnvironmentVariable("NOTESNOOK_CORS")?.Split(",") ?? new string[] { };
@@ -24,7 +24,6 @@ namespace Streetwriters.Common.Enums
STREETWRITERS = 0,
APPLE = 1,
GOOGLE = 2,
PADDLE = 3,
GIFT_CARD = 4,
PADDLE = 3
}
}
@@ -26,7 +26,6 @@ namespace Streetwriters.Common.Enums
BETA = 2,
PREMIUM = 5,
PREMIUM_EXPIRED = 6,
PREMIUM_CANCELED = 7,
PREMIUM_PAUSED = 8,
PREMIUM_CANCELED = 7
}
}
@@ -18,8 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@@ -32,20 +30,13 @@ namespace Streetwriters.Common.Extensions
{
public static class AppBuilderExtensions
{
public static IApplicationBuilder UseVersion(this IApplicationBuilder app, Server server)
public static IApplicationBuilder UseVersion(this IApplicationBuilder app)
{
app.Map("/version", (app) =>
{
app.Run(async context =>
{
context.Response.ContentType = "application/json";
var data = new Dictionary<string, object>
{
{ "version", Constants.COMPATIBILITY_VERSION },
{ "id", server.Id },
{ "instance", Constants.INSTANCE_NAME }
};
await context.Response.WriteAsync(JsonSerializer.Serialize(data));
await context.Response.WriteAsync(Version.AsString());
});
});
return app;
@@ -37,21 +37,19 @@ namespace Streetwriters.Common.Extensions
request.Content = content;
}
if (headers != null)
foreach (var header in headers)
{
foreach (var header in headers)
if (header.Key == "Content-Type" || header.Key == "Content-Length")
{
if (header.Key == "Content-Type" || header.Key == "Content-Length")
{
request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
continue;
}
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
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 && response.Content.Headers.ContentType.ToString().Contains("application/json"))
if (response.Content.Headers.ContentLength > 0)
{
var res = await response.Content.ReadFromJsonAsync<T>();
res.Success = response.IsSuccessStatusCode;
@@ -60,7 +58,7 @@ namespace Streetwriters.Common.Extensions
}
else
{
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode };
}
}
@@ -18,20 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Microsoft.Extensions.DependencyInjection;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Repositories;
namespace Streetwriters.Common.Extensions
{
public static class ServiceCollectionServiceExtensions
{
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database) where T : class
{
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddScoped<Repository<T>>();
return services;
}
public static IServiceCollection AddDefaultCors(this IServiceCollection services)
{
services.AddCors(options =>
@@ -26,11 +26,15 @@ namespace System
{
public static class StringExtensions
{
public static string Sha256(this string input)
public static string ToSha256(this string rawData, int maxLength = 12)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return Convert.ToBase64String(hash);
// 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)
@@ -18,8 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Reactive.Disposables;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Streetwriters.Common.Interfaces;
using WampSharp.AspNetCore.WebSockets.Server;
@@ -40,27 +38,5 @@ namespace Streetwriters.Common.Extensions
{
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
}
public static IDisposable SubscribeWithSemaphore<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
{
var semaphore = new SemaphoreSlim(1, 1);
var subscriber = realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) =>
{
await semaphore.WaitAsync();
try
{
await handler.Process(message);
}
finally
{
semaphore.Release();
}
});
return Disposable.Create(() =>
{
subscriber.Dispose();
semaphore.Dispose();
});
}
}
}
@@ -1,23 +0,0 @@
using System.IO;
using WebMarkupMin.Core;
using WebMarkupMin.Core.Loggers;
namespace Streetwriters.Common.Helpers
{
public static class HtmlHelper
{
public static string ReadMinifiedHtmlFile(string path)
{
var settings = new HtmlMinificationSettings()
{
WhitespaceMinificationMode = WhitespaceMinificationMode.Medium,
};
var cssMinifier = new KristensenCssMinifier();
var jsMinifier = new CrockfordJsMinifier();
var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
}
}
}
+2 -10
View File
@@ -17,7 +17,6 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Streetwriters.Common.Messages;
@@ -28,9 +27,9 @@ namespace Streetwriters.Common.Helpers
{
public class WampHelper
{
public static async Task<IWampRealmProxy> OpenWampChannelAsync(string server, string realmName)
public static async Task<IWampRealmProxy> OpenWampChannelAsync<T>(string server, string realmName)
{
DefaultWampChannelFactory channelFactory = new();
DefaultWampChannelFactory channelFactory = new DefaultWampChannelFactory();
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
@@ -44,12 +43,5 @@ namespace Streetwriters.Common.Helpers
var subject = realm.Services.GetSubject<T>(topicName);
subject.OnNext(message);
}
public static void PublishMessages<T>(IWampRealmProxy realm, string topicName, IEnumerable<T> messages)
{
var subject = realm.Services.GetSubject<T>(topicName);
foreach (var message in messages)
subject.OnNext(message);
}
}
}
@@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using MimeKit;
using MimeKit.Cryptography;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Interfaces
{
public interface IEmailSender
{
Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
);
}
}
@@ -17,14 +17,11 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Net.Http;
namespace Streetwriters.Common.Interfaces
{
public interface IResponse
{
bool Success { get; set; }
int StatusCode { get; set; }
HttpContent Content { get; set; }
}
}
@@ -0,0 +1,41 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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; }
}
}
@@ -1,13 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using MimeKit;
using MimeKit.Cryptography;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Interfaces
{
public interface IURLAnalyzer
{
Task<bool> IsURLSafeAsync(string uri);
}
}
@@ -1,16 +0,0 @@
using System.Threading.Tasks;
using Streetwriters.Common.Models;
using WampSharp.V2.Rpc;
namespace Streetwriters.Common.Interfaces
{
public interface IUserAccountService
{
[WampProcedure("co.streetwriters.identity.users.get_user")]
Task<UserModel?> GetUserAsync(string clientId, string userId);
[WampProcedure("co.streetwriters.identity.users.delete_user")]
Task DeleteUserAsync(string clientId, string userId, string password);
// [WampProcedure("co.streetwriters.identity.users.create_user")]
// Task<UserModel> CreateUserAsync();
}
}
@@ -1,14 +0,0 @@
using System.Threading.Tasks;
using Streetwriters.Common.Helpers;
using Streetwriters.Common.Models;
using WampSharp.V2.Rpc;
namespace Streetwriters.Common.Interfaces
{
public interface IUserSubscriptionService
{
[WampProcedure("co.streetwriters.subscriptions.subscriptions.get_user_subscription")]
Task<Subscription> GetUserSubscriptionAsync(string clientId, string userId);
Subscription TransformUserSubscription(Subscription subscription);
}
}
@@ -17,7 +17,6 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Streetwriters.Common.Enums;
@@ -59,8 +58,6 @@ namespace Streetwriters.Common.Messages
[JsonPropertyName("productId")]
public string ProductId { get; set; }
[JsonPropertyName("extend")]
public bool Extend { get; set; }
}
}
@@ -1,69 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Messages
{
public class CreateSubscriptionMessageV2
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("provider")]
public SubscriptionProvider Provider { get; set; }
[JsonPropertyName("appId")]
public ApplicationType AppId { get; set; }
[JsonPropertyName("plan")]
public SubscriptionPlan Plan { get; set; }
[JsonPropertyName("status")]
public SubscriptionStatus Status { get; set; }
[JsonPropertyName("start")]
public long StartTime { get; set; }
[JsonPropertyName("expiry")]
public long ExpiryTime { get; set; }
[JsonPropertyName("orderId")]
public string OrderId { get; set; }
[JsonPropertyName("subscriptionId")]
public string SubscriptionId { get; set; }
[JsonPropertyName("productId")]
public string ProductId { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("trialExpiry")]
public long TrialExpiryTime { get; set; }
[JsonPropertyName("googlePurchaseToken")]
public string? GooglePurchaseToken { get; set; }
}
}
@@ -43,6 +43,6 @@ namespace Streetwriters.Common.Messages
public Message Message { get; set; }
[JsonPropertyName("originTokenId")]
public string? OriginTokenId { get; set; }
public string OriginTokenId { get; set; }
}
}
@@ -1,11 +0,0 @@
namespace Streetwriters.Common.Models
{
public class EmailTemplate
{
public int? Id { get; set; }
public object Data { get; set; }
public string Subject { get; set; }
public string Html { get; set; }
public string Text { get; set; }
}
}
@@ -1,21 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class GetCustomerResponse : PaddleResponse
{
[JsonPropertyName("data")]
public PaddleCustomer Customer { get; set; }
}
public class PaddleCustomer
{
[JsonPropertyName("email")]
public string Email { get; set; }
}
}
@@ -1,214 +1,31 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class GetSubscriptionResponse : PaddleResponse
public class SubscriptionResponse : Response
{
[JsonPropertyName("data")]
public Data Data { get; set; }
}
public partial class Data
{
// [JsonPropertyName("id")]
// public string Id { get; set; }
// [JsonPropertyName("status")]
// public string Status { get; set; }
[JsonPropertyName("customer_id")]
public string CustomerId { get; set; }
// [JsonPropertyName("address_id")]
// public string AddressId { get; set; }
// [JsonPropertyName("business_id")]
// public object BusinessId { get; set; }
// [JsonPropertyName("currency_code")]
// public string CurrencyCode { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
// [JsonPropertyName("started_at")]
// public DateTimeOffset StartedAt { get; set; }
[JsonPropertyName("first_billed_at")]
public DateTimeOffset? FirstBilledAt { get; set; }
// [JsonPropertyName("next_billed_at")]
// public DateTimeOffset NextBilledAt { get; set; }
// [JsonPropertyName("paused_at")]
// public object PausedAt { get; set; }
// [JsonPropertyName("canceled_at")]
// public object CanceledAt { get; set; }
// [JsonPropertyName("collection_mode")]
// public string CollectionMode { get; set; }
// [JsonPropertyName("billing_details")]
// public object BillingDetails { get; set; }
// [JsonPropertyName("current_billing_period")]
// public CurrentBillingPeriod CurrentBillingPeriod { get; set; }
[JsonPropertyName("billing_cycle")]
public BillingCycle BillingCycle { get; set; }
// [JsonPropertyName("scheduled_change")]
// public object ScheduledChange { get; set; }
// [JsonPropertyName("items")]
// public Item[] Items { get; set; }
// [JsonPropertyName("custom_data")]
// public object CustomData { get; set; }
[JsonPropertyName("management_urls")]
public ManagementUrls ManagementUrls { get; set; }
// [JsonPropertyName("discount")]
// public object Discount { get; set; }
// [JsonPropertyName("import_meta")]
// public object ImportMeta { get; set; }
}
public partial class BillingCycle
{
[JsonPropertyName("frequency")]
public long Frequency { get; set; }
[JsonPropertyName("interval")]
public string Interval { get; set; }
}
// public partial class CurrentBillingPeriod
// {
// [JsonPropertyName("starts_at")]
// public DateTimeOffset StartsAt { get; set; }
// [JsonPropertyName("ends_at")]
// public DateTimeOffset EndsAt { get; set; }
// }
// public partial class Item
// {
// [JsonPropertyName("status")]
// public string Status { get; set; }
// [JsonPropertyName("quantity")]
// public long Quantity { get; set; }
// [JsonPropertyName("recurring")]
// public bool Recurring { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
// [JsonPropertyName("previously_billed_at")]
// public DateTimeOffset PreviouslyBilledAt { get; set; }
// [JsonPropertyName("next_billed_at")]
// public DateTimeOffset NextBilledAt { get; set; }
// [JsonPropertyName("trial_dates")]
// public object TrialDates { get; set; }
// [JsonPropertyName("price")]
// public Price Price { get; set; }
// }
// public partial class Price
// {
// [JsonPropertyName("id")]
// public string Id { get; set; }
// [JsonPropertyName("product_id")]
// public string ProductId { get; set; }
// [JsonPropertyName("type")]
// public string Type { get; set; }
// [JsonPropertyName("description")]
// public string Description { get; set; }
// [JsonPropertyName("name")]
// public string Name { get; set; }
// [JsonPropertyName("tax_mode")]
// public string TaxMode { get; set; }
// [JsonPropertyName("billing_cycle")]
// public BillingCycle BillingCycle { get; set; }
// [JsonPropertyName("trial_period")]
// public object TrialPeriod { get; set; }
// [JsonPropertyName("unit_price")]
// public UnitPrice UnitPrice { get; set; }
// [JsonPropertyName("unit_price_overrides")]
// public object[] UnitPriceOverrides { get; set; }
// [JsonPropertyName("custom_data")]
// public object CustomData { get; set; }
// [JsonPropertyName("status")]
// public string Status { get; set; }
// [JsonPropertyName("quantity")]
// public Quantity Quantity { get; set; }
// [JsonPropertyName("import_meta")]
// public object ImportMeta { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
// }
// public partial class Quantity
// {
// [JsonPropertyName("minimum")]
// public long Minimum { get; set; }
// [JsonPropertyName("maximum")]
// public long Maximum { get; set; }
// }
// public partial class UnitPrice
// {
// [JsonPropertyName("amount")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Amount { get; set; }
// [JsonPropertyName("currency_code")]
// public string CurrencyCode { get; set; }
// }
public partial class ManagementUrls
{
[JsonPropertyName("update_payment_method")]
public Uri UpdatePaymentMethod { get; set; }
[JsonPropertyName("cancel")]
public Uri Cancel { get; set; }
[JsonPropertyName("subscription")]
public ISubscription Subscription { get; set; }
}
}
@@ -1,21 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public class GetTransactionInvoiceResponse : PaddleResponse
{
[JsonPropertyName("data")]
public Invoice Invoice { get; set; }
}
public partial class Invoice
{
[JsonPropertyName("url")]
public string Url { get; set; }
}
}
@@ -1,15 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class GetTransactionResponse : PaddleResponse
{
[JsonPropertyName("data")]
public TransactionV2 Transaction { get; set; }
}
}
-29
View File
@@ -1,29 +0,0 @@
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Models
{
public class GiftCard : IDocument
{
public GiftCard()
{
Id = ObjectId.GenerateNewId().ToString();
}
public string Code { get; set; }
public string OrderId { get; set; }
public string OrderIdType { get; set; }
public string ProductId { get; set; }
public string RedeemedBy { get; set; }
public long RedeemedAt { get; set; }
public long Timestamp { get; set; }
public long Term { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
public string Id { get; set; }
}
}
@@ -1,41 +0,0 @@
using System;
using System.Text.Json.Serialization;
namespace Streetwriters.Common.Models
{
public partial class ListPaymentsResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("response")]
public Payment[] Payments { get; set; }
}
public partial class Payment
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("subscription_id")]
public long SubscriptionId { get; set; }
[JsonPropertyName("amount")]
public double Amount { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; }
[JsonPropertyName("payout_date")]
public string PayoutDate { get; set; }
[JsonPropertyName("is_paid")]
public short IsPaid { get; set; }
[JsonPropertyName("is_one_off_charge")]
public bool IsOneOffCharge { get; set; }
[JsonPropertyName("receipt_url")]
public string ReceiptUrl { get; set; }
}
}
@@ -1,77 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Text.Json.Serialization;
public partial class ListTransactionsResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("response")]
public Transaction[] Transactions { get; set; }
}
public partial class Transaction
{
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
[JsonPropertyName("checkout_id")]
public string CheckoutId { get; set; }
[JsonPropertyName("amount")]
public string Amount { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("created_at")]
public string CreatedAt { get; set; }
[JsonPropertyName("passthrough")]
public object Passthrough { get; set; }
[JsonPropertyName("product_id")]
public long ProductId { get; set; }
[JsonPropertyName("is_subscription")]
public bool IsSubscription { get; set; }
[JsonPropertyName("is_one_off")]
public bool IsOneOff { get; set; }
[JsonPropertyName("subscription")]
public PaddleSubscription Subscription { get; set; }
[JsonPropertyName("user")]
public PaddleTransactionUser User { get; set; }
[JsonPropertyName("receipt_url")]
public string ReceiptUrl { get; set; }
}
public partial class PaddleSubscription
{
[JsonPropertyName("subscription_id")]
public long SubscriptionId { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
}
public partial class PaddleTransactionUser
{
[JsonPropertyName("user_id")]
public long UserId { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
[JsonPropertyName("marketing_consent")]
public bool MarketingConsent { get; set; }
}
}
@@ -1,511 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class ListTransactionsResponseV2 : PaddleResponse
{
[JsonPropertyName("data")]
public TransactionV2[] Transactions { get; set; }
}
public partial class TransactionV2
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("customer_id")]
public string CustomerId { get; set; }
// [JsonPropertyName("address_id")]
// public string AddressId { get; set; }
// [JsonPropertyName("business_id")]
// public object BusinessId { get; set; }
[JsonPropertyName("custom_data")]
public Dictionary<string, string> CustomData { get; set; }
[JsonPropertyName("origin")]
public string Origin { get; set; }
// [JsonPropertyName("collection_mode")]
// public string CollectionMode { get; set; }
// [JsonPropertyName("subscription_id")]
// public string SubscriptionId { get; set; }
// [JsonPropertyName("invoice_id")]
// public string InvoiceId { get; set; }
// [JsonPropertyName("invoice_number")]
// public string InvoiceNumber { get; set; }
[JsonPropertyName("billing_details")]
public BillingDetails BillingDetails { get; set; }
[JsonPropertyName("billing_period")]
public BillingPeriod BillingPeriod { get; set; }
// [JsonPropertyName("currency_code")]
// public string CurrencyCode { get; set; }
// [JsonPropertyName("discount_id")]
// public string DiscountId { get; set; }
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
[JsonPropertyName("billed_at")]
public DateTimeOffset? BilledAt { get; set; }
[JsonPropertyName("items")]
public Item[] Items { get; set; }
[JsonPropertyName("details")]
public Details Details { get; set; }
// [JsonPropertyName("payments")]
// public Payment[] Payments { get; set; }
// [JsonPropertyName("checkout")]
// public Checkout Checkout { get; set; }
}
public partial class BillingDetails
{
// [JsonPropertyName("enable_checkout")]
// public bool EnableCheckout { get; set; }
[JsonPropertyName("payment_terms")]
public PaymentTerms PaymentTerms { get; set; }
// [JsonPropertyName("purchase_order_number")]
// public string PurchaseOrderNumber { get; set; }
// [JsonPropertyName("additional_information")]
// public object AdditionalInformation { get; set; }
}
public partial class PaymentTerms
{
[JsonPropertyName("interval")]
public string Interval { get; set; }
[JsonPropertyName("frequency")]
public long Frequency { get; set; }
}
public partial class BillingPeriod
{
[JsonPropertyName("starts_at")]
public DateTimeOffset StartsAt { get; set; }
[JsonPropertyName("ends_at")]
public DateTimeOffset EndsAt { get; set; }
}
// public partial class Checkout
// {
// [JsonPropertyName("url")]
// public Uri Url { get; set; }
// }
public partial class Details
{
// [JsonPropertyName("tax_rates_used")]
// public TaxRatesUsed[] TaxRatesUsed { get; set; }
[JsonPropertyName("totals")]
public Totals Totals { get; set; }
// [JsonPropertyName("adjusted_totals")]
// public AdjustedTotals AdjustedTotals { get; set; }
// [JsonPropertyName("payout_totals")]
// public Dictionary<string, string> PayoutTotals { get; set; }
// [JsonPropertyName("adjusted_payout_totals")]
// public AdjustedTotals AdjustedPayoutTotals { get; set; }
[JsonPropertyName("line_items")]
public LineItem[] LineItems { get; set; }
}
public partial class Totals
{
[JsonPropertyName("subtotal")]
public long Subtotal { get; set; }
[JsonPropertyName("tax")]
public long Tax { get; set; }
[JsonPropertyName("discount")]
public long Discount { get; set; }
[JsonPropertyName("total")]
public long Total { get; set; }
[JsonPropertyName("grand_total")]
public long GrandTotal { get; set; }
// [JsonPropertyName("fee")]
// public object Fee { get; set; }
// [JsonPropertyName("credit")]
// public long Credit { get; set; }
// [JsonPropertyName("credit_to_balance")]
// public long CreditToBalance { get; set; }
[JsonPropertyName("balance")]
public long Balance { get; set; }
// [JsonPropertyName("earnings")]
// public object Earnings { get; set; }
[JsonPropertyName("currency_code")]
public string CurrencyCode { get; set; }
}
// public partial class AdjustedTotals
// {
// [JsonPropertyName("subtotal")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Subtotal { get; set; }
// [JsonPropertyName("tax")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Tax { get; set; }
// [JsonPropertyName("total")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Total { get; set; }
// [JsonPropertyName("fee")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Fee { get; set; }
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
// [JsonPropertyName("chargeback_fee")]
// public ChargebackFee ChargebackFee { get; set; }
// [JsonPropertyName("earnings")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Earnings { get; set; }
// [JsonPropertyName("currency_code")]
// public string CurrencyCode { get; set; }
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
// [JsonPropertyName("grand_total")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long? GrandTotal { get; set; }
// }
// public partial class ChargebackFee
// {
// [JsonPropertyName("amount")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Amount { get; set; }
// [JsonPropertyName("original")]
// public object Original { get; set; }
// }
public partial class LineItem
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("price_id")]
public string PriceId { get; set; }
// [JsonPropertyName("quantity")]
// public long Quantity { get; set; }
// [JsonPropertyName("totals")]
// public Totals Totals { get; set; }
// [JsonPropertyName("product")]
// public Product Product { get; set; }
// [JsonPropertyName("tax_rate")]
// public string TaxRate { get; set; }
// [JsonPropertyName("unit_totals")]
// public Totals UnitTotals { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("proration")]
public Proration Proration { get; set; }
}
// public partial class Product
// {
// [JsonPropertyName("id")]
// public string Id { get; set; }
// [JsonPropertyName("name")]
// public string Name { get; set; }
// [JsonPropertyName("description")]
// public string Description { get; set; }
// [JsonPropertyName("type")]
// public TypeEnum Type { get; set; }
// [JsonPropertyName("tax_category")]
// public TypeEnum TaxCategory { get; set; }
// [JsonPropertyName("image_url")]
// public Uri ImageUrl { get; set; }
// [JsonPropertyName("custom_data")]
// public CustomData CustomData { get; set; }
// [JsonPropertyName("status")]
// public Status Status { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
// [JsonPropertyName("import_meta")]
// public object ImportMeta { get; set; }
// }
// public partial class CustomData
// {
// [JsonPropertyName("features")]
// public Features Features { get; set; }
// [JsonPropertyName("suggested_addons")]
// public string[] SuggestedAddons { get; set; }
// [JsonPropertyName("upgrade_description")]
// public string UpgradeDescription { get; set; }
// }
// public partial class Features
// {
// [JsonPropertyName("aircraft_performance")]
// public bool AircraftPerformance { get; set; }
// [JsonPropertyName("compliance_monitoring")]
// public bool ComplianceMonitoring { get; set; }
// [JsonPropertyName("flight_log_management")]
// public bool FlightLogManagement { get; set; }
// [JsonPropertyName("payment_by_invoice")]
// public bool PaymentByInvoice { get; set; }
// [JsonPropertyName("route_planning")]
// public bool RoutePlanning { get; set; }
// [JsonPropertyName("sso")]
// public bool Sso { get; set; }
// }
public partial class Proration
{
[JsonPropertyName("billing_period")]
public BillingPeriod BillingPeriod { get; set; }
}
// public partial class Totals
// {
// [JsonPropertyName("subtotal")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Subtotal { get; set; }
// [JsonPropertyName("discount")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Discount { get; set; }
// [JsonPropertyName("tax")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Tax { get; set; }
// [JsonPropertyName("total")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Total { get; set; }
// }
// public partial class TaxRatesUsed
// {
// [JsonPropertyName("tax_rate")]
// public string TaxRate { get; set; }
// [JsonPropertyName("totals")]
// public Totals Totals { get; set; }
// }
public partial class Item
{
[JsonPropertyName("price")]
public Price Price { get; set; }
[JsonPropertyName("quantity")]
public long Quantity { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("proration")]
public Proration Proration { get; set; }
}
public partial class Price
{
[JsonPropertyName("id")]
public string Id { get; set; }
// [JsonPropertyName("description")]
// public string Description { get; set; }
// [JsonPropertyName("type")]
// public TypeEnum Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
// [JsonPropertyName("product_id")]
// public string ProductId { get; set; }
// [JsonPropertyName("billing_cycle")]
// public PaymentTerms BillingCycle { get; set; }
// [JsonPropertyName("trial_period")]
// public object TrialPeriod { get; set; }
// [JsonPropertyName("tax_mode")]
// public TaxMode TaxMode { get; set; }
// [JsonPropertyName("unit_price")]
// public UnitPrice UnitPrice { get; set; }
// [JsonPropertyName("unit_price_overrides")]
// public object[] UnitPriceOverrides { get; set; }
// [JsonPropertyName("custom_data")]
// public object CustomData { get; set; }
// [JsonPropertyName("quantity")]
// public Quantity Quantity { get; set; }
// [JsonPropertyName("status")]
// public Status Status { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("updated_at")]
// public DateTimeOffset UpdatedAt { get; set; }
// [JsonPropertyName("import_meta")]
// public object ImportMeta { get; set; }
}
// public partial class Quantity
// {
// [JsonPropertyName("minimum")]
// public long Minimum { get; set; }
// [JsonPropertyName("maximum")]
// public long Maximum { get; set; }
// }
// public partial class UnitPrice
// {
// [JsonPropertyName("amount")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Amount { get; set; }
// [JsonPropertyName("currency_code")]
// public CurrencyCode CurrencyCode { get; set; }
// }
// public partial class Payment
// {
// [JsonPropertyName("payment_attempt_id")]
// public Guid PaymentAttemptId { get; set; }
// [JsonPropertyName("stored_payment_method_id")]
// public Guid StoredPaymentMethodId { get; set; }
// [JsonPropertyName("payment_method_id")]
// public string PaymentMethodId { get; set; }
// [JsonPropertyName("amount")]
// [JsonConverter(typeof(ParseStringConverter))]
// public long Amount { get; set; }
// [JsonPropertyName("status")]
// public string Status { get; set; }
// [JsonPropertyName("error_code")]
// public string ErrorCode { get; set; }
// [JsonPropertyName("method_details")]
// public MethodDetails MethodDetails { get; set; }
// [JsonPropertyName("created_at")]
// public DateTimeOffset CreatedAt { get; set; }
// [JsonPropertyName("captured_at")]
// public DateTimeOffset? CapturedAt { get; set; }
// }
// public partial class MethodDetails
// {
// [JsonPropertyName("type")]
// public string Type { get; set; }
// [JsonPropertyName("card")]
// public Card Card { get; set; }
// }
// public partial class Card
// {
// [JsonPropertyName("type")]
// public string Type { get; set; }
// [JsonPropertyName("last4")]
// public string Last4 { get; set; }
// [JsonPropertyName("expiry_month")]
// public long ExpiryMonth { get; set; }
// [JsonPropertyName("expiry_year")]
// public long ExpiryYear { get; set; }
// [JsonPropertyName("cardholder_name")]
// public string CardholderName { get; set; }
// }
public partial class Pagination
{
[JsonPropertyName("per_page")]
public long PerPage { get; set; }
[JsonPropertyName("next")]
public Uri Next { get; set; }
[JsonPropertyName("has_more")]
public bool HasMore { get; set; }
[JsonPropertyName("estimated_total")]
public long EstimatedTotal { get; set; }
}
}
@@ -1,47 +0,0 @@
using System;
using System.Text.Json.Serialization;
namespace Streetwriters.Common.Models
{
public partial class ListUsersResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("response")]
public PaddleUser[] Users { get; set; }
}
public class PaddleUser
{
[JsonPropertyName("subscription_id")]
public long SubscriptionId { get; set; }
[JsonPropertyName("plan_id")]
public long PlanId { get; set; }
[JsonPropertyName("user_id")]
public long UserId { get; set; }
[JsonPropertyName("user_email")]
public string UserEmail { get; set; }
[JsonPropertyName("marketing_consent")]
public bool MarketingConsent { get; set; }
[JsonPropertyName("update_url")]
public string UpdateUrl { get; set; }
[JsonPropertyName("cancel_url")]
public string CancelUrl { get; set; }
[JsonPropertyName("state")]
public string State { get; set; }
[JsonPropertyName("signup_date")]
public string SignupDate { get; set; }
[JsonPropertyName("quantity")]
public long Quantity { get; set; }
}
}
+2
View File
@@ -26,9 +26,11 @@ 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()
@@ -1,24 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class PaddleResponse
{
[JsonPropertyName("error")]
public PaddleError Error { get; set; }
}
public class PaddleError
{
public string? Type { get; set; }
public string? Code { get; set; }
public string? Detail { get; set; }
[JsonPropertyName("documentation_url")]
public string? DocumentationUrl { get; set; }
}
}
@@ -1,20 +0,0 @@
using System;
using System.Text.Json.Serialization;
namespace Streetwriters.Common.Models
{
public partial class RefundPaymentResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("response")]
public Refund Refund { get; set; }
}
public partial class Refund
{
[JsonPropertyName("refund_request_id")]
public long RefundRequestId { get; set; }
}
}
-3
View File
@@ -17,7 +17,6 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Net.Http;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Streetwriters.Common.Interfaces;
@@ -29,7 +28,5 @@ namespace Streetwriters.Common.Models
[JsonIgnore]
public bool Success { get; set; }
public int StatusCode { get; set; }
[JsonIgnore]
public HttpContent Content { get; set; }
}
}
+2
View File
@@ -20,9 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using AspNetCore.Identity.Mongo.Model;
using Streetwriters.Data.Attributes;
namespace Streetwriters.Common.Models
{
[BsonCollection("identity", "roles")]
public class Role : MongoRole
{
// [DataMember(Name = "email")]
+10 -31
View File
@@ -17,7 +17,6 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
@@ -25,10 +24,12 @@ using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Data.Attributes;
namespace Streetwriters.Common.Models
{
public class Subscription
[BsonCollection("subscriptions", "subscriptions")]
public class Subscription : ISubscription
{
public Subscription()
{
@@ -41,17 +42,16 @@ namespace Streetwriters.Common.Models
public string Id { get; set; }
[JsonPropertyName("userId")]
public required string UserId { get; set; }
public string UserId { get; set; }
[JsonIgnore]
public string? OrderId { get; set; }
public string OrderId { get; set; }
[JsonIgnore]
public string? SubscriptionId { get; set; }
public string SubscriptionId { get; set; }
[BsonRepresentation(BsonType.Int32)]
[JsonPropertyName("appId")]
public required ApplicationType AppId { get; set; }
public ApplicationType AppId { get; set; }
[JsonPropertyName("start")]
public long StartDate { get; set; }
@@ -65,39 +65,18 @@ namespace Streetwriters.Common.Models
[BsonRepresentation(BsonType.Int32)]
[JsonPropertyName("type")]
[Obsolete("Use SubscriptionPlan and SubscriptionStatus instead.")]
public SubscriptionType Type { get; set; }
[JsonPropertyName("cancelURL")]
public string? CancelURL { get; set; }
public string CancelURL { get; set; }
[JsonPropertyName("updateURL")]
public string? UpdateURL { get; set; }
[JsonPropertyName("googlePurchaseToken")]
public string? GooglePurchaseToken { get; set; }
public string UpdateURL { get; set; }
[JsonPropertyName("productId")]
public string? ProductId { get; set; }
public string ProductId { get; set; }
[JsonIgnore]
public int TrialExtensionCount { get; set; }
[JsonPropertyName("trialExpiry")]
public long TrialExpiryDate { get; set; }
[JsonPropertyName("trialsAvailed")]
public SubscriptionPlan[]? TrialsAvailed { get; set; }
[JsonPropertyName("updatedAt")]
public long UpdatedAt { get; set; }
[BsonRepresentation(BsonType.Int32)]
[JsonPropertyName("plan")]
public SubscriptionPlan Plan { get; set; }
[BsonRepresentation(BsonType.Int32)]
[JsonPropertyName("status")]
public SubscriptionStatus Status { get; set; }
}
}
@@ -1,57 +0,0 @@
namespace Streetwriters.Common.Models
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
public partial class SubscriptionPreviewResponse : PaddleResponse
{
[JsonPropertyName("data")]
public SubscriptionPreviewData Data { get; set; }
}
public partial class SubscriptionPreviewData
{
[JsonPropertyName("currency_code")]
public string CurrencyCode { get; set; }
[JsonPropertyName("billing_cycle")]
public BillingCycle BillingCycle { get; set; }
[JsonPropertyName("update_summary")]
public UpdateSummary UpdateSummary { get; set; }
[JsonPropertyName("immediate_transaction")]
public TransactionV2 ImmediateTransaction { get; set; }
[JsonPropertyName("next_transaction")]
public TransactionV2 NextTransaction { get; set; }
[JsonPropertyName("recurring_transaction_details")]
public Details RecurringTransactionDetails { get; set; }
}
public partial class UpdateSummary
{
[JsonPropertyName("charge")]
public UpdateSummaryItem Charge { get; set; }
[JsonPropertyName("credit")]
public UpdateSummaryItem Credit { get; set; }
[JsonPropertyName("result")]
public UpdateSummaryItem Result { get; set; }
}
public partial class UpdateSummaryItem
{
[JsonPropertyName("amount")]
public long Amount { get; set; }
[JsonPropertyName("action")]
public string? Action { get; set; }
}
}
+2
View File
@@ -20,9 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using AspNetCore.Identity.Mongo.Model;
using Streetwriters.Data.Attributes;
namespace Streetwriters.Common.Models
{
[BsonCollection("identity", "users")]
public class User : MongoUser
{
}
-3
View File
@@ -35,9 +35,6 @@ namespace Streetwriters.Common.Models
[JsonPropertyName("isEmailConfirmed")]
public bool IsEmailConfirmed { get; set; }
[JsonPropertyName("marketingConsent")]
public bool MarketingConsent { get; set; }
[JsonPropertyName("mfa")]
public MFAConfig MFA { get; set; }
}
+12 -13
View File
@@ -17,7 +17,6 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
@@ -35,10 +34,10 @@ namespace Streetwriters.Common
if (!string.IsNullOrEmpty(originCertPath) && !string.IsNullOrEmpty(originCertKeyPath))
this.SSLCertificate = X509Certificate2.CreateFromPemFile(originCertPath, originCertKeyPath);
}
public string Id { get; set; }
public int Port { get; set; }
public string Hostname { get; set; }
public Uri PublicURL { get; set; }
public string Domain { get; set; }
public X509Certificate2 SSLCertificate { get; }
public bool IsSecure { get => this.SSLCertificate != null; }
@@ -63,14 +62,14 @@ namespace Streetwriters.Common
public class Servers
{
#if (DEBUG || STAGING)
public static string GetLocalIPv4()
#if DEBUG
public static string GetLocalIPv4(NetworkInterfaceType _type)
{
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
string output = "";
foreach (NetworkInterface item in interfaces)
{
if ((item.NetworkInterfaceType == NetworkInterfaceType.Ethernet || item.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) && item.OperationalStatus == OperationalStatus.Up)
if (item.NetworkInterfaceType == _type && item.OperationalStatus == OperationalStatus.Up)
{
foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses)
{
@@ -83,40 +82,40 @@ namespace Streetwriters.Common
}
return output;
}
public readonly static string HOST = GetLocalIPv4();
public readonly static string HOST = GetLocalIPv4(NetworkInterfaceType.Ethernet);
public static Server S3Server { get; } = new()
{
Port = 4568,
Hostname = HOST
Hostname = HOST,
Domain = HOST
};
#endif
public static Server NotesnookAPI { get; } = new(Constants.NOTESNOOK_CERT_PATH, Constants.NOTESNOOK_CERT_KEY_PATH)
{
Domain = Constants.NOTESNOOK_SERVER_DOMAIN,
Port = Constants.NOTESNOOK_SERVER_PORT,
Hostname = Constants.NOTESNOOK_SERVER_HOST,
Id = "notesnook-sync"
};
public static Server MessengerServer { get; } = new(Constants.SSE_CERT_PATH, Constants.SSE_CERT_KEY_PATH)
{
Domain = Constants.SSE_SERVER_DOMAIN,
Port = Constants.SSE_SERVER_PORT,
Hostname = Constants.SSE_SERVER_HOST,
Id = "sse"
};
public static Server IdentityServer { get; } = new(Constants.IDENTITY_CERT_PATH, Constants.IDENTITY_CERT_KEY_PATH)
{
PublicURL = Constants.IDENTITY_SERVER_URL,
Domain = Constants.IDENTITY_SERVER_DOMAIN,
Port = Constants.IDENTITY_SERVER_PORT,
Hostname = Constants.IDENTITY_SERVER_HOST,
Id = "auth"
};
public static Server SubscriptionServer { get; } = new(Constants.SUBSCRIPTIONS_CERT_PATH, Constants.SUBSCRIPTIONS_CERT_KEY_PATH)
{
Domain = Constants.SUBSCRIPTIONS_SERVER_DOMAIN,
Port = Constants.SUBSCRIPTIONS_SERVER_PORT,
Hostname = Constants.SUBSCRIPTIONS_SERVER_HOST,
Id = "subscription"
};
}
}
@@ -1,134 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using MimeKit;
using MimeKit.Cryptography;
using Org.BouncyCastle.Bcpg;
using Scriban;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Services
{
public class EmailSender : IEmailSender, IAsyncDisposable
{
private readonly SmtpClient mailClient = new();
public async Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
)
{
if (!mailClient.IsConnected)
{
if (int.TryParse(Common.Constants.SMTP_PORT, out int port))
{
await mailClient.ConnectAsync(
Common.Constants.SMTP_HOST,
port,
MailKit.Security.SecureSocketOptions.Auto
);
}
else
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
}
if (!mailClient.IsAuthenticated)
await mailClient.AuthenticateAsync(
Common.Constants.SMTP_USERNAME,
Common.Constants.SMTP_PASSWORD
);
var message = new MimeMessage();
var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
message.From.Add(sender);
message.To.Add(new MailboxAddress("", email));
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
if (!string.IsNullOrEmpty(Common.Constants.SMTP_REPLYTO_EMAIL))
message.ReplyTo.Add(MailboxAddress.Parse(Common.Constants.SMTP_REPLYTO_EMAIL));
message.Body = await GetEmailBodyAsync(
template,
client,
sender,
gpgContext,
attachments
);
await mailClient.SendAsync(message);
}
private static async Task<MimeEntity> GetEmailBodyAsync(
EmailTemplate template,
IClient client,
MailboxAddress sender,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
)
{
var builder = new BodyBuilder();
try
{
builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
if (attachments != null)
{
foreach (var attachment in attachments)
{
builder.Attachments.Add(attachment.Key, attachment.Value);
}
}
var key = gpgContext?.GetSigningKey(sender);
if (key != null)
{
using (MemoryStream outputStream = new())
{
using (Stream armoredStream = new ArmoredOutputStream(outputStream))
{
key.PublicKey.Encode(armoredStream);
}
outputStream.Seek(0, SeekOrigin.Begin);
builder.Attachments.Add(
$"{client.Id}_pub.asc",
Encoding.ASCII.GetBytes(
Encoding.ASCII.GetString(outputStream.ToArray())
)
);
}
return await MultipartSigned.CreateAsync(
gpgContext,
sender,
DigestAlgorithm.Sha256,
builder.ToMessageBody()
);
}
else
{
return builder.ToMessageBody();
}
}
catch (Exception ex)
{
await Slogger<EmailSender>.Error("GetEmailBodyAsync", ex.ToString());
return builder.ToMessageBody();
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await mailClient.DisconnectAsync(true);
mailClient.Dispose();
}
}
}
@@ -1,138 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.WebUtilities;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Services
{
public class PaddleBillingService
{
#if DEBUG
private const string PADDLE_BASE_URI = "https://sandbox-api.paddle.com";
#else
private const string PADDLE_BASE_URI = "https://api.paddle.com";
#endif
private readonly HttpClient httpClient = new();
public PaddleBillingService(string paddleApiKey)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", paddleApiKey);
}
public async Task<GetSubscriptionResponse?> GetSubscriptionAsync(string subscriptionId)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
var response = await httpClient.GetAsync(url);
return await response.Content.ReadFromJsonAsync<GetSubscriptionResponse>();
}
public async Task<GetTransactionResponse?> GetTransactionAsync(string transactionId)
{
var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}";
var response = await httpClient.GetAsync(url);
return await response.Content.ReadFromJsonAsync<GetTransactionResponse>();
}
public async Task<GetTransactionInvoiceResponse?> GetTransactionInvoiceAsync(string transactionId)
{
var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}/invoice";
var response = await httpClient.GetAsync(url);
return await response.Content.ReadFromJsonAsync<GetTransactionInvoiceResponse>();
}
public async Task<ListTransactionsResponseV2?> ListTransactionsAsync(string? subscriptionId = null, string? customerId = null, string[]? status = null, string[]? origin = null)
{
var url = $"{PADDLE_BASE_URI}/transactions";
var parameters = new Dictionary<string, string?>()
{
{ "subscription_id", subscriptionId },
{ "customer_id", customerId },
{ "status", string.Join(',', status ?? ["billed","completed"]) },
{ "order_by", "billed_at[DESC]" }
};
if (origin is not null) parameters.Add("origin", string.Join(',', origin));
var response = await httpClient.GetAsync(QueryHelpers.AddQueryString(url, parameters));
return await response.Content.ReadFromJsonAsync<ListTransactionsResponseV2>();
}
public async Task<PaddleResponse?> RefundTransactionAsync(string transactionId, string transactionItemId, string reason = "")
{
var url = $"{PADDLE_BASE_URI}/adjustments";
var response = await httpClient.PostAsync(url, JsonContent.Create(new Dictionary<string, object>
{
{ "action", "refund" },
{
"items",
new object[]
{
new Dictionary<string, string> {
{"item_id", transactionItemId},
{"type", "full"}
}
}
},
{ "reason", reason },
{ "transaction_id", transactionId }
}));
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
}
public async Task<SubscriptionPreviewResponse?> PreviewSubscriptionChangeAsync(string subscriptionId, string newProductId, bool isTrialing)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/preview";
var response = await httpClient.PatchAsync(url, JsonContent.Create(new
{
proration_billing_mode = isTrialing ? "do_not_bill" : "prorated_immediately",
items = new[] { new { price_id = newProductId, quantity = 1 } }
}));
return await response.Content.ReadFromJsonAsync<SubscriptionPreviewResponse>();
}
public async Task<PaddleResponse?> ChangeSubscriptionAsync(string subscriptionId, string newProductId, bool isTrialing)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
var response = await httpClient.PatchAsync(url, JsonContent.Create(new
{
proration_billing_mode = isTrialing ? "do_not_bill" : "prorated_immediately",
items = new[] { new { price_id = newProductId, quantity = 1 } }
}));
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
}
public async Task<PaddleResponse?> CancelSubscriptionAsync(string subscriptionId)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/cancel";
var response = await httpClient.PostAsync(url, JsonContent.Create(new { effective_from = "immediately" }));
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
}
public async Task<PaddleResponse?> PauseSubscriptionAsync(string subscriptionId)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/pause";
var response = await httpClient.PostAsync(url, JsonContent.Create(new { }));
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
}
public async Task<PaddleResponse?> ResumeSubscriptionAsync(string subscriptionId)
{
var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}";
var response = await httpClient.PatchAsync(url, JsonContent.Create(new Dictionary<string, string?>
{
{"scheduled_change", null}
}));
return await response.Content.ReadFromJsonAsync<PaddleResponse>();
}
public async Task<GetCustomerResponse?> FindCustomerFromTransactionAsync(string transactionId)
{
var transaction = await GetTransactionAsync(transactionId);
if (transaction == null) return null;
var url = $"{PADDLE_BASE_URI}/customers/{transaction.Transaction.CustomerId}";
var response = await httpClient.GetFromJsonAsync<GetCustomerResponse>(url);
return response;
}
}
}

Some files were not shown because too many files have changed in this diff Show More