Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d20a9cff0 |
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
**/Dockerfile
|
**/Dockerfile
|
||||||
**/bin
|
**/bin
|
||||||
**/obj
|
**/obj
|
||||||
**/.env
|
**/.env
|
||||||
**/.env.local
|
**/.env.local
|
||||||
@@ -1,77 +1,31 @@
|
|||||||
# Description: Name of your self hosted instance. Used in the client apps for identification purposes
|
# Required variables
|
||||||
# Required: yes
|
NOTESNOOK_API_SECRET= # This should be a randomly generated secret
|
||||||
# Example: notesnook-instance-sg
|
|
||||||
INSTANCE_NAME=self-hosted-notesnook-instance
|
|
||||||
|
|
||||||
# Description: This secret is used for generating, validating, and introspecting auth tokens. It must be a randomly generated token (preferably >32 characters).
|
# SMTP settings required for delivering emails
|
||||||
# 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_USERNAME=
|
SMTP_USERNAME=
|
||||||
# Description: Password for the SMTP connection. Check your email provider's documentation to get the appropriate value.
|
|
||||||
# Required: yes
|
|
||||||
SMTP_PASSWORD=
|
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=
|
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=
|
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-
|
# MessageBird is used for 2FA via SMS
|
||||||
# Required: no
|
MESSAGEBIRD_ACCESS_KEY=
|
||||||
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=
|
|
||||||
|
|
||||||
# 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.
|
# Server discovery settings
|
||||||
# Required: no
|
# The domain must be without protocol
|
||||||
# Example: https://app.notesnook.com,http://localhost:3000
|
# e.g. example.org NOT http://example.org
|
||||||
NOTESNOOK_CORS_ORIGINS=
|
NOTESNOOK_SERVER_DOMAIN=
|
||||||
|
IDENTITY_SERVER_DOMAIN=
|
||||||
|
SSE_SERVER_DOMAIN=
|
||||||
|
|
||||||
# Description: Add known proxies for incoming HTTP requests
|
# url of the web app instance you want to use
|
||||||
# Required: no
|
# e.g. http://localhost:3000
|
||||||
# Example: 192.168.1.2,192.168.1.3
|
# Note: no slashes at the end
|
||||||
KNOWN_PROXIES=
|
NOTESNOOK_APP_HOST=
|
||||||
|
|
||||||
# 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).
|
# Minio is used for S3 storage
|
||||||
# Note: the URL has no slashes at the end
|
MINIO_ROOT_USER= # aka. AccessKeyId (must be > 3 characters)
|
||||||
# Required: yes
|
MINIO_ROOT_PASSWORD= # aka. AccessKey (must be > 8 characters)
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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=
|
|
||||||
|
|||||||
@@ -1,93 +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
|
|
||||||
context: .
|
|
||||||
|
|
||||||
- image: streetwriters/cors-proxy
|
|
||||||
file: ./cors-proxy/Dockerfile
|
|
||||||
context: ./cors-proxy/
|
|
||||||
|
|
||||||
- image: streetwriters/identity
|
|
||||||
file: ./Streetwriters.Identity/Dockerfile
|
|
||||||
context: .
|
|
||||||
|
|
||||||
- image: streetwriters/sse
|
|
||||||
file: ./Streetwriters.Messenger/Dockerfile
|
|
||||||
context: .
|
|
||||||
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
|
|
||||||
|
|
||||||
- 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: ${{ matrix.repos.context }}
|
|
||||||
file: ${{ matrix.repos.file }}
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
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
|
|
||||||
+267
-267
@@ -1,267 +1,267 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
[Dd]ebug/
|
[Dd]ebug/
|
||||||
[Dd]ebugPublic/
|
[Dd]ebugPublic/
|
||||||
[Rr]elease/
|
[Rr]elease/
|
||||||
[Rr]eleases/
|
[Rr]eleases/
|
||||||
x64/
|
x64/
|
||||||
x86/
|
x86/
|
||||||
bld/
|
bld/
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
[Oo]bj/
|
[Oo]bj/
|
||||||
[Ll]og/
|
[Ll]og/
|
||||||
|
|
||||||
# Visual Studio 2015 cache/options directory
|
# Visual Studio 2015 cache/options directory
|
||||||
.vs/
|
.vs/
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
#wwwroot/
|
#wwwroot/
|
||||||
|
|
||||||
# MSTest test Results
|
# MSTest test Results
|
||||||
[Tt]est[Rr]esult*/
|
[Tt]est[Rr]esult*/
|
||||||
[Bb]uild[Ll]og.*
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
# NUNIT
|
# NUNIT
|
||||||
*.VisualState.xml
|
*.VisualState.xml
|
||||||
TestResult.xml
|
TestResult.xml
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
# Build Results of an ATL Project
|
||||||
[Dd]ebugPS/
|
[Dd]ebugPS/
|
||||||
[Rr]eleasePS/
|
[Rr]eleasePS/
|
||||||
dlldata.c
|
dlldata.c
|
||||||
|
|
||||||
# DNX
|
# DNX
|
||||||
project.lock.json
|
project.lock.json
|
||||||
project.fragment.lock.json
|
project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
|
|
||||||
*_i.c
|
*_i.c
|
||||||
*_p.c
|
*_p.c
|
||||||
*_i.h
|
*_i.h
|
||||||
*.ilk
|
*.ilk
|
||||||
*.meta
|
*.meta
|
||||||
*.obj
|
*.obj
|
||||||
*.pch
|
*.pch
|
||||||
*.pdb
|
*.pdb
|
||||||
*.pgc
|
*.pgc
|
||||||
*.pgd
|
*.pgd
|
||||||
*.rsp
|
*.rsp
|
||||||
*.sbr
|
*.sbr
|
||||||
*.tlb
|
*.tlb
|
||||||
*.tli
|
*.tli
|
||||||
*.tlh
|
*.tlh
|
||||||
*.tmp
|
*.tmp
|
||||||
*.tmp_proj
|
*.tmp_proj
|
||||||
*.log
|
*.log
|
||||||
*.vspscc
|
*.vspscc
|
||||||
*.vssscc
|
*.vssscc
|
||||||
.builds
|
.builds
|
||||||
*.pidb
|
*.pidb
|
||||||
*.svclog
|
*.svclog
|
||||||
*.scc
|
*.scc
|
||||||
|
|
||||||
# Chutzpah Test files
|
# Chutzpah Test files
|
||||||
_Chutzpah*
|
_Chutzpah*
|
||||||
|
|
||||||
# Visual C++ cache files
|
# Visual C++ cache files
|
||||||
ipch/
|
ipch/
|
||||||
*.aps
|
*.aps
|
||||||
*.ncb
|
*.ncb
|
||||||
*.opendb
|
*.opendb
|
||||||
*.opensdf
|
*.opensdf
|
||||||
*.sdf
|
*.sdf
|
||||||
*.cachefile
|
*.cachefile
|
||||||
*.VC.db
|
*.VC.db
|
||||||
*.VC.VC.opendb
|
*.VC.VC.opendb
|
||||||
|
|
||||||
# Visual Studio profiler
|
# Visual Studio profiler
|
||||||
*.psess
|
*.psess
|
||||||
*.vsp
|
*.vsp
|
||||||
*.vspx
|
*.vspx
|
||||||
*.sap
|
*.sap
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
# TFS 2012 Local Workspace
|
||||||
$tf/
|
$tf/
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
# Guidance Automation Toolkit
|
||||||
*.gpState
|
*.gpState
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
# ReSharper is a .NET coding add-in
|
||||||
_ReSharper*/
|
_ReSharper*/
|
||||||
*.[Rr]e[Ss]harper
|
*.[Rr]e[Ss]harper
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
||||||
|
|
||||||
# JustCode is a .NET coding add-in
|
# JustCode is a .NET coding add-in
|
||||||
.JustCode
|
.JustCode
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
# TeamCity is a build add-in
|
||||||
_TeamCity*
|
_TeamCity*
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
# DotCover is a Code Coverage Tool
|
||||||
*.dotCover
|
*.dotCover
|
||||||
|
|
||||||
# NCrunch
|
# NCrunch
|
||||||
_NCrunch_*
|
_NCrunch_*
|
||||||
.*crunch*.local.xml
|
.*crunch*.local.xml
|
||||||
nCrunchTemp_*
|
nCrunchTemp_*
|
||||||
|
|
||||||
# MightyMoose
|
# MightyMoose
|
||||||
*.mm.*
|
*.mm.*
|
||||||
AutoTest.Net/
|
AutoTest.Net/
|
||||||
|
|
||||||
# Web workbench (sass)
|
# Web workbench (sass)
|
||||||
.sass-cache/
|
.sass-cache/
|
||||||
|
|
||||||
# Installshield output folder
|
# Installshield output folder
|
||||||
[Ee]xpress/
|
[Ee]xpress/
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
# DocProject is a documentation generator add-in
|
||||||
DocProject/buildhelp/
|
DocProject/buildhelp/
|
||||||
DocProject/Help/*.HxT
|
DocProject/Help/*.HxT
|
||||||
DocProject/Help/*.HxC
|
DocProject/Help/*.HxC
|
||||||
DocProject/Help/*.hhc
|
DocProject/Help/*.hhc
|
||||||
DocProject/Help/*.hhk
|
DocProject/Help/*.hhk
|
||||||
DocProject/Help/*.hhp
|
DocProject/Help/*.hhp
|
||||||
DocProject/Help/Html2
|
DocProject/Help/Html2
|
||||||
DocProject/Help/html
|
DocProject/Help/html
|
||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
publish/
|
||||||
|
|
||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
#*.pubxml
|
#*.pubxml
|
||||||
*.publishproj
|
*.publishproj
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
# in these scripts will be unencrypted
|
# in these scripts will be unencrypted
|
||||||
PublishScripts/
|
PublishScripts/
|
||||||
|
|
||||||
# NuGet Packages
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
# The packages folder can be ignored because of Package Restore
|
# The packages folder can be ignored because of Package Restore
|
||||||
**/packages/*
|
**/packages/*
|
||||||
# except build/, which is used as an MSBuild target.
|
# except build/, which is used as an MSBuild target.
|
||||||
!**/packages/build/
|
!**/packages/build/
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
#!**/packages/repositories.config
|
#!**/packages/repositories.config
|
||||||
# NuGet v3's project.json files produces more ignoreable files
|
# NuGet v3's project.json files produces more ignoreable files
|
||||||
*.nuget.props
|
*.nuget.props
|
||||||
*.nuget.targets
|
*.nuget.targets
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
# Microsoft Azure Build Output
|
||||||
csx/
|
csx/
|
||||||
*.build.csdef
|
*.build.csdef
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
# Microsoft Azure Emulator
|
||||||
ecf/
|
ecf/
|
||||||
rcf/
|
rcf/
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
# Windows Store app package directories and files
|
||||||
AppPackages/
|
AppPackages/
|
||||||
BundleArtifacts/
|
BundleArtifacts/
|
||||||
Package.StoreAssociation.xml
|
Package.StoreAssociation.xml
|
||||||
_pkginfo.txt
|
_pkginfo.txt
|
||||||
|
|
||||||
# Visual Studio cache files
|
# Visual Studio cache files
|
||||||
# files ending in .cache can be ignored
|
# files ending in .cache can be ignored
|
||||||
*.[Cc]ache
|
*.[Cc]ache
|
||||||
# but keep track of directories ending in .cache
|
# but keep track of directories ending in .cache
|
||||||
!*.[Cc]ache/
|
!*.[Cc]ache/
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
ClientBin/
|
ClientBin/
|
||||||
~$*
|
~$*
|
||||||
*~
|
*~
|
||||||
*.dbmdl
|
*.dbmdl
|
||||||
*.dbproj.schemaview
|
*.dbproj.schemaview
|
||||||
*.jfm
|
*.jfm
|
||||||
*.pfx
|
*.pfx
|
||||||
*.publishsettings
|
*.publishsettings
|
||||||
node_modules/
|
node_modules/
|
||||||
orleans.codegen.cs
|
orleans.codegen.cs
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
#bower_components/
|
#bower_components/
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
# RIA/Silverlight projects
|
||||||
Generated_Code/
|
Generated_Code/
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
# Backup & report files from converting an old project file
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
# because we have git ;-)
|
# because we have git ;-)
|
||||||
_UpgradeReport_Files/
|
_UpgradeReport_Files/
|
||||||
Backup*/
|
Backup*/
|
||||||
UpgradeLog*.XML
|
UpgradeLog*.XML
|
||||||
UpgradeLog*.htm
|
UpgradeLog*.htm
|
||||||
|
|
||||||
# SQL Server files
|
# SQL Server files
|
||||||
*.mdf
|
*.mdf
|
||||||
*.ldf
|
*.ldf
|
||||||
|
|
||||||
# Business Intelligence projects
|
# Business Intelligence projects
|
||||||
*.rdl.data
|
*.rdl.data
|
||||||
*.bim.layout
|
*.bim.layout
|
||||||
*.bim_*.settings
|
*.bim_*.settings
|
||||||
|
|
||||||
# Microsoft Fakes
|
# Microsoft Fakes
|
||||||
FakesAssemblies/
|
FakesAssemblies/
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
# GhostDoc plugin setting file
|
||||||
*.GhostDoc.xml
|
*.GhostDoc.xml
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
# Node.js Tools for Visual Studio
|
||||||
.ntvs_analysis.dat
|
.ntvs_analysis.dat
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
# Visual Studio 6 build log
|
||||||
*.plg
|
*.plg
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
# Visual Studio 6 workspace options file
|
||||||
*.opt
|
*.opt
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
# Visual Studio LightSwitch build output
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
**/*.Server/GeneratedArtifacts
|
**/*.Server/GeneratedArtifacts
|
||||||
**/*.Server/ModelManifest.xml
|
**/*.Server/ModelManifest.xml
|
||||||
_Pvt_Extensions
|
_Pvt_Extensions
|
||||||
|
|
||||||
# Paket dependency manager
|
# Paket dependency manager
|
||||||
.paket/paket.exe
|
.paket/paket.exe
|
||||||
paket-files/
|
paket-files/
|
||||||
|
|
||||||
# FAKE - F# Make
|
# FAKE - F# Make
|
||||||
.fake/
|
.fake/
|
||||||
|
|
||||||
# JetBrains Rider
|
# JetBrains Rider
|
||||||
.idea/
|
.idea/
|
||||||
*.sln.iml
|
*.sln.iml
|
||||||
|
|
||||||
# CodeRush
|
# CodeRush
|
||||||
.cr/
|
.cr/
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
# Python Tools for Visual Studio (PTVS)
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
keys/
|
keys/
|
||||||
dist/
|
dist/
|
||||||
keystore/
|
appsettings.json
|
||||||
.env.local
|
keystore/
|
||||||
Notesnook.API/sync/
|
.env.local
|
||||||
Vendored
+6
-3
@@ -9,7 +9,8 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build-notesnook",
|
"preLaunchTask": "build-notesnook",
|
||||||
"program": "bin/Debug/net9.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": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Notesnook.API",
|
"cwd": "${workspaceFolder}/Notesnook.API",
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build-identity",
|
"preLaunchTask": "build-identity",
|
||||||
"program": "bin/Debug/net9.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": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Streetwriters.Identity",
|
"cwd": "${workspaceFolder}/Streetwriters.Identity",
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
@@ -39,7 +41,8 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build-messenger",
|
"preLaunchTask": "build-messenger",
|
||||||
"program": "bin/Debug/net9.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": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Streetwriters.Messenger",
|
"cwd": "${workspaceFolder}/Streetwriters.Messenger",
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
|
|||||||
Vendored
+41
-41
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "build-notesnook",
|
"label": "build-notesnook",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"${workspaceFolder}/Notesnook.API/Notesnook.API.csproj",
|
"${workspaceFolder}/Notesnook.API/Notesnook.API.csproj",
|
||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "build-identity",
|
"label": "build-identity",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"${workspaceFolder}/Streetwriters.Identity/Streetwriters.Identity.csproj",
|
"${workspaceFolder}/Streetwriters.Identity/Streetwriters.Identity.csproj",
|
||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "build-messenger",
|
"label": "build-messenger",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"${workspaceFolder}/Streetwriters.Messenger/Streetwriters.Messenger.csproj",
|
"${workspaceFolder}/Streetwriters.Messenger/Streetwriters.Messenger.csproj",
|
||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Notesnook Sync Server is written & maintained by:
|
Notesnook Sync Server is written & maintained by:
|
||||||
- Abdullah Atta <abdullahatta@streetwriters.co>
|
- Abdullah Atta <abdullahatta@streetwriters.co>
|
||||||
- Ammar Ahmed <ammarahmed6506@gmail.com>
|
- Ammar Ahmed <ammarahmed6506@gmail.com>
|
||||||
|
|||||||
+128
-128
@@ -1,128 +1,128 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
diverse, inclusive, and healthy community.
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
- Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
- Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
- Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
- Focusing on what is best not just for us as individuals, but for the
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
overall community
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
- The use of sexualized language or imagery, and sexual attention or
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
advances of any kind
|
advances of any kind
|
||||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
- Public or private harassment
|
- Public or private harassment
|
||||||
- Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
address, without their explicit permission
|
||||||
- Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
or harmful.
|
or harmful.
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
decisions when appropriate.
|
decisions when appropriate.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
an individual is officially representing the community in public spaces.
|
an individual is officially representing the community in public spaces.
|
||||||
Examples of representing our community include using an official e-mail address,
|
Examples of representing our community include using an official e-mail address,
|
||||||
posting via an official social media account, or acting as an appointed
|
posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event.
|
representative at an online or offline event.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
support@streetwriters.co.
|
support@streetwriters.co.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
reporter of any incident.
|
reporter of any incident.
|
||||||
|
|
||||||
## Enforcement Guidelines
|
## Enforcement Guidelines
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
### 1. Correction
|
### 1. Correction
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
unprofessional or unwelcome in the community.
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
clarity around the nature of the violation and an explanation of why the
|
clarity around the nature of the violation and an explanation of why the
|
||||||
behavior was inappropriate. A public apology may be requested.
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
### 2. Warning
|
### 2. Warning
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
**Community Impact**: A violation through a single incident or series
|
||||||
of actions.
|
of actions.
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
interaction with the people involved, including unsolicited interaction with
|
interaction with the people involved, including unsolicited interaction with
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
like social media. Violating these terms may lead to a temporary or
|
like social media. Violating these terms may lead to a temporary or
|
||||||
permanent ban.
|
permanent ban.
|
||||||
|
|
||||||
### 3. Temporary Ban
|
### 3. Temporary Ban
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
**Community Impact**: A serious violation of community standards, including
|
||||||
sustained inappropriate behavior.
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
communication with the community for a specified period of time. No public or
|
communication with the community for a specified period of time. No public or
|
||||||
private interaction with the people involved, including unsolicited interaction
|
private interaction with the people involved, including unsolicited interaction
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
Violating these terms may lead to a permanent ban.
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
the community.
|
the community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
version 2.0, available at
|
version 2.0, available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
https://www.contributor-covenant.org/translations.
|
https://www.contributor-covenant.org/translations.
|
||||||
|
|||||||
@@ -17,91 +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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Repositories;
|
using Notesnook.API.Repositories;
|
||||||
using Streetwriters.Data.Interfaces;
|
|
||||||
using Streetwriters.Data.Repositories;
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
namespace Notesnook.API.Accessors
|
namespace Notesnook.API.Accessors
|
||||||
{
|
{
|
||||||
public class SyncItemsRepositoryAccessor : ISyncItemsRepositoryAccessor
|
public class SyncItemsRepositoryAccessor : ISyncItemsRepositoryAccessor
|
||||||
{
|
{
|
||||||
public SyncItemsRepository Notes { get; }
|
public SyncItemsRepository<Note> Notes { get; }
|
||||||
public SyncItemsRepository Notebooks { get; }
|
public SyncItemsRepository<Notebook> Notebooks { get; }
|
||||||
public SyncItemsRepository Shortcuts { get; }
|
public SyncItemsRepository<Shortcut> Shortcuts { get; }
|
||||||
public SyncItemsRepository Relations { get; }
|
public SyncItemsRepository<Relation> Relations { get; }
|
||||||
public SyncItemsRepository Reminders { get; }
|
public SyncItemsRepository<Reminder> Reminders { get; }
|
||||||
public SyncItemsRepository Contents { get; }
|
public SyncItemsRepository<Content> Contents { get; }
|
||||||
public SyncItemsRepository LegacySettings { get; }
|
public SyncItemsRepository<Setting> Settings { get; }
|
||||||
public SyncItemsRepository Settings { get; }
|
public SyncItemsRepository<Attachment> Attachments { get; }
|
||||||
public SyncItemsRepository Attachments { get; }
|
|
||||||
public SyncItemsRepository Colors { get; }
|
|
||||||
public SyncItemsRepository Vaults { get; }
|
|
||||||
public SyncItemsRepository Tags { get; }
|
|
||||||
public Repository<UserSettings> UsersSettings { get; }
|
public Repository<UserSettings> UsersSettings { get; }
|
||||||
public Repository<Monograph> Monographs { get; }
|
public Repository<Monograph> Monographs { get; }
|
||||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
|
||||||
public Repository<InboxSyncItem> InboxItems { get; }
|
|
||||||
public Repository<SyncDevice> SyncDevices { get; }
|
|
||||||
public Repository<DeviceIdsChunk> DeviceIdsChunks { get; }
|
|
||||||
|
|
||||||
public SyncItemsRepositoryAccessor(IDbContext dbContext,
|
public SyncItemsRepositoryAccessor(SyncItemsRepository<Note> _notes,
|
||||||
|
SyncItemsRepository<Notebook> _notebooks,
|
||||||
[FromKeyedServices(Collections.NotebooksKey)]
|
SyncItemsRepository<Content> _content,
|
||||||
IMongoCollection<SyncItem> notebooks,
|
SyncItemsRepository<Setting> _settings,
|
||||||
[FromKeyedServices(Collections.NotesKey)]
|
SyncItemsRepository<Attachment> _attachments,
|
||||||
IMongoCollection<SyncItem> notes,
|
SyncItemsRepository<Shortcut> _shortcuts,
|
||||||
[FromKeyedServices(Collections.ContentKey)]
|
SyncItemsRepository<Relation> _relations,
|
||||||
IMongoCollection<SyncItem> content,
|
SyncItemsRepository<Reminder> _reminders,
|
||||||
[FromKeyedServices(Collections.SettingsKey)]
|
Repository<UserSettings> _usersSettings,
|
||||||
IMongoCollection<SyncItem> settings,
|
Repository<Monograph> _monographs)
|
||||||
[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,
|
|
||||||
Repository<SyncDevice> syncDevices,
|
|
||||||
Repository<DeviceIdsChunk> deviceIdsChunks,
|
|
||||||
ILogger<SyncItemsRepository> logger)
|
|
||||||
{
|
{
|
||||||
UsersSettings = usersSettings;
|
Notebooks = _notebooks;
|
||||||
Monographs = monographs;
|
Notes = _notes;
|
||||||
InboxApiKey = inboxApiKey;
|
Contents = _content;
|
||||||
InboxItems = inboxItems;
|
Settings = _settings;
|
||||||
SyncDevices = syncDevices;
|
Attachments = _attachments;
|
||||||
DeviceIdsChunks = deviceIdsChunks;
|
UsersSettings = _usersSettings;
|
||||||
Notebooks = new SyncItemsRepository(dbContext, notebooks, logger);
|
Monographs = _monographs;
|
||||||
Notes = new SyncItemsRepository(dbContext, notes, logger);
|
Shortcuts = _shortcuts;
|
||||||
Contents = new SyncItemsRepository(dbContext, content, logger);
|
Reminders = _reminders;
|
||||||
Settings = new SyncItemsRepository(dbContext, settings, logger);
|
Relations = _relations;
|
||||||
LegacySettings = new SyncItemsRepository(dbContext, legacySettings, logger);
|
|
||||||
Attachments = new SyncItemsRepository(dbContext, attachments, logger);
|
|
||||||
Shortcuts = new SyncItemsRepository(dbContext, shortcuts, logger);
|
|
||||||
Reminders = new SyncItemsRepository(dbContext, reminders, logger);
|
|
||||||
Relations = new SyncItemsRepository(dbContext, relations, logger);
|
|
||||||
Colors = new SyncItemsRepository(dbContext, colors, logger);
|
|
||||||
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
|
|
||||||
Tags = new SyncItemsRepository(dbContext, tags, logger);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Notesnook.API.Authorization
|
namespace Notesnook.API.Authorization
|
||||||
{
|
{
|
||||||
public class NotesnookUserRequirement : AuthorizationHandler<NotesnookUserRequirement>, IAuthorizationRequirement
|
public class NotesnookUserRequirement : AuthorizationHandler<NotesnookUserRequirement>, IAuthorizationRequirement
|
||||||
{
|
{
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotesnookUserRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotesnookUserRequirement requirement)
|
||||||
{
|
{
|
||||||
var isInAudience = context.User.HasClaim("aud", "notesnook");
|
var isInAudience = context.User.HasClaim("aud", "notesnook");
|
||||||
var hasRole = context.User.HasClaim("role", "notesnook");
|
var hasRole = context.User.HasClaim("role", "notesnook");
|
||||||
if (isInAudience && hasRole)
|
if (isInAudience && hasRole)
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,45 +29,49 @@ namespace Notesnook.API.Authorization
|
|||||||
{
|
{
|
||||||
public class SyncRequirement : AuthorizationHandler<SyncRequirement>, IAuthorizationRequirement
|
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/attachments"] = "use attachments",
|
||||||
["/sync"] = "sync your notes",
|
["/sync"] = "sync your notes",
|
||||||
["/hubs/sync"] = "sync your notes",
|
["/hubs/sync"] = "sync your notes",
|
||||||
["/hubs/sync/v2"] = "sync your notes",
|
|
||||||
["/monographs"] = "publish monographs"
|
["/monographs"] = "publish monographs"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private string[] allowedClaims = { "trial", "premium", "premium_canceled" };
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement)
|
||||||
{
|
{
|
||||||
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
|
||||||
var result = this.IsAuthorized(context.User, path);
|
var result = this.IsAuthorized(context.User, path);
|
||||||
if (result.Succeeded) context.Succeed(requirement);
|
if (result.Succeeded) context.Succeed(requirement);
|
||||||
else if (result.AuthorizationFailure?.FailureReasons.Any() == true)
|
else
|
||||||
context.Fail(result.AuthorizationFailure.FailureReasons.First());
|
{
|
||||||
else context.Fail();
|
var hasReason = result.AuthorizationFailure.FailureReasons.Count() > 0;
|
||||||
|
if (hasReason)
|
||||||
|
context.Fail(result.AuthorizationFailure.FailureReasons.First());
|
||||||
|
else context.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
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))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
var reason = new[]
|
var reason = new AuthorizationFailureReason[]
|
||||||
{
|
{
|
||||||
new AuthorizationFailureReason(this, "Invalid token.")
|
new AuthorizationFailureReason(this, "Invalid token.")
|
||||||
};
|
};
|
||||||
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasSyncScope = User?.HasClaim("scope", "notesnook.sync") ?? false;
|
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
|
||||||
var isInAudience = User?.HasClaim("aud", "notesnook") ?? false;
|
var isInAudience = User.HasClaim("aud", "notesnook");
|
||||||
var hasRole = User?.HasClaim("role", "notesnook") ?? false;
|
var hasRole = User.HasClaim("role", "notesnook");
|
||||||
|
|
||||||
var isEmailVerified = User?.HasClaim("verified", "true") ?? false;
|
var isEmailVerified = User.HasClaim("verified", "true");
|
||||||
|
|
||||||
if (!isEmailVerified)
|
if (!isEmailVerified)
|
||||||
{
|
{
|
||||||
@@ -80,7 +84,7 @@ namespace Notesnook.API.Authorization
|
|||||||
}
|
}
|
||||||
|
|
||||||
var error = $"Please confirm your email to {phrase}.";
|
var error = $"Please confirm your email to {phrase}.";
|
||||||
var reason = new[]
|
var reason = new AuthorizationFailureReason[]
|
||||||
{
|
{
|
||||||
new AuthorizationFailureReason(this, error)
|
new AuthorizationFailureReason(this, error)
|
||||||
};
|
};
|
||||||
@@ -88,6 +92,7 @@ namespace Notesnook.API.Authorization
|
|||||||
// context.Fail(new AuthorizationFailureReason(this, error));
|
// context.Fail(new AuthorizationFailureReason(this, error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isProOrTrial = User.HasClaim((c) => c.Type == "notesnook:status" && allowedClaims.Contains(c.Value));
|
||||||
if (hasSyncScope && isInAudience && hasRole && isEmailVerified)
|
if (hasSyncScope && isInAudience && hasRole && isEmailVerified)
|
||||||
return PolicyAuthorizationResult.Success(); //(requirement);
|
return PolicyAuthorizationResult.Success(); //(requirement);
|
||||||
return PolicyAuthorizationResult.Forbid();
|
return PolicyAuthorizationResult.Forbid();
|
||||||
|
|||||||
@@ -1,22 +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 InboxItemsKey = "inbox_items";
|
|
||||||
public const string InboxApiKeysKey = "inbox_api_keys";
|
|
||||||
public const string SyncDevicesKey = "sync_devices";
|
|
||||||
public const string DeviceIdsChunksKey = "device_ids_chunks";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +1,51 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using AngleSharp.Text;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Notesnook.API.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Streetwriters.Data.Repositories;
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Accessors;
|
namespace Notesnook.API.Controllers
|
||||||
using Notesnook.API.Models;
|
{
|
||||||
using Streetwriters.Common;
|
// TODO: this should be moved out into its own microservice
|
||||||
using Streetwriters.Common.Accessors;
|
[ApiController]
|
||||||
using Streetwriters.Common.Interfaces;
|
[Route("announcements")]
|
||||||
using Streetwriters.Common.Models;
|
public class AnnouncementController : ControllerBase
|
||||||
using Streetwriters.Data.Repositories;
|
{
|
||||||
|
private Repository<Announcement> Announcements { get; set; }
|
||||||
namespace Notesnook.API.Controllers
|
public AnnouncementController(Repository<Announcement> announcements)
|
||||||
{
|
{
|
||||||
// TODO: this should be moved out into its own microservice
|
Announcements = announcements;
|
||||||
[ApiController]
|
}
|
||||||
[Route("announcements")]
|
|
||||||
public class AnnouncementController(Repository<Announcement> announcements, WampServiceAccessor serviceAccessor) : ControllerBase
|
[HttpGet("active")]
|
||||||
{
|
[AllowAnonymous]
|
||||||
[HttpGet("active")]
|
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string userId)
|
||||||
[AllowAnonymous]
|
{
|
||||||
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string? userId)
|
var announcements = await Announcements.FindAsync((a) => a.IsActive);
|
||||||
{
|
return Ok(announcements.Where((a) => a.UserIds != null && a.UserIds.Length > 0
|
||||||
var filter = Builders<Announcement>.Filter.Eq(x => x.IsActive, true);
|
? a.UserIds.Contains(userId)
|
||||||
if (!string.IsNullOrEmpty(userId))
|
: true));
|
||||||
{
|
}
|
||||||
var userFilter = Builders<Announcement>.Filter.Or(
|
}
|
||||||
Builders<Announcement>.Filter.Eq(x => x.UserIds, null),
|
}
|
||||||
Builders<Announcement>.Filter.Size(x => x.UserIds, 0),
|
|
||||||
Builders<Announcement>.Filter.AnyEq(x => x.UserIds, userId)
|
|
||||||
);
|
|
||||||
filter = Builders<Announcement>.Filter.And(filter, userFilter);
|
|
||||||
}
|
|
||||||
var userAnnouncements = await announcements.Collection.Find(filter).ToListAsync();
|
|
||||||
foreach (var announcement in userAnnouncements)
|
|
||||||
{
|
|
||||||
if (userId != null && 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" || action.Data == null) continue;
|
|
||||||
|
|
||||||
action.Data = action.Data.Replace("{{UserId}}", userId ?? "");
|
|
||||||
|
|
||||||
if (action.Data.Contains("{{Email}}"))
|
|
||||||
{
|
|
||||||
var user = string.IsNullOrEmpty(userId) ? null : await serviceAccessor.UserAccountService.GetUserAsync(Clients.Notesnook.Id, userId);
|
|
||||||
action.Data = action.Data.Replace("{{Email}}", user?.Email ?? "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(userAnnouncements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,206 +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 Microsoft.Extensions.Logging;
|
|
||||||
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(
|
|
||||||
Repository<InboxApiKey> inboxApiKeysRepository,
|
|
||||||
Repository<UserSettings> userSettingsRepository,
|
|
||||||
Repository<InboxSyncItem> inboxItemsRepository,
|
|
||||||
SyncDeviceService syncDeviceService,
|
|
||||||
ILogger<InboxController> logger) : ControllerBase
|
|
||||||
{
|
|
||||||
|
|
||||||
[HttpGet("api-keys")]
|
|
||||||
[Authorize(Policy = "Notesnook")]
|
|
||||||
public async Task<IActionResult> GetApiKeysAsync()
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var apiKeys = await inboxApiKeysRepository.FindAsync(t => t.UserId == userId);
|
|
||||||
return Ok(apiKeys);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Couldn't get inbox api keys for user {UserId}", userId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("api-keys")]
|
|
||||||
[Authorize(Policy = "Notesnook")]
|
|
||||||
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
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 inboxApiKeysRepository.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 inboxApiKeysRepository.InsertAsync(inboxApiKey);
|
|
||||||
return Ok(inboxApiKey);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Couldn't create inbox api key for {UserId}.", userId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("api-keys/{apiKey}")]
|
|
||||||
[Authorize(Policy = "Notesnook")]
|
|
||||||
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(apiKey))
|
|
||||||
{
|
|
||||||
return BadRequest(new { error = "Api key is required." });
|
|
||||||
}
|
|
||||||
|
|
||||||
await inboxApiKeysRepository.DeleteAsync(t => t.UserId == userId && t.Key == apiKey);
|
|
||||||
return Ok(new { message = "Api key deleted successfully." });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Couldn't delete inbox api key for user {UserId}", userId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("public-encryption-key")]
|
|
||||||
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
|
|
||||||
public async Task<IActionResult> GetPublicKeyAsync()
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userSetting = await userSettingsRepository.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)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Couldn't get user's inbox's public key for user {UserId}", userId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("items")]
|
|
||||||
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
|
|
||||||
public async Task<IActionResult> CreateInboxItemAsync([FromBody] InboxSyncItem request)
|
|
||||||
{
|
|
||||||
var userId = User.GetUserId();
|
|
||||||
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 inboxItemsRepository.InsertAsync(request);
|
|
||||||
await syncDeviceService.AddIdsToAllDevicesAsync(userId, [new(request.ItemId, "inbox_item")]);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Couldn't create inbox item for user {UserId}", userId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,26 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp;
|
|
||||||
using AngleSharp.Dom;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Authorization;
|
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Services;
|
|
||||||
using Streetwriters.Common;
|
|
||||||
using Streetwriters.Common.Helpers;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Messages;
|
|
||||||
using Streetwriters.Data.Interfaces;
|
using Streetwriters.Data.Interfaces;
|
||||||
using Streetwriters.Data.Repositories;
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
@@ -46,174 +32,80 @@ namespace Notesnook.API.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("monographs")]
|
[Route("monographs")]
|
||||||
[Authorize("Sync")]
|
[Authorize("Sync")]
|
||||||
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer, SyncDeviceService syncDeviceService, ILogger<MonographsController> logger) : 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 const int MAX_DOC_SIZE = 15 * 1024 * 1024;
|
||||||
|
public MonographsController(Repository<Monograph> monographs, IUnitOfWork unitOfWork)
|
||||||
private static FilterDefinition<Monograph> CreateMonographFilter(string userId, Monograph monograph)
|
|
||||||
{
|
{
|
||||||
var userIdFilter = Builders<Monograph>.Filter.Eq("UserId", userId);
|
Monographs = monographs;
|
||||||
monograph.ItemId ??= monograph.Id;
|
unit = unitOfWork;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[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.GetUserId();
|
id = monograph.Id
|
||||||
var jti = this.User.FindFirstValue("jti");
|
});
|
||||||
|
|
||||||
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;
|
|
||||||
monograph.ViewCount = 0;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
logger.LogError(e, "Failed to publish monograph");
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch]
|
[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.GetUserId();
|
id = monograph.Id
|
||||||
var jti = this.User.FindFirstValue("jti");
|
});
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
logger.LogError(e, "Failed to update monograph");
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetUserMonographsAsync()
|
public async Task<IActionResult> GetUserMonographsAsync()
|
||||||
{
|
{
|
||||||
var userId = this.User.GetUserId();
|
var userId = this.User.FindFirstValue("sub");
|
||||||
|
if (userId == null) return Unauthorized();
|
||||||
|
|
||||||
var userMonographs = (await monographs.Collection.FindAsync(
|
var userMonographs = await Monographs.FindAsync((m) => m.UserId == userId);
|
||||||
Builders<Monograph>.Filter.And(
|
return Ok(userMonographs.Select((m) => m.Id));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
|
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var monograph = await FindMonographAsync(id);
|
var monograph = await Monographs.FindOneAsync((m) => m.Id == id);
|
||||||
if (monograph == null || monograph.Deleted)
|
if (monograph == null)
|
||||||
{
|
{
|
||||||
return NotFound(new
|
return NotFound(new
|
||||||
{
|
{
|
||||||
@@ -222,163 +114,21 @@ namespace Notesnook.API.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (monograph.SelfDestruct)
|
||||||
|
await Monographs.DeleteByIdAsync(monograph.Id);
|
||||||
|
|
||||||
if (monograph.EncryptedContent == null)
|
if (monograph.EncryptedContent == null)
|
||||||
monograph.Content = monograph.CompressedContent?.DecompressBrotli();
|
monograph.Content = monograph.CompressedContent.DecompressBrotli();
|
||||||
monograph.ItemId ??= monograph.Id;
|
|
||||||
return Ok(monograph);
|
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");
|
|
||||||
|
|
||||||
var cookieName = $"viewed_{id}";
|
|
||||||
var hasVisitedBefore = Request.Cookies.ContainsKey(cookieName);
|
|
||||||
|
|
||||||
if (monograph.SelfDestruct)
|
|
||||||
{
|
|
||||||
await monographs.Collection.ReplaceOneAsync(
|
|
||||||
CreateMonographFilter(monograph.UserId, monograph),
|
|
||||||
new Monograph
|
|
||||||
{
|
|
||||||
ItemId = id,
|
|
||||||
Id = monograph.Id,
|
|
||||||
Deleted = true,
|
|
||||||
UserId = monograph.UserId,
|
|
||||||
ViewCount = 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await MarkMonographForSyncAsync(monograph.UserId, id);
|
|
||||||
}
|
|
||||||
else if (!hasVisitedBefore)
|
|
||||||
{
|
|
||||||
await monographs.Collection.UpdateOneAsync(
|
|
||||||
CreateMonographFilter(monograph.UserId, monograph),
|
|
||||||
Builders<Monograph>.Update.Inc(m => m.ViewCount, 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
var cookieOptions = new CookieOptions
|
|
||||||
{
|
|
||||||
Path = $"/monographs/{id}",
|
|
||||||
HttpOnly = true,
|
|
||||||
Secure = Request.IsHttps,
|
|
||||||
Expires = DateTimeOffset.UtcNow.AddMonths(1)
|
|
||||||
};
|
|
||||||
Response.Cookies.Append(cookieName, "1", cookieOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Content(SVG_PIXEL, "image/svg+xml");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}/analytics")]
|
|
||||||
public async Task<IActionResult> GetMonographAnalyticsAsync([FromRoute] string id)
|
|
||||||
{
|
|
||||||
if (!FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User))
|
|
||||||
return BadRequest(new { error = "Monograph analytics are only available on the Pro & Believer plans." });
|
|
||||||
|
|
||||||
var userId = this.User.GetUserId();
|
|
||||||
var monograph = await FindMonographAsync(id);
|
|
||||||
if (monograph == null || monograph.Deleted || monograph.UserId != userId)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new { totalViews = monograph.ViewCount });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[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.GetUserId();
|
Monographs.DeleteById(id);
|
||||||
|
if (!await unit.Commit()) return BadRequest();
|
||||||
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,
|
|
||||||
ViewCount = 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await MarkMonographForSyncAsync(userId, id, deviceId, jti);
|
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
|
|
||||||
{
|
|
||||||
if (deviceId == null) return;
|
|
||||||
|
|
||||||
await syncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, [new(monographId, "monograph")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task MarkMonographForSyncAsync(string userId, string monographId)
|
|
||||||
{
|
|
||||||
await syncDeviceService.AddIdsToAllDevicesAsync(userId, [new(monographId, "monograph")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string? content)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(content)) return string.Empty;
|
|
||||||
if (Constants.IS_SELF_HOSTED) return content;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = JsonSerializer.Deserialize<MonographContent>(content) ?? throw new Exception("Invalid monograph 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))
|
|
||||||
{
|
|
||||||
logger.LogInformation("Malicious URL detected: {Url}", 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)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to cleanup monograph content");
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,184 +17,94 @@ 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/>.
|
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 Amazon.S3.Model;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Amazon.S3.Model;
|
||||||
using MongoDB.Driver;
|
using System.Threading.Tasks;
|
||||||
using Notesnook.API.Accessors;
|
using System.Security.Claims;
|
||||||
using Notesnook.API.Helpers;
|
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using System;
|
||||||
using Streetwriters.Common;
|
|
||||||
using Streetwriters.Common.Accessors;
|
|
||||||
using Streetwriters.Common.Extensions;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Models;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Controllers
|
namespace Notesnook.API.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("s3")]
|
[Route("s3")]
|
||||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
|
||||||
[Authorize("Sync")]
|
[Authorize("Sync")]
|
||||||
public class S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor repositories, WampServiceAccessor serviceAccessor, ILogger<S3Controller> logger) : ControllerBase
|
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||||
|
public class S3Controller : ControllerBase
|
||||||
{
|
{
|
||||||
|
private IS3Service S3Service { get; set; }
|
||||||
|
public S3Controller(IS3Service s3Service)
|
||||||
|
{
|
||||||
|
S3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<IActionResult> Upload([FromQuery] string name)
|
public IActionResult Upload([FromQuery] string name)
|
||||||
{
|
{
|
||||||
try
|
var userId = this.User.FindFirstValue("sub");
|
||||||
{
|
var url = S3Service.GetUploadObjectUrl(userId, name);
|
||||||
var userId = this.User.GetUserId();
|
if (url == null) return BadRequest("Could not create signed url.");
|
||||||
|
return Ok(url);
|
||||||
var fileSize = HttpContext.Request.ContentLength ?? 0;
|
|
||||||
bool hasBody = fileSize > 0;
|
|
||||||
|
|
||||||
if (!hasBody)
|
|
||||||
{
|
|
||||||
return Ok(Request.GetEncodedUrl() + "&access_token=" + Request.Headers.Authorization.ToString().Replace("Bearer ", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Constants.IS_SELF_HOSTED) await UploadFileAsync(userId, name, fileSize);
|
|
||||||
else await UploadFileWithChecksAsync(userId, name, fileSize);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error uploading attachment for user.");
|
|
||||||
return BadRequest(new { error = "Failed to upload attachment." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UploadFileWithChecksAsync(string userId, string name, long fileSize)
|
|
||||||
{
|
|
||||||
var userSettings = await repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
|
||||||
|
|
||||||
var subscription = await serviceAccessor.UserSubscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
|
||||||
|
|
||||||
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
|
|
||||||
throw new Exception("Max file size exceeded.");
|
|
||||||
|
|
||||||
userSettings.StorageLimit = StorageHelper.RolloverStorageLimit(userSettings.StorageLimit);
|
|
||||||
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit.Value + fileSize))
|
|
||||||
throw new Exception("Storage limit exceeded.");
|
|
||||||
|
|
||||||
var uploadedFileSize = await UploadFileAsync(userId, name, fileSize);
|
|
||||||
|
|
||||||
userSettings.StorageLimit.Value += uploadedFileSize;
|
|
||||||
await repositories.UsersSettings.Collection.UpdateOneAsync(
|
|
||||||
Builders<UserSettings>.Filter.Eq(u => u.UserId, userId),
|
|
||||||
Builders<UserSettings>.Update.Set(u => u.StorageLimit, userSettings.StorageLimit)
|
|
||||||
);
|
|
||||||
|
|
||||||
// extra check in case user sets wrong ContentLength in the HTTP header
|
|
||||||
if (uploadedFileSize != fileSize && StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit.Value))
|
|
||||||
{
|
|
||||||
await s3Service.DeleteObjectAsync(userId, name);
|
|
||||||
throw new Exception("Storage limit exceeded.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<long> UploadFileAsync(string userId, string name, long fileSize)
|
|
||||||
{
|
|
||||||
var url = await s3Service.GetInternalUploadObjectUrlAsync(userId, name) ?? throw new Exception("Could not create signed url.");
|
|
||||||
|
|
||||||
var httpClient = new HttpClient();
|
|
||||||
var content = new StreamContent(HttpContext.Request.BodyReader.AsStream());
|
|
||||||
content.Headers.ContentLength = fileSize;
|
|
||||||
var response = await httpClient.SendRequestAsync<Response>(url, null, HttpMethod.Put, content);
|
|
||||||
if (!response.Success) throw new Exception(response.Content != null ? await response.Content.ReadAsStringAsync() : "Could not upload file.");
|
|
||||||
|
|
||||||
return await s3Service.GetObjectSizeAsync(userId, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("multipart")]
|
[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.GetUserId();
|
var userId = this.User.FindFirstValue("sub");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var meta = await s3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
|
var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
|
||||||
return Ok(meta);
|
return Ok(meta);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error starting multipart upload for user.");
|
|
||||||
return BadRequest(new { error = "Failed to start multipart upload." });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("multipart")]
|
[HttpDelete("multipart")]
|
||||||
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
|
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
|
||||||
{
|
{
|
||||||
var userId = this.User.GetUserId();
|
var userId = this.User.FindFirstValue("sub");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await s3Service.AbortMultipartUploadAsync(userId, name, uploadId);
|
await S3Service.AbortMultipartUploadAsync(userId, name, uploadId);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error aborting multipart upload for user.");
|
|
||||||
return BadRequest(new { error = "Failed to abort multipart upload." });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("multipart")]
|
[HttpPost("multipart")]
|
||||||
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequestWrapper uploadRequestWrapper)
|
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequest uploadRequest)
|
||||||
{
|
{
|
||||||
var userId = this.User.GetUserId();
|
var userId = this.User.FindFirstValue("sub");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await s3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest());
|
await S3Service.CompleteMultipartUploadAsync(userId, uploadRequest);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) { return BadRequest(ex.Message); }
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error completing multipart upload for user.");
|
|
||||||
return BadRequest(new { error = "Failed to complete multipart upload." });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[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 = S3Service.GetDownloadObjectUrl(userId, name);
|
||||||
var userId = this.User.GetUserId();
|
if (url == null) return BadRequest("Could not create signed url.");
|
||||||
var url = await s3Service.GetDownloadObjectUrlAsync(userId, name);
|
return Ok(url);
|
||||||
if (url == null) return BadRequest("Could not create signed url.");
|
|
||||||
return Ok(url);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error generating download url for user.");
|
|
||||||
return BadRequest(new { error = "Failed to get attachment url." });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpHead]
|
[HttpHead]
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> Info([FromQuery] string name)
|
public async Task<IActionResult> Info([FromQuery] string name)
|
||||||
{
|
{
|
||||||
try
|
var userId = this.User.FindFirstValue("sub");
|
||||||
{
|
var size = await S3Service.GetObjectSizeAsync(userId, name);
|
||||||
var userId = this.User.GetUserId();
|
if (size == null) return BadRequest();
|
||||||
var size = await s3Service.GetObjectSizeAsync(userId, name);
|
|
||||||
HttpContext.Response.Headers.ContentLength = size;
|
HttpContext.Response.Headers.ContentLength = size;
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error getting object info for user.");
|
|
||||||
return BadRequest(new { error = "Failed to get attachment info." });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
@@ -202,14 +112,13 @@ namespace Notesnook.API.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userId = this.User.GetUserId();
|
var userId = this.User.FindFirstValue("sub");
|
||||||
await s3Service.DeleteObjectAsync(userId, name);
|
await S3Service.DeleteObjectAsync(userId, name);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error deleting object for user.");
|
return BadRequest(ex.Message);
|
||||||
return BadRequest(new { error = "Failed to delete attachment." });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +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 Microsoft.Extensions.Logging;
|
|
||||||
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(SyncDeviceService syncDeviceService, ILogger<SyncDeviceController> logger) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> RegisterDevice([FromQuery] string deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userId = this.User.GetUserId();
|
|
||||||
await syncDeviceService.RegisterDeviceAsync(userId, deviceId);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to register device: {DeviceId}", deviceId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpDelete]
|
|
||||||
public async Task<IActionResult> UnregisterDevice([FromQuery] string deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userId = this.User.GetUserId();
|
|
||||||
await syncDeviceService.UnregisterDeviceAsync(userId, deviceId);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to unregister device: {DeviceId}", deviceId);
|
|
||||||
return BadRequest(new { error = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +1,118 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Claims;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Timeouts;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models.Responses;
|
||||||
using Notesnook.API.Models.Responses;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common.Extensions;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
namespace Notesnook.API.Controllers
|
|
||||||
{
|
namespace Notesnook.API.Controllers
|
||||||
[ApiController]
|
{
|
||||||
[Authorize]
|
[ApiController]
|
||||||
[Route("users")]
|
[Authorize]
|
||||||
public class UsersController(IUserService UserService, ILogger<UsersController> logger) : ControllerBase
|
[Route("users")]
|
||||||
{
|
public class UsersController : ControllerBase
|
||||||
[HttpPost]
|
{
|
||||||
[AllowAnonymous]
|
private readonly HttpClient httpClient;
|
||||||
public async Task<IActionResult> Signup()
|
private readonly IHttpContextAccessor HttpContextAccessor;
|
||||||
{
|
private IUserService UserService { get; set; }
|
||||||
try
|
public UsersController(IUserService userService, IHttpContextAccessor accessor)
|
||||||
{
|
{
|
||||||
await UserService.CreateUserAsync();
|
httpClient = new HttpClient();
|
||||||
return Ok();
|
HttpContextAccessor = accessor;
|
||||||
}
|
UserService = userService;
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to sign up user");
|
[HttpPost]
|
||||||
return BadRequest(new { error = ex.Message });
|
[AllowAnonymous]
|
||||||
}
|
public async Task<IActionResult> Signup()
|
||||||
}
|
{
|
||||||
|
try
|
||||||
[HttpGet]
|
{
|
||||||
public async Task<IActionResult> GetUser()
|
await UserService.CreateUserAsync();
|
||||||
{
|
return Ok();
|
||||||
var userId = User.GetUserId();
|
}
|
||||||
try
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
UserResponse response = await UserService.GetUserAsync(userId);
|
await Slogger<UsersController>.Error(nameof(Signup), "Couldn't sign up.", ex.ToString());
|
||||||
if (!response.Success) return BadRequest();
|
return BadRequest(new { error = ex.Message });
|
||||||
return Ok(response);
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
[HttpGet]
|
||||||
logger.LogError(ex, "Failed to get user with id: {UserId}", userId);
|
public async Task<IActionResult> GetUser()
|
||||||
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)
|
|
||||||
{
|
[HttpPatch]
|
||||||
var userId = User.GetUserId();
|
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
|
||||||
try
|
{
|
||||||
{
|
UserResponse response = await UserService.GetUserAsync(false);
|
||||||
await UserService.SetUserKeysAsync(userId, keys);
|
|
||||||
return Ok();
|
if (user.AttachmentsKey != null)
|
||||||
}
|
await UserService.SetUserAttachmentsKeyAsync(response.UserId, user.AttachmentsKey);
|
||||||
catch (Exception ex)
|
else return BadRequest();
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to update user with id: {UserId}", userId);
|
return Ok();
|
||||||
return BadRequest(new { error = ex.Message });
|
}
|
||||||
}
|
|
||||||
}
|
[HttpPost("reset")]
|
||||||
|
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
|
||||||
[HttpPost("reset")]
|
{
|
||||||
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
|
var userId = this.User.FindFirstValue("sub");
|
||||||
{
|
|
||||||
var userId = this.User.GetUserId();
|
if (await UserService.ResetUserAsync(userId, removeAttachments))
|
||||||
|
return Ok();
|
||||||
if (await UserService.ResetUserAsync(userId, removeAttachments))
|
return BadRequest();
|
||||||
return Ok();
|
}
|
||||||
return BadRequest();
|
|
||||||
}
|
[HttpPost("delete")]
|
||||||
|
public async Task<IActionResult> Delete()
|
||||||
[HttpPost("delete")]
|
{
|
||||||
[RequestTimeout(5 * 60 * 1000)]
|
try
|
||||||
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
|
{
|
||||||
{
|
var userId = this.User.FindFirstValue("sub");
|
||||||
var userId = this.User.GetUserId();
|
|
||||||
var jti = User.FindFirstValue("jti");
|
if (await UserService.DeleteUserAsync(userId, User.FindFirstValue("jti")))
|
||||||
try
|
{
|
||||||
{
|
Response response = await this.httpClient.ForwardAsync<Response>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account/unregister", HttpMethod.Post);
|
||||||
await UserService.DeleteUserAsync(userId, jti, form.Password);
|
if (!response.Success) return BadRequest();
|
||||||
return Ok();
|
|
||||||
}
|
return Ok();
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
return BadRequest();
|
||||||
logger.LogError(ex, "Failed to delete user with id: {UserId}", userId);
|
}
|
||||||
return BadRequest(new { error = ex.Message });
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
}
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+28
-48
@@ -1,48 +1,28 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-alpine AS base
|
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
|
# restore all project dependencies
|
||||||
ARG TARGETARCH
|
COPY Streetwriters.Data/*.csproj ./Streetwriters.Data/
|
||||||
ARG BUILDPLATFORM
|
RUN dotnet restore /app/Streetwriters.Data/Streetwriters.Data.csproj --use-current-runtime
|
||||||
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
|
|
||||||
|
COPY Streetwriters.Common/*.csproj ./Streetwriters.Common/
|
||||||
WORKDIR /src
|
RUN dotnet restore /app/Streetwriters.Common/Streetwriters.Common.csproj --use-current-runtime
|
||||||
|
|
||||||
COPY Streetwriters.Data/*.csproj ./Streetwriters.Data/
|
COPY Notesnook.API/*.csproj ./Notesnook.API/
|
||||||
COPY Streetwriters.Common/*.csproj ./Streetwriters.Common/
|
RUN dotnet restore /app/Notesnook.API/Notesnook.API.csproj --use-current-runtime
|
||||||
COPY Notesnook.API/*.csproj ./Notesnook.API/
|
|
||||||
|
# copy everything else
|
||||||
# restore dependencies
|
COPY Streetwriters.Data/ ./Streetwriters.Data/
|
||||||
RUN dotnet restore -v d /src/Notesnook.API/Notesnook.API.csproj --use-current-runtime
|
COPY Streetwriters.Common/ ./Streetwriters.Common/
|
||||||
|
COPY Notesnook.API/ ./Notesnook.API/
|
||||||
COPY Streetwriters.Data/ ./Streetwriters.Data/
|
|
||||||
COPY Streetwriters.Common/ ./Streetwriters.Common/
|
# build
|
||||||
COPY Notesnook.API/ ./Notesnook.API/
|
WORKDIR /app/Notesnook.API/
|
||||||
|
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
|
||||||
WORKDIR /src/Notesnook.API/
|
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
|
# final stage/image
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0
|
||||||
FROM build AS publish
|
WORKDIR /app
|
||||||
RUN dotnet publish -c Release -o /app/publish \
|
COPY --from=build /app/out .
|
||||||
#--runtime alpine-x64 \
|
ENTRYPOINT ["dotnet", "Notesnook.API.dll"]
|
||||||
--self-contained 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
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["./Notesnook.API"]
|
|
||||||
@@ -1,39 +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 readonly Meter meter = new("Notesnook.API.Metrics.Sync", "1.0.0");
|
|
||||||
private readonly Counter<int> fetchCounter;
|
|
||||||
private readonly Counter<int> pushCounter;
|
|
||||||
private readonly Counter<int> pushV2Counter;
|
|
||||||
private readonly Counter<int> fetchV2Counter;
|
|
||||||
private readonly Histogram<long> fetchV2Duration;
|
|
||||||
private readonly Histogram<long> pushV2Duration;
|
|
||||||
private SyncEventCounterSource()
|
|
||||||
{
|
|
||||||
fetchCounter = meter.CreateCounter<int>("sync.fetches", "fetches", "Total fetches");
|
|
||||||
pushCounter = meter.CreateCounter<int>("sync.pushes", "pushes", "Total pushes");
|
|
||||||
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 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)
|
|
||||||
{
|
|
||||||
meter.Dispose();
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +1,71 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Net;
|
||||||
using System.Net;
|
using System.Text.Json;
|
||||||
using System.Net.WebSockets;
|
using System.Threading.Tasks;
|
||||||
using System.Text;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using System.Text.Json;
|
using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Authorization.Policy;
|
namespace Notesnook.API.Extensions
|
||||||
using Microsoft.AspNetCore.Http;
|
{
|
||||||
|
public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
|
||||||
namespace Notesnook.API.Extensions
|
{
|
||||||
{
|
private readonly IAuthorizationMiddlewareResultHandler _handler;
|
||||||
public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
|
|
||||||
{
|
public AuthorizationResultTransformer()
|
||||||
private readonly IAuthorizationMiddlewareResultHandler _handler;
|
{
|
||||||
|
_handler = new AuthorizationMiddlewareResultHandler();
|
||||||
public AuthorizationResultTransformer()
|
}
|
||||||
{
|
|
||||||
_handler = new AuthorizationMiddlewareResultHandler();
|
public async Task HandleAsync(
|
||||||
}
|
RequestDelegate requestDelegate,
|
||||||
|
HttpContext httpContext,
|
||||||
public async Task HandleAsync(
|
AuthorizationPolicy authorizationPolicy,
|
||||||
RequestDelegate requestDelegate,
|
PolicyAuthorizationResult policyAuthorizationResult)
|
||||||
HttpContext httpContext,
|
{
|
||||||
AuthorizationPolicy authorizationPolicy,
|
var isWebsocket = httpContext.Request.Headers.Upgrade == "websocket";
|
||||||
PolicyAuthorizationResult policyAuthorizationResult)
|
|
||||||
{
|
if (!isWebsocket && policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
|
||||||
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
|
{
|
||||||
{
|
var error = string.Join("\n", policyAuthorizationResult.AuthorizationFailure.FailureReasons.Select((r) => r.Message));
|
||||||
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.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
httpContext.Response.ContentType = "application/json";
|
httpContext.Response.ContentType = "application/json";
|
||||||
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
|
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +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));
|
|
||||||
|
|
||||||
public static string GetUserId(this ClaimsPrincipal user)
|
|
||||||
=> user.FindFirstValue("sub") ?? throw new Exception("User ID not found in claims.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MongoDB.Driver
|
namespace MongoDB.Driver
|
||||||
{
|
{
|
||||||
public static class TransactionHelper
|
public static class TransactionHelper
|
||||||
{
|
{
|
||||||
public static async Task StartTransaction(this IMongoClient client, Action<CancellationToken> operate, CancellationToken ct)
|
public static async Task StartTransaction(this IMongoClient client, Action<CancellationToken> operate, CancellationToken ct)
|
||||||
{
|
{
|
||||||
using (var session = await client.StartSessionAsync())
|
using (var session = await client.StartSessionAsync())
|
||||||
{
|
{
|
||||||
var transactionOptions = new TransactionOptions(readPreference: ReadPreference.Nearest, readConcern: ReadConcern.Local, writeConcern: WriteConcern.WMajority);
|
var transactionOptions = new TransactionOptions(readPreference: ReadPreference.Nearest, readConcern: ReadConcern.Local, writeConcern: WriteConcern.WMajority);
|
||||||
await session.WithTransactionAsync((handle, token) =>
|
await session.WithTransactionAsync((handle, token) =>
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
return Task.Run(() =>
|
||||||
{
|
{
|
||||||
operate(token);
|
operate(token);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, transactionOptions, ct);
|
}, transactionOptions, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,414 +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.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Amazon.S3;
|
|
||||||
using Amazon.S3.Model;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for S3 failover behavior
|
|
||||||
/// </summary>
|
|
||||||
public class S3FailoverConfig
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum number of retry attempts per endpoint
|
|
||||||
/// </summary>
|
|
||||||
public int MaxRetries { get; set; } = 3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delay between retries in milliseconds
|
|
||||||
/// </summary>
|
|
||||||
public int RetryDelayMs { get; set; } = 1000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to use exponential backoff for retries
|
|
||||||
/// </summary>
|
|
||||||
public bool UseExponentialBackoff { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to allow failover for write operations (PUT, POST, DELETE).
|
|
||||||
/// Default is false to prevent data consistency issues.
|
|
||||||
/// </summary>
|
|
||||||
public bool AllowWriteFailover { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of exception types that should trigger failover
|
|
||||||
/// </summary>
|
|
||||||
public HashSet<Type> FailoverExceptions { get; set; } = new()
|
|
||||||
{
|
|
||||||
typeof(AmazonS3Exception),
|
|
||||||
typeof(System.Net.Http.HttpRequestException),
|
|
||||||
typeof(System.Net.Sockets.SocketException),
|
|
||||||
typeof(System.Threading.Tasks.TaskCanceledException),
|
|
||||||
typeof(TimeoutException)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of S3 error codes that should trigger failover
|
|
||||||
/// </summary>
|
|
||||||
public HashSet<string> FailoverErrorCodes { get; set; } = new()
|
|
||||||
{
|
|
||||||
"ServiceUnavailable",
|
|
||||||
"SlowDown",
|
|
||||||
"InternalError",
|
|
||||||
"RequestTimeout"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result of a failover operation
|
|
||||||
/// </summary>
|
|
||||||
public class S3FailoverResult<T>
|
|
||||||
{
|
|
||||||
public T? Result { get; set; }
|
|
||||||
public bool UsedFailover { get; set; }
|
|
||||||
public int ClientIndex { get; set; } = 0;
|
|
||||||
public int AttemptsUsed { get; set; }
|
|
||||||
public Exception? LastException { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper class for S3 operations with automatic failover to multiple endpoints
|
|
||||||
/// </summary>
|
|
||||||
public class S3FailoverHelper
|
|
||||||
{
|
|
||||||
private readonly List<AmazonS3Client> clients;
|
|
||||||
private readonly S3FailoverConfig config;
|
|
||||||
private readonly ILogger? logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize with a list of S3 clients (first is primary, rest are failover endpoints)
|
|
||||||
/// </summary>
|
|
||||||
public S3FailoverHelper(
|
|
||||||
IEnumerable<AmazonS3Client> clients,
|
|
||||||
S3FailoverConfig? config = null,
|
|
||||||
ILogger? logger = null)
|
|
||||||
{
|
|
||||||
if (clients == null) throw new ArgumentNullException(nameof(clients));
|
|
||||||
this.clients = new List<AmazonS3Client>(clients);
|
|
||||||
if (this.clients.Count == 0) throw new ArgumentException("At least one S3 client is required", nameof(clients));
|
|
||||||
this.config = config ?? new S3FailoverConfig();
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize with params array of S3 clients
|
|
||||||
/// </summary>
|
|
||||||
public S3FailoverHelper(
|
|
||||||
S3FailoverConfig? config = null,
|
|
||||||
ILogger? logger = null,
|
|
||||||
params AmazonS3Client[] clients)
|
|
||||||
{
|
|
||||||
if (clients == null || clients.Length == 0)
|
|
||||||
throw new ArgumentException("At least one S3 client is required", nameof(clients));
|
|
||||||
this.clients = new List<AmazonS3Client>(clients);
|
|
||||||
this.config = config ?? new S3FailoverConfig();
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute an S3 operation with automatic failover
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="operation">The S3 operation to execute</param>
|
|
||||||
/// <param name="operationName">Name of the operation for logging</param>
|
|
||||||
/// <param name="isWriteOperation">Whether this is a write operation (PUT/POST/DELETE). Write operations only use primary endpoint by default.</param>
|
|
||||||
public async Task<T> ExecuteWithFailoverAsync<T>(
|
|
||||||
Func<AmazonS3Client, Task<T>> operation,
|
|
||||||
string operationName = "S3Operation",
|
|
||||||
bool isWriteOperation = false)
|
|
||||||
{
|
|
||||||
var result = await ExecuteWithFailoverInternalAsync(operation, operationName, isWriteOperation);
|
|
||||||
if (result.Result == null)
|
|
||||||
{
|
|
||||||
throw result.LastException ?? new Exception($"Failed to execute {operationName} on all endpoints");
|
|
||||||
}
|
|
||||||
return result.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute an S3 operation with automatic failover and return detailed result
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="operation">The S3 operation to execute</param>
|
|
||||||
/// <param name="operationName">Name of the operation for logging</param>
|
|
||||||
/// <param name="isWriteOperation">Whether this is a write operation (PUT/POST/DELETE). Write operations only use primary endpoint by default.</param>
|
|
||||||
private async Task<S3FailoverResult<T>> ExecuteWithFailoverInternalAsync<T>(
|
|
||||||
Func<AmazonS3Client, Task<T>> operation,
|
|
||||||
string operationName = "S3Operation",
|
|
||||||
bool isWriteOperation = false)
|
|
||||||
{
|
|
||||||
var result = new S3FailoverResult<T>();
|
|
||||||
Exception? lastException = null;
|
|
||||||
|
|
||||||
// Determine max clients to try based on write operation flag
|
|
||||||
var maxClientsToTry = (isWriteOperation && !config.AllowWriteFailover) ? 1 : clients.Count;
|
|
||||||
|
|
||||||
if (isWriteOperation && !config.AllowWriteFailover && clients.Count > 1)
|
|
||||||
{
|
|
||||||
logger?.LogDebug(
|
|
||||||
"Write operation {Operation} will only use primary endpoint. Failover is disabled for write operations.",
|
|
||||||
operationName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try each client in sequence (first is primary, rest are failovers)
|
|
||||||
for (int i = 0; i < maxClientsToTry; i++)
|
|
||||||
{
|
|
||||||
var client = clients[i];
|
|
||||||
var clientName = i == 0 ? "primary" : $"failover-{i}";
|
|
||||||
var isPrimary = i == 0;
|
|
||||||
|
|
||||||
if (!isPrimary && lastException != null)
|
|
||||||
{
|
|
||||||
logger?.LogWarning(lastException,
|
|
||||||
"Previous S3 endpoint failed for {Operation}. Attempting {ClientName} (endpoint {Index}/{Total}).",
|
|
||||||
operationName, clientName, i + 1, maxClientsToTry);
|
|
||||||
}
|
|
||||||
|
|
||||||
var (success, value, exception, attempts) = await TryExecuteAsync(client, operation, operationName, clientName);
|
|
||||||
result.AttemptsUsed += attempts;
|
|
||||||
|
|
||||||
if (success && value != null)
|
|
||||||
{
|
|
||||||
result.Result = value;
|
|
||||||
result.UsedFailover = !isPrimary;
|
|
||||||
result.ClientIndex = i;
|
|
||||||
|
|
||||||
if (!isPrimary)
|
|
||||||
{
|
|
||||||
logger?.LogInformation(
|
|
||||||
"Successfully failed over to {ClientName} S3 endpoint for {Operation}",
|
|
||||||
clientName, operationName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastException = exception;
|
|
||||||
|
|
||||||
// If this is not the last client and should retry, log and continue
|
|
||||||
if (i < maxClientsToTry - 1 && ShouldFailover(exception))
|
|
||||||
{
|
|
||||||
logger?.LogWarning(exception,
|
|
||||||
"Endpoint {ClientName} failed for {Operation}. {Remaining} endpoint(s) remaining.",
|
|
||||||
clientName, operationName, maxClientsToTry - i - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All clients failed
|
|
||||||
result.LastException = lastException;
|
|
||||||
logger?.LogError(lastException,
|
|
||||||
"All S3 endpoints failed for {Operation}. Total endpoints tried: {EndpointCount}, Total attempts: {Attempts}",
|
|
||||||
operationName, maxClientsToTry, result.AttemptsUsed);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} /// <summary>
|
|
||||||
/// Try to execute an operation with retries
|
|
||||||
/// </summary>
|
|
||||||
private async Task<(bool success, T? value, Exception? exception, int attempts)> TryExecuteAsync<T>(
|
|
||||||
AmazonS3Client client,
|
|
||||||
Func<AmazonS3Client, Task<T>> operation,
|
|
||||||
string operationName,
|
|
||||||
string endpointName)
|
|
||||||
{
|
|
||||||
Exception? lastException = null;
|
|
||||||
int attempts = 0;
|
|
||||||
|
|
||||||
for (int retry = 0; retry <= config.MaxRetries; retry++)
|
|
||||||
{
|
|
||||||
attempts++;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await operation(client);
|
|
||||||
return (true, result, null, attempts);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
lastException = ex;
|
|
||||||
|
|
||||||
if (retry < config.MaxRetries && ShouldRetry(ex))
|
|
||||||
{
|
|
||||||
var delay = CalculateRetryDelay(retry);
|
|
||||||
logger?.LogWarning(ex,
|
|
||||||
"Attempt {Attempt}/{MaxAttempts} failed for {Operation} on {Endpoint}. Retrying in {Delay}ms",
|
|
||||||
retry + 1, config.MaxRetries + 1, operationName, endpointName, delay);
|
|
||||||
|
|
||||||
await Task.Delay(delay);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger?.LogError(ex,
|
|
||||||
"Operation {Operation} failed on {Endpoint} after {Attempts} attempts",
|
|
||||||
operationName, endpointName, attempts);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, default, lastException, attempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determine if an exception should trigger a retry
|
|
||||||
/// </summary>
|
|
||||||
private bool ShouldRetry(Exception exception)
|
|
||||||
{
|
|
||||||
// Check if exception type is in the retry list
|
|
||||||
var exceptionType = exception.GetType();
|
|
||||||
if (config.FailoverExceptions.Contains(exceptionType))
|
|
||||||
{
|
|
||||||
// For S3 exceptions, check error codes
|
|
||||||
if (exception is AmazonS3Exception s3Exception)
|
|
||||||
{
|
|
||||||
return config.FailoverErrorCodes.Contains(s3Exception.ErrorCode);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determine if an exception should trigger failover to secondary endpoint
|
|
||||||
/// </summary>
|
|
||||||
private bool ShouldFailover(Exception? exception)
|
|
||||||
{
|
|
||||||
if (exception == null) return false;
|
|
||||||
return ShouldRetry(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate delay for retry based on retry attempt number
|
|
||||||
/// </summary>
|
|
||||||
private int CalculateRetryDelay(int retryAttempt)
|
|
||||||
{
|
|
||||||
if (!config.UseExponentialBackoff)
|
|
||||||
{
|
|
||||||
return config.RetryDelayMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exponential backoff: delay * 2^retryAttempt
|
|
||||||
return config.RetryDelayMs * (int)Math.Pow(2, retryAttempt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a void operation with automatic failover
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="operation">The S3 operation to execute</param>
|
|
||||||
/// <param name="operationName">Name of the operation for logging</param>
|
|
||||||
/// <param name="isWriteOperation">Whether this is a write operation (PUT/POST/DELETE). Write operations only use primary endpoint by default.</param>
|
|
||||||
public async Task ExecuteWithFailoverAsync(
|
|
||||||
Func<AmazonS3Client, Task> operation,
|
|
||||||
string operationName = "S3Operation",
|
|
||||||
bool isWriteOperation = false)
|
|
||||||
{
|
|
||||||
await ExecuteWithFailoverAsync<object?>(async (client) =>
|
|
||||||
{
|
|
||||||
await operation(client);
|
|
||||||
return null;
|
|
||||||
}, operationName, isWriteOperation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class S3ClientFactory
|
|
||||||
{
|
|
||||||
public static List<AmazonS3Client> CreateS3Clients(
|
|
||||||
string serviceUrls,
|
|
||||||
string regions,
|
|
||||||
string accessKeyIds,
|
|
||||||
string secretKeys,
|
|
||||||
bool forcePathStyle = true)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(serviceUrls))
|
|
||||||
return new List<AmazonS3Client>();
|
|
||||||
|
|
||||||
var urls = SplitAndTrim(serviceUrls);
|
|
||||||
var regionList = SplitAndTrim(regions);
|
|
||||||
var keyIds = SplitAndTrim(accessKeyIds);
|
|
||||||
var secrets = SplitAndTrim(secretKeys);
|
|
||||||
|
|
||||||
if (urls.Length != regionList.Length ||
|
|
||||||
urls.Length != keyIds.Length ||
|
|
||||||
urls.Length != secrets.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("All S3 configuration parameters must have the same number of values");
|
|
||||||
}
|
|
||||||
|
|
||||||
var clients = new List<AmazonS3Client>();
|
|
||||||
|
|
||||||
for (int i = 0; i < urls.Length; i++)
|
|
||||||
{
|
|
||||||
var url = urls[i];
|
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Get corresponding values from other arrays
|
|
||||||
var region = regionList[i];
|
|
||||||
var keyId = keyIds[i];
|
|
||||||
var secret = secrets[i];
|
|
||||||
|
|
||||||
// Validate that all required values are present
|
|
||||||
if (string.IsNullOrWhiteSpace(region) ||
|
|
||||||
string.IsNullOrWhiteSpace(keyId) ||
|
|
||||||
string.IsNullOrWhiteSpace(secret))
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(
|
|
||||||
$"Skipping S3 client at index {i}: Missing required values (URL={url}, Region={region}, KeyId={keyId?.Length > 0}, Secret={secret?.Length > 0})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var config = new AmazonS3Config
|
|
||||||
{
|
|
||||||
ServiceURL = url,
|
|
||||||
AuthenticationRegion = region,
|
|
||||||
ForcePathStyle = forcePathStyle,
|
|
||||||
SignatureMethod = Amazon.Runtime.SigningAlgorithm.HmacSHA256,
|
|
||||||
SignatureVersion = "4"
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = new AmazonS3Client(keyId, secret, config);
|
|
||||||
clients.Add(client);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log configuration error but continue with other clients
|
|
||||||
System.Diagnostics.Debug.WriteLine($"Failed to create S3 client for URL {url}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clients;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string[] SplitAndTrim(string? input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return input.Split(';', StringSplitOptions.None)
|
|
||||||
.Select(s => s.Trim())
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +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, long limit)
|
|
||||||
{
|
|
||||||
var storageLimit = GetStorageLimitForPlan(subscription);
|
|
||||||
if (storageLimit == -1) return false;
|
|
||||||
return limit > storageLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsFileSizeExceeded(Subscription subscription, long fileSize)
|
|
||||||
{
|
|
||||||
var maxFileSize = MAX_FILE_SIZE[subscription.Plan];
|
|
||||||
return fileSize > maxFileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Limit RolloverStorageLimit(Limit? limit)
|
|
||||||
{
|
|
||||||
var updatedAt = DateTimeOffset.FromUnixTimeMilliseconds(limit?.UpdatedAt ?? 0);
|
|
||||||
if (limit == null || DateTimeOffset.UtcNow.Year > updatedAt.Year || DateTimeOffset.UtcNow.Month > updatedAt.Month)
|
|
||||||
{
|
|
||||||
limit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 };
|
|
||||||
return limit;
|
|
||||||
}
|
|
||||||
return limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
/*
|
||||||
|
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.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Notesnook.API.Authorization;
|
||||||
|
using Notesnook.API.Interfaces;
|
||||||
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Hubs
|
||||||
|
{
|
||||||
|
public interface ISyncHubClient
|
||||||
|
{
|
||||||
|
Task SyncItem(SyncTransferItem transferItem);
|
||||||
|
Task RemoteSyncCompleted(long lastSynced);
|
||||||
|
Task SyncCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize("Sync")]
|
||||||
|
public class SyncHub : Hub<ISyncHubClient>
|
||||||
|
{
|
||||||
|
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||||
|
private readonly IUnitOfWork unit;
|
||||||
|
|
||||||
|
public SyncHub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
Repositories = syncItemsRepositoryAccessor;
|
||||||
|
unit = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync"));
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault();
|
||||||
|
throw new HubException(reason?.Message ?? "Unauthorized");
|
||||||
|
}
|
||||||
|
var id = Context.User.FindFirstValue("sub");
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, id);
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception exception)
|
||||||
|
{
|
||||||
|
var id = Context.User.FindFirstValue("sub");
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, id);
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SyncItem(BatchedSyncTransferItem transferItem)
|
||||||
|
{
|
||||||
|
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
if (string.IsNullOrEmpty(userId)) return 0;
|
||||||
|
|
||||||
|
var others = Clients.OthersInGroup(userId);
|
||||||
|
|
||||||
|
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
|
||||||
|
long dateSynced = transferItem.LastSynced > userSettings.LastSynced ? transferItem.LastSynced : userSettings.LastSynced;
|
||||||
|
|
||||||
|
Parallel.For(0, transferItem.Items.Length, async (i) =>
|
||||||
|
{
|
||||||
|
var data = transferItem.Items[i];
|
||||||
|
var type = transferItem.Types[i];
|
||||||
|
var id = transferItem.Ids[i];
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
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 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncCompleted(long dateSynced)
|
||||||
|
{
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
|
||||||
|
long lastSynced = dateSynced > userSettings.LastSynced ? dateSynced : userSettings.LastSynced;
|
||||||
|
|
||||||
|
userSettings.LastSynced = lastSynced;
|
||||||
|
|
||||||
|
await this.Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
|
||||||
|
|
||||||
|
await Clients.OthersInGroup(userId).RemoteSyncCompleted(lastSynced);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<SyncTransferItem> FetchItems(long lastSyncedTimestamp, [EnumeratorCancellation]
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = Context.User.FindFirstValue("sub");
|
||||||
|
|
||||||
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
if (userSettings.LastSynced > 0 && lastSyncedTimestamp > userSettings.LastSynced)
|
||||||
|
throw new HubException($"Provided timestamp value is too large. Server timestamp: {userSettings.LastSynced} Sent timestamp: {lastSyncedTimestamp}");
|
||||||
|
|
||||||
|
// var client = Clients.Caller;
|
||||||
|
|
||||||
|
if (lastSyncedTimestamp > 0 && userSettings.LastSynced == lastSyncedTimestamp)
|
||||||
|
{
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
LastSynced = userSettings.LastSynced,
|
||||||
|
Synced = true
|
||||||
|
};
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var attachments = await Repositories.Attachments.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var notes = await Repositories.Notes.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var notebooks = await Repositories.Notebooks.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var contents = await Repositories.Contents.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var settings = await Repositories.Settings.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var shortcuts = await Repositories.Shortcuts.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var reminders = await Repositories.Reminders.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var relations = await Repositories.Relations.GetItemsSyncedAfterAsync(userId, lastSyncedTimestamp);
|
||||||
|
|
||||||
|
var collections = new Dictionary<string, IEnumerable<object>>
|
||||||
|
{
|
||||||
|
["attachment"] = attachments,
|
||||||
|
["note"] = notes,
|
||||||
|
["notebook"] = notebooks,
|
||||||
|
["content"] = contents,
|
||||||
|
["shortcut"] = shortcuts,
|
||||||
|
["reminder"] = reminders,
|
||||||
|
["relation"] = relations,
|
||||||
|
["settings"] = settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userSettings.VaultKey != null)
|
||||||
|
{
|
||||||
|
collections.Add("vaultKey", new object[] { userSettings.VaultKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = collections.Values.Sum((a) => a.Count());
|
||||||
|
if (total == 0)
|
||||||
|
{
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
Synced = true,
|
||||||
|
LastSynced = userSettings.LastSynced
|
||||||
|
};
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
foreach (var item in collection.Value)
|
||||||
|
{
|
||||||
|
if (item == null) continue;
|
||||||
|
// Check the cancellation token regularly so that the server will stop producing items if the client disconnects.
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return new SyncTransferItem
|
||||||
|
{
|
||||||
|
LastSynced = userSettings.LastSynced,
|
||||||
|
Synced = false,
|
||||||
|
Item = JsonSerializer.Serialize(item),
|
||||||
|
ItemType = collection.Key,
|
||||||
|
Total = total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePack.MessagePackObject]
|
||||||
|
public struct BatchedSyncTransferItem
|
||||||
|
{
|
||||||
|
[MessagePack.Key("lastSynced")]
|
||||||
|
public long LastSynced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("items")]
|
||||||
|
public string[] Items { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("types")]
|
||||||
|
public string[] Types { get; set; }
|
||||||
|
[MessagePack.Key("ids")]
|
||||||
|
public string[] Ids { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("current")]
|
||||||
|
public int Current { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePack.MessagePackObject]
|
||||||
|
public struct SyncTransferItem
|
||||||
|
{
|
||||||
|
[MessagePack.Key("synced")]
|
||||||
|
public bool Synced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("lastSynced")]
|
||||||
|
public long LastSynced { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("item")]
|
||||||
|
public string Item { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("itemType")]
|
||||||
|
public string ItemType { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
[MessagePack.Key("current")]
|
||||||
|
public int Current { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,340 +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 Microsoft.Extensions.Logging;
|
|
||||||
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();
|
|
||||||
Task PushCompletedV2(string deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
public class SyncV2Hub : Hub<ISyncV2HubClient>
|
|
||||||
{
|
|
||||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
|
||||||
private SyncDeviceService SyncDeviceService { 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, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
|
||||||
ILogger<SyncV2Hub> Logger { get; }
|
|
||||||
|
|
||||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
Repositories = syncItemsRepositoryAccessor;
|
|
||||||
unit = unitOfWork;
|
|
||||||
SyncDeviceService = syncDeviceService;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => new ItemKey(i.ItemId, pushItem.Type)));
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> PushCompletedV2(string deviceId)
|
|
||||||
{
|
|
||||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
|
|
||||||
await Clients.OthersInGroup(userId).PushCompleted();
|
|
||||||
await Clients.OthersInGroup(userId).PushCompletedV2(deviceId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> 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.Type == type).Select((id) => id.ItemId).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 = await SyncDeviceService.GetDeviceAsync(userId, deviceId);
|
|
||||||
if (device == null)
|
|
||||||
device = await SyncDeviceService.RegisterDeviceAsync(userId, deviceId);
|
|
||||||
else
|
|
||||||
await SyncDeviceService.UpdateLastAccessTimeAsync(userId, deviceId);
|
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var ids = await SyncDeviceService.FetchUnsyncedIdsAsync(userId, deviceId);
|
|
||||||
if (!device.IsSyncReset && ids.Count == 0)
|
|
||||||
return new SyncV2Metadata { Synced = true };
|
|
||||||
|
|
||||||
var chunks = PrepareChunks(
|
|
||||||
userId,
|
|
||||||
ids,
|
|
||||||
size: 100,
|
|
||||||
resetSync: device.IsSyncReset,
|
|
||||||
maxBytes: 7 * 1024 * 1024
|
|
||||||
);
|
|
||||||
|
|
||||||
await foreach (var chunk in chunks)
|
|
||||||
{
|
|
||||||
if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items.");
|
|
||||||
|
|
||||||
if (!device.IsSyncReset)
|
|
||||||
{
|
|
||||||
ids.ExceptWith(chunk.Items.Select(i => new ItemKey(i.ItemId, chunk.Type)));
|
|
||||||
await SyncDeviceService.WritePendingIdsAsync(userId, deviceId, ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMonographs)
|
|
||||||
{
|
|
||||||
var unsyncedMonographIds = ids.Where(k => k.Type == "monograph").Select(k => k.ItemId);
|
|
||||||
FilterDefinition<Monograph> filter = device.IsSyncReset
|
|
||||||
? Builders<Monograph>.Filter.Eq(m => m.UserId, userId)
|
|
||||||
: Builders<Monograph>.Filter.And(
|
|
||||||
Builders<Monograph>.Filter.Eq(m => m.UserId, userId),
|
|
||||||
Builders<Monograph>.Filter.Or(
|
|
||||||
Builders<Monograph>.Filter.In(m => m.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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeInboxItems)
|
|
||||||
{
|
|
||||||
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
|
|
||||||
var userInboxItems = device.IsSyncReset
|
|
||||||
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
|
|
||||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId ?? m.Id.ToString()));
|
|
||||||
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
|
|
||||||
{
|
|
||||||
throw new HubException("Client rejected inbox items.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await SyncDeviceService.ResetAsync(userId, deviceId);
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-8
@@ -17,14 +17,13 @@ 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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Streetwriters.Common.Enums
|
namespace Notesnook.API.Interfaces
|
||||||
{
|
{
|
||||||
public enum SubscriptionStatus
|
public interface IEncrypted
|
||||||
{
|
{
|
||||||
ACTIVE,
|
string Cipher { get; set; }
|
||||||
TRIAL,
|
string IV { get; set; }
|
||||||
CANCELED,
|
long Length { get; set; }
|
||||||
PAUSED,
|
string Salt { get; set; }
|
||||||
EXPIRED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,18 +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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
using Notesnook.API.Models;
|
||||||
{
|
using Streetwriters.Common.Interfaces;
|
||||||
public class UserKeys
|
|
||||||
{
|
|
||||||
public EncryptedData? AttachmentsKey { get; set; }
|
|
||||||
public EncryptedData? MonographPasswordsKey { get; set; }
|
|
||||||
public InboxKeys? InboxKeys { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InboxKeys
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IMonograph : IDocument
|
||||||
{
|
{
|
||||||
public string? Public { get; set; }
|
string Title { get; set; }
|
||||||
public EncryptedData? Private { get; set; }
|
string UserId { get; set; }
|
||||||
|
byte[] CompressedContent { get; set; }
|
||||||
|
EncryptedData EncryptedContent { get; set; }
|
||||||
|
long DatePublished { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,11 +30,10 @@ namespace Notesnook.API.Interfaces
|
|||||||
{
|
{
|
||||||
Task DeleteObjectAsync(string userId, string name);
|
Task DeleteObjectAsync(string userId, string name);
|
||||||
Task DeleteDirectoryAsync(string userId);
|
Task DeleteDirectoryAsync(string userId);
|
||||||
Task<long> GetObjectSizeAsync(string userId, string name);
|
Task<long?> GetObjectSizeAsync(string userId, string name);
|
||||||
Task<string?> GetUploadObjectUrlAsync(string userId, string name);
|
string GetUploadObjectUrl(string userId, string name);
|
||||||
Task<string?> GetInternalUploadObjectUrlAsync(string userId, string name);
|
string GetDownloadObjectUrl(string userId, string name);
|
||||||
Task<string?> GetDownloadObjectUrlAsync(string userId, string name);
|
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null);
|
||||||
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null);
|
|
||||||
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
|
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
|
||||||
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
|
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,43 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Runtime.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson.Serialization.Serializers;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Attributes;
|
||||||
namespace Notesnook.API.Models
|
using Streetwriters.Common.Converters;
|
||||||
{
|
using Streetwriters.Common.Interfaces;
|
||||||
public class MonographMetadata
|
|
||||||
{
|
namespace Notesnook.API.Interfaces
|
||||||
[DataMember(Name = "id")]
|
{
|
||||||
[JsonPropertyName("id")]
|
[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<ISyncItem, SyncItem>))]
|
||||||
[MessagePack.Key("id")]
|
[JsonInterfaceConverter(typeof(InterfaceConverter<ISyncItem, SyncItem>))]
|
||||||
public required string ItemId
|
public interface ISyncItem
|
||||||
{
|
{
|
||||||
get; set;
|
long DateSynced
|
||||||
}
|
{
|
||||||
|
get; set;
|
||||||
[JsonPropertyName("title")]
|
}
|
||||||
public string? Title { get; set; }
|
|
||||||
|
string UserId { get; set; }
|
||||||
[JsonPropertyName("selfDestruct")]
|
string Algorithm { get; set; }
|
||||||
public bool SelfDestruct { get; set; }
|
string IV { get; set; }
|
||||||
|
}
|
||||||
[JsonPropertyName("datePublished")]
|
}
|
||||||
public long DatePublished { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("password")]
|
|
||||||
public EncryptedData? Password { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("deleted")]
|
|
||||||
public bool Deleted { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,23 +26,15 @@ namespace Notesnook.API.Interfaces
|
|||||||
{
|
{
|
||||||
public interface ISyncItemsRepositoryAccessor
|
public interface ISyncItemsRepositoryAccessor
|
||||||
{
|
{
|
||||||
SyncItemsRepository Notes { get; }
|
SyncItemsRepository<Note> Notes { get; }
|
||||||
SyncItemsRepository Notebooks { get; }
|
SyncItemsRepository<Notebook> Notebooks { get; }
|
||||||
SyncItemsRepository Shortcuts { get; }
|
SyncItemsRepository<Shortcut> Shortcuts { get; }
|
||||||
SyncItemsRepository Reminders { get; }
|
SyncItemsRepository<Reminder> Reminders { get; }
|
||||||
SyncItemsRepository Relations { get; }
|
SyncItemsRepository<Relation> Relations { get; }
|
||||||
SyncItemsRepository Contents { get; }
|
SyncItemsRepository<Content> Contents { get; }
|
||||||
SyncItemsRepository LegacySettings { get; }
|
SyncItemsRepository<Setting> Settings { get; }
|
||||||
SyncItemsRepository Attachments { get; }
|
SyncItemsRepository<Attachment> Attachments { get; }
|
||||||
SyncItemsRepository Settings { get; }
|
|
||||||
SyncItemsRepository Colors { get; }
|
|
||||||
SyncItemsRepository Vaults { get; }
|
|
||||||
SyncItemsRepository Tags { get; }
|
|
||||||
Repository<UserSettings> UsersSettings { get; }
|
Repository<UserSettings> UsersSettings { get; }
|
||||||
Repository<Monograph> Monographs { get; }
|
Repository<Monograph> Monographs { get; }
|
||||||
Repository<InboxApiKey> InboxApiKey { get; }
|
|
||||||
Repository<InboxSyncItem> InboxItems { get; }
|
|
||||||
Repository<SyncDevice> SyncDevices { get; }
|
|
||||||
Repository<DeviceIdsChunk> DeviceIdsChunks { get; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading;
|
||||||
using Notesnook.API.Models;
|
using System.Threading.Tasks;
|
||||||
using Notesnook.API.Models.Responses;
|
using Notesnook.API.Models.Responses;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
namespace Notesnook.API.Interfaces
|
|
||||||
{
|
namespace Notesnook.API.Interfaces
|
||||||
public interface IUserService
|
{
|
||||||
{
|
public interface IUserService
|
||||||
Task CreateUserAsync();
|
{
|
||||||
Task DeleteUserAsync(string userId);
|
Task CreateUserAsync();
|
||||||
Task DeleteUserAsync(string userId, string? jti, string password);
|
Task<bool> DeleteUserAsync(string userId, string jti);
|
||||||
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
|
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
|
||||||
Task<UserResponse> GetUserAsync(string userId);
|
Task<UserResponse> GetUserAsync(bool repair = true);
|
||||||
Task SetUserKeysAsync(string userId, UserKeys keys);
|
Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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 Notesnook.API.Models;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
|
namespace Notesnook.API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUserSettings : IDocument
|
||||||
|
{
|
||||||
|
string UserId { get; set; }
|
||||||
|
|
||||||
|
long LastSynced
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
EncryptedData VaultKey
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
string Salt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Interfaces;
|
|
||||||
using Notesnook.API.Models;
|
|
||||||
using Notesnook.API.Services;
|
|
||||||
using Quartz;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Jobs
|
|
||||||
{
|
|
||||||
public class DeviceCleanupJob(ISyncItemsRepositoryAccessor repositories) : IJob
|
|
||||||
{
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
|
||||||
{
|
|
||||||
var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-1).ToUnixTimeMilliseconds();
|
|
||||||
var deviceFilter = Builders<SyncDevice>.Filter.Lt(x => x.LastAccessTime, cutoffDate);
|
|
||||||
|
|
||||||
using var cursor = await repositories.SyncDevices.Collection.Find(deviceFilter, new FindOptions { BatchSize = 1000 })
|
|
||||||
.Project(x => x.DeviceId)
|
|
||||||
.ToCursorAsync();
|
|
||||||
|
|
||||||
var deleteModels = new List<WriteModel<DeviceIdsChunk>>();
|
|
||||||
while (await cursor.MoveNextAsync())
|
|
||||||
{
|
|
||||||
if (!cursor.Current.Any()) continue;
|
|
||||||
deleteModels.Add(new DeleteManyModel<DeviceIdsChunk>(Builders<DeviceIdsChunk>.Filter.In(x => x.DeviceId, cursor.Current)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteModels.Count > 0)
|
|
||||||
{
|
|
||||||
var bulkOptions = new BulkWriteOptions { IsOrdered = false };
|
|
||||||
await repositories.DeviceIdsChunks.Collection.BulkWriteAsync(deleteModels, bulkOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
await repositories.SyncDevices.Collection.DeleteManyAsync(deviceFilter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
using System.Collections.Generic;
|
||||||
{
|
|
||||||
public class Algorithms
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
public static string Default => "xcha-argon2i13-7";
|
public class Algorithms
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,159 +1,161 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
namespace Notesnook.API.Models
|
|
||||||
{
|
namespace Notesnook.API.Models
|
||||||
public class Announcement
|
{
|
||||||
{
|
[BsonCollection("notesnook", "announcements")]
|
||||||
public Announcement()
|
public class Announcement
|
||||||
{
|
{
|
||||||
this.Id = ObjectId.GenerateNewId().ToString();
|
public Announcement()
|
||||||
}
|
{
|
||||||
|
this.Id = ObjectId.GenerateNewId().ToString();
|
||||||
[BsonId]
|
}
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
|
||||||
[BsonElement("id")]
|
[BsonId]
|
||||||
[JsonPropertyName("id")]
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
public string Id { get; set; }
|
[BsonElement("id")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
[JsonPropertyName("type")]
|
public string Id { get; set; }
|
||||||
[BsonElement("type")]
|
|
||||||
public required string Type { get; set; }
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
[JsonPropertyName("timestamp")]
|
public string Type { get; set; }
|
||||||
[BsonElement("timestamp")]
|
|
||||||
public long Timestamp { get; set; }
|
[JsonPropertyName("timestamp")]
|
||||||
|
[BsonElement("timestamp")]
|
||||||
[JsonPropertyName("platforms")]
|
public long Timestamp { get; set; }
|
||||||
[BsonElement("platforms")]
|
|
||||||
public required string[] Platforms { get; set; }
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
[JsonPropertyName("isActive")]
|
public string[] Platforms { get; set; }
|
||||||
[BsonElement("isActive")]
|
|
||||||
public bool IsActive { get; set; }
|
[JsonPropertyName("isActive")]
|
||||||
|
[BsonElement("isActive")]
|
||||||
[JsonPropertyName("userTypes")]
|
public bool IsActive { get; set; }
|
||||||
[BsonElement("userTypes")]
|
|
||||||
public required string[] UserTypes { get; set; }
|
[JsonPropertyName("userTypes")]
|
||||||
|
[BsonElement("userTypes")]
|
||||||
[JsonPropertyName("appVersion")]
|
public string[] UserTypes { get; set; }
|
||||||
[BsonElement("appVersion")]
|
|
||||||
public int AppVersion { get; set; }
|
[JsonPropertyName("appVersion")]
|
||||||
|
[BsonElement("appVersion")]
|
||||||
[JsonPropertyName("body")]
|
public int AppVersion { get; set; }
|
||||||
[BsonElement("body")]
|
|
||||||
public required BodyComponent[] Body { get; set; }
|
[JsonPropertyName("body")]
|
||||||
|
[BsonElement("body")]
|
||||||
[JsonIgnore]
|
public BodyComponent[] Body { get; set; }
|
||||||
[BsonElement("userIds")]
|
|
||||||
public string[]? UserIds { get; set; }
|
[JsonIgnore]
|
||||||
|
[BsonElement("userIds")]
|
||||||
|
public string[] UserIds { get; set; }
|
||||||
[Obsolete]
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
[DataMember(Name = "title")]
|
[Obsolete]
|
||||||
[BsonElement("title")]
|
[JsonPropertyName("title")]
|
||||||
public string? Title { get; set; }
|
[DataMember(Name = "title")]
|
||||||
|
[BsonElement("title")]
|
||||||
[Obsolete]
|
public string Title { get; set; }
|
||||||
[JsonPropertyName("description")]
|
|
||||||
[BsonElement("description")]
|
[Obsolete]
|
||||||
public string? Description { get; set; }
|
[JsonPropertyName("description")]
|
||||||
|
[BsonElement("description")]
|
||||||
[Obsolete]
|
public string Description { get; set; }
|
||||||
[JsonPropertyName("callToActions")]
|
|
||||||
[BsonElement("callToActions")]
|
[Obsolete]
|
||||||
public CallToAction[]? CallToActions { get; set; }
|
[JsonPropertyName("callToActions")]
|
||||||
}
|
[BsonElement("callToActions")]
|
||||||
|
public CallToAction[] CallToActions { get; set; }
|
||||||
public class BodyComponent
|
}
|
||||||
{
|
|
||||||
[JsonPropertyName("type")]
|
public class BodyComponent
|
||||||
[BsonElement("type")]
|
{
|
||||||
public required string Type { get; set; }
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
[JsonPropertyName("platforms")]
|
public string Type { get; set; }
|
||||||
[BsonElement("platforms")]
|
|
||||||
public string[]? Platforms { get; set; }
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
[JsonPropertyName("style")]
|
public string[] Platforms { get; set; }
|
||||||
[BsonElement("style")]
|
|
||||||
public Style? Style { get; set; }
|
[JsonPropertyName("style")]
|
||||||
|
[BsonElement("style")]
|
||||||
[JsonPropertyName("src")]
|
public Style Style { get; set; }
|
||||||
[BsonElement("src")]
|
|
||||||
public string? Src { get; set; }
|
[JsonPropertyName("src")]
|
||||||
|
[BsonElement("src")]
|
||||||
[JsonPropertyName("text")]
|
public string Src { get; set; }
|
||||||
[BsonElement("text")]
|
|
||||||
public string? Text { get; set; }
|
[JsonPropertyName("text")]
|
||||||
|
[BsonElement("text")]
|
||||||
[JsonPropertyName("value")]
|
public string Text { get; set; }
|
||||||
[BsonElement("value")]
|
|
||||||
public string? Value { get; set; }
|
[JsonPropertyName("value")]
|
||||||
|
[BsonElement("value")]
|
||||||
[JsonPropertyName("items")]
|
public string Value { get; set; }
|
||||||
[BsonElement("items")]
|
|
||||||
public BodyComponent[]? Items { get; set; }
|
[JsonPropertyName("items")]
|
||||||
|
[BsonElement("items")]
|
||||||
[JsonPropertyName("actions")]
|
public BodyComponent[] Items { get; set; }
|
||||||
[BsonElement("actions")]
|
|
||||||
public required CallToAction[] Actions { get; set; }
|
[JsonPropertyName("actions")]
|
||||||
}
|
[BsonElement("actions")]
|
||||||
|
public CallToAction[] Actions { get; set; }
|
||||||
public class Style
|
}
|
||||||
{
|
|
||||||
[JsonPropertyName("marginTop")]
|
public class Style
|
||||||
[BsonElement("marginTop")]
|
{
|
||||||
public int MarginTop { get; set; }
|
[JsonPropertyName("marginTop")]
|
||||||
|
[BsonElement("marginTop")]
|
||||||
[JsonPropertyName("marginBottom")]
|
public int MarginTop { get; set; }
|
||||||
[BsonElement("marginBottom")]
|
|
||||||
public int MarginBottom { get; set; }
|
[JsonPropertyName("marginBottom")]
|
||||||
|
[BsonElement("marginBottom")]
|
||||||
[JsonPropertyName("textAlign")]
|
public int MarginBottom { get; set; }
|
||||||
[BsonElement("textAlign")]
|
|
||||||
public string? TextAlign { get; set; }
|
[JsonPropertyName("textAlign")]
|
||||||
}
|
[BsonElement("textAlign")]
|
||||||
|
public string TextAlign { get; set; }
|
||||||
public class CallToAction
|
}
|
||||||
{
|
|
||||||
[JsonPropertyName("type")]
|
public class CallToAction
|
||||||
[BsonElement("type")]
|
{
|
||||||
public required string Type { get; set; }
|
[JsonPropertyName("type")]
|
||||||
|
[BsonElement("type")]
|
||||||
[JsonPropertyName("platforms")]
|
public string Type { get; set; }
|
||||||
[BsonElement("platforms")]
|
|
||||||
public string[]? Platforms { get; set; }
|
[JsonPropertyName("platforms")]
|
||||||
|
[BsonElement("platforms")]
|
||||||
[JsonPropertyName("data")]
|
public string[] Platforms { get; set; }
|
||||||
[BsonElement("data")]
|
|
||||||
public string? Data { get; set; }
|
[JsonPropertyName("data")]
|
||||||
|
[BsonElement("data")]
|
||||||
[JsonPropertyName("title")]
|
public string Data { get; set; }
|
||||||
[BsonElement("title")]
|
|
||||||
public string? Title { get; set; }
|
[JsonPropertyName("title")]
|
||||||
}
|
[BsonElement("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Amazon.S3.Model;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Models;
|
|
||||||
|
|
||||||
public class CompleteMultipartUploadRequestWrapper
|
|
||||||
{
|
|
||||||
public required string Key { get; set; }
|
|
||||||
public required List<PartETagWrapper> PartETags { get; set; }
|
|
||||||
public required 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
|
||||||
{
|
|
||||||
public class DeleteAccountForm
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public required string Password
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
|
||||||
{
|
|
||||||
public class DeviceIdsChunk
|
|
||||||
{
|
|
||||||
[BsonId]
|
|
||||||
public ObjectId Id { get; set; }
|
|
||||||
public required string UserId { get; set; }
|
|
||||||
public required string DeviceId { get; set; }
|
|
||||||
public required string Key { get; set; }
|
|
||||||
public required string[] Ids { get; set; } = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +1,56 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
[MessagePack.MessagePackObject]
|
public class EncryptedData : IEncrypted
|
||||||
public class EncryptedData
|
{
|
||||||
{
|
[JsonPropertyName("iv")]
|
||||||
[MessagePack.Key("iv")]
|
[BsonElement("iv")]
|
||||||
[JsonPropertyName("iv")]
|
[DataMember(Name = "iv")]
|
||||||
[BsonElement("iv")]
|
public string IV
|
||||||
[DataMember(Name = "iv")]
|
{
|
||||||
public required string IV { get; set; }
|
get; set;
|
||||||
|
}
|
||||||
[MessagePack.Key("cipher")]
|
|
||||||
[JsonPropertyName("cipher")]
|
[JsonPropertyName("cipher")]
|
||||||
[BsonElement("cipher")]
|
[BsonElement("cipher")]
|
||||||
[DataMember(Name = "cipher")]
|
[DataMember(Name = "cipher")]
|
||||||
public required string Cipher { get; set; }
|
public string Cipher
|
||||||
|
{
|
||||||
[MessagePack.Key("length")]
|
get; set;
|
||||||
[JsonPropertyName("length")]
|
}
|
||||||
[BsonElement("length")]
|
|
||||||
[DataMember(Name = "length")]
|
[JsonPropertyName("length")]
|
||||||
public long Length { get; set; }
|
[BsonElement("length")]
|
||||||
|
[DataMember(Name = "length")]
|
||||||
[MessagePack.Key("salt")]
|
public long Length { get; set; }
|
||||||
[JsonPropertyName("salt")]
|
|
||||||
[BsonElement("salt")]
|
[JsonPropertyName("salt")]
|
||||||
[DataMember(Name = "salt")]
|
[BsonElement("salt")]
|
||||||
public required string Salt { get; set; }
|
[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("userId")]
|
|
||||||
public required string UserId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public required string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("key")]
|
|
||||||
public string Key { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("dateCreated")]
|
|
||||||
public long DateCreated { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("expiryDate")]
|
|
||||||
public long ExpiryDate { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("lastUsedAt")]
|
|
||||||
public long LastUsedAt { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +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 required EncryptedKey Key { get; set; }
|
|
||||||
|
|
||||||
[DataMember(Name = "salt")]
|
|
||||||
[JsonPropertyName("salt")]
|
|
||||||
[MessagePack.Key("salt")]
|
|
||||||
[Required]
|
|
||||||
public required string Salt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[MessagePack.MessagePackObject]
|
|
||||||
public class EncryptedKey
|
|
||||||
{
|
|
||||||
[DataMember(Name = "alg")]
|
|
||||||
[JsonPropertyName("alg")]
|
|
||||||
[MessagePack.Key("alg")]
|
|
||||||
[Required]
|
|
||||||
public required string Algorithm { get; set; }
|
|
||||||
|
|
||||||
[DataMember(Name = "cipher")]
|
|
||||||
[JsonPropertyName("cipher")]
|
|
||||||
[MessagePack.Key("cipher")]
|
|
||||||
[Required]
|
|
||||||
public required string Cipher { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("length")]
|
|
||||||
[DataMember(Name = "length")]
|
|
||||||
[MessagePack.Key("length")]
|
|
||||||
[Required]
|
|
||||||
public long Length
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +1,62 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using System.Runtime.Serialization;
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
namespace Notesnook.API.Models
|
|
||||||
{
|
namespace Notesnook.API.Models
|
||||||
public class ObjectWithId
|
{
|
||||||
{
|
[BsonCollection("notesnook", "monographs")]
|
||||||
[BsonId]
|
public class Monograph : IMonograph
|
||||||
[BsonIgnoreIfDefault]
|
{
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
public Monograph()
|
||||||
public required string Id { get; set; }
|
{
|
||||||
|
Id = ObjectId.GenerateNewId().ToString();
|
||||||
public required string ItemId { get; set; }
|
}
|
||||||
}
|
|
||||||
|
[BsonId]
|
||||||
public class Monograph
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
{
|
public string Id { get; set; }
|
||||||
public Monograph()
|
|
||||||
{
|
[JsonPropertyName("title")]
|
||||||
Id = ObjectId.GenerateNewId().ToString();
|
public string Title { get; set; }
|
||||||
}
|
|
||||||
|
[JsonPropertyName("userId")]
|
||||||
[DataMember(Name = "id")]
|
public string UserId { get; set; }
|
||||||
[JsonPropertyName("id")]
|
|
||||||
[MessagePack.Key("id")]
|
[JsonPropertyName("selfDestruct")]
|
||||||
public string? ItemId { get; set; }
|
public bool SelfDestruct { get; set; }
|
||||||
|
|
||||||
[BsonId]
|
[JsonPropertyName("encryptedContent")]
|
||||||
[BsonIgnoreIfDefault]
|
public EncryptedData EncryptedContent { get; set; }
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
|
||||||
[JsonIgnore]
|
[JsonPropertyName("datePublished")]
|
||||||
[MessagePack.IgnoreMember]
|
public long DatePublished { get; set; }
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
[JsonPropertyName("title")]
|
[BsonIgnore]
|
||||||
public string? Title { get; set; }
|
public string Content { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("userId")]
|
[JsonIgnore]
|
||||||
public string? UserId { get; set; }
|
public byte[] CompressedContent { get; set; }
|
||||||
|
}
|
||||||
[JsonPropertyName("selfDestruct")]
|
|
||||||
public bool SelfDestruct { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("encryptedContent")]
|
|
||||||
public EncryptedData? EncryptedContent { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("datePublished")]
|
|
||||||
public long DatePublished { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
[BsonIgnore]
|
|
||||||
public string? Content { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public byte[]? CompressedContent { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("password")]
|
|
||||||
public EncryptedData? Password { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("deleted")]
|
|
||||||
public bool Deleted { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("viewCount")]
|
|
||||||
public int ViewCount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
namespace Notesnook.API.Models
|
||||||
|
{
|
||||||
namespace Notesnook.API.Models
|
public class MultipartUploadMeta
|
||||||
{
|
{
|
||||||
public class MultipartUploadMeta
|
public string UploadId { get; set; }
|
||||||
{
|
public string[] Parts { get; set; }
|
||||||
public string UploadId { get; set; } = string.Empty;
|
}
|
||||||
public string[] Parts { get; set; } = Array.Empty<string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Notesnook.API.Models;
|
|
||||||
|
|
||||||
public class PartETagWrapper
|
|
||||||
{
|
|
||||||
public int PartNumber { get; set; }
|
|
||||||
public string ETag { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Streetwriters.Common.Models;
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
namespace Notesnook.API.Models.Responses
|
namespace Notesnook.API.Models.Responses
|
||||||
{
|
{
|
||||||
public class SignupResponse : Response
|
public class SignupResponse : Response
|
||||||
{
|
{
|
||||||
[JsonPropertyName("userId")]
|
[JsonPropertyName("userId")]
|
||||||
public string? UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("errors")]
|
[JsonPropertyName("errors")]
|
||||||
public string[]? Errors { get; set; }
|
public string[] Errors { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
using System.Net.Http;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using Streetwriters.Common.Interfaces;
|
||||||
using Streetwriters.Common.Interfaces;
|
using Streetwriters.Common.Models;
|
||||||
using Streetwriters.Common.Models;
|
|
||||||
|
namespace Notesnook.API.Models.Responses
|
||||||
namespace Notesnook.API.Models.Responses
|
{
|
||||||
{
|
public class UserResponse : UserModel, IResponse
|
||||||
public class UserResponse : UserModel, IResponse
|
{
|
||||||
{
|
[JsonPropertyName("salt")]
|
||||||
[JsonPropertyName("salt")]
|
public string Salt { get; set; }
|
||||||
public string? Salt { get; set; }
|
|
||||||
|
[JsonPropertyName("attachmentsKey")]
|
||||||
[JsonPropertyName("attachmentsKey")]
|
public EncryptedData AttachmentsKey { get; set; }
|
||||||
public EncryptedData? AttachmentsKey { get; set; }
|
|
||||||
|
[JsonPropertyName("subscription")]
|
||||||
[JsonPropertyName("monographPasswordsKey")]
|
public ISubscription Subscription { get; set; }
|
||||||
public EncryptedData? MonographPasswordsKey { get; set; }
|
|
||||||
|
[JsonIgnore]
|
||||||
[JsonPropertyName("inboxKeys")]
|
public bool Success { get; set; }
|
||||||
public InboxKeys? InboxKeys { get; set; }
|
public int StatusCode { get; set; }
|
||||||
|
}
|
||||||
[JsonPropertyName("subscription")]
|
}
|
||||||
public Subscription? Subscription { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("storageUsed")]
|
|
||||||
public long StorageUsed { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("totalStorage")]
|
|
||||||
public long TotalStorage { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public int StatusCode { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public HttpContent? Content { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
public class S3Options
|
public class S3Options
|
||||||
{
|
{
|
||||||
public string ServiceUrl { get; set; } = string.Empty;
|
public string ServiceUrl { get; set; }
|
||||||
public string Region { get; set; } = string.Empty;
|
public string Region { get; set; }
|
||||||
public string AccessKeyId { get; set; } = string.Empty;
|
public string AccessKeyId { get; set; }
|
||||||
public string SecretAccessKey { get; set; } = string.Empty;
|
public string SecretAccessKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
|
||||||
{
|
|
||||||
public class SyncDevice
|
|
||||||
{
|
|
||||||
[BsonId]
|
|
||||||
public ObjectId Id { get; set; }
|
|
||||||
|
|
||||||
public required string UserId { get; set; }
|
|
||||||
public required string DeviceId { get; set; }
|
|
||||||
public required long LastAccessTime { get; set; }
|
|
||||||
public required bool IsSyncReset { get; set; }
|
|
||||||
public string? AppVersion { get; set; }
|
|
||||||
public string? DatabaseVersion { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.IO;
|
|
||||||
using MongoDB.Bson.Serialization;
|
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using MongoDB.Bson.Serialization.Serializers;
|
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
[MessagePack.MessagePackObject]
|
public class SyncItem : ISyncItem
|
||||||
public class SyncItem
|
|
||||||
{
|
{
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
[MessagePack.IgnoreMember]
|
|
||||||
[JsonPropertyName("dateSynced")]
|
[JsonPropertyName("dateSynced")]
|
||||||
public long DateSynced
|
public long DateSynced
|
||||||
{
|
{
|
||||||
@@ -43,29 +38,31 @@ namespace Notesnook.API.Models
|
|||||||
|
|
||||||
[DataMember(Name = "userId")]
|
[DataMember(Name = "userId")]
|
||||||
[JsonPropertyName("userId")]
|
[JsonPropertyName("userId")]
|
||||||
[MessagePack.Key("userId")]
|
public string UserId
|
||||||
public string? UserId
|
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("iv")]
|
[JsonPropertyName("iv")]
|
||||||
[DataMember(Name = "iv")]
|
[DataMember(Name = "iv")]
|
||||||
[MessagePack.Key("iv")]
|
|
||||||
[Required]
|
[Required]
|
||||||
public string IV { get; set; } = string.Empty;
|
public string IV
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[JsonPropertyName("cipher")]
|
[JsonPropertyName("cipher")]
|
||||||
[DataMember(Name = "cipher")]
|
[DataMember(Name = "cipher")]
|
||||||
[MessagePack.Key("cipher")]
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Cipher { get; set; } = string.Empty;
|
public string Cipher
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
[DataMember(Name = "id")]
|
[DataMember(Name = "id")]
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
[MessagePack.Key("id")]
|
public string ItemId
|
||||||
public string? ItemId
|
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
@@ -74,7 +71,6 @@ namespace Notesnook.API.Models
|
|||||||
[BsonIgnoreIfDefault]
|
[BsonIgnoreIfDefault]
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[MessagePack.IgnoreMember]
|
|
||||||
public ObjectId Id
|
public ObjectId Id
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
@@ -82,7 +78,6 @@ namespace Notesnook.API.Models
|
|||||||
|
|
||||||
[JsonPropertyName("length")]
|
[JsonPropertyName("length")]
|
||||||
[DataMember(Name = "length")]
|
[DataMember(Name = "length")]
|
||||||
[MessagePack.Key("length")]
|
|
||||||
[Required]
|
[Required]
|
||||||
public long Length
|
public long Length
|
||||||
{
|
{
|
||||||
@@ -91,7 +86,6 @@ namespace Notesnook.API.Models
|
|||||||
|
|
||||||
[JsonPropertyName("v")]
|
[JsonPropertyName("v")]
|
||||||
[DataMember(Name = "v")]
|
[DataMember(Name = "v")]
|
||||||
[MessagePack.Key("v")]
|
|
||||||
[Required]
|
[Required]
|
||||||
public double Version
|
public double Version
|
||||||
{
|
{
|
||||||
@@ -100,97 +94,34 @@ namespace Notesnook.API.Models
|
|||||||
|
|
||||||
[JsonPropertyName("alg")]
|
[JsonPropertyName("alg")]
|
||||||
[DataMember(Name = "alg")]
|
[DataMember(Name = "alg")]
|
||||||
[MessagePack.Key("alg")]
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Algorithm { get; set; } = string.Empty;
|
public string Algorithm
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = Algorithms.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
|
[BsonCollection("notesnook", "attachments")]
|
||||||
{
|
public class Attachment : SyncItem { }
|
||||||
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, SyncItem value)
|
|
||||||
{
|
|
||||||
var writer = context.Writer;
|
|
||||||
writer.WriteStartDocument();
|
|
||||||
|
|
||||||
if (value.Id != ObjectId.Empty)
|
[BsonCollection("notesnook", "content")]
|
||||||
{
|
public class Content : SyncItem { }
|
||||||
writer.WriteName("_id");
|
|
||||||
writer.WriteObjectId(value.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteName("DateSynced");
|
[BsonCollection("notesnook", "notes")]
|
||||||
writer.WriteInt64(value.DateSynced);
|
public class Note : SyncItem { }
|
||||||
|
|
||||||
writer.WriteName("UserId");
|
[BsonCollection("notesnook", "notebooks")]
|
||||||
writer.WriteString(value.UserId);
|
public class Notebook : SyncItem { }
|
||||||
|
|
||||||
writer.WriteName("IV");
|
[BsonCollection("notesnook", "relations")]
|
||||||
writer.WriteString(value.IV);
|
public class Relation : SyncItem { }
|
||||||
|
|
||||||
writer.WriteName("Cipher");
|
[BsonCollection("notesnook", "reminders")]
|
||||||
writer.WriteString(value.Cipher);
|
public class Reminder : SyncItem { }
|
||||||
|
|
||||||
writer.WriteName("ItemId");
|
[BsonCollection("notesnook", "settings")]
|
||||||
writer.WriteString(value.ItemId);
|
public class Setting : SyncItem { }
|
||||||
|
|
||||||
writer.WriteName("Length");
|
[BsonCollection("notesnook", "shortcuts")]
|
||||||
writer.WriteInt64(value.Length);
|
public class Shortcut : SyncItem { }
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,44 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Interfaces;
|
using Streetwriters.Data.Attributes;
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
public class Limit
|
[BsonCollection("notesnook", "user_settings")]
|
||||||
{
|
public class UserSettings : IUserSettings
|
||||||
private long _value = 0;
|
{
|
||||||
public long Value
|
public UserSettings()
|
||||||
{
|
{
|
||||||
get => _value;
|
this.Id = ObjectId.GenerateNewId().ToString();
|
||||||
set
|
}
|
||||||
{
|
public string UserId { get; set; }
|
||||||
_value = value;
|
public long LastSynced { get; set; }
|
||||||
UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
public string Salt { get; set; }
|
||||||
}
|
public EncryptedData VaultKey { get; set; }
|
||||||
}
|
public EncryptedData AttachmentsKey { get; set; }
|
||||||
public long UpdatedAt
|
|
||||||
{
|
[BsonId]
|
||||||
get;
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
set;
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserSettings
|
|
||||||
{
|
|
||||||
public UserSettings()
|
|
||||||
{
|
|
||||||
this.Id = ObjectId.GenerateNewId();
|
|
||||||
}
|
|
||||||
public required string UserId { get; set; }
|
|
||||||
public long LastSynced { get; set; }
|
|
||||||
public required 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; }
|
|
||||||
|
|
||||||
[BsonId]
|
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
|
||||||
public ObjectId Id { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<StartupObject>Notesnook.API.Program</StartupObject>
|
<StartupObject>Notesnook.API.Program</StartupObject>
|
||||||
<Nullable>enable</Nullable>
|
<LangVersion>10.0</LangVersion>
|
||||||
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
<PackageReference Include="AWSSDK.Core" Version="3.7.12.5" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Aws.S3" Version="9.0.0" />
|
|
||||||
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
|
|
||||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
<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.JwtBearer" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" 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="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.SignalR.Protocols.MessagePack" Version="6.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
|
||||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
</ItemGroup>
|
||||||
<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" />
|
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
|
||||||
@@ -31,4 +26,4 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+65
-69
@@ -1,69 +1,65 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Net;
|
#if !DEBUG
|
||||||
using System.Threading.Tasks;
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
#endif
|
||||||
using Microsoft.Extensions.Hosting;
|
using System;
|
||||||
using Microsoft.Extensions.Logging;
|
using System.Threading.Tasks;
|
||||||
using Streetwriters.Common;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
namespace Notesnook.API
|
using Streetwriters.Common;
|
||||||
{
|
using System.Linq;
|
||||||
public class Program
|
|
||||||
{
|
namespace Notesnook.API
|
||||||
public static async Task Main(string[] args)
|
{
|
||||||
{
|
public class Program
|
||||||
#if (DEBUG || STAGING)
|
{
|
||||||
DotNetEnv.Env.TraversePath().Load(".env.local");
|
public static async Task Main(string[] args)
|
||||||
#else
|
{
|
||||||
DotNetEnv.Env.TraversePath().Load(".env");
|
#if DEBUG
|
||||||
#endif
|
DotNetEnv.Env.TraversePath().Load(".env.local");
|
||||||
IHost host = CreateHostBuilder(args).Build();
|
#else
|
||||||
await host.RunAsync();
|
DotNetEnv.Env.TraversePath().Load(".env");
|
||||||
}
|
#endif
|
||||||
|
IHost host = CreateHostBuilder(args).Build();
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
await host.RunAsync();
|
||||||
Host.CreateDefaultBuilder(args)
|
}
|
||||||
.ConfigureLogging(logging =>
|
|
||||||
{
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
logging.ClearProviders();
|
Host.CreateDefaultBuilder(args)
|
||||||
logging.AddConsole();
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
logging.AddSystemdConsole();
|
{
|
||||||
})
|
webBuilder
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.UseStartup<Startup>()
|
||||||
{
|
.UseKestrel((options) =>
|
||||||
webBuilder
|
{
|
||||||
.UseStartup<Startup>()
|
options.Limits.MaxRequestBodySize = long.MaxValue;
|
||||||
.UseKestrel((options) =>
|
options.ListenAnyIP(Servers.NotesnookAPI.Port);
|
||||||
{
|
if (Servers.NotesnookAPI.IsSecure)
|
||||||
options.Limits.MaxRequestBodySize = long.MaxValue;
|
{
|
||||||
options.ListenAnyIP(Servers.NotesnookAPI.Port);
|
options.ListenAnyIP(443, listenerOptions =>
|
||||||
if (Servers.NotesnookAPI.IsSecure && Servers.NotesnookAPI.SSLCertificate != null)
|
{
|
||||||
{
|
listenerOptions.UseHttps(Servers.NotesnookAPI.SSLCertificate);
|
||||||
options.ListenAnyIP(443, listenerOptions =>
|
});
|
||||||
{
|
}
|
||||||
listenerOptions.UseHttps(Servers.NotesnookAPI.SSLCertificate);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
options.Listen(IPAddress.Parse("127.0.0.1"), 5067);
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,187 +19,82 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel;
|
|
||||||
using Microsoft.VisualBasic;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Notesnook.API.Hubs;
|
|
||||||
using Notesnook.API.Interfaces;
|
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Data.Attributes;
|
||||||
using Streetwriters.Data.DbContexts;
|
|
||||||
using Streetwriters.Data.Interfaces;
|
|
||||||
using Streetwriters.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Repositories
|
namespace Notesnook.API.Repositories
|
||||||
{
|
{
|
||||||
public class SyncItemsRepository : Repository<SyncItem>
|
public class SyncItemsRepository<T> where T : SyncItem
|
||||||
{
|
{
|
||||||
private readonly string collectionName;
|
const string BASE_DATA_DIR = "data";
|
||||||
private readonly ILogger<SyncItemsRepository> logger;
|
private string GetCollectionName()
|
||||||
public SyncItemsRepository(IDbContext dbContext, IMongoCollection<SyncItem> collection, ILogger<SyncItemsRepository> logger) : base(dbContext, collection)
|
|
||||||
{
|
{
|
||||||
this.collectionName = collection.CollectionNamespace.CollectionName;
|
var attribute = (BsonCollectionAttribute)typeof(T).GetCustomAttributes(
|
||||||
this.logger = logger;
|
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 string GetUserDirectoryPath(string userId)
|
||||||
private bool IsValidAlgorithm(string algorithm)
|
|
||||||
{
|
{
|
||||||
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));
|
try
|
||||||
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>
|
|
||||||
{
|
{
|
||||||
BatchSize = batchSize,
|
return System.IO.Directory.EnumerateFiles(GetUserDirectoryPath(userId), searchPattern, System.IO.SearchOption.TopDirectoryOnly);
|
||||||
AllowDiskUse = true,
|
}
|
||||||
AllowPartialResults = false,
|
catch
|
||||||
NoCursorTimeout = true,
|
{
|
||||||
Sort = new SortDefinitionBuilder<SyncItem>().Ascending("_id")
|
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) });
|
try
|
||||||
|
|
||||||
if (!all) filters.Add(Builders<SyncItem>.Filter.In("ItemId", ids));
|
|
||||||
|
|
||||||
return Collection.FindAsync(Builders<SyncItem>.Filter.And(filters), new FindOptions<SyncItem>
|
|
||||||
{
|
{
|
||||||
BatchSize = batchSize,
|
var files = Directory.GetFiles(GetUserDirectoryPath(userId), $"{id}-*", System.IO.SearchOption.TopDirectoryOnly);
|
||||||
AllowDiskUse = true,
|
return files.Length > 0 ? files[0] : null;
|
||||||
AllowPartialResults = false,
|
}
|
||||||
NoCursorTimeout = true
|
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)
|
public void DeleteByUserId(string userId)
|
||||||
{
|
{
|
||||||
var filter = Builders<SyncItem>.Filter.Eq("UserId", userId);
|
Directory.Delete(GetUserDirectoryPath(userId), true);
|
||||||
dbContext.AddCommand((handle, ct) => Collection.DeleteManyAsync(handle, filter, null, ct));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
Directory.CreateDirectory(GetUserDirectoryPath(userId));
|
||||||
{
|
var oldPath = FindItemById(userId, id);
|
||||||
throw new Exception($"Size of item \"{item.ItemId}\" is too large. Maximum allowed size is 15 MB.");
|
var newPath = Path.Join(GetUserDirectoryPath(userId), $"{id}-{dateSynced}");
|
||||||
}
|
await File.WriteAllTextAsync(newPath, item);
|
||||||
|
if (oldPath != null) File.Delete(oldPath);
|
||||||
if (!IsValidAlgorithm(item.Algorithm))
|
|
||||||
{
|
|
||||||
throw new Exception($"Invalid alg identifier {item.Algorithm}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle case where the cipher is corrupted.
|
|
||||||
if (!IsBase64String(item.Cipher))
|
|
||||||
{
|
|
||||||
logger.LogError("Corrupted item {ItemId} in collection {CollectionName}. Length: {Length}, Cipher: {Cipher}",
|
|
||||||
item.ItemId, this.collectionName, item.Length, 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))
|
|
||||||
{
|
|
||||||
logger.LogError("Corrupted item {ItemId} in collection {CollectionName}. Length: {Length}, Cipher: {Cipher}",
|
|
||||||
item.ItemId, this.collectionName, item.Length, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,18 +27,10 @@ using Amazon;
|
|||||||
using Amazon.Runtime;
|
using Amazon.Runtime;
|
||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using Amazon.S3.Model;
|
using Amazon.S3.Model;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Accessors;
|
|
||||||
using Notesnook.API.Helpers;
|
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common.Accessors;
|
|
||||||
using Streetwriters.Common.Enums;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Models;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Services
|
namespace Notesnook.API.Services
|
||||||
{
|
{
|
||||||
@@ -50,11 +42,8 @@ namespace Notesnook.API.Services
|
|||||||
|
|
||||||
public class S3Service : IS3Service
|
public class S3Service : IS3Service
|
||||||
{
|
{
|
||||||
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME;
|
private readonly string BUCKET_NAME = "nn-attachments";
|
||||||
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? Constants.S3_BUCKET_NAME;
|
private AmazonS3Client S3Client { get; }
|
||||||
private readonly S3FailoverHelper S3Client;
|
|
||||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
|
||||||
private WampServiceAccessor ServiceAccessor { get; }
|
|
||||||
|
|
||||||
// When running in a dockerized environment the sync server doesn't have access
|
// 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
|
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
|
||||||
@@ -64,49 +53,52 @@ namespace Notesnook.API.Services
|
|||||||
// URLs generated by S3 are host specific. Changing their hostname on the fly causes
|
// URLs generated by S3 are host specific. Changing their hostname on the fly causes
|
||||||
// SignatureDoesNotMatch error.
|
// SignatureDoesNotMatch error.
|
||||||
// That is why we create 2 separate S3 clients. One for internal traffic and one for external.
|
// That is why we create 2 separate S3 clients. One for internal traffic and one for external.
|
||||||
private readonly S3FailoverHelper S3InternalClient;
|
private AmazonS3Client S3InternalClient { get; }
|
||||||
private readonly HttpClient httpClient = new();
|
private HttpClient httpClient = new HttpClient();
|
||||||
|
|
||||||
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, WampServiceAccessor wampServiceAccessor, ILogger<S3Service> logger)
|
public S3Service()
|
||||||
{
|
{
|
||||||
Repositories = syncItemsRepositoryAccessor;
|
var config = new AmazonS3Config
|
||||||
ServiceAccessor = wampServiceAccessor;
|
|
||||||
S3Client = new S3FailoverHelper(
|
|
||||||
S3ClientFactory.CreateS3Clients(
|
|
||||||
Constants.S3_SERVICE_URL,
|
|
||||||
Constants.S3_REGION,
|
|
||||||
Constants.S3_ACCESS_KEY_ID,
|
|
||||||
Constants.S3_ACCESS_KEY,
|
|
||||||
forcePathStyle: true
|
|
||||||
),
|
|
||||||
logger: logger
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Constants.S3_INTERNAL_SERVICE_URL) && !string.IsNullOrEmpty(Constants.S3_INTERNAL_BUCKET_NAME))
|
|
||||||
{
|
{
|
||||||
S3InternalClient = new S3FailoverHelper(
|
#if DEBUG
|
||||||
S3ClientFactory.CreateS3Clients(
|
ServiceURL = Servers.S3Server.ToString(),
|
||||||
Constants.S3_INTERNAL_SERVICE_URL,
|
#else
|
||||||
Constants.S3_REGION,
|
ServiceURL = Constants.S3_SERVICE_URL,
|
||||||
Constants.S3_ACCESS_KEY_ID,
|
AuthenticationRegion = Constants.S3_REGION,
|
||||||
Constants.S3_ACCESS_KEY,
|
#endif
|
||||||
forcePathStyle: true
|
ForcePathStyle = true,
|
||||||
),
|
SignatureMethod = SigningAlgorithm.HmacSHA256,
|
||||||
logger: logger
|
SignatureVersion = "4"
|
||||||
);
|
};
|
||||||
|
#if DEBUG
|
||||||
|
S3Client = new AmazonS3Client("S3RVER", "S3RVER", config);
|
||||||
|
#else
|
||||||
|
S3Client = new AmazonS3Client(Constants.S3_ACCESS_KEY_ID, Constants.S3_ACCESS_KEY, config);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Constants.S3_INTERNAL_SERVICE_URL))
|
||||||
|
{
|
||||||
|
S3InternalClient = new AmazonS3Client(Constants.S3_ACCESS_KEY_ID, Constants.S3_ACCESS_KEY, new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = Constants.S3_INTERNAL_SERVICE_URL,
|
||||||
|
AuthenticationRegion = Constants.S3_REGION,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
SignatureMethod = SigningAlgorithm.HmacSHA256,
|
||||||
|
SignatureVersion = "4"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else S3InternalClient = S3Client;
|
|
||||||
|
|
||||||
AWSConfigsS3.UseSignatureVersion4 = true;
|
AWSConfigsS3.UseSignatureVersion4 = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteObjectAsync(string userId, string name)
|
public async Task DeleteObjectAsync(string userId, string name)
|
||||||
{
|
{
|
||||||
var objectName = GetFullObjectName(userId, name) ?? throw new Exception("Invalid object name.");
|
var objectName = GetFullObjectName(userId, name);
|
||||||
|
if (objectName == null) throw new Exception("Invalid object name."); ;
|
||||||
|
|
||||||
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.DeleteObjectAsync(INTERNAL_BUCKET_NAME, objectName), operationName: "DeleteObject", isWriteOperation: true);
|
var response = await GetS3Client(S3ClientMode.INTERNAL).DeleteObjectAsync(BUCKET_NAME, objectName);
|
||||||
|
|
||||||
if (!IsSuccessStatusCode((int)response.HttpStatusCode))
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode)))
|
||||||
throw new Exception("Could not delete object.");
|
throw new Exception("Could not delete object.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +106,7 @@ namespace Notesnook.API.Services
|
|||||||
{
|
{
|
||||||
var request = new ListObjectsV2Request
|
var request = new ListObjectsV2Request
|
||||||
{
|
{
|
||||||
BucketName = INTERNAL_BUCKET_NAME,
|
BucketName = BUCKET_NAME,
|
||||||
Prefix = userId,
|
Prefix = userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,7 +114,7 @@ namespace Notesnook.API.Services
|
|||||||
var keys = new List<KeyVersion>();
|
var keys = new List<KeyVersion>();
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.ListObjectsV2Async(request), operationName: "ListObjectsV2");
|
response = await GetS3Client(S3ClientMode.INTERNAL).ListObjectsV2Async(request);
|
||||||
response.S3Objects.ForEach(obj => keys.Add(new KeyVersion
|
response.S3Objects.ForEach(obj => keys.Add(new KeyVersion
|
||||||
{
|
{
|
||||||
Key = obj.Key,
|
Key = obj.Key,
|
||||||
@@ -134,63 +126,51 @@ namespace Notesnook.API.Services
|
|||||||
|
|
||||||
if (keys.Count <= 0) return;
|
if (keys.Count <= 0) return;
|
||||||
|
|
||||||
var deleteObjectsResponse = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.DeleteObjectsAsync(new DeleteObjectsRequest
|
var deleteObjectsResponse = await S3Client
|
||||||
|
.DeleteObjectsAsync(new DeleteObjectsRequest
|
||||||
{
|
{
|
||||||
BucketName = INTERNAL_BUCKET_NAME,
|
BucketName = BUCKET_NAME,
|
||||||
Objects = keys,
|
Objects = keys,
|
||||||
}), operationName: "DeleteObjects", isWriteOperation: true);
|
});
|
||||||
|
|
||||||
if (!IsSuccessStatusCode((int)deleteObjectsResponse.HttpStatusCode))
|
if (!IsSuccessStatusCode((int)deleteObjectsResponse.HttpStatusCode))
|
||||||
throw new Exception("Could not delete directory.");
|
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 = await this.GetPresignedURLAsync(userId, name, HttpVerb.HEAD, S3ClientMode.INTERNAL);
|
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 request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||||
var response = await httpClient.SendAsync(request);
|
var response = await httpClient.SendAsync(request);
|
||||||
return response.Content.Headers.ContentLength ?? 0;
|
return response.Content.Headers.ContentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<string?> GetUploadObjectUrlAsync(string userId, string name)
|
public string GetUploadObjectUrl(string userId, string name)
|
||||||
{
|
{
|
||||||
return await this.GetPresignedURLAsync(userId, name, HttpVerb.PUT);
|
var url = this.GetPresignedURL(userId, name, HttpVerb.PUT);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetInternalUploadObjectUrlAsync(string userId, string name)
|
|
||||||
{
|
|
||||||
return await this.GetPresignedURLAsync(userId, name, HttpVerb.PUT, S3ClientMode.INTERNAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetDownloadObjectUrlAsync(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 = await this.GetPresignedURLAsync(userId, name, HttpVerb.GET);
|
|
||||||
if (url == null) return null;
|
if (url == null) return null;
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null)
|
public string GetDownloadObjectUrl(string userId, string name)
|
||||||
|
{
|
||||||
|
var url = this.GetPresignedURL(userId, name, HttpVerb.GET);
|
||||||
|
if (url == null) return null;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null)
|
||||||
{
|
{
|
||||||
var objectName = GetFullObjectName(userId, name);
|
var objectName = GetFullObjectName(userId, name);
|
||||||
if (userId == null || objectName == null) throw new Exception("Could not initiate multipart upload.");
|
if (userId == null || objectName == null) throw new Exception("Could not initiate multipart upload.");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(uploadId))
|
if (string.IsNullOrEmpty(uploadId))
|
||||||
{
|
{
|
||||||
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.InitiateMultipartUploadAsync(INTERNAL_BUCKET_NAME, objectName), operationName: "InitiateMultipartUpload", isWriteOperation: true);
|
var response = await GetS3Client(S3ClientMode.INTERNAL).InitiateMultipartUploadAsync(BUCKET_NAME, objectName);
|
||||||
if (!IsSuccessStatusCode((int)response.HttpStatusCode)) throw new Exception("Failed to initiate multipart upload.");
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to initiate multipart upload.");
|
||||||
|
|
||||||
uploadId = response.UploadId;
|
uploadId = response.UploadId;
|
||||||
}
|
}
|
||||||
@@ -198,7 +178,7 @@ namespace Notesnook.API.Services
|
|||||||
var signedUrls = new string[parts];
|
var signedUrls = new string[parts];
|
||||||
for (var i = 0; i < parts; ++i)
|
for (var i = 0; i < parts; ++i)
|
||||||
{
|
{
|
||||||
signedUrls[i] = await GetPresignedURLForUploadPartAsync(objectName, uploadId, i + 1);
|
signedUrls[i] = GetPresignedURLForUploadPart(objectName, uploadId, i + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MultipartUploadMeta
|
return new MultipartUploadMeta
|
||||||
@@ -213,126 +193,75 @@ namespace Notesnook.API.Services
|
|||||||
var objectName = GetFullObjectName(userId, name);
|
var objectName = GetFullObjectName(userId, name);
|
||||||
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
||||||
|
|
||||||
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.AbortMultipartUploadAsync(INTERNAL_BUCKET_NAME, objectName, uploadId), operationName: "AbortMultipartUpload", isWriteOperation: true);
|
var response = await GetS3Client(S3ClientMode.INTERNAL).AbortMultipartUploadAsync(BUCKET_NAME, objectName, uploadId);
|
||||||
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to abort multipart upload.");
|
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 S3InternalClient.ExecuteWithFailoverAsync((client) => client.ListPartsAsync(INTERNAL_BUCKET_NAME, objectName, uploadId), operationName: "ListParts");
|
|
||||||
long totalSize = 0;
|
|
||||||
foreach (var part in parts.Parts)
|
|
||||||
{
|
|
||||||
totalSize += part.Size;
|
|
||||||
}
|
|
||||||
return totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest)
|
public async Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest)
|
||||||
{
|
{
|
||||||
var objectName = GetFullObjectName(userId, uploadRequest.Key);
|
var objectName = GetFullObjectName(userId, uploadRequest.Key);
|
||||||
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
|
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.");
|
|
||||||
}
|
|
||||||
userSettings.StorageLimit ??= StorageHelper.RolloverStorageLimit(userSettings.StorageLimit);
|
|
||||||
|
|
||||||
if (!Constants.IS_SELF_HOSTED)
|
|
||||||
{
|
|
||||||
var subscription = await ServiceAccessor.UserSubscriptionService.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.Value += fileSize;
|
|
||||||
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit.Value))
|
|
||||||
{
|
|
||||||
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
|
||||||
throw new Exception("Storage limit reached.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadRequest.Key = objectName;
|
uploadRequest.Key = objectName;
|
||||||
uploadRequest.BucketName = INTERNAL_BUCKET_NAME;
|
uploadRequest.BucketName = BUCKET_NAME;
|
||||||
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.CompleteMultipartUploadAsync(uploadRequest), operationName: "CompleteMultipartUpload", isWriteOperation: true);
|
var response = await GetS3Client(S3ClientMode.INTERNAL).CompleteMultipartUploadAsync(uploadRequest);
|
||||||
if (!IsSuccessStatusCode((int)response.HttpStatusCode)) throw new Exception("Failed to complete multipart upload.");
|
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
|
||||||
|
|
||||||
if (!Constants.IS_SELF_HOSTED)
|
|
||||||
{
|
|
||||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
|
|
||||||
await Repositories.UsersSettings.Collection.UpdateOneAsync(
|
|
||||||
Builders<UserSettings>.Filter.Eq(u => u.UserId, userId),
|
|
||||||
Builders<UserSettings>.Update.Set(u => u.StorageLimit, userSettings.StorageLimit)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> GetPresignedURLAsync(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);
|
var objectName = GetFullObjectName(userId, name);
|
||||||
if (userId == null || objectName == null) return null;
|
if (userId == null || objectName == null) return null;
|
||||||
|
|
||||||
var client = mode == S3ClientMode.INTERNAL ? S3InternalClient : S3Client;
|
var request = new GetPreSignedUrlRequest
|
||||||
var bucketName = mode == S3ClientMode.INTERNAL ? INTERNAL_BUCKET_NAME : BUCKET_NAME;
|
|
||||||
|
|
||||||
return await client.ExecuteWithFailoverAsync(client =>
|
|
||||||
{
|
{
|
||||||
var request = new GetPreSignedUrlRequest
|
BucketName = BUCKET_NAME,
|
||||||
{
|
Expires = System.DateTime.Now.AddHours(1),
|
||||||
BucketName = bucketName,
|
Verb = httpVerb,
|
||||||
Expires = System.DateTime.Now.AddHours(1),
|
Key = objectName,
|
||||||
Verb = httpVerb,
|
#if DEBUG
|
||||||
Key = objectName,
|
Protocol = Protocol.HTTP,
|
||||||
#if (DEBUG || STAGING)
|
|
||||||
Protocol = Protocol.HTTP,
|
|
||||||
#else
|
#else
|
||||||
Protocol = client.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
|
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
return client.GetPreSignedURLAsync(request);
|
return GetS3Client(mode).GetPreSignedURL(request);
|
||||||
}, operationName: "GetPreSignedURL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<string> GetPresignedURLForUploadPartAsync(string objectName, string uploadId, int partNumber, S3ClientMode mode = S3ClientMode.EXTERNAL)
|
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber)
|
||||||
{
|
{
|
||||||
|
|
||||||
var client = mode == S3ClientMode.INTERNAL ? S3InternalClient : S3Client;
|
return GetS3Client().GetPreSignedURL(new GetPreSignedUrlRequest
|
||||||
var bucketName = mode == S3ClientMode.INTERNAL ? INTERNAL_BUCKET_NAME : BUCKET_NAME;
|
|
||||||
|
|
||||||
return client.ExecuteWithFailoverAsync(c => c.GetPreSignedURLAsync(new GetPreSignedUrlRequest
|
|
||||||
{
|
{
|
||||||
BucketName = bucketName,
|
BucketName = BUCKET_NAME,
|
||||||
Expires = System.DateTime.Now.AddHours(1),
|
Expires = System.DateTime.Now.AddHours(1),
|
||||||
Verb = HttpVerb.PUT,
|
Verb = HttpVerb.PUT,
|
||||||
Key = objectName,
|
Key = objectName,
|
||||||
PartNumber = partNumber,
|
PartNumber = partNumber,
|
||||||
UploadId = uploadId,
|
UploadId = uploadId,
|
||||||
#if (DEBUG || STAGING)
|
#if DEBUG
|
||||||
Protocol = Protocol.HTTP,
|
Protocol = Protocol.HTTP,
|
||||||
#else
|
#else
|
||||||
Protocol = c.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
|
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
|
||||||
#endif
|
#endif
|
||||||
}), operationName: "GetPreSignedURL");
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetFullObjectName(string userId, string name)
|
private string GetFullObjectName(string userId, string name)
|
||||||
{
|
{
|
||||||
if (userId == null || !Regex.IsMatch(name, "[0-9a-zA-Z!" + Regex.Escape("-") + "_.*'()]")) return null;
|
if (userId == null || !Regex.IsMatch(name, "[0-9a-zA-Z!" + Regex.Escape("-") + "_.*'()]")) return null;
|
||||||
return $"{userId}/{name}";
|
return $"{userId}/{name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsSuccessStatusCode(int statusCode)
|
bool IsSuccessStatusCode(int statusCode)
|
||||||
{
|
{
|
||||||
return ((int)statusCode >= 200) && ((int)statusCode <= 299);
|
return ((int)statusCode >= 200) && ((int)statusCode <= 299);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AmazonS3Client GetS3Client(S3ClientMode mode = S3ClientMode.EXTERNAL)
|
||||||
|
{
|
||||||
|
if (mode == S3ClientMode.INTERNAL && S3InternalClient != null) return S3InternalClient;
|
||||||
|
return S3Client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,224 +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;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Interfaces;
|
|
||||||
using Notesnook.API.Models;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Services
|
|
||||||
{
|
|
||||||
public readonly record struct ItemKey(string ItemId, string Type)
|
|
||||||
{
|
|
||||||
public override string ToString() => $"{ItemId}:{Type}";
|
|
||||||
}
|
|
||||||
public class SyncDeviceService(ISyncItemsRepositoryAccessor repositories, ILogger<SyncDeviceService> logger)
|
|
||||||
{
|
|
||||||
private static FilterDefinition<SyncDevice> DeviceFilter(string userId, string deviceId) =>
|
|
||||||
Builders<SyncDevice>.Filter.Eq(x => x.UserId, userId) &
|
|
||||||
Builders<SyncDevice>.Filter.Eq(x => x.DeviceId, deviceId);
|
|
||||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId, string deviceId, string key) =>
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId) &
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.DeviceId, deviceId) &
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Key, key);
|
|
||||||
|
|
||||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId, string deviceId) =>
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId) &
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.DeviceId, deviceId);
|
|
||||||
|
|
||||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId) =>
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId);
|
|
||||||
|
|
||||||
private static FilterDefinition<SyncDevice> UserFilter(string userId) => Builders<SyncDevice>.Filter.Eq(x => x.UserId, userId);
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<HashSet<ItemKey>> GetIdsAsync(string userId, string deviceId, string key)
|
|
||||||
{
|
|
||||||
var cursor = await repositories.DeviceIdsChunks.Collection.FindAsync(DeviceIdsChunkFilter(userId, deviceId, key));
|
|
||||||
var result = new HashSet<ItemKey>();
|
|
||||||
while (await cursor.MoveNextAsync())
|
|
||||||
{
|
|
||||||
foreach (var chunk in cursor.Current)
|
|
||||||
{
|
|
||||||
foreach (var id in chunk.Ids)
|
|
||||||
{
|
|
||||||
var parts = id.Split(':', 2);
|
|
||||||
result.Add(new ItemKey(parts[0], parts[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int MaxIdsPerChunk = 25_000;
|
|
||||||
public async Task AppendIdsAsync(string userId, string deviceId, string key, IEnumerable<ItemKey> ids)
|
|
||||||
{
|
|
||||||
var filter = DeviceIdsChunkFilter(userId, deviceId, key) & Builders<DeviceIdsChunk>.Filter.Where(x => x.Ids.Length < MaxIdsPerChunk);
|
|
||||||
var chunk = await repositories.DeviceIdsChunks.Collection.Find(filter).FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (chunk != null)
|
|
||||||
{
|
|
||||||
var update = Builders<DeviceIdsChunk>.Update.AddToSetEach(x => x.Ids, ids.Select(i => i.ToString()));
|
|
||||||
await repositories.DeviceIdsChunks.Collection.UpdateOneAsync(
|
|
||||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Id, chunk.Id),
|
|
||||||
update
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newChunk = new DeviceIdsChunk
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Key = key,
|
|
||||||
Ids = [.. ids.Select(i => i.ToString())]
|
|
||||||
};
|
|
||||||
await repositories.DeviceIdsChunks.Collection.InsertOneAsync(newChunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptyChunksFilter = DeviceIdsChunkFilter(userId, deviceId, key) & Builders<DeviceIdsChunk>.Filter.Size(x => x.Ids, 0);
|
|
||||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(emptyChunksFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteIdsAsync(string userId, string deviceId, string key, IEnumerable<ItemKey> ids)
|
|
||||||
{
|
|
||||||
var writes = new List<WriteModel<DeviceIdsChunk>>
|
|
||||||
{
|
|
||||||
new DeleteManyModel<DeviceIdsChunk>(DeviceIdsChunkFilter(userId, deviceId, key))
|
|
||||||
};
|
|
||||||
var chunks = ids.Chunk(MaxIdsPerChunk);
|
|
||||||
foreach (var chunk in chunks)
|
|
||||||
{
|
|
||||||
var newChunk = new DeviceIdsChunk
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Key = key,
|
|
||||||
Ids = [.. chunk.Select(i => i.ToString())]
|
|
||||||
};
|
|
||||||
writes.Add(new InsertOneModel<DeviceIdsChunk>(newChunk));
|
|
||||||
}
|
|
||||||
await repositories.DeviceIdsChunks.Collection.BulkWriteAsync(writes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HashSet<ItemKey>> FetchUnsyncedIdsAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
var device = await GetDeviceAsync(userId, deviceId);
|
|
||||||
if (device == null || device.IsSyncReset) return [];
|
|
||||||
|
|
||||||
var unsyncedIds = await GetIdsAsync(userId, deviceId, "unsynced");
|
|
||||||
var pendingIds = await GetIdsAsync(userId, deviceId, "pending");
|
|
||||||
|
|
||||||
unsyncedIds = [.. unsyncedIds, .. pendingIds];
|
|
||||||
|
|
||||||
if (unsyncedIds.Count == 0) return [];
|
|
||||||
|
|
||||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId, "unsynced"));
|
|
||||||
await WriteIdsAsync(userId, deviceId, "pending", unsyncedIds);
|
|
||||||
|
|
||||||
return unsyncedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WritePendingIdsAsync(string userId, string deviceId, HashSet<ItemKey> ids)
|
|
||||||
{
|
|
||||||
await WriteIdsAsync(userId, deviceId, "pending", ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ResetAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
await repositories.SyncDevices.Collection.UpdateOneAsync(DeviceFilter(userId, deviceId), Builders<SyncDevice>.Update
|
|
||||||
.Set(x => x.IsSyncReset, false));
|
|
||||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId, "pending"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SyncDevice?> GetDeviceAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
return await repositories.SyncDevices.Collection.Find(DeviceFilter(userId, deviceId)).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<SyncDevice> ListDevicesAsync(string userId)
|
|
||||||
{
|
|
||||||
using var cursor = await repositories.SyncDevices.Collection.FindAsync(UserFilter(userId));
|
|
||||||
while (await cursor.MoveNextAsync())
|
|
||||||
{
|
|
||||||
foreach (var device in cursor.Current)
|
|
||||||
{
|
|
||||||
yield return device;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ResetDevicesAsync(string userId)
|
|
||||||
{
|
|
||||||
await repositories.SyncDevices.Collection.DeleteManyAsync(UserFilter(userId));
|
|
||||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateLastAccessTimeAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
await repositories.SyncDevices.Collection.UpdateOneAsync(DeviceFilter(userId, deviceId), Builders<SyncDevice>.Update
|
|
||||||
.Set(x => x.LastAccessTime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddIdsToOtherDevicesAsync(string userId, string deviceId, IEnumerable<ItemKey> ids)
|
|
||||||
{
|
|
||||||
await UpdateLastAccessTimeAsync(userId, deviceId);
|
|
||||||
await foreach (var device in ListDevicesAsync(userId))
|
|
||||||
{
|
|
||||||
if (device.DeviceId == deviceId || device.IsSyncReset) continue;
|
|
||||||
await AppendIdsAsync(userId, device.DeviceId, "unsynced", ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddIdsToAllDevicesAsync(string userId, IEnumerable<ItemKey> ids)
|
|
||||||
{
|
|
||||||
await foreach (var device in ListDevicesAsync(userId))
|
|
||||||
{
|
|
||||||
if (device.IsSyncReset) continue;
|
|
||||||
await AppendIdsAsync(userId, device.DeviceId, "unsynced", ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SyncDevice> RegisterDeviceAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
var newDevice = new SyncDevice
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
IsSyncReset = true
|
|
||||||
};
|
|
||||||
await repositories.SyncDevices.Collection.InsertOneAsync(newDevice);
|
|
||||||
return newDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnregisterDeviceAsync(string userId, string deviceId)
|
|
||||||
{
|
|
||||||
await repositories.SyncDevices.Collection.DeleteOneAsync(DeviceFilter(userId, deviceId));
|
|
||||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,83 +23,86 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Notesnook.API.Helpers;
|
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Models.Responses;
|
using Notesnook.API.Models.Responses;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common.Accessors;
|
|
||||||
using Streetwriters.Common.Enums;
|
using Streetwriters.Common.Enums;
|
||||||
using Streetwriters.Common.Extensions;
|
using Streetwriters.Common.Extensions;
|
||||||
|
using Streetwriters.Common.Interfaces;
|
||||||
using Streetwriters.Common.Messages;
|
using Streetwriters.Common.Messages;
|
||||||
using Streetwriters.Common.Models;
|
using Streetwriters.Common.Models;
|
||||||
using Streetwriters.Data.Interfaces;
|
using Streetwriters.Data.Interfaces;
|
||||||
|
|
||||||
namespace Notesnook.API.Services
|
namespace Notesnook.API.Services
|
||||||
{
|
{
|
||||||
public class UserService(IHttpContextAccessor accessor,
|
public class UserService : IUserService
|
||||||
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor,
|
|
||||||
IUnitOfWork unitOfWork, IS3Service s3Service, SyncDeviceService syncDeviceService, WampServiceAccessor serviceAccessor, ILogger<UserService> logger) : IUserService
|
|
||||||
{
|
{
|
||||||
private static readonly System.Security.Cryptography.RandomNumberGenerator Rng = System.Security.Cryptography.RandomNumberGenerator.Create();
|
private static readonly System.Security.Cryptography.RandomNumberGenerator Rng = System.Security.Cryptography.RandomNumberGenerator.Create();
|
||||||
private readonly HttpClient httpClient = new();
|
private readonly HttpClient httpClient;
|
||||||
private IHttpContextAccessor HttpContextAccessor { get; } = accessor;
|
private IHttpContextAccessor HttpContextAccessor { get; }
|
||||||
private ISyncItemsRepositoryAccessor Repositories { get; } = syncItemsRepositoryAccessor;
|
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||||
private IS3Service S3Service { get; set; } = s3Service;
|
private IS3Service S3Service { get; set; }
|
||||||
private readonly IUnitOfWork unit = unitOfWork;
|
private readonly IUnitOfWork unit;
|
||||||
|
|
||||||
|
public UserService(IHttpContextAccessor accessor,
|
||||||
|
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor,
|
||||||
|
IUnitOfWork unitOfWork, IS3Service s3Service)
|
||||||
|
{
|
||||||
|
httpClient = new HttpClient();
|
||||||
|
|
||||||
|
Repositories = syncItemsRepositoryAccessor;
|
||||||
|
HttpContextAccessor = accessor;
|
||||||
|
unit = unitOfWork;
|
||||||
|
S3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CreateUserAsync()
|
public async Task CreateUserAsync()
|
||||||
{
|
{
|
||||||
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
|
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
|
||||||
if (!response.Success || (response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
|
if (!response.Success || (response.Errors != null && response.Errors.Length > 0))
|
||||||
{
|
{
|
||||||
logger.LogError("Failed to sign up user: {Response}", JsonSerializer.Serialize(response));
|
await Slogger<UserService>.Error(nameof(CreateUserAsync), "Couldn't sign up.", JsonSerializer.Serialize(response));
|
||||||
if (response.Errors != null && response.Errors.Length > 0)
|
if (response.Errors != null && response.Errors.Length > 0) throw new Exception(string.Join(" ", response.Errors));
|
||||||
throw new Exception(string.Join(" ", response.Errors));
|
|
||||||
else throw new Exception("Could not create a new account. Error code: " + response.StatusCode);
|
else throw new Exception("Could not create a new account. Error code: " + response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Repositories.UsersSettings.InsertAsync(new UserSettings
|
await Repositories.UsersSettings.InsertAsync(new UserSettings
|
||||||
{
|
{
|
||||||
UserId = response.UserId,
|
UserId = response.UserId,
|
||||||
StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 },
|
|
||||||
LastSynced = 0,
|
LastSynced = 0,
|
||||||
Salt = GetSalt()
|
Salt = GetSalt()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Constants.IS_SELF_HOSTED)
|
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,
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
Provider = SubscriptionProvider.STREETWRITERS,
|
Provider = SubscriptionProvider.STREETWRITERS,
|
||||||
Status = SubscriptionStatus.ACTIVE,
|
Type = SubscriptionType.BASIC,
|
||||||
Plan = SubscriptionPlan.FREE,
|
|
||||||
UserId = response.UserId,
|
UserId = response.UserId,
|
||||||
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("New user created: {Response}", JsonSerializer.Serialize(response));
|
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 user = await serviceAccessor.UserAccountService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
|
UserResponse response = await httpClient.ForwardAsync<UserResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer.ToString()}/account", HttpMethod.Get);
|
||||||
|
if (!response.Success) return response;
|
||||||
|
|
||||||
Subscription? subscription = null;
|
ISubscription subscription = null;
|
||||||
if (Constants.IS_SELF_HOSTED)
|
if (Constants.IS_SELF_HOSTED)
|
||||||
{
|
{
|
||||||
subscription = new Subscription
|
subscription = new Subscription
|
||||||
{
|
{
|
||||||
AppId = ApplicationType.NOTESNOOK,
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
Provider = SubscriptionProvider.STREETWRITERS,
|
Provider = SubscriptionProvider.STREETWRITERS,
|
||||||
Plan = SubscriptionPlan.BELIEVER,
|
Type = SubscriptionType.PREMIUM,
|
||||||
Status = SubscriptionStatus.ACTIVE,
|
UserId = response.UserId,
|
||||||
UserId = user.UserId,
|
|
||||||
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
// this date doesn't matter as the subscription is static.
|
// this date doesn't matter as the subscription is static.
|
||||||
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds()
|
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds()
|
||||||
@@ -107,82 +110,61 @@ namespace Notesnook.API.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
subscription = await serviceAccessor.UserSubscriptionService.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.");
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == response.UserId);
|
||||||
|
if (repair && userSettings == null)
|
||||||
// reset user's attachment limit every month
|
|
||||||
var limit = StorageHelper.RolloverStorageLimit(userSettings.StorageLimit);
|
|
||||||
if (userSettings.StorageLimit == null || limit.UpdatedAt != userSettings.StorageLimit?.UpdatedAt)
|
|
||||||
{
|
{
|
||||||
userSettings.StorageLimit = limit;
|
await Slogger<UserService>.Error(nameof(GetUserAsync), "Repairing user settings.", JsonSerializer.Serialize(response));
|
||||||
await Repositories.UsersSettings.Collection.UpdateOneAsync(
|
userSettings = new UserSettings
|
||||||
Builders<UserSettings>.Filter.Eq(u => u.UserId, user.UserId),
|
{
|
||||||
Builders<UserSettings>.Update.Set(u => u.StorageLimit, userSettings.StorageLimit)
|
UserId = response.UserId,
|
||||||
);
|
LastSynced = 0,
|
||||||
|
Salt = GetSalt()
|
||||||
|
};
|
||||||
|
await Repositories.UsersSettings.InsertAsync(userSettings);
|
||||||
}
|
}
|
||||||
|
response.AttachmentsKey = userSettings.AttachmentsKey;
|
||||||
return new UserResponse
|
response.Salt = userSettings.Salt;
|
||||||
{
|
response.Subscription = subscription;
|
||||||
UserId = user.UserId,
|
return response;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.");
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||||
|
userSettings.AttachmentsKey = (EncryptedData)key;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserAsync(string userId)
|
public async Task<bool> DeleteUserAsync(string userId, string jti)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Deleting user {UserId}", userId);
|
|
||||||
var cc = new CancellationTokenSource();
|
var cc = new CancellationTokenSource();
|
||||||
|
|
||||||
Repositories.Notes.DeleteByUserId(userId);
|
Repositories.Notes.DeleteByUserId(userId);
|
||||||
@@ -190,60 +172,40 @@ namespace Notesnook.API.Services
|
|||||||
Repositories.Shortcuts.DeleteByUserId(userId);
|
Repositories.Shortcuts.DeleteByUserId(userId);
|
||||||
Repositories.Contents.DeleteByUserId(userId);
|
Repositories.Contents.DeleteByUserId(userId);
|
||||||
Repositories.Settings.DeleteByUserId(userId);
|
Repositories.Settings.DeleteByUserId(userId);
|
||||||
Repositories.LegacySettings.DeleteByUserId(userId);
|
|
||||||
Repositories.Attachments.DeleteByUserId(userId);
|
Repositories.Attachments.DeleteByUserId(userId);
|
||||||
Repositories.Reminders.DeleteByUserId(userId);
|
Repositories.Reminders.DeleteByUserId(userId);
|
||||||
Repositories.Relations.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.UsersSettings.Delete((u) => u.UserId == userId);
|
||||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
|
||||||
|
|
||||||
var result = await unit.Commit();
|
|
||||||
logger.LogInformation("User data deleted for user {UserId}: {Result}", userId, result);
|
|
||||||
if (!result) throw new Exception("Could not delete user data.");
|
|
||||||
|
|
||||||
await syncDeviceService.ResetDevicesAsync(userId);
|
|
||||||
|
|
||||||
if (!Constants.IS_SELF_HOSTED)
|
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,
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
UserId = userId
|
UserId = userId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await S3Service.DeleteDirectoryAsync(userId);
|
await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteUserAsync(string userId, string? jti, string password)
|
|
||||||
{
|
|
||||||
logger.LogInformation("Deleting user account: {UserId}", userId);
|
|
||||||
|
|
||||||
await serviceAccessor.UserAccountService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
|
|
||||||
|
|
||||||
await DeleteUserAsync(userId);
|
|
||||||
|
|
||||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
|
||||||
{
|
{
|
||||||
SendToAll = jti == null,
|
SendToAll = false,
|
||||||
OriginTokenId = jti,
|
OriginTokenId = jti,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Message = new Message
|
Message = new Message
|
||||||
{
|
{
|
||||||
Type = "logout",
|
Type = "userDeleted",
|
||||||
Data = JsonSerializer.Serialize(new { reason = "Account deleted." })
|
Data = JsonSerializer.Serialize(new { reason = "accountDeleted" })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await S3Service.DeleteDirectoryAsync(userId);
|
||||||
|
|
||||||
|
return await unit.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
|
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
|
||||||
{
|
{
|
||||||
|
|
||||||
var cc = new CancellationTokenSource();
|
var cc = new CancellationTokenSource();
|
||||||
|
|
||||||
Repositories.Notes.DeleteByUserId(userId);
|
Repositories.Notes.DeleteByUserId(userId);
|
||||||
@@ -251,25 +213,16 @@ namespace Notesnook.API.Services
|
|||||||
Repositories.Shortcuts.DeleteByUserId(userId);
|
Repositories.Shortcuts.DeleteByUserId(userId);
|
||||||
Repositories.Contents.DeleteByUserId(userId);
|
Repositories.Contents.DeleteByUserId(userId);
|
||||||
Repositories.Settings.DeleteByUserId(userId);
|
Repositories.Settings.DeleteByUserId(userId);
|
||||||
Repositories.LegacySettings.DeleteByUserId(userId);
|
|
||||||
Repositories.Attachments.DeleteByUserId(userId);
|
Repositories.Attachments.DeleteByUserId(userId);
|
||||||
Repositories.Reminders.DeleteByUserId(userId);
|
Repositories.Reminders.DeleteByUserId(userId);
|
||||||
Repositories.Relations.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.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
|
||||||
if (!await unit.Commit()) return false;
|
if (!await unit.Commit()) return false;
|
||||||
|
|
||||||
await syncDeviceService.ResetDevicesAsync(userId);
|
|
||||||
|
|
||||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
|
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
|
||||||
|
|
||||||
userSettings.AttachmentsKey = null;
|
userSettings.AttachmentsKey = null;
|
||||||
userSettings.MonographPasswordsKey = null;
|
|
||||||
userSettings.VaultKey = null;
|
userSettings.VaultKey = null;
|
||||||
userSettings.InboxKeys = null;
|
|
||||||
userSettings.LastSynced = 0;
|
userSettings.LastSynced = 0;
|
||||||
|
|
||||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
|
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
|
||||||
@@ -280,7 +233,7 @@ namespace Notesnook.API.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSalt()
|
private string GetSalt()
|
||||||
{
|
{
|
||||||
byte[] salt = new byte[16];
|
byte[] salt = new byte[16];
|
||||||
Rng.GetNonZeroBytes(salt);
|
Rng.GetNonZeroBytes(salt);
|
||||||
|
|||||||
+51
-108
@@ -24,7 +24,6 @@ using System.Security.Claims;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Amazon.Runtime;
|
|
||||||
using IdentityModel.AspNetCore.OAuth2Introspection;
|
using IdentityModel.AspNetCore.OAuth2Introspection;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
@@ -35,12 +34,10 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -50,19 +47,13 @@ using Notesnook.API.Authorization;
|
|||||||
using Notesnook.API.Extensions;
|
using Notesnook.API.Extensions;
|
||||||
using Notesnook.API.Hubs;
|
using Notesnook.API.Hubs;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Jobs;
|
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Repositories;
|
using Notesnook.API.Repositories;
|
||||||
using Notesnook.API.Services;
|
using Notesnook.API.Services;
|
||||||
using OpenTelemetry.Metrics;
|
|
||||||
using OpenTelemetry.Resources;
|
|
||||||
using Quartz;
|
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common.Extensions;
|
using Streetwriters.Common.Extensions;
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Messages;
|
using Streetwriters.Common.Messages;
|
||||||
using Streetwriters.Common.Models;
|
using Streetwriters.Common.Models;
|
||||||
using Streetwriters.Common.Services;
|
|
||||||
using Streetwriters.Data;
|
using Streetwriters.Data;
|
||||||
using Streetwriters.Data.DbContexts;
|
using Streetwriters.Data.DbContexts;
|
||||||
using Streetwriters.Data.Interfaces;
|
using Streetwriters.Data.Interfaces;
|
||||||
@@ -82,11 +73,12 @@ namespace Notesnook.API
|
|||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton(MongoDbContext.CreateMongoDbClient(new DbSettings
|
var dbSettings = new DbSettings
|
||||||
{
|
{
|
||||||
ConnectionString = Constants.MONGODB_CONNECTION_STRING,
|
ConnectionString = Constants.MONGODB_CONNECTION_STRING,
|
||||||
DatabaseName = Constants.MONGODB_DATABASE_NAME
|
DatabaseName = Constants.MONGODB_DATABASE_NAME
|
||||||
}));
|
};
|
||||||
|
services.AddSingleton<IDbSettings>(dbSettings);
|
||||||
|
|
||||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
@@ -114,15 +106,26 @@ namespace Notesnook.API
|
|||||||
policy.RequireAuthenticatedUser();
|
policy.RequireAuthenticatedUser();
|
||||||
policy.Requirements.Add(new SyncRequirement());
|
policy.Requirements.Add(new SyncRequirement());
|
||||||
});
|
});
|
||||||
|
options.AddPolicy("Verified", policy =>
|
||||||
options.AddPolicy(InboxApiKeyAuthenticationDefaults.AuthenticationScheme, policy =>
|
|
||||||
{
|
{
|
||||||
policy.AuthenticationSchemes.Add(InboxApiKeyAuthenticationDefaults.AuthenticationScheme);
|
policy.AuthenticationSchemes.Add("introspection");
|
||||||
policy.RequireAuthenticatedUser();
|
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") ?? throw new Exception("Notesnook policy not found");
|
options.DefaultPolicy = options.GetPolicy("Notesnook");
|
||||||
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
|
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddOAuth2Introspection("introspection", options =>
|
.AddOAuth2Introspection("introspection", options =>
|
||||||
@@ -140,82 +143,57 @@ namespace Notesnook.API
|
|||||||
|
|
||||||
options.Events.OnTokenValidated = (context) =>
|
options.Events.OnTokenValidated = (context) =>
|
||||||
{
|
{
|
||||||
if (long.TryParse(context.Principal?.FindFirst("exp")?.Value, out long expiryTime))
|
if (long.TryParse(context.Principal.FindFirst("exp")?.Value, out long expiryTime))
|
||||||
{
|
{
|
||||||
context.Properties.ExpiresUtc = DateTimeOffset.FromUnixTimeSeconds(expiryTime);
|
context.Properties.ExpiresUtc = DateTimeOffset.FromUnixTimeSeconds(expiryTime);
|
||||||
}
|
}
|
||||||
context.Properties.AllowRefresh = true;
|
context.Properties.AllowRefresh = true;
|
||||||
context.Properties.IsPersistent = true;
|
context.Properties.IsPersistent = true;
|
||||||
context.HttpContext.User = context.Principal ?? throw new Exception("No principal found in token.");
|
context.HttpContext.User = context.Principal;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256();
|
|
||||||
options.SaveToken = true;
|
options.SaveToken = true;
|
||||||
options.EnableCaching = true;
|
options.EnableCaching = true;
|
||||||
options.CacheDuration = TimeSpan.FromMinutes(30);
|
options.CacheDuration = TimeSpan.FromMinutes(30);
|
||||||
})
|
});
|
||||||
.AddScheme<InboxApiKeyAuthenticationSchemeOptions, InboxApiKeyAuthenticationHandler>(
|
|
||||||
InboxApiKeyAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
options => { }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Serializer.RegisterSerializer(new SyncItemBsonSerializer());
|
|
||||||
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
|
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
|
||||||
|
{
|
||||||
BsonClassMap.RegisterClassMap<UserSettings>();
|
BsonClassMap.RegisterClassMap<UserSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!BsonClassMap.IsClassMapRegistered(typeof(EncryptedData)))
|
if (!BsonClassMap.IsClassMapRegistered(typeof(EncryptedData)))
|
||||||
|
{
|
||||||
BsonClassMap.RegisterClassMap<EncryptedData>();
|
BsonClassMap.RegisterClassMap<EncryptedData>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
|
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
|
||||||
|
{
|
||||||
BsonClassMap.RegisterClassMap<CallToAction>();
|
BsonClassMap.RegisterClassMap<CallToAction>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!BsonClassMap.IsClassMapRegistered(typeof(SyncDevice)))
|
if (!BsonClassMap.IsClassMapRegistered(typeof(Announcement)))
|
||||||
BsonClassMap.RegisterClassMap<SyncDevice>();
|
{
|
||||||
|
BsonClassMap.RegisterClassMap<Announcement>();
|
||||||
|
}
|
||||||
|
|
||||||
services.AddScoped<IDbContext, MongoDbContext>();
|
services.AddScoped<IDbContext, MongoDbContext>();
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
services.AddScoped(typeof(Repository<>));
|
||||||
|
services.AddScoped(typeof(SyncItemsRepository<>));
|
||||||
|
|
||||||
services.AddRepository<UserSettings>("user_settings", "notesnook")
|
services.TryAddTransient<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||||
.AddRepository<Monograph>("monographs", "notesnook")
|
services.TryAddTransient<IUserService, UserService>();
|
||||||
.AddRepository<Announcement>("announcements", "notesnook")
|
services.TryAddTransient<IS3Service, S3Service>();
|
||||||
.AddRepository<DeviceIdsChunk>(Collections.DeviceIdsChunksKey, "notesnook")
|
|
||||||
.AddRepository<SyncDevice>(Collections.SyncDevicesKey, "notesnook")
|
|
||||||
.AddRepository<InboxApiKey>(Collections.InboxApiKeysKey, "notesnook")
|
|
||||||
.AddRepository<InboxSyncItem>(Collections.InboxItemsKey, "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.InboxItemsKey)
|
|
||||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
|
||||||
|
|
||||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
|
||||||
services.AddScoped<SyncDeviceService>();
|
|
||||||
services.AddScoped<IUserService, UserService>();
|
|
||||||
services.AddScoped<IS3Service, S3Service>();
|
|
||||||
services.AddScoped<IURLAnalyzer, URLAnalyzer>();
|
|
||||||
|
|
||||||
services.AddWampServiceAccessor(Servers.NotesnookAPI);
|
|
||||||
|
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
|
|
||||||
services.AddHealthChecks();
|
services.AddHealthChecks().AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
|
||||||
|
|
||||||
services.AddSignalR((hub) =>
|
services.AddSignalR((hub) =>
|
||||||
{
|
{
|
||||||
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
|
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
|
||||||
hub.ClientTimeoutInterval = TimeSpan.FromMinutes(10);
|
|
||||||
hub.EnableDetailedErrors = true;
|
hub.EnableDetailedErrors = true;
|
||||||
}).AddMessagePackProtocol().AddJsonProtocol();
|
}).AddMessagePackProtocol();
|
||||||
|
|
||||||
services.AddResponseCompression(options =>
|
services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
@@ -232,56 +210,30 @@ namespace Notesnook.API
|
|||||||
{
|
{
|
||||||
options.Level = CompressionLevel.Fastest;
|
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.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
app.UseForwardedHeadersWithKnownProxies(env);
|
if (!env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.UseOpenTelemetryPrometheusScrapingEndpoint((context) => context.Request.Path == "/metrics" && context.Connection.LocalPort == 5067);
|
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
app.UseCors("notesnook");
|
app.UseCors("notesnook");
|
||||||
app.UseVersion(Servers.NotesnookAPI);
|
app.UseVersion();
|
||||||
|
|
||||||
app.UseWamp(WampServers.NotesnookServer, (realm, server) =>
|
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, null);
|
||||||
await service.DeleteUserAsync(ev.UserId);
|
|
||||||
});
|
|
||||||
|
|
||||||
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) =>
|
|
||||||
{
|
|
||||||
IDistributedCache cache = app.GetScopedService<IDistributedCache>();
|
|
||||||
ev.Keys.ForEach((key) => cache.Remove(key));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,7 +246,7 @@ namespace Notesnook.API
|
|||||||
{
|
{
|
||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
endpoints.MapHealthChecks("/health");
|
endpoints.MapHealthChecks("/health");
|
||||||
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
|
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
|
||||||
{
|
{
|
||||||
options.CloseOnAuthenticationExpiration = false;
|
options.CloseOnAuthenticationExpiration = false;
|
||||||
options.Transports = HttpTransportType.WebSockets;
|
options.Transports = HttpTransportType.WebSockets;
|
||||||
@@ -302,13 +254,4 @@ namespace Notesnook.API
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Information",
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"Microsoft.Hosting.Lifetime": "Information",
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
"Microsoft.AspNetCore.SignalR": "Trace",
|
}
|
||||||
"Microsoft.AspNetCore.Http.Connections": "Trace"
|
},
|
||||||
}
|
"MongoDbSettings": {
|
||||||
},
|
"ConnectionString": "mongodb://localhost:27017/notesnook",
|
||||||
"MongoDbSettings": {
|
"DatabaseName": "notesnook"
|
||||||
"ConnectionString": "mongodb://localhost:27017/notesnook",
|
}
|
||||||
"DatabaseName": "notesnook"
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
PORT=5181
|
|
||||||
NOTESNOOK_API_SERVER_URL=http://localhost:5264/
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.env
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "notesnook-inbox-api",
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^5.1.0",
|
|
||||||
"express-rate-limit": "^8.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=="],
|
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@8.1.0", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA=="],
|
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
|
||||||
|
|
||||||
"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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +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",
|
|
||||||
"express-rate-limit": "^8.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import _sodium, { base64_variants } from "libsodium-wrappers-sumo";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { rateLimit } from "express-rate-limit";
|
|
||||||
|
|
||||||
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" | "salt">;
|
|
||||||
iv: string;
|
|
||||||
alg: string;
|
|
||||||
cipher: string;
|
|
||||||
length: number;
|
|
||||||
salt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encrypt(rawData: string, publicKey: string): EncryptedInboxItem {
|
|
||||||
try {
|
|
||||||
const password = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
|
|
||||||
const saltBytes = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
|
||||||
const key = sodium.crypto_pwhash(
|
|
||||||
sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
|
||||||
password,
|
|
||||||
saltBytes,
|
|
||||||
3, // operations limit
|
|
||||||
1024 * 1024 * 8, // memory limit (8MB)
|
|
||||||
sodium.crypto_pwhash_ALG_ARGON2I13
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
key
|
|
||||||
);
|
|
||||||
const inboxPublicKey = sodium.from_base64(
|
|
||||||
publicKey,
|
|
||||||
base64_variants.URLSAFE_NO_PADDING
|
|
||||||
);
|
|
||||||
const encryptedKey = sodium.crypto_box_seal(key, inboxPublicKey);
|
|
||||||
|
|
||||||
return {
|
|
||||||
v: 1,
|
|
||||||
key: {
|
|
||||||
cipher: sodium.to_base64(
|
|
||||||
encryptedKey,
|
|
||||||
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,
|
|
||||||
salt: sodium.to_base64(saltBytes, base64_variants.URLSAFE_NO_PADDING),
|
|
||||||
};
|
|
||||||
} 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.use(
|
|
||||||
rateLimit({
|
|
||||||
windowMs: 1 * 60 * 1000, // 1 minute
|
|
||||||
limit: 60,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.post("/inbox", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const apiKey = req.headers["authorization"];
|
|
||||||
if (!apiKey) {
|
|
||||||
return res.status(401).json({ error: "unauthorized" });
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
const validationResult = RawInboxItemSchema.safeParse(req.body);
|
|
||||||
if (!validationResult.success) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "invalid item",
|
|
||||||
details: validationResult.error.issues,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedItem = encrypt(
|
|
||||||
JSON.stringify(validationResult.data),
|
|
||||||
inboxPublicKey
|
|
||||||
);
|
|
||||||
console.log("[info] encrypted item");
|
|
||||||
|
|
||||||
await postEncryptedInboxItem(apiKey, encryptedItem);
|
|
||||||
console.log("[info] posted encrypted inbox item successfully");
|
|
||||||
|
|
||||||
return res.status(200).json({ success: true });
|
|
||||||
} 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;
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ This repo contains the full source code of the Notesnook Sync Server licensed un
|
|||||||
|
|
||||||
Requirements:
|
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)
|
2. [git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
The first step is to `clone` the repository:
|
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 sync server can easily be started using Docker.
|
||||||
|
|
||||||
|
The first step is to `clone` the repository:
|
||||||
|
|
||||||
```bash
|
```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:
|
And then use Docker Compose to start the servers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
This takes care of setting up everything including MongoDB, Minio etc.
|
This takes care of setting up everything including MongoDB, Minio etc.
|
||||||
|
|
||||||
## TODO Self-hosting
|
## 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 Sync server
|
||||||
- [x] Open source the Identity server
|
- [x] Open source the Identity server
|
||||||
- [x] Open source the SSE Messaging infrastructure
|
- [x] Open source the SSE Messaging infrastructure
|
||||||
- [x] Fully Dockerize all services
|
- [x] Fully Dockerize all services
|
||||||
- [x] Use self-hosted Minio for S3 storage
|
- [x] Use self-hosted Minio for S3 storage
|
||||||
- [x] Publish on DockerHub
|
- [ ] Publish on DockerHub
|
||||||
- [x] Add settings to change server URLs in Notesnook client apps (starting from v3.0.18)
|
|
||||||
- [ ] Write self hosting docs
|
- [ ] Write self hosting docs
|
||||||
|
- [ ] Add settings to change server URLs in Notesnook client apps
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +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.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
|
|
||||||
namespace Streetwriters.Common.Accessors
|
|
||||||
{
|
|
||||||
public class WampServiceAccessor(Server server) : IHostedService
|
|
||||||
{
|
|
||||||
public IUserAccountService UserAccountService { get; set; }
|
|
||||||
public IUserSubscriptionService? UserSubscriptionService { get; set; }
|
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await InitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitAsync()
|
|
||||||
{
|
|
||||||
this.UserAccountService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(InitAsync);
|
|
||||||
if (!Constants.IS_SELF_HOSTED && server != Servers.SubscriptionServer)
|
|
||||||
{
|
|
||||||
this.UserSubscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(InitAsync);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+32
-34
@@ -1,35 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System;
|
||||||
using MongoDB.Bson;
|
using System.Text.Json.Serialization;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
using System.Runtime.Serialization;
|
namespace Streetwriters.Common.Attributes
|
||||||
|
{
|
||||||
namespace Notesnook.API.Models
|
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
|
||||||
{
|
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
|
||||||
|
{
|
||||||
public class MonographContent
|
public JsonInterfaceConverterAttribute(Type converterType)
|
||||||
{
|
: base(converterType)
|
||||||
[JsonPropertyName("data")]
|
{
|
||||||
public required string Data { get; set; }
|
}
|
||||||
[JsonPropertyName("type")]
|
}
|
||||||
public required string Type { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,83 +1,82 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Streetwriters.Common.Enums;
|
using Streetwriters.Common.Enums;
|
||||||
using Streetwriters.Common.Interfaces;
|
using Streetwriters.Common.Interfaces;
|
||||||
using Streetwriters.Common.Messages;
|
using Streetwriters.Common.Messages;
|
||||||
using Streetwriters.Common.Models;
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
namespace Streetwriters.Common
|
namespace Streetwriters.Common
|
||||||
{
|
{
|
||||||
public class Clients
|
public class Clients
|
||||||
{
|
{
|
||||||
public static readonly Client Notesnook = new()
|
private static Client Notesnook = new Client
|
||||||
{
|
{
|
||||||
Id = "notesnook",
|
Id = "notesnook",
|
||||||
Name = "Notesnook",
|
Name = "Notesnook",
|
||||||
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL ?? "noreply@notesnook.com",
|
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL,
|
||||||
SenderName = "Notesnook",
|
SenderName = Constants.NOTESNOOK_SENDER_NAME,
|
||||||
Type = ApplicationType.NOTESNOOK,
|
Type = ApplicationType.NOTESNOOK,
|
||||||
AppId = ApplicationType.NOTESNOOK,
|
AppId = ApplicationType.NOTESNOOK,
|
||||||
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
|
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
|
||||||
EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified",
|
EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified",
|
||||||
OnEmailConfirmed = async (userId) =>
|
OnEmailConfirmed = async (userId) =>
|
||||||
{
|
{
|
||||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Message = new Message
|
Message = new Message
|
||||||
{
|
{
|
||||||
Type = "emailConfirmed",
|
Type = "emailConfirmed",
|
||||||
Data = null
|
Data = null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Dictionary<string, Client> ClientsMap = new()
|
public static Dictionary<string, Client> ClientsMap = new Dictionary<string, Client>
|
||||||
{
|
{
|
||||||
{ "notesnook", Notesnook }
|
{ "notesnook", Notesnook }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Client? FindClientById(string? id)
|
public static Client FindClientById(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id) || !IsValidClient(id)) return null;
|
if (!IsValidClient(id)) return null;
|
||||||
return ClientsMap[id];
|
return ClientsMap[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Client? FindClientByAppId(ApplicationType? appId)
|
public static Client FindClientByAppId(ApplicationType appId)
|
||||||
{
|
{
|
||||||
if (appId is null) return null;
|
switch (appId)
|
||||||
switch (appId)
|
{
|
||||||
{
|
case ApplicationType.NOTESNOOK:
|
||||||
case ApplicationType.NOTESNOOK:
|
return ClientsMap["notesnook"];
|
||||||
return ClientsMap["notesnook"];
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
public static bool IsValidClient(string id)
|
||||||
public static bool IsValidClient(string id)
|
{
|
||||||
{
|
return ClientsMap.ContainsKey(id);
|
||||||
return ClientsMap.ContainsKey(id);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,96 +1,80 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Streetwriters.Common
|
namespace Streetwriters.Common
|
||||||
{
|
{
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public static int COMPATIBILITY_VERSION = 1;
|
public static bool IS_SELF_HOSTED => Environment.GetEnvironmentVariable("SELF_HOSTED") == "1";
|
||||||
public static bool IS_SELF_HOSTED => ReadSecret("SELF_HOSTED") == "1";
|
|
||||||
public static bool DISABLE_SIGNUPS => ReadSecret("DISABLE_SIGNUPS") == "true";
|
// S3 related
|
||||||
public static string INSTANCE_NAME => ReadSecret("INSTANCE_NAME") ?? "default";
|
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
|
||||||
|
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
|
||||||
// S3 related
|
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
|
||||||
public static string S3_ACCESS_KEY => ReadSecret("S3_ACCESS_KEY") ?? throw new InvalidOperationException("S3_ACCESS_KEY is required");
|
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
|
||||||
public static string S3_ACCESS_KEY_ID => ReadSecret("S3_ACCESS_KEY_ID") ?? throw new InvalidOperationException("S3_ACCESS_KEY_ID is required");
|
|
||||||
public static string S3_SERVICE_URL => ReadSecret("S3_SERVICE_URL") ?? throw new InvalidOperationException("S3_SERVICE_URL is required");
|
// SMTP settings
|
||||||
public static string S3_REGION => ReadSecret("S3_REGION") ?? throw new InvalidOperationException("S3_REGION is required");
|
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
|
||||||
public static string S3_BUCKET_NAME => ReadSecret("S3_BUCKET_NAME") ?? throw new InvalidOperationException("S3_BUCKET_NAME is required");
|
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
|
||||||
public static string? S3_INTERNAL_BUCKET_NAME => ReadSecret("S3_INTERNAL_BUCKET_NAME");
|
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
|
||||||
public static string? S3_INTERNAL_SERVICE_URL => ReadSecret("S3_INTERNAL_SERVICE_URL");
|
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
|
||||||
|
public static string SMTP_REPLYTO_NAME => Environment.GetEnvironmentVariable("SMTP_REPLYTO_NAME");
|
||||||
// SMTP settings
|
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
|
||||||
public static string? SMTP_USERNAME => ReadSecret("SMTP_USERNAME");
|
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL");
|
||||||
public static string? SMTP_PASSWORD => ReadSecret("SMTP_PASSWORD");
|
public static string NOTESNOOK_SENDER_NAME => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_NAME");
|
||||||
public static string? SMTP_HOST => ReadSecret("SMTP_HOST");
|
|
||||||
public static string? SMTP_PORT => ReadSecret("SMTP_PORT");
|
public static string NOTESNOOK_APP_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_APP_HOST");
|
||||||
public static string? SMTP_REPLYTO_EMAIL => ReadSecret("SMTP_REPLYTO_EMAIL");
|
public static string NOTESNOOK_API_SECRET => Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET");
|
||||||
public static string? NOTESNOOK_SENDER_EMAIL => ReadSecret("NOTESNOOK_SENDER_EMAIL") ?? ReadSecret("SMTP_USERNAME");
|
|
||||||
|
// MessageBird is used for SMS sending
|
||||||
public static string? NOTESNOOK_APP_HOST => ReadSecret("NOTESNOOK_APP_HOST");
|
public static string MESSAGEBIRD_ACCESS_KEY => Environment.GetEnvironmentVariable("MESSAGEBIRD_ACCESS_KEY");
|
||||||
public static string NOTESNOOK_API_SECRET => ReadSecret("NOTESNOOK_API_SECRET") ?? throw new InvalidOperationException("NOTESNOOK_API_SECRET is required");
|
|
||||||
|
// Server discovery
|
||||||
// MessageBird is used for SMS sending
|
public static int NOTESNOOK_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_PORT"));
|
||||||
public static string? TWILIO_ACCOUNT_SID => ReadSecret("TWILIO_ACCOUNT_SID");
|
public static string NOTESNOOK_SERVER_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_HOST");
|
||||||
public static string? TWILIO_AUTH_TOKEN => ReadSecret("TWILIO_AUTH_TOKEN");
|
public static string NOTESNOOK_SERVER_DOMAIN => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_DOMAIN");
|
||||||
public static string? TWILIO_SERVICE_SID => ReadSecret("TWILIO_SERVICE_SID");
|
public static string NOTESNOOK_CERT_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_PATH");
|
||||||
// Server discovery
|
public static string NOTESNOOK_CERT_KEY_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_KEY_PATH");
|
||||||
public static int NOTESNOOK_SERVER_PORT => int.Parse(ReadSecret("NOTESNOOK_SERVER_PORT") ?? "80");
|
|
||||||
public static string? NOTESNOOK_SERVER_HOST => ReadSecret("NOTESNOOK_SERVER_HOST");
|
public static int IDENTITY_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("IDENTITY_SERVER_PORT"));
|
||||||
public static string? NOTESNOOK_CERT_PATH => ReadSecret("NOTESNOOK_CERT_PATH");
|
public static string IDENTITY_SERVER_HOST => Environment.GetEnvironmentVariable("IDENTITY_SERVER_HOST");
|
||||||
public static string? NOTESNOOK_CERT_KEY_PATH => ReadSecret("NOTESNOOK_CERT_KEY_PATH");
|
public static string IDENTITY_SERVER_DOMAIN => Environment.GetEnvironmentVariable("IDENTITY_SERVER_DOMAIN");
|
||||||
public static string[] KNOWN_PROXIES => (ReadSecret("KNOWN_PROXIES") ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
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 IDENTITY_SERVER_PORT => int.Parse(ReadSecret("IDENTITY_SERVER_PORT") ?? "80");
|
|
||||||
public static string? IDENTITY_SERVER_HOST => ReadSecret("IDENTITY_SERVER_HOST");
|
public static int SSE_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SSE_SERVER_PORT"));
|
||||||
public static Uri? IDENTITY_SERVER_URL => ReadSecret("IDENTITY_SERVER_URL") is string url ? new Uri(url) : null;
|
public static string SSE_SERVER_HOST => Environment.GetEnvironmentVariable("SSE_SERVER_HOST");
|
||||||
public static string? IDENTITY_CERT_PATH => ReadSecret("IDENTITY_CERT_PATH");
|
public static string SSE_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SSE_SERVER_DOMAIN");
|
||||||
public static string? IDENTITY_CERT_KEY_PATH => ReadSecret("IDENTITY_CERT_KEY_PATH");
|
public static string SSE_CERT_PATH => Environment.GetEnvironmentVariable("SSE_CERT_PATH");
|
||||||
|
public static string SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
|
||||||
public static int SSE_SERVER_PORT => int.Parse(ReadSecret("SSE_SERVER_PORT") ?? "80");
|
|
||||||
public static string? SSE_SERVER_HOST => ReadSecret("SSE_SERVER_HOST");
|
// internal
|
||||||
public static string? SSE_CERT_PATH => ReadSecret("SSE_CERT_PATH");
|
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
|
||||||
public static string? SSE_CERT_KEY_PATH => ReadSecret("SSE_CERT_KEY_PATH");
|
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME");
|
||||||
|
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
|
||||||
// internal
|
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT"));
|
||||||
public static string? WEBRISK_API_URI => ReadSecret("WEBRISK_API_URI");
|
public static string SUBSCRIPTIONS_SERVER_HOST => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_HOST");
|
||||||
public static string MONGODB_CONNECTION_STRING => ReadSecret("MONGODB_CONNECTION_STRING") ?? throw new ArgumentNullException("MONGODB_CONNECTION_STRING environment variable is not set");
|
public static string SUBSCRIPTIONS_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_DOMAIN");
|
||||||
public static string MONGODB_DATABASE_NAME => ReadSecret("MONGODB_DATABASE_NAME") ?? throw new ArgumentNullException("MONGODB_DATABASE_NAME environment variable is not set");
|
public static string SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH");
|
||||||
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(ReadSecret("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
|
public static string SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH");
|
||||||
public static string? SUBSCRIPTIONS_SERVER_HOST => ReadSecret("SUBSCRIPTIONS_SERVER_HOST");
|
public static string[] NOTESNOOK_CORS_ORIGINS => Environment.GetEnvironmentVariable("NOTESNOOK_CORS")?.Split(",") ?? new string[] { };
|
||||||
public static string? SUBSCRIPTIONS_CERT_PATH => ReadSecret("SUBSCRIPTIONS_CERT_PATH");
|
}
|
||||||
public static string? SUBSCRIPTIONS_CERT_KEY_PATH => ReadSecret("SUBSCRIPTIONS_CERT_KEY_PATH");
|
}
|
||||||
public static string[] NOTESNOOK_CORS_ORIGINS => ReadSecret("NOTESNOOK_CORS")?.Split(",") ?? [];
|
|
||||||
|
|
||||||
public static string? ReadSecret(string name)
|
|
||||||
{
|
|
||||||
var value = Environment.GetEnvironmentVariable(name);
|
|
||||||
if (!string.IsNullOrEmpty(value)) return value;
|
|
||||||
var file = Environment.GetEnvironmentVariable(name + "_FILE");
|
|
||||||
if (!string.IsNullOrEmpty(file) && System.IO.File.Exists(file))
|
|
||||||
{
|
|
||||||
return System.IO.File.ReadAllText(file);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Streetwriters.Common.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts simple interface into an object (assumes that there is only one class of TInterface)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInterface">Interface type</typeparam>
|
||||||
|
/// <typeparam name="TClass">Class type</typeparam>
|
||||||
|
public class InterfaceConverter<TInterface, TClass> : JsonConverter<TInterface> where TClass : TInterface
|
||||||
|
{
|
||||||
|
public override TInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<TClass>(ref reader, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
JsonSerializer.Serialize(writer, null, options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var type = value.GetType();
|
||||||
|
JsonSerializer.Serialize(writer, value, type, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Streetwriters.Common.Enums
|
namespace Streetwriters.Common.Enums
|
||||||
{
|
{
|
||||||
public enum ApplicationType
|
public enum ApplicationType
|
||||||
{
|
{
|
||||||
NOTESNOOK = 0
|
NOTESNOOK = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Streetwriters.Common.Enums
|
namespace Streetwriters.Common.Enums
|
||||||
{
|
{
|
||||||
public class MFAMethods
|
public class MFAMethods
|
||||||
{
|
{
|
||||||
public static string Email => "email";
|
public static string Email => "email";
|
||||||
public static string SMS => "sms";
|
public static string SMS => "sms";
|
||||||
public static string App => "app";
|
public static string App => "app";
|
||||||
public static string RecoveryCode => "recoveryCode";
|
public static string RecoveryCode => "recoveryCode";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Streetwriters.Common.Enums
|
namespace Streetwriters.Common.Enums
|
||||||
{
|
{
|
||||||
public enum SubscriptionProvider
|
public enum SubscriptionProvider
|
||||||
{
|
{
|
||||||
STREETWRITERS = 0,
|
STREETWRITERS = 0,
|
||||||
APPLE = 1,
|
APPLE = 1,
|
||||||
GOOGLE = 2,
|
GOOGLE = 2,
|
||||||
PADDLE = 3,
|
PADDLE = 3
|
||||||
GIFT_CARD = 4,
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -26,7 +26,6 @@ namespace Streetwriters.Common.Enums
|
|||||||
BETA = 2,
|
BETA = 2,
|
||||||
PREMIUM = 5,
|
PREMIUM = 5,
|
||||||
PREMIUM_EXPIRED = 6,
|
PREMIUM_EXPIRED = 6,
|
||||||
PREMIUM_CANCELED = 7,
|
PREMIUM_CANCELED = 7
|
||||||
PREMIUM_PAUSED = 8,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,113 +1,77 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using System.Net;
|
using Microsoft.AspNetCore.Http;
|
||||||
using System.Text.Json;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using WampSharp.AspNetCore.WebSockets.Server;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using WampSharp.Binding;
|
||||||
using Microsoft.AspNetCore.Http;
|
using WampSharp.V2;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using WampSharp.V2.Realm;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
namespace Streetwriters.Common.Extensions
|
||||||
using WampSharp.AspNetCore.WebSockets.Server;
|
{
|
||||||
using WampSharp.Binding;
|
public static class AppBuilderExtensions
|
||||||
using WampSharp.V2;
|
{
|
||||||
using WampSharp.V2.Realm;
|
public static IApplicationBuilder UseVersion(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
namespace Streetwriters.Common.Extensions
|
app.Map("/version", (app) =>
|
||||||
{
|
{
|
||||||
public static class AppBuilderExtensions
|
app.Run(async context =>
|
||||||
{
|
{
|
||||||
public static IApplicationBuilder UseVersion(this IApplicationBuilder app, Server server)
|
await context.Response.WriteAsync(Version.AsString());
|
||||||
{
|
});
|
||||||
app.Map("/version", (app) =>
|
});
|
||||||
{
|
return app;
|
||||||
app.Run(async context =>
|
}
|
||||||
{
|
|
||||||
context.Response.ContentType = "application/json";
|
public static IApplicationBuilder UseWamp<T>(this IApplicationBuilder app, WampServer<T> server, Action<IWampHostedRealm, WampServer<T>> action) where T : new()
|
||||||
var data = new Dictionary<string, object>
|
{
|
||||||
{
|
WampHost host = new WampHost();
|
||||||
{ "version", Constants.COMPATIBILITY_VERSION },
|
|
||||||
{ "id", server.Id ?? "unknown" },
|
app.Map(server.Endpoint, builder =>
|
||||||
{ "instance", Constants.INSTANCE_NAME }
|
{
|
||||||
};
|
builder.UseWebSockets();
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(data));
|
host.RegisterTransport(new AspNetCoreWebSocketTransport(builder),
|
||||||
});
|
new JTokenJsonBinding(),
|
||||||
});
|
new JTokenMsgpackBinding());
|
||||||
return app;
|
});
|
||||||
}
|
|
||||||
|
host.Open();
|
||||||
public static IApplicationBuilder UseWamp(this IApplicationBuilder app, WampServer server, Action<IWampHostedRealm, WampServer> action)
|
|
||||||
{
|
action.Invoke(host.RealmContainer.GetRealmByName(server.Realm), server);
|
||||||
WampHost host = new();
|
|
||||||
|
return app;
|
||||||
app.Map(server.Endpoint, builder =>
|
}
|
||||||
{
|
|
||||||
builder.UseWebSockets();
|
public static T GetService<T>(this IApplicationBuilder app)
|
||||||
host.RegisterTransport(new AspNetCoreWebSocketTransport(builder),
|
{
|
||||||
new JTokenJsonBinding(),
|
return app.ApplicationServices.GetRequiredService<T>();
|
||||||
new JTokenMsgpackBinding());
|
}
|
||||||
});
|
|
||||||
|
public static T GetScopedService<T>(this IApplicationBuilder app)
|
||||||
host.Open();
|
{
|
||||||
|
using (var scope = app.ApplicationServices.CreateScope())
|
||||||
action.Invoke(host.RealmContainer.GetRealmByName(server.Realm), server);
|
{
|
||||||
|
return scope.ServiceProvider.GetRequiredService<T>();
|
||||||
return app;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static T GetService<T>(this IApplicationBuilder app) where T : notnull
|
|
||||||
{
|
|
||||||
return app.ApplicationServices.GetRequiredService<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T GetScopedService<T>(this IApplicationBuilder app) where T : notnull
|
|
||||||
{
|
|
||||||
using var scope = app.ApplicationServices.CreateScope();
|
|
||||||
return scope.ServiceProvider.GetRequiredService<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IApplicationBuilder UseForwardedHeadersWithKnownProxies(this IApplicationBuilder app, IWebHostEnvironment env, string forwardedForHeaderName = null)
|
|
||||||
{
|
|
||||||
if (!env.IsDevelopment())
|
|
||||||
{
|
|
||||||
var forwardedHeadersOptions = new ForwardedHeadersOptions
|
|
||||||
{
|
|
||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(forwardedForHeaderName))
|
|
||||||
{
|
|
||||||
forwardedHeadersOptions.ForwardedForHeaderName = forwardedForHeaderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var proxy in Constants.KNOWN_PROXIES)
|
|
||||||
{
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Parse(proxy));
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,76 +1,72 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Net.Http;
|
||||||
using System.Net.Http;
|
using System.Net.Http.Json;
|
||||||
using System.Net.Http.Json;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Streetwriters.Common.Interfaces;
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
|
namespace Streetwriters.Common.Extensions
|
||||||
namespace Streetwriters.Common.Extensions
|
{
|
||||||
{
|
public static class HttpClientExtensions
|
||||||
public static class HttpClientExtensions
|
{
|
||||||
{
|
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary headers, HttpMethod method, HttpContent content = null) where T : IResponse, new()
|
||||||
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary? headers, HttpMethod method, HttpContent? content = null) where T : IResponse, new()
|
{
|
||||||
{
|
var request = new HttpRequestMessage(method, url);
|
||||||
var request = new HttpRequestMessage(method, url);
|
|
||||||
|
if (method != HttpMethod.Get && method != HttpMethod.Delete)
|
||||||
if (method != HttpMethod.Get && method != HttpMethod.Delete)
|
{
|
||||||
{
|
request.Content = content;
|
||||||
request.Content = content;
|
}
|
||||||
}
|
|
||||||
|
foreach (var header in headers)
|
||||||
if (headers != null)
|
{
|
||||||
{
|
if (header.Key == "Content-Type" || header.Key == "Content-Length")
|
||||||
foreach (var header in headers)
|
{
|
||||||
{
|
if (request.Content != null)
|
||||||
if (header.Key == "Content-Type" || header.Key == "Content-Length")
|
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||||
{
|
continue;
|
||||||
request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
}
|
||||||
continue;
|
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
||||||
}
|
}
|
||||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
|
|
||||||
}
|
var response = await httpClient.SendAsync(request);
|
||||||
}
|
if (response.Content.Headers.ContentLength > 0)
|
||||||
|
{
|
||||||
var response = await httpClient.SendAsync(request);
|
var res = await response.Content.ReadFromJsonAsync<T>();
|
||||||
if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType?.ToString()?.Contains("application/json") == true)
|
res.Success = response.IsSuccessStatusCode;
|
||||||
{
|
res.StatusCode = (int)response.StatusCode;
|
||||||
var res = await response.Content.ReadFromJsonAsync<T>();
|
return res;
|
||||||
if (res != null)
|
}
|
||||||
{
|
else
|
||||||
res.Success = response.IsSuccessStatusCode;
|
{
|
||||||
res.StatusCode = (int)response.StatusCode;
|
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode };
|
||||||
return res;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
|
||||||
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
|
{
|
||||||
}
|
var httpContext = accessor.HttpContext;
|
||||||
|
var content = new StreamContent(httpContext.Request.BodyReader.AsStream());
|
||||||
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
|
return httpClient.SendRequestAsync<T>(url, httpContext.Request.Headers, method, content);
|
||||||
{
|
}
|
||||||
var httpContext = accessor.HttpContext ?? throw new InvalidOperationException("HttpContext is not available");
|
}
|
||||||
var content = new StreamContent(httpContext.Request.BodyReader.AsStream());
|
|
||||||
return httpClient.SendRequestAsync<T>(url, httpContext.Request.Headers, method, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,61 +1,44 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Streetwriters.Common.Accessors;
|
|
||||||
using Streetwriters.Data.DbContexts;
|
namespace Streetwriters.Common.Extensions
|
||||||
using Streetwriters.Data.Repositories;
|
{
|
||||||
|
public static class ServiceCollectionServiceExtensions
|
||||||
namespace Streetwriters.Common.Extensions
|
{
|
||||||
{
|
public static IServiceCollection AddDefaultCors(this IServiceCollection services)
|
||||||
public static class ServiceCollectionServiceExtensions
|
{
|
||||||
{
|
services.AddCors(options =>
|
||||||
public static IServiceCollection AddWampServiceAccessor(this IServiceCollection services, Server server)
|
{
|
||||||
{
|
options.AddPolicy("notesnook", (b) =>
|
||||||
services.AddSingleton<WampServiceAccessor>((provider) => new WampServiceAccessor(server));
|
{
|
||||||
services.AddHostedService(provider => provider.GetRequiredService<WampServiceAccessor>());
|
if (Constants.NOTESNOOK_CORS_ORIGINS.Length <= 0)
|
||||||
return services;
|
b.AllowAnyOrigin();
|
||||||
}
|
else
|
||||||
|
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
|
||||||
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database) where T : class
|
|
||||||
{
|
b.AllowAnyMethod()
|
||||||
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
|
.AllowAnyHeader();
|
||||||
services.AddScoped<Repository<T>>();
|
});
|
||||||
return services;
|
});
|
||||||
}
|
return services;
|
||||||
|
}
|
||||||
public static IServiceCollection AddDefaultCors(this IServiceCollection services)
|
}
|
||||||
{
|
}
|
||||||
services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("notesnook", (b) =>
|
|
||||||
{
|
|
||||||
if (Constants.NOTESNOOK_CORS_ORIGINS.Length <= 0)
|
|
||||||
b.AllowAnyOrigin();
|
|
||||||
else
|
|
||||||
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
|
|
||||||
|
|
||||||
b.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,87 +1,91 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace System
|
namespace System
|
||||||
{
|
{
|
||||||
public static class StringExtensions
|
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);
|
// Create a SHA256
|
||||||
var hash = SHA256.HashData(bytes);
|
using (SHA256 sha256Hash = SHA256.Create())
|
||||||
return Convert.ToBase64String(hash);
|
{
|
||||||
}
|
// ComputeHash - returns byte array
|
||||||
|
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
|
||||||
public static byte[] CompressBrotli(this string input)
|
return ToHex(bytes, 0, maxLength);
|
||||||
{
|
}
|
||||||
var raw = Encoding.Default.GetBytes(input);
|
}
|
||||||
using (MemoryStream memory = new MemoryStream())
|
|
||||||
{
|
public static byte[] CompressBrotli(this string input)
|
||||||
using (BrotliStream brotli = new BrotliStream(memory, CompressionLevel.Optimal))
|
{
|
||||||
{
|
var raw = Encoding.Default.GetBytes(input);
|
||||||
brotli.Write(raw, 0, raw.Length);
|
using (MemoryStream memory = new MemoryStream())
|
||||||
}
|
{
|
||||||
return memory.ToArray();
|
using (BrotliStream brotli = new BrotliStream(memory, CompressionLevel.Optimal))
|
||||||
}
|
{
|
||||||
}
|
brotli.Write(raw, 0, raw.Length);
|
||||||
|
}
|
||||||
public static string DecompressBrotli(this byte[] compressed)
|
return memory.ToArray();
|
||||||
{
|
}
|
||||||
using (BrotliStream stream = new BrotliStream(new MemoryStream(compressed), CompressionMode.Decompress))
|
}
|
||||||
{
|
|
||||||
const int size = 4096;
|
public static string DecompressBrotli(this byte[] compressed)
|
||||||
byte[] buffer = new byte[size];
|
{
|
||||||
using (MemoryStream memory = new MemoryStream())
|
using (BrotliStream stream = new BrotliStream(new MemoryStream(compressed), CompressionMode.Decompress))
|
||||||
{
|
{
|
||||||
int count = 0;
|
const int size = 4096;
|
||||||
do
|
byte[] buffer = new byte[size];
|
||||||
{
|
using (MemoryStream memory = new MemoryStream())
|
||||||
count = stream.Read(buffer, 0, size);
|
{
|
||||||
if (count > 0)
|
int count = 0;
|
||||||
{
|
do
|
||||||
memory.Write(buffer, 0, count);
|
{
|
||||||
}
|
count = stream.Read(buffer, 0, size);
|
||||||
}
|
if (count > 0)
|
||||||
while (count > 0);
|
{
|
||||||
return Encoding.Default.GetString(memory.ToArray());
|
memory.Write(buffer, 0, count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
while (count > 0);
|
||||||
|
return Encoding.Default.GetString(memory.ToArray());
|
||||||
private static string ToHex(byte[] bytes, int startIndex, int length)
|
}
|
||||||
{
|
}
|
||||||
char[] c = new char[length * 2];
|
}
|
||||||
byte b;
|
|
||||||
for (int bx = startIndex, cx = startIndex; bx < length; ++bx, ++cx)
|
private static string ToHex(byte[] bytes, int startIndex, int length)
|
||||||
{
|
{
|
||||||
b = ((byte)(bytes[bx] >> 4));
|
char[] c = new char[length * 2];
|
||||||
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
byte b;
|
||||||
|
for (int bx = startIndex, cx = startIndex; bx < length; ++bx, ++cx)
|
||||||
b = ((byte)(bytes[bx] & 0x0F));
|
{
|
||||||
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
b = ((byte)(bytes[bx] >> 4));
|
||||||
}
|
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
||||||
return new string(c);
|
|
||||||
}
|
b = ((byte)(bytes[bx] & 0x0F));
|
||||||
}
|
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
|
||||||
|
}
|
||||||
|
return new string(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,66 +1,42 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using System.Threading;
|
using Streetwriters.Common.Interfaces;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using WampSharp.AspNetCore.WebSockets.Server;
|
||||||
using Streetwriters.Common.Interfaces;
|
using WampSharp.Binding;
|
||||||
using WampSharp.AspNetCore.WebSockets.Server;
|
using WampSharp.V2;
|
||||||
using WampSharp.Binding;
|
using WampSharp.V2.Realm;
|
||||||
using WampSharp.V2;
|
|
||||||
using WampSharp.V2.Realm;
|
namespace Streetwriters.Common.Extensions
|
||||||
|
{
|
||||||
namespace Streetwriters.Common.Extensions
|
public static class WampRealmExtensions
|
||||||
{
|
{
|
||||||
public static class WampRealmExtensions
|
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, Action<T> onNext)
|
||||||
{
|
{
|
||||||
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, Action<T> onNext)
|
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(onNext);
|
||||||
{
|
}
|
||||||
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(onNext);
|
|
||||||
}
|
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
|
||||||
|
{
|
||||||
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
|
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
|
||||||
{
|
}
|
||||||
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,62 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Streetwriters.Common.Enums;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using WebMarkupMin.Core;
|
|
||||||
using WebMarkupMin.Core.Loggers;
|
|
||||||
|
|
||||||
namespace Streetwriters.Common.Helpers
|
|
||||||
{
|
|
||||||
public enum Features
|
|
||||||
{
|
|
||||||
SMS_2FA,
|
|
||||||
MONOGRAPH_ANALYTICS
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class FeatureAuthorizationHelper
|
|
||||||
{
|
|
||||||
private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var claimKey = $"{clientId}:status";
|
|
||||||
var status = user.FindFirstValue(claimKey);
|
|
||||||
switch (status)
|
|
||||||
{
|
|
||||||
case "free":
|
|
||||||
return SubscriptionPlan.FREE;
|
|
||||||
case "believer":
|
|
||||||
return SubscriptionPlan.BELIEVER;
|
|
||||||
case "education":
|
|
||||||
return SubscriptionPlan.EDUCATION;
|
|
||||||
case "essential":
|
|
||||||
return SubscriptionPlan.ESSENTIAL;
|
|
||||||
case "pro":
|
|
||||||
return SubscriptionPlan.PRO;
|
|
||||||
case "legacy_pro":
|
|
||||||
return SubscriptionPlan.LEGACY_PRO;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsFeatureAllowed(Features feature, string clientId, ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
if (Constants.IS_SELF_HOSTED)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var status = GetUserSubscriptionPlan(clientId, user);
|
|
||||||
|
|
||||||
switch (feature)
|
|
||||||
{
|
|
||||||
case Features.SMS_2FA:
|
|
||||||
case Features.MONOGRAPH_ANALYTICS:
|
|
||||||
return status == SubscriptionPlan.LEGACY_PRO ||
|
|
||||||
status == SubscriptionPlan.PRO ||
|
|
||||||
status == SubscriptionPlan.EDUCATION ||
|
|
||||||
status == SubscriptionPlan.BELIEVER;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Streetwriters.Common.Messages;
|
using Streetwriters.Common.Messages;
|
||||||
@@ -28,28 +27,15 @@ namespace Streetwriters.Common.Helpers
|
|||||||
{
|
{
|
||||||
public class WampHelper
|
public class WampHelper
|
||||||
{
|
{
|
||||||
public static async Task<IWampChannel> 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);
|
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
|
||||||
|
|
||||||
var isConnected = false;
|
await channel.Open();
|
||||||
while (!isConnected)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await channel.Open();
|
|
||||||
isConnected = true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await Task.Delay(5000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel;
|
return channel.RealmProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
|
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
|
||||||
@@ -57,12 +43,5 @@ namespace Streetwriters.Common.Helpers
|
|||||||
var subject = realm.Services.GetSubject<T>(topicName);
|
var subject = realm.Services.GetSubject<T>(topicName);
|
||||||
subject.OnNext(message);
|
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,38 +1,38 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Streetwriters.Common.Enums;
|
using Streetwriters.Common.Enums;
|
||||||
|
|
||||||
namespace Streetwriters.Common.Interfaces
|
namespace Streetwriters.Common.Interfaces
|
||||||
{
|
{
|
||||||
public interface IClient
|
public interface IClient
|
||||||
{
|
{
|
||||||
string Id { get; set; }
|
string Id { get; set; }
|
||||||
string Name { get; set; }
|
string Name { get; set; }
|
||||||
ApplicationType Type { get; set; }
|
ApplicationType Type { get; set; }
|
||||||
ApplicationType AppId { get; set; }
|
ApplicationType AppId { get; set; }
|
||||||
string SenderEmail { get; set; }
|
string SenderEmail { get; set; }
|
||||||
string SenderName { get; set; }
|
string SenderName { get; set; }
|
||||||
string EmailConfirmedRedirectURL { get; }
|
string EmailConfirmedRedirectURL { get; }
|
||||||
string AccountRecoveryRedirectURL { get; }
|
string AccountRecoveryRedirectURL { get; }
|
||||||
Func<string, Task>? OnEmailConfirmed { get; set; }
|
Func<string, Task> OnEmailConfirmed { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using MongoDB.Bson;
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
namespace Streetwriters.Common.Interfaces
|
public interface IDocument
|
||||||
{
|
{
|
||||||
public interface IDocument
|
string Id
|
||||||
{
|
{
|
||||||
ObjectId Id
|
get; set;
|
||||||
{
|
}
|
||||||
get; set;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Streetwriters.Common.Interfaces;
|
using Streetwriters.Common.Interfaces;
|
||||||
|
|
||||||
namespace Streetwriters.Common.Interfaces
|
namespace Streetwriters.Common.Interfaces
|
||||||
{
|
{
|
||||||
public interface IMessageHandler<T>
|
public interface IMessageHandler<T>
|
||||||
{
|
{
|
||||||
Task Process(T message);
|
Task Process(T message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
-38
@@ -1,38 +1,32 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using Streetwriters.Common.Enums;
|
||||||
using System.Text.Json.Serialization;
|
using Streetwriters.Common.Models;
|
||||||
using Streetwriters.Common.Enums;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
namespace Streetwriters.Common.Messages
|
public interface IOffer : IDocument
|
||||||
{
|
{
|
||||||
public class ClearCacheMessage
|
ApplicationType AppId { get; set; }
|
||||||
{
|
string PromoCode { get; set; }
|
||||||
public ClearCacheMessage(List<string> keys)
|
PromoCode[] Codes { get; set; }
|
||||||
{
|
}
|
||||||
this.Keys = keys;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("keys")]
|
|
||||||
public List<string> Keys { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
Affero GNU General Public License for more details.
|
Affero GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the Affero GNU General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Net.Http;
|
namespace Streetwriters.Common.Interfaces
|
||||||
|
{
|
||||||
namespace Streetwriters.Common.Interfaces
|
public interface IResponse
|
||||||
{
|
{
|
||||||
public interface IResponse
|
bool Success { get; set; }
|
||||||
{
|
int StatusCode { get; set; }
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user