142 Commits

Author SHA1 Message Date
Abdullah Atta
02ccd6cd06 cors: make hostname configurable via env variable 2025-12-29 14:02:48 +05:00
Abdullah Atta
75369a5988 cors: add self hostable cors proxy 2025-12-29 12:33:11 +05:00
Abdullah Atta
a3235ca381 common: add support for reading secret from file
this is required to support `docker secrets`
2025-12-26 14:57:38 +05:00
Abdullah Atta
119d5b0e7a docker: update to net9.0 2025-12-23 16:30:19 +05:00
Abdullah Atta
b98612be7a sync: migrate sync devices from fs to mongodb 2025-12-22 20:11:43 +05:00
Abdullah Atta
c7bb053cea common: simplify wamp logic
previously we were opening a new channel for each topic which was unnecessary
2025-12-22 13:38:58 +05:00
Abdullah Atta
265b456c46 s3: add support for failover 2025-12-17 09:06:26 +05:00
Abdullah Atta
347507f00a global: switch to dotnet9 2025-12-15 22:58:25 +05:00
Abdullah Atta
8dd9d0dc62 api: add support for {{Email}} placeholder in announcements 2025-11-28 13:01:28 +05:00
Abdullah Atta
e489ce7376 common: add missing enum conversion for legacy_pro 2025-11-24 11:58:23 +05:00
Abdullah Atta
5ca9c142e3 monograph: {id}/stats -> {id}/analytics 2025-11-08 12:42:10 +05:00
01zulfi
23b0b2ddfc monographs: add stats endpoint (#69)
* also don't mark monograph for sync when tracking view count
2025-11-07 10:28:03 +05:00
01zulfi
7a8873db32 monographs: fix view count setting to 0 when updating (#68) 2025-11-07 09:59:52 +05:00
01zulfi
294769fd71 monographs: track view count (#67) 2025-11-06 13:15:23 +05:00
01zulfi
55136597aa fix: fix Streetwriters.Common build error (#65) 2025-11-05 22:52:49 +05:00
Abdullah Atta
9d3ef51c0c common: throw error if s3 env variables not defined 2025-11-05 22:43:38 +05:00
Abdullah Atta
de23ea3e66 s3: add s3 health check 2025-11-05 22:42:58 +05:00
Abdullah Atta
54d0fdcf4f s3: fix storage limit rolloever 2025-11-05 22:42:45 +05:00
Abdullah Atta
1e8a205719 s3: improve error reporting 2025-11-05 22:41:24 +05:00
Abdullah Atta
70da841531 inbox: minor fixes and improvements 2025-10-30 13:12:05 +05:00
Abdullah Atta
5445c51d8d identity: fix error on sessions clear 2025-10-30 13:12:05 +05:00
Tate M Walker
dd05c55875 global: add support for specifying known proxies (#63)
* Add KNOWN_PROXIES

* Add known proxy setup in Startup.cs

Refactor forwarded headers configuration to use a variable for options.

* Document KNOWN_PROXIES in .env file

Added documentation for KNOWN_PROXIES environment variable

* Clean up

Restored license comments and formatting in Constants.cs.

* Apply suggestion from @thecodrr

* Added KnownProxies functionality at Streetwriters.Common level

---------

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2025-10-29 10:14:49 +05:00
Abdullah Atta
ab9efaea7f identity: fix sign ups 2025-10-22 10:13:58 +05:00
Abdullah Atta
8bc1a52a60 s3: fix uploads for legacy users 2025-10-22 10:13:51 +05:00
Abdullah Atta
75a4462fd1 identity: add client id checks in grant validators 2025-10-14 21:50:57 +05:00
Abdullah Atta
8db33889b6 api: remove legacy sync hub 2025-10-14 21:16:59 +05:00
Abdullah Atta
50f159a37b s3: improve upload limit checks 2025-10-14 21:16:22 +05:00
Abdullah Atta
6e35edb715 global: add null safety checks 2025-10-14 21:15:51 +05:00
Abdullah Atta
be432dfd24 global: migrate to using ILogger 2025-10-14 09:29:07 +05:00
01zulfi
0cc3365e44 inbox: add rate limiting (#59) 2025-10-14 09:16:35 +05:00
01zulfi
a8cc02ef1a inbox: send salt in encrypted inbox item (#57) 2025-10-13 11:31:12 +05:00
Abdullah Atta
d1421d640f identity: fix user subscription claim value incorrect for legacy pro users 2025-10-13 11:27:07 +05:00
Abdullah Atta
131df3df04 s3: use internal upload object url for uploading files 2025-10-09 14:16:59 +05:00
Abdullah Atta
1ecd8adee1 s3: fix attachments not uploading on self hosted servers
this was due to s3 endpoints requesting user's subscription
which didn't exist in case of
self hosted setups
2025-10-09 14:13:11 +05:00
01zulfi
32b24dead2 inbox: sync inbox items 2025-10-08 12:49:41 +05:00
Abdullah Atta
4779a21134 ci: fix workflow 2025-10-07 17:05:10 +05:00
Abdullah Atta
6215c3c916 ci: run workflow on tag creation 2025-10-07 17:01:52 +05:00
Abdullah Atta
16ce7942be sync: fix error on monographs sync 2025-10-07 16:44:15 +05:00
Abdullah Atta
3a2e5f884b monograph: refactor to fix type errors 2025-10-07 16:44:15 +05:00
Abdullah Atta
2b6a58ff04 api: handle subscription fetch errors 2025-10-07 16:44:15 +05:00
Abdullah Atta
72e825a12c common: update subscription models 2025-10-07 16:44:15 +05:00
Abdullah Atta
8aeeba0eeb s3: fix ContentLength header is required error 2025-10-07 16:44:15 +05:00
Abdullah Atta
2952dd2c63 sync: fix monograph sync 2025-10-07 16:44:15 +05:00
Abdullah Atta
908d64bd4f api: fix minor issues 2025-10-07 16:44:15 +05:00
Abdullah Atta
41a185bd9f s3: disable download limits based on file size 2025-10-07 16:44:15 +05:00
Abdullah Atta
61dd2e1f74 s3: add limit on download file size 2025-10-07 16:44:15 +05:00
Abdullah Atta
3bb140aeb3 common: handle paddle billing errors 2025-10-07 16:44:15 +05:00
Abdullah Atta
7172510c9e api: fix sync not working on android 2025-10-07 16:44:15 +05:00
Abdullah Atta
cfe2875a67 api: refactor user subscription check for monograph embed & links 2025-10-07 16:44:15 +05:00
Abdullah Atta
9860df2379 identity: refactor sms mfa authorization check 2025-10-07 16:44:15 +05:00
Abdullah Atta
cc459f9fea common: add paddle v1 api client 2025-10-07 16:44:15 +05:00
Abdullah Atta
9e6a25ec1d common: fix plan change if subscription is trialing 2025-10-07 16:44:15 +05:00
Abdullah Atta
6304d8178f identity: fix delete subscription handling 2025-10-07 16:44:15 +05:00
Abdullah Atta
500a64de18 identity: use subscription v2 types & api 2025-10-07 16:44:15 +05:00
Abdullah Atta
55a2223198 common: add subscribe with semaphore to wamp to allow serial messaging 2025-10-07 16:44:15 +05:00
Abdullah Atta
3beb716b83 common: add paddle billing api client 2025-10-07 16:44:15 +05:00
Abdullah Atta
ed6e3c56f2 api: minor refactors aimed at improving memory consumption 2025-10-07 16:44:15 +05:00
Abdullah Atta
579e65b0be api: add support for storage limits 2025-10-07 16:44:15 +05:00
Abdullah Atta
b3dcdda697 api: improve sync hub auth 2025-10-07 16:44:15 +05:00
Abdullah Atta
44a9ff57e7 common: add new subscription types 2025-10-07 16:44:15 +05:00
Abdullah Atta
4361b90425 api: minor refactors 2025-10-07 16:44:15 +05:00
01zulfi
3471ecb21a feat: set up public inbox api server (#52)
* feat: set up public inbox api server

* feat: add zod validation for raw inbox item

* chore: update encrypted item type && raw inbox item schema

* feat: use symmetric & asymmetric combination for inbox encryption

* chore: improve error handling

* chore: update encrypted item type

* feat: add Dockerfile for Notesnook.Inbox.Api
2025-10-06 12:22:25 +05:00
01zulfi
5a9b98fd06 inbox: add GET public inbox key & POST inbox items endpoint (#51)
* inbox: add GET public inbox key && POST inbox items endpoint

* inbox: update SyncItem to support inbox items

* inbox: update post inbox items request payload

* inbox: update post inbox item endpoint
2025-10-06 12:21:31 +05:00
Abdullah Atta
34e5dc6a20 identity: fix super_strict rate limiting policy 2025-09-25 09:07:56 +05:00
Abdullah Atta
8f8c60e0b3 api: fix error on user patch 2025-09-17 11:38:33 +05:00
01zulfi
9b774d640c feat: get, add, & delete user's inbox api tokens (#50)
* feat: get, add, & delete user's inbox api tokens

* inbox: generate inbox api key on the server

* inbox: use nanoid to generate api key && set created date on server

* inbox: set api key in constructor && increase default expiry date to 1 year
2025-09-16 08:40:52 +05:00
Abdullah Atta
4a0aee1c44 monograph: fix json content serialized as html 2025-09-15 12:30:28 +05:00
Abdullah Atta
97fbd3226d monograph: add support for webrisk api for analyzing urls for pro users 2025-09-15 11:22:37 +05:00
Abdullah Atta
0f43b3ee66 monograph: remove links, embeds & images for non-pro users 2025-09-15 09:19:16 +05:00
Abdullah Atta
b469da70e8 api: only expose /metrics endpoint internally 2025-09-10 09:33:19 +05:00
01zulfi
10e33de897 api: include inbox keys in get user endpoint (#48) 2025-09-09 19:44:08 +05:00
01zulfi
6e8fb81ade sync: ensure monographs sync for first time ever on an existing device (#44) 2025-09-06 09:33:18 +05:00
01zulfi
34a09ad15d inbox: store user's inbox keys (#47) 2025-09-06 09:32:44 +05:00
Abdullah Atta
201a235357 identity: make otp rate limiting more strict 2025-08-25 10:46:31 +05:00
01zulfi
e68b8f7e7c sync: add RequestFetchV2 hub method (#43) 2025-08-20 11:20:50 +05:00
Abdullah Atta
a5b3a12914 monograph: fix some monographs can't be unpublished 2025-08-19 14:23:56 +05:00
Abdullah Atta
3ed30b206c api: run device cleanup job 1st of every month 2025-08-19 14:00:55 +05:00
Abdullah Atta
1344199807 api: add job to cleanup stale devices every 30 days 2025-08-19 12:24:43 +05:00
Abdullah Atta
33a189fe91 api: minor refactor 2025-08-18 23:44:22 +05:00
Abdullah Atta
2f361db9df monographs: fix monographs not appearing on clients 2025-08-18 23:44:22 +05:00
Minecon724
bf6cd6cd46 docker: fix the mongo healthcheck (#42) 2025-08-18 23:43:01 +05:00
Abdullah Atta
54266d1ba3 sync: disable SendMonographs on sync since its not supported by clients yet 2025-08-18 18:23:42 +05:00
Abdullah Atta
d3894d2a9f sync: fix StorageLimit does not match any field or property 2025-08-18 17:54:14 +05:00
Abdullah Atta
00c089e677 docker: get rid of initiate-rs0 2025-08-18 14:22:31 +05:00
dyw770
fa8f69157a docker: Fix MongoDB container volume error (#12) 2025-08-18 13:34:31 +05:00
dyw770
30d5394425 api: fix s3 multipart upload (#22)
* api: fix s3 multipart upload use external s3 url

* api: fix s3 multipart upload use external s3 url

* api: fix CompleteMultipartUploadRequest can not deserialize

* api: start multipart upload use s3 internal url

* Update Notesnook.API/Models/PartETagWrapper.cs

remove default constructor

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>

* api: remove default constructor

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>

* api: merge method call

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>

* api: revocation due to conflict

* api: revocation due to conflict

---------

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2025-08-18 13:33:49 +05:00
Vsevolod Volkov
037bf4c3ea api: update S3 client mode for presigned URL generation to EXTERNAL (#33)
Signed-off-by: Vsevolod Volkov <73965070+StLyn4@users.noreply.github.com>
2025-08-18 11:48:48 +05:00
01zulfi
6d6342dbff api: add user's monograph passwords key (#41) 2025-08-18 11:45:42 +05:00
01zulfi
8df70c81fc monograph: fix existing item condition in update endpoint (#40) 2025-08-04 13:00:55 +05:00
01zulfi
bf2e6efeff monograph: add sync support (#39)
* monograph: add sync support

* monograph: fix password field && improve syncing logic && fix delete endpoint

* sync: get rid of unnecessary .ToList & ToListAsync

* sync: AddIdsToAllDevices is no longer asynchronous

* monograph: simplify and fix several bugs

- we were sending the triggerSync event to all users instead of all devices
- asynchronous methods did not have the `Async` suffix
- we weren't properly replacing the deleted monograph

* monograph: fix minor issues
* fix publishing
* don't return deleted monograph in monographs/:id endpoint
* persist UserId when soft deleting monograph

* monograph: check soft delete status in several endpoints

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-08-04 11:51:15 +05:00
Abdullah Atta
1e2ef0685d sync: don't break the loop if device id is same 2025-08-02 09:17:36 +05:00
Abdullah Atta
c1f0e24d21 sync: update LastAccessTime on push, pull & register 2025-08-01 11:39:18 +05:00
Abdullah Atta
a96b0e1e42 sync: use synchronous locks instead of semaphores 2025-08-01 10:52:48 +05:00
Abdullah Atta
76af2cbfc8 identity: fix 2fa recovery codes not working 2025-07-15 13:35:07 +05:00
Abdullah Atta
34fa43f302 global: add some basic rate limiting 2025-07-15 13:34:31 +05:00
Abdullah Atta
8c267e51f4 common: add gift card to SubscriptionProvider 2025-07-15 13:33:54 +05:00
Abdullah Atta
182558136a identify: validate mfa methods against user's enabled methods 2025-04-30 11:48:50 +05:00
Abdullah Atta
8d3b0d6dbf identity: validate mfa method against user's mfa methods 2025-04-30 11:45:18 +05:00
Abdullah Atta
0841ca1aa8 common: add gift card model 2025-04-10 12:20:03 +05:00
Abdullah Atta
11dff4f0cc identity: move email sender to common 2025-04-10 12:19:49 +05:00
Abdullah Atta
bbabf51073 global: newline changes 2025-04-10 11:09:28 +05:00
Abdullah Atta
a135bd50d7 db: increase heartbeat interval to 60 seconds 2024-12-02 09:38:21 +05:00
Abdullah Atta
e5bf3367cc global: add support for -DSTAGING 2024-11-28 14:38:28 +05:00
Abdullah Atta
c6bcd4a84d monograph: add new ItemId property that is a simple string
This fixes the issue where an invalid objectid causes error when publishing a monograph
2024-11-28 14:21:57 +05:00
Abdullah Atta
07675632e0 identity: enable user's 2fa method after verifying 2fa code
This must be done after the MFA code is verified otherwise the security stamp will be updated making the 2FA code invalid
2024-11-28 14:20:35 +05:00
Abdullah Atta
8d15e176ff docker: DISABLE_ACCOUNT_CREATION -> DISABLE_SIGNUPS 2024-10-25 12:40:53 +05:00
Abdullah Atta
e3f97bc47e docker: use named volumes instead of creating folders in $HOME 2024-10-12 13:39:57 +05:00
Abdullah Atta
9482e1ddc1 docker: add health checks for mongodb and minio 2024-10-12 13:18:12 +05:00
Abdullah Atta
0447ab6e55 docker: fix variable descriptions 2024-10-12 12:07:31 +05:00
Abdullah Atta
682d904dc9 docker: make monograph self hostable 2024-10-12 12:00:40 +05:00
Abdullah Atta
d8ee28389a docker: make .env more consistent 2024-10-12 12:00:29 +05:00
Abdullah Atta
309dcafa02 monograph: add new id/view endpoint for self destruction 2024-10-12 11:59:10 +05:00
Abdullah Atta
1b97ba77da identity: use new server PublicURL 2024-10-12 11:56:57 +05:00
Abdullah Atta
6d19112fb6 common: replace server Domain with PublicURL 2024-10-12 11:56:34 +05:00
Abdullah Atta
1c68942a6d identity: replace Sodium.Core with Geralt 2024-10-12 11:55:52 +05:00
Maniues
3cc84d7603 Update self-hosting TO-DO (#10)
* Update self-hosting TO-DO

* Add starting version and update note
2024-09-26 18:53:17 +05:00
Abdullah Atta
7f94a647c7 Merge pull request #8 from dyw770/master
docker: fix db persistent configuration
2024-09-06 21:00:28 +05:00
dyw770
ba006974a0 docker: fix db persistent configuration 2024-09-05 23:18:12 +08:00
Abdullah Atta
c3772c86ee Merge pull request #7 from dyw770/master
docker: add db persistence configuration and modify the value of the S3_INTERNAL_SERVICE_URL environment variable
2024-09-05 20:09:21 +05:00
dyw770
53695174b5 docker: Add db persistence configuration and modify the value of the S3_INTERNAL_SERVICE_URL environment variable 2024-09-05 12:06:04 +08:00
Abdullah Atta
356488beab s3: use protocol from service url (fixes #6) 2024-09-04 10:22:07 +05:00
Abdullah Atta
b12eb39797 docker: use wget for healthcheck 2024-08-29 12:23:14 +05:00
Abdullah Atta
962b805054 api: remove s3 objects bigger than the maximum size 2024-08-29 12:20:32 +05:00
Abdullah Atta
f3216330a1 docker: remove even more unnecessary env vars 2024-08-29 12:19:37 +05:00
Abdullah Atta
63069ae573 sync: more stable deserializer for syncitem model 2024-08-11 10:18:55 +05:00
Abdullah Atta
cd06a31d1b docker: expose non-80 port inside containers 2024-08-09 11:20:07 +05:00
Abdullah Atta
21a9b4c203 docker: add service to validate enviroment variables before starting containers 2024-08-08 09:14:58 +05:00
Abdullah Atta
a1003ffdd5 docker: minor cleanup 2024-08-07 15:25:43 +05:00
Abdullah Atta
c66a084ed6 docker: explain why smtp configuration is required 2024-08-07 15:16:30 +05:00
Abdullah Atta
dfabfcbc23 common: simplify compatibility version 2024-08-07 15:06:58 +05:00
Abdullah Atta
e324b588a1 docker: fill out as many vars in .env as possible 2024-08-07 09:02:18 +05:00
Abdullah Atta
15b6947ff0 docker: remove support for 3rd-party S3 providers 2024-08-07 08:50:58 +05:00
Abdullah Atta
c441a1750c docker: add documentation for all variables in .env 2024-08-06 15:52:18 +05:00
Abdullah Atta
9f1f3e14d7 global: remove unncessary environment variables 2024-08-06 15:52:00 +05:00
Abdullah Atta
90118488cb docker: update .env 2024-08-02 11:37:47 +05:00
Abdullah Atta
e99f0f33d2 docker: fix attachments not uploading 2024-08-02 11:36:29 +05:00
Abdullah Atta
881354ab83 global: fix failing build 2024-08-01 12:20:37 +05:00
Abdullah Atta
5c1944d29f common: send more info in /version endpoint 2024-08-01 12:05:50 +05:00
Abdullah Atta
cbd0c01d28 identity: add support for disabling new signups 2024-08-01 10:32:51 +05:00
Abdullah Atta
ad590f6011 identity: auto enable 2fa by email on self hosted instance 2024-08-01 10:05:46 +05:00
Abdullah Atta
2f5bd75d4e identity: confirm email automatically on self hosted instances 2024-08-01 09:56:06 +05:00
Abdullah Atta
3c8c8ebc81 chore: update docker set up instructions 2024-07-31 13:28:59 +05:00
228 changed files with 15955 additions and 12297 deletions

View File

@@ -1,5 +1,5 @@
**/Dockerfile
**/bin
**/obj
**/.env
**/Dockerfile
**/bin
**/obj
**/.env
**/.env.local

102
.env
View File

@@ -1,51 +1,77 @@
# Required variables
NOTESNOOK_API_SECRET= # This should be a randomly generated secret
# Description: Name of your self hosted instance. Used in the client apps for identification purposes
# Required: yes
# Example: notesnook-instance-sg
INSTANCE_NAME=self-hosted-notesnook-instance
# SMTP settings required for delivering emails
# Description: This secret is used for generating, validating, and introspecting auth tokens. It must be a randomly generated token (preferably >32 characters).
# Required: yes
NOTESNOOK_API_SECRET=
# Description: Use this flag to disable creation of new accounts on your instance (i.e. in case it is exposed to the Internet).
# Required: yes
# Possible values: true/false
DISABLE_SIGNUPS=false
### SMTP Configuration ###
# SMTP Configuration is required for sending emails for password reset, 2FA emails etc. You can get SMTP settings from your email provider.
# Description: Username for the SMTP connection (most time it is the email address of your account). Check your email provider's documentation to get the appropriate value.
# Required: yes
SMTP_USERNAME=
# Description: Password for the SMTP connection. Check your email provider's documentation to get the appropriate value.
# Required: yes
SMTP_PASSWORD=
# Description: Host on which the the SMTP connection is running. Check your email provider's documentation to get the appropriate value.
# Required: yes
# Example: smtp.gmail.com
SMTP_HOST=
# Description: Port on which the the SMTP connection is running. Check your email provider's documentation to get the appropriate value.
# Required: yes
# Example: 465
SMTP_PORT=
NOTESNOOK_SENDER_EMAIL= # optional
NOTESNOOK_SENDER_NAME= # optional
SMTP_REPLYTO_NAME= # optional
SMTP_REPLYTO_EMAIL= # optional
# MessageBird or Twilio are used for 2FA via SMS
# You can setup either of them or none of them but keep in mind
# that 2FA via SMS will not work if you haven't set up at least
# one SMS provider.
MESSAGEBIRD_ACCESS_KEY=
# Description: Twilio account SID is required for sending SMS with 2FA codes. Learn more here: https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-
# Required: no
TWILIO_ACCOUNT_SID=
# Description: Twilio account auth is required for sending SMS with 2FA codes. Learn more here: https://help.twilio.com/articles/223136027-Auth-Tokens-and-How-to-Change-Them
# Required: no
TWILIO_AUTH_TOKEN=
# Description: The unique string that we created to identify the Service resource.
# Required: no
# Example: VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
TWILIO_SERVICE_SID=
# Server discovery settings
# The domain must be without protocol
# e.g. example.org NOT http://example.org
NOTESNOOK_SERVER_DOMAIN=
IDENTITY_SERVER_DOMAIN=
SSE_SERVER_DOMAIN=
# Description: Add the origins for which you want to allow CORS. Leave it empty to allow all origins to access your server. If you want to allow multiple origins, seperate each origin with a comma.
# Required: no
# Example: https://app.notesnook.com,http://localhost:3000
NOTESNOOK_CORS_ORIGINS=
# Add the origins on which you want to enable CORS.
# Leave it empty to allow all origins to access your server.
# Seperate each origin with a comma
# e.g. https://app.notesnook.com,http://localhost:3000
NOTESNOOK_CORS_ORIGINS= # optional
# Description: Add known proxies for incoming HTTP requests
# Required: no
# Example: 192.168.1.2,192.168.1.3
KNOWN_PROXIES=
# url of the web app instance you want to use
# e.g. https://app.notesnook.com
# Note: no slashes at the end
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).
# Note: the URL has no slashes at the end
# Required: yes
# Example: https://app.notesnook.com
NOTESNOOK_APP_PUBLIC_URL=https://app.notesnook.com
# Description: This is the public URL for the monograph frontend.
# Required: yes
# Example: https://monogr.ph
MONOGRAPH_PUBLIC_URL=http://localhost:6264
# Description: This is the public URL for the Authentication server. Used for generating email confirmation & password reset URLs.
# Required: yes
# Example: https://auth.streetwriters.co
AUTH_SERVER_PUBLIC_URL=http://localhost:8264
# Description: This is the public URL for the S3 attachments server (minio). It'll be used by the Notesnook clients for uploading/downloading attachments.
# Required: yes
# Example: https://attachments.notesnook.com
ATTACHMENTS_SERVER_PUBLIC_URL=http://localhost:9000
# Minio is used for S3 storage
MINIO_ROOT_USER= # aka. AccessKeyId (must be > 3 characters)
MINIO_ROOT_PASSWORD= # aka. AccessKey (must be > 8 characters)
# If you don't want to use Minio, you can use any other S3 compatible
# storage service.
S3_ACCESS_KEY=
S3_ACCESS_KEY_ID=
S3_SERVICE_URL=
S3_REGION=
S3_BUCKET_NAME=attachments # required
# 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=

View File

@@ -10,8 +10,9 @@
name: Publish Docker images
on:
release:
types: [published]
push:
tags:
- "v*"
jobs:
push_to_registry:
@@ -22,12 +23,19 @@ jobs:
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
@@ -41,7 +49,7 @@ jobs:
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
platforms: linux/amd64,linux/arm64
- name: Log in to Docker Hub
uses: docker/login-action@v3
@@ -70,10 +78,10 @@ jobs:
id: push
uses: docker/build-push-action@v6
with:
context: .
context: ${{ matrix.repos.context }}
file: ${{ matrix.repos.file }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
cache-from: ${{ matrix.repos.image }}:latest

532
.gitignore vendored
View File

@@ -1,267 +1,267 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
keys/
dist/
keystore/
.env.local
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
keys/
dist/
keystore/
.env.local
Notesnook.API/sync/

6
.vscode/launch.json vendored
View File

@@ -9,7 +9,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-notesnook",
"program": "bin/Debug/net8.0/Notesnook.API.dll",
"program": "bin/Debug/net9.0/Notesnook.API.dll",
"args": [],
"cwd": "${workspaceFolder}/Notesnook.API",
"stopAtEntry": false,
@@ -24,7 +24,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-identity",
"program": "bin/Debug/net8.0/Streetwriters.Identity.dll",
"program": "bin/Debug/net9.0/Streetwriters.Identity.dll",
"args": [],
"cwd": "${workspaceFolder}/Streetwriters.Identity",
"stopAtEntry": false,
@@ -39,7 +39,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-messenger",
"program": "bin/Debug/net8.0/Streetwriters.Messenger.dll",
"program": "bin/Debug/net9.0/Streetwriters.Messenger.dll",
"args": [],
"cwd": "${workspaceFolder}/Streetwriters.Messenger",
"stopAtEntry": false,

82
.vscode/tasks.json vendored
View File

@@ -1,41 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-notesnook",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Notesnook.API/Notesnook.API.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-identity",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Streetwriters.Identity/Streetwriters.Identity.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-messenger",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Streetwriters.Messenger/Streetwriters.Messenger.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
]
}
{
"version": "2.0.0",
"tasks": [
{
"label": "build-notesnook",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Notesnook.API/Notesnook.API.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-identity",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Streetwriters.Identity/Streetwriters.Identity.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-messenger",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Streetwriters.Messenger/Streetwriters.Messenger.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
]
}

View File

@@ -1,3 +1,3 @@
Notesnook Sync Server is written & maintained by:
- Abdullah Atta <abdullahatta@streetwriters.co>
- Ammar Ahmed <ammarahmed6506@gmail.com>
Notesnook Sync Server is written & maintained by:
- Abdullah Atta <abdullahatta@streetwriters.co>
- Ammar Ahmed <ammarahmed6506@gmail.com>

View File

@@ -1,128 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@streetwriters.co.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
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/translations.
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@streetwriters.co.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
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/translations.

1322
LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ 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.Models;
@@ -43,6 +44,10 @@ namespace Notesnook.API.Accessors
public SyncItemsRepository Tags { get; }
public Repository<UserSettings> UsersSettings { 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,
@@ -71,22 +76,32 @@ namespace Notesnook.API.Accessors
[FromKeyedServices(Collections.TagsKey)]
IMongoCollection<SyncItem> tags,
Repository<UserSettings> usersSettings, Repository<Monograph> monographs)
Repository<UserSettings> usersSettings,
Repository<Monograph> monographs,
Repository<InboxApiKey> inboxApiKey,
Repository<InboxSyncItem> inboxItems,
Repository<SyncDevice> syncDevices,
Repository<DeviceIdsChunk> deviceIdsChunks,
ILogger<SyncItemsRepository> logger)
{
UsersSettings = usersSettings;
Monographs = monographs;
Notebooks = new SyncItemsRepository(dbContext, notebooks);
Notes = new SyncItemsRepository(dbContext, notes);
Contents = new SyncItemsRepository(dbContext, content);
Settings = new SyncItemsRepository(dbContext, settings);
LegacySettings = new SyncItemsRepository(dbContext, legacySettings);
Attachments = new SyncItemsRepository(dbContext, attachments);
Shortcuts = new SyncItemsRepository(dbContext, shortcuts);
Reminders = new SyncItemsRepository(dbContext, reminders);
Relations = new SyncItemsRepository(dbContext, relations);
Colors = new SyncItemsRepository(dbContext, colors);
Vaults = new SyncItemsRepository(dbContext, vaults);
Tags = new SyncItemsRepository(dbContext, tags);
InboxApiKey = inboxApiKey;
InboxItems = inboxItems;
SyncDevices = syncDevices;
DeviceIdsChunks = deviceIdsChunks;
Notebooks = new SyncItemsRepository(dbContext, notebooks, logger);
Notes = new SyncItemsRepository(dbContext, notes, logger);
Contents = new SyncItemsRepository(dbContext, content, logger);
Settings = new SyncItemsRepository(dbContext, settings, logger);
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);
}
}
}

View File

@@ -0,0 +1,101 @@
/*
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");
}
}
}
}

View File

@@ -1,36 +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 NotesnookUserRequirement : AuthorizationHandler<NotesnookUserRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotesnookUserRequirement requirement)
{
var isInAudience = context.User.HasClaim("aud", "notesnook");
var hasRole = context.User.HasClaim("role", "notesnook");
if (isInAudience && hasRole)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
/*
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 NotesnookUserRequirement : AuthorizationHandler<NotesnookUserRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NotesnookUserRequirement requirement)
{
var isInAudience = context.User.HasClaim("aud", "notesnook");
var hasRole = context.User.HasClaim("role", "notesnook");
if (isInAudience && hasRole)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

View File

@@ -1,63 +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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Notesnook.API.Authorization
{
public class ProUserRequirement : AuthorizationHandler<ProUserRequirement>, IAuthorizationRequirement
{
private readonly Dictionary<string, string> pathErrorPhraseMap = new()
{
["/s3"] = "upload attachments",
["/s3/multipart"] = "upload attachments",
};
private readonly string[] allowedClaims = ["trial", "premium", "premium_canceled"];
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProUserRequirement requirement)
{
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
var isProOrTrial = context.User.Claims.Any((c) => c.Type == "notesnook:status" && allowedClaims.Contains(c.Value));
if (isProOrTrial) context.Succeed(requirement);
else
{
var phrase = "continue";
foreach (var item in pathErrorPhraseMap)
{
if (path != null && path.StartsWithSegments(item.Key))
phrase = item.Value;
}
var error = $"Please upgrade to Pro to {phrase}.";
context.Fail(new AuthorizationFailureReason(this, error));
}
return Task.CompletedTask;
}
public override Task HandleAsync(AuthorizationHandlerContext context)
{
return this.HandleRequirementAsync(context, this);
}
}
}

View File

@@ -43,16 +43,16 @@ namespace Notesnook.API.Authorization
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
var result = this.IsAuthorized(context.User, path);
if (result.Succeeded) context.Succeed(requirement);
else if (result.AuthorizationFailure.FailureReasons.Any())
else if (result.AuthorizationFailure?.FailureReasons.Any() == true)
context.Fail(result.AuthorizationFailure.FailureReasons.First());
else context.Fail();
return Task.CompletedTask;
}
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal User, PathString requestPath)
public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal? User, PathString requestPath)
{
var id = User.FindFirstValue("sub");
var id = User?.FindFirstValue("sub");
if (string.IsNullOrEmpty(id))
{
@@ -63,11 +63,11 @@ namespace Notesnook.API.Authorization
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
}
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
var isInAudience = User.HasClaim("aud", "notesnook");
var hasRole = User.HasClaim("role", "notesnook");
var hasSyncScope = User?.HasClaim("scope", "notesnook.sync") ?? false;
var isInAudience = User?.HasClaim("aud", "notesnook") ?? false;
var hasRole = User?.HasClaim("role", "notesnook") ?? false;
var isEmailVerified = User.HasClaim("verified", "true");
var isEmailVerified = User?.HasClaim("verified", "true") ?? false;
if (!isEmailVerified)
{

View File

@@ -14,5 +14,9 @@ namespace Notesnook.API
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";
}
}

View File

@@ -1,69 +1,82 @@
/*
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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using Notesnook.API.Models;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Controllers
{
// TODO: this should be moved out into its own microservice
[ApiController]
[Route("announcements")]
public class AnnouncementController : ControllerBase
{
private Repository<Announcement> Announcements { get; set; }
public AnnouncementController(Repository<Announcement> announcements)
{
Announcements = announcements;
}
[HttpGet("active")]
[AllowAnonymous]
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string userId)
{
var totalActive = await Announcements.Collection.CountDocumentsAsync(Builders<Announcement>.Filter.Eq("IsActive", true));
if (totalActive <= 0) return Ok(new Announcement[] { });
var announcements = (await Announcements.FindAsync((a) => a.IsActive)).Where((a) => a.UserIds == null || a.UserIds.Length == 0 || a.UserIds.Contains(userId));
foreach (var announcement in announcements)
{
if (announcement.UserIds != null && !announcement.UserIds.Contains(userId)) continue;
foreach (var item in announcement.Body)
{
if (item.Type != "callToActions") continue;
foreach (var action in item.Actions)
{
if (action.Type != "link") continue;
action.Data = action.Data.Replace("{{UserId}}", userId ?? "0");
}
}
}
return Ok(announcements);
}
}
}
/*
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 AngleSharp.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using Notesnook.API.Accessors;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
using Streetwriters.Data.Repositories;
namespace Notesnook.API.Controllers
{
// TODO: this should be moved out into its own microservice
[ApiController]
[Route("announcements")]
public class AnnouncementController(Repository<Announcement> announcements, WampServiceAccessor serviceAccessor) : ControllerBase
{
[HttpGet("active")]
[AllowAnonymous]
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string? userId)
{
var filter = Builders<Announcement>.Filter.Eq(x => x.IsActive, true);
if (!string.IsNullOrEmpty(userId))
{
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);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
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 });
}
}
}
}

View File

@@ -18,14 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
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.Services;
using Streetwriters.Common;
using Streetwriters.Common.Helpers;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Data.Interfaces;
using Streetwriters.Data.Repositories;
@@ -34,86 +46,174 @@ namespace Notesnook.API.Controllers
[ApiController]
[Route("monographs")]
[Authorize("Sync")]
public class MonographsController : ControllerBase
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer, SyncDeviceService syncDeviceService, ILogger<MonographsController> logger) : ControllerBase
{
private Repository<Monograph> Monographs { get; set; }
private readonly IUnitOfWork unit;
const string SVG_PIXEL = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><circle r='9'/></svg>";
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)
{
Monographs = monographs;
unit = unitOfWork;
var userIdFilter = Builders<Monograph>.Filter.Eq("UserId", userId);
monograph.ItemId ??= monograph.Id;
return ObjectId.TryParse(monograph.ItemId, out ObjectId id)
? Builders<Monograph>.Filter
.And(userIdFilter,
Builders<Monograph>.Filter.Or(
Builders<Monograph>.Filter.Eq("_id", id), Builders<Monograph>.Filter.Eq("ItemId", monograph.ItemId)
)
)
: Builders<Monograph>.Filter
.And(userIdFilter,
Builders<Monograph>.Filter.Eq("ItemId", monograph.ItemId)
);
}
private static FilterDefinition<Monograph> CreateMonographFilter(string itemId)
{
return ObjectId.TryParse(itemId, out ObjectId id)
? Builders<Monograph>.Filter.Or(
Builders<Monograph>.Filter.Eq("_id", id),
Builders<Monograph>.Filter.Eq("ItemId", itemId))
: Builders<Monograph>.Filter.Eq("ItemId", itemId);
}
private async Task<Monograph> FindMonographAsync(string userId, Monograph monograph)
{
var result = await monographs.Collection.FindAsync(CreateMonographFilter(userId, monograph), new FindOptions<Monograph>
{
Limit = 1
});
return await result.FirstOrDefaultAsync();
}
private async Task<Monograph> FindMonographAsync(string itemId)
{
var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions<Monograph>
{
Limit = 1
});
return await result.FirstOrDefaultAsync();
}
[HttpPost]
public async Task<IActionResult> PublishAsync([FromBody] Monograph monograph)
public async Task<IActionResult> PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
{
var userId = this.User.FindFirstValue("sub");
if (userId == null) return Unauthorized();
if (await Monographs.GetAsync(monograph.Id) != null) return base.Conflict("This monograph is already published.");
if (monograph.EncryptedContent == null)
monograph.CompressedContent = monograph.Content.CompressBrotli();
monograph.UserId = userId;
monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (monograph.EncryptedContent?.Cipher.Length > MAX_DOC_SIZE || monograph.CompressedContent?.Length > MAX_DOC_SIZE)
return base.BadRequest("Monograph is too big. Max allowed size is 15mb.");
Monographs.Insert(monograph);
if (!await unit.Commit()) return BadRequest();
return Ok(new
try
{
id = monograph.Id
});
var userId = this.User.GetUserId();
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]
public async Task<IActionResult> UpdateAsync([FromBody] Monograph monograph)
public async Task<IActionResult> UpdateAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
{
if (await Monographs.GetAsync(monograph.Id) == null) 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 = 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
try
{
id = monograph.Id
});
var userId = this.User.GetUserId();
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]
public async Task<IActionResult> GetUserMonographsAsync()
{
var userId = this.User.FindFirstValue("sub");
if (userId == null) return Unauthorized();
var userId = this.User.GetUserId();
var monographs = (await Monographs.Collection.FindAsync(Builders<Monograph>.Filter.Eq("UserId", userId), new FindOptions<Monograph, ObjectWithId>
{
Projection = Builders<Monograph>.Projection.Include("_id"),
})).ToEnumerable();
return Ok(monographs.Select((m) => m.Id));
var userMonographs = (await monographs.Collection.FindAsync(
Builders<Monograph>.Filter.And(
Builders<Monograph>.Filter.Eq("UserId", userId),
Builders<Monograph>.Filter.Ne("Deleted", true)
)
, new FindOptions<Monograph, ObjectWithId>
{
Projection = Builders<Monograph>.Projection.Include("_id").Include("ItemId"),
})).ToEnumerable();
return Ok(userMonographs.Select((m) => m.ItemId ?? m.Id));
}
[HttpGet("{id}")]
[AllowAnonymous]
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
{
var monograph = await Monographs.GetAsync(id);
if (monograph == null)
var monograph = await FindMonographAsync(id);
if (monograph == null || monograph.Deleted)
{
return NotFound(new
{
@@ -123,36 +223,162 @@ namespace Notesnook.API.Controllers
}
if (monograph.EncryptedContent == null)
monograph.Content = monograph.CompressedContent.DecompressBrotli();
monograph.Content = monograph.CompressedContent?.DecompressBrotli();
monograph.ItemId ??= monograph.Id;
return Ok(monograph);
}
[HttpGet("{id}/destruct")]
[HttpGet("{id}/view")]
[AllowAnonymous]
public async Task<IActionResult> DestructMonographAsync([FromRoute] string id)
public async Task<IActionResult> TrackView([FromRoute] string id)
{
var monograph = await Monographs.GetAsync(id);
if (monograph == null)
{
return NotFound(new
{
error = "invalid_id",
error_description = $"No such monograph found."
});
}
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.DeleteByIdAsync(monograph.Id);
{
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)
);
return Ok();
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}")]
public async Task<IActionResult> DeleteAsync([FromRoute] string id)
public async Task<IActionResult> DeleteAsync([FromQuery] string? deviceId, [FromRoute] string id)
{
Monographs.DeleteById(id);
if (!await unit.Commit()) return BadRequest();
var userId = this.User.GetUserId();
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();
}
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;
}
}
}
}

View File

@@ -17,110 +17,199 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Amazon.S3.Model;
using System.Threading.Tasks;
using System.Security.Claims;
using Notesnook.API.Interfaces;
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.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Notesnook.API.Accessors;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Controllers
{
[ApiController]
[Route("s3")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public class S3Controller : ControllerBase
[Authorize("Sync")]
public class S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor repositories, WampServiceAccessor serviceAccessor, ILogger<S3Controller> logger) : ControllerBase
{
private IS3Service S3Service { get; set; }
public S3Controller(IS3Service s3Service)
{
S3Service = s3Service;
}
[HttpPut]
[Authorize("Pro")]
public IActionResult Upload([FromQuery] string name)
{
var userId = this.User.FindFirstValue("sub");
var url = S3Service.GetUploadObjectUrl(userId, name);
if (url == null) return BadRequest("Could not create signed url.");
return Ok(url);
}
[HttpGet("multipart")]
[Authorize("Pro")]
public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string uploadId)
{
var userId = this.User.FindFirstValue("sub");
try
{
var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
return Ok(meta);
}
catch (Exception ex) { return BadRequest(ex.Message); }
}
[HttpDelete("multipart")]
[Authorize("Pro")]
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
{
var userId = this.User.FindFirstValue("sub");
try
{
await S3Service.AbortMultipartUploadAsync(userId, name, uploadId);
return Ok();
}
catch (Exception ex) { return BadRequest(ex.Message); }
}
[HttpPost("multipart")]
[Authorize("Pro")]
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequest uploadRequest)
{
var userId = this.User.FindFirstValue("sub");
try
{
await S3Service.CompleteMultipartUploadAsync(userId, uploadRequest);
return Ok();
}
catch (Exception ex) { return BadRequest(ex.Message); }
}
[HttpGet]
[Authorize("Sync")]
public IActionResult Download([FromQuery] string name)
{
var userId = this.User.FindFirstValue("sub");
var url = S3Service.GetDownloadObjectUrl(userId, name);
if (url == null) return BadRequest("Could not create signed url.");
return Ok(url);
}
[HttpHead]
[Authorize("Sync")]
public async Task<IActionResult> Info([FromQuery] string name)
{
var userId = this.User.FindFirstValue("sub");
var size = await S3Service.GetObjectSizeAsync(userId, name);
HttpContext.Response.Headers.ContentLength = size;
return Ok();
}
[HttpDelete]
[Authorize("Sync")]
public async Task<IActionResult> DeleteAsync([FromQuery] string name)
public async Task<IActionResult> Upload([FromQuery] string name)
{
try
{
var userId = this.User.FindFirstValue("sub");
await S3Service.DeleteObjectAsync(userId, name);
var userId = this.User.GetUserId();
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)
{
return BadRequest(ex.Message);
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")]
public async Task<IActionResult> MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string? uploadId)
{
var userId = this.User.GetUserId();
try
{
var meta = await s3Service.StartMultipartUploadAsync(userId, name, parts, uploadId);
return Ok(meta);
}
catch (Exception ex)
{
logger.LogError(ex, "Error starting multipart upload for user.");
return BadRequest(new { error = "Failed to start multipart upload." });
}
}
[HttpDelete("multipart")]
public async Task<IActionResult> AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId)
{
var userId = this.User.GetUserId();
try
{
await s3Service.AbortMultipartUploadAsync(userId, name, uploadId);
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Error aborting multipart upload for user.");
return BadRequest(new { error = "Failed to abort multipart upload." });
}
}
[HttpPost("multipart")]
public async Task<IActionResult> CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequestWrapper uploadRequestWrapper)
{
var userId = this.User.GetUserId();
try
{
await s3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest());
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Error completing multipart upload for user.");
return BadRequest(new { error = "Failed to complete multipart upload." });
}
}
[HttpGet]
public async Task<IActionResult> Download([FromQuery] string name)
{
try
{
var userId = this.User.GetUserId();
var url = await s3Service.GetDownloadObjectUrlAsync(userId, name);
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]
public async Task<IActionResult> Info([FromQuery] string name)
{
try
{
var userId = this.User.GetUserId();
var size = await s3Service.GetObjectSizeAsync(userId, name);
HttpContext.Response.Headers.ContentLength = size;
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting object info for user.");
return BadRequest(new { error = "Failed to get attachment info." });
}
}
[HttpDelete]
public async Task<IActionResult> DeleteAsync([FromQuery] string name)
{
try
{
var userId = this.User.GetUserId();
await s3Service.DeleteObjectAsync(userId, name);
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Error deleting object for user.");
return BadRequest(new { error = "Failed to delete attachment." });
}
}
}

View File

@@ -24,6 +24,7 @@ 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;
@@ -36,20 +37,20 @@ namespace Notesnook.API.Controllers
[ApiController]
[Authorize]
[Route("devices")]
public class SyncDeviceController : ControllerBase
public class SyncDeviceController(SyncDeviceService syncDeviceService, ILogger<SyncDeviceController> logger) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> RegisterDevice([FromQuery] string deviceId)
{
try
{
var userId = this.User.FindFirstValue("sub");
new SyncDeviceService(new SyncDevice(ref userId, ref deviceId)).RegisterDevice();
var userId = this.User.GetUserId();
await syncDeviceService.RegisterDeviceAsync(userId, deviceId);
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't register device.", ex.ToString());
logger.LogError(ex, "Failed to register device: {DeviceId}", deviceId);
return BadRequest(new { error = ex.Message });
}
}
@@ -60,13 +61,13 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub");
new SyncDeviceService(new SyncDevice(ref userId, ref deviceId)).UnregisterDevice();
var userId = this.User.GetUserId();
await syncDeviceService.UnregisterDeviceAsync(userId, deviceId);
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't unregister device.", ex.ToString());
logger.LogError(ex, "Failed to unregister device: {DeviceId}", deviceId);
return BadRequest(new { error = ex.Message });
}
}

View File

@@ -1,116 +1,116 @@
/*
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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.AspNetCore.Mvc;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Streetwriters.Common;
namespace Notesnook.API.Controllers
{
[ApiController]
[Authorize]
[Route("users")]
public class UsersController(IUserService UserService) : ControllerBase
{
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Signup()
{
try
{
await UserService.CreateUserAsync();
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(Signup), "Couldn't sign up.", ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> GetUser()
{
var userId = User.FindFirstValue("sub");
try
{
UserResponse response = await UserService.GetUserAsync(userId);
if (!response.Success) return BadRequest(response);
return Ok(response);
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't get user for id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserResponse user)
{
var userId = User.FindFirstValue("sub");
try
{
if (user.AttachmentsKey != null)
await UserService.SetUserAttachmentsKeyAsync(userId, user.AttachmentsKey);
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't update user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("reset")]
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
{
var userId = this.User.FindFirstValue("sub");
if (await UserService.ResetUserAsync(userId, removeAttachments))
return Ok();
return BadRequest();
}
[HttpPost("delete")]
[RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
{
var userId = this.User.FindFirstValue("sub");
var jti = User.FindFirstValue("jti");
try
{
await UserService.DeleteUserAsync(userId, jti, form.Password);
return Ok();
}
catch (Exception ex)
{
await Slogger<UsersController>.Error(nameof(GetUser), "Couldn't delete user with id.", userId, ex.ToString());
return BadRequest(new { error = ex.Message });
}
}
}
}
/*
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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Streetwriters.Common;
namespace Notesnook.API.Controllers
{
[ApiController]
[Authorize]
[Route("users")]
public class UsersController(IUserService UserService, ILogger<UsersController> logger) : ControllerBase
{
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Signup()
{
try
{
await UserService.CreateUserAsync();
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to sign up user");
return BadRequest(new { error = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> GetUser()
{
var userId = User.GetUserId();
try
{
UserResponse response = await UserService.GetUserAsync(userId);
if (!response.Success) return BadRequest();
return Ok(response);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to get user with id: {UserId}", userId);
return BadRequest(new { error = ex.Message });
}
}
[HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserKeys keys)
{
var userId = User.GetUserId();
try
{
await UserService.SetUserKeysAsync(userId, keys);
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update user with id: {UserId}", userId);
return BadRequest(new { error = ex.Message });
}
}
[HttpPost("reset")]
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
{
var userId = this.User.GetUserId();
if (await UserService.ResetUserAsync(userId, removeAttachments))
return Ok();
return BadRequest();
}
[HttpPost("delete")]
[RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
{
var userId = this.User.GetUserId();
var jti = User.FindFirstValue("jti");
try
{
await UserService.DeleteUserAsync(userId, jti, form.Password);
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to delete user with id: {UserId}", userId);
return BadRequest(new { error = ex.Message });
}
}
}
}

View File

@@ -1,52 +1,50 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG TARGETARCH
ARG BUILDPLATFORM
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
WORKDIR /src
COPY Streetwriters.Data/*.csproj ./Streetwriters.Data/
COPY Streetwriters.Common/*.csproj ./Streetwriters.Common/
COPY Notesnook.API/*.csproj ./Notesnook.API/
# restore dependencies
RUN dotnet restore -v d /src/Notesnook.API/Notesnook.API.csproj --use-current-runtime
COPY Streetwriters.Data/ ./Streetwriters.Data/
COPY Streetwriters.Common/ ./Streetwriters.Common/
COPY Notesnook.API/ ./Notesnook.API/
WORKDIR /src/Notesnook.API/
RUN dotnet build -c Release -o /app/build -a $TARGETARCH
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish \
#--runtime alpine-x64 \
--self-contained true \
/p:TrimMode=partial \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true \
/p:JsonSerializerIsReflectionEnabledByDefault=true \
-a $TARGETARCH
FROM --platform=$BUILDPLATFORM base AS final
ARG TARGETARCH
ARG BUILDPLATFORM
# create a new user and change directory ownership
RUN adduser --disabled-password \
--home /app \
--gecos '' dotnetuser && chown -R dotnetuser /app
# impersonate into the new user
USER dotnetuser
WORKDIR /app
COPY --from=publish /app/publish .
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-alpine AS base
WORKDIR /app
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
ARG TARGETARCH
ARG BUILDPLATFORM
ENV DOTNET_TC_QuickJitForLoops="1" DOTNET_ReadyToRun="0" DOTNET_TieredPGO="1" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="true"
WORKDIR /src
COPY Streetwriters.Data/*.csproj ./Streetwriters.Data/
COPY Streetwriters.Common/*.csproj ./Streetwriters.Common/
COPY Notesnook.API/*.csproj ./Notesnook.API/
# restore dependencies
RUN dotnet restore -v d /src/Notesnook.API/Notesnook.API.csproj --use-current-runtime
COPY Streetwriters.Data/ ./Streetwriters.Data/
COPY Streetwriters.Common/ ./Streetwriters.Common/
COPY Notesnook.API/ ./Notesnook.API/
WORKDIR /src/Notesnook.API/
RUN dotnet build -c Release -o /app/build -a $TARGETARCH
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish \
#--runtime alpine-x64 \
--self-contained true \
/p:TrimMode=partial \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true \
/p:JsonSerializerIsReflectionEnabledByDefault=true \
-a $TARGETARCH
FROM --platform=$BUILDPLATFORM base AS final
ARG TARGETARCH
ARG BUILDPLATFORM
# create a new user and change directory ownership
RUN adduser --disabled-password \
--home /app \
--gecos '' dotnetuser && chown -R dotnetuser /app
# impersonate into the new user
USER dotnetuser
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["./Notesnook.API"]

View File

@@ -7,19 +7,17 @@ public sealed class SyncEventCounterSource : EventSource
{
public static readonly SyncEventCounterSource Log = new();
private Meter meter = new("Notesnook.API.Metrics.Sync", "1.0.0");
private Counter<int> fetchCounter;
private Counter<int> pushCounter;
private Counter<int> legacyFetchCounter;
private Counter<int> pushV2Counter;
private Counter<int> fetchV2Counter;
private Histogram<long> fetchV2Duration;
private Histogram<long> pushV2Duration;
private 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");
legacyFetchCounter = meter.CreateCounter<int>("sync.legacy-fetches", "fetches", "Total legacy fetches");
fetchV2Counter = meter.CreateCounter<int>("sync.v2.fetches", "fetches", "Total v2 fetches");
pushV2Counter = meter.CreateCounter<int>("sync.v2.pushes", "pushes", "Total v2 pushes");
fetchV2Duration = meter.CreateHistogram<long>("sync.v2.fetch_duration");
@@ -27,7 +25,6 @@ public sealed class SyncEventCounterSource : EventSource
}
public void Fetch() => fetchCounter.Add(1);
public void LegacyFetch() => legacyFetchCounter.Add(1);
public void FetchV2() => fetchV2Counter.Add(1);
public void PushV2() => pushV2Counter.Add(1);
public void Push() => pushCounter.Add(1);
@@ -36,14 +33,7 @@ public sealed class SyncEventCounterSource : EventSource
protected override void Dispose(bool disposing)
{
legacyFetchCounter = null;
fetchV2Counter = null;
pushV2Counter = null;
pushCounter = null;
fetchCounter = null;
meter.Dispose();
meter = null;
base.Dispose(disposing);
}
}

View File

@@ -1,71 +1,62 @@
/*
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.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
namespace Notesnook.API.Extensions
{
public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
{
private readonly IAuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultTransformer()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(
RequestDelegate requestDelegate,
HttpContext httpContext,
AuthorizationPolicy authorizationPolicy,
PolicyAuthorizationResult policyAuthorizationResult)
{
var isWebsocket = httpContext.Request.Headers.Upgrade == "websocket";
if (!isWebsocket && policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
{
var error = string.Join("\n", policyAuthorizationResult.AuthorizationFailure.FailureReasons.Select((r) => r.Message));
if (!string.IsNullOrEmpty(error))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
return;
}
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
else if (isWebsocket)
{
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, PolicyAuthorizationResult.Success());
}
else
{
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
}
/*
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.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
namespace Notesnook.API.Extensions
{
public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
{
private readonly IAuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultTransformer()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(
RequestDelegate requestDelegate,
HttpContext httpContext,
AuthorizationPolicy authorizationPolicy,
PolicyAuthorizationResult policyAuthorizationResult)
{
if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
{
var error = string.Join("\n", policyAuthorizationResult.AuthorizationFailure.FailureReasons.Select((r) => r.Message));
if (!string.IsNullOrEmpty(error))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(new { error }));
return;
}
}
await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
}

View File

@@ -0,0 +1,17 @@
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.");
}
}

View File

@@ -1,44 +1,44 @@
/*
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;
using System.Threading.Tasks;
namespace MongoDB.Driver
{
public static class TransactionHelper
{
public static async Task StartTransaction(this IMongoClient client, Action<CancellationToken> operate, CancellationToken ct)
{
using (var session = await client.StartSessionAsync())
{
var transactionOptions = new TransactionOptions(readPreference: ReadPreference.Nearest, readConcern: ReadConcern.Local, writeConcern: WriteConcern.WMajority);
await session.WithTransactionAsync((handle, token) =>
{
return Task.Run(() =>
{
operate(token);
return true;
});
}, transactionOptions, ct);
}
}
}
/*
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;
using System.Threading.Tasks;
namespace MongoDB.Driver
{
public static class TransactionHelper
{
public static async Task StartTransaction(this IMongoClient client, Action<CancellationToken> operate, CancellationToken ct)
{
using (var session = await client.StartSessionAsync())
{
var transactionOptions = new TransactionOptions(readPreference: ReadPreference.Nearest, readConcern: ReadConcern.Local, writeConcern: WriteConcern.WMajority);
await session.WithTransactionAsync((handle, token) =>
{
return Task.Run(() =>
{
operate(token);
return true;
});
}, transactionOptions, ct);
}
}
}
}

View File

@@ -0,0 +1,414 @@
/*
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();
}
}
}

View File

@@ -0,0 +1,79 @@
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]);
}
}
}

View File

@@ -1,473 +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.Runtime.CompilerServices;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using MongoDB.Driver;
using Notesnook.API.Authorization;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Repositories;
using Streetwriters.Common.Models;
using Streetwriters.Data.Interfaces;
namespace Notesnook.API.Hubs
{
public struct RunningPush
{
public long Timestamp { get; set; }
public long Validity { get; set; }
public string ConnectionId { get; set; }
}
public interface ISyncHubClient
{
Task PushItems(SyncTransferItemV2 transferItem);
Task<bool> SendItems(SyncTransferItemV2 transferItem);
Task PushCompleted(long lastSynced);
}
public class GlobalSync
{
private const long PUSH_VALIDITY_EXTENSION_PERIOD = 16 * 1000; // 16 second
private const int PUSH_VALIDITY_PERIOD_PER_ITEM = 5 * 100; // 0.5 second
private const long BASE_PUSH_VALIDITY_PERIOD = 5 * 1000; // 5 seconds
private const long BASE_PUSH_VALIDITY_PERIOD_NEW = 16 * 1000; // 16 seconds
private readonly static Dictionary<string, List<RunningPush>> PushOperations = new();
public static void ClearPushOperations(string userId, string connectionId)
{
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations.ToArray())
if (push.ConnectionId == connectionId || !IsPushValid(push, now))
operations.Remove(push);
}
}
public static bool IsPushing(string userId, string connectionId)
{
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations)
if (push.ConnectionId == connectionId && IsPushValid(push, now)) return true;
}
return false;
}
public static bool IsUserPushing(string userId)
{
var count = 0;
if (PushOperations.TryGetValue(userId, out List<RunningPush> operations))
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var push in operations)
if (IsPushValid(push, now)) ++count;
}
return count > 0;
}
public static void StartPush(string userId, string connectionId, long? totalItems = null)
{
if (IsPushing(userId, connectionId)) return;
if (!PushOperations.ContainsKey(userId))
PushOperations[userId] = new List<RunningPush>();
PushOperations[userId].Add(new RunningPush
{
ConnectionId = connectionId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Validity = totalItems.HasValue ? BASE_PUSH_VALIDITY_PERIOD + (totalItems.Value * PUSH_VALIDITY_PERIOD_PER_ITEM) : BASE_PUSH_VALIDITY_PERIOD_NEW
});
}
public static void ExtendPush(string userId, string connectionId)
{
if (!IsPushing(userId, connectionId) || !PushOperations.ContainsKey(userId))
{
StartPush(userId, connectionId);
return;
}
var index = PushOperations[userId].FindIndex((push) => push.ConnectionId == connectionId);
if (index < 0)
{
StartPush(userId, connectionId);
return;
}
var pushOperation = PushOperations[userId][index];
pushOperation.Validity += PUSH_VALIDITY_EXTENSION_PERIOD;
}
private static bool IsPushValid(RunningPush push, long now)
{
return now < push.Timestamp + push.Validity;
}
}
[Authorize("Sync")]
public class SyncHub : Hub<ISyncHubClient>
{
private ISyncItemsRepositoryAccessor Repositories { get; }
private readonly IUnitOfWork unit;
private readonly string[] CollectionKeys = new[] {
"settings",
"attachment",
"note",
"notebook",
"content",
"shortcut",
"reminder",
"relation", // relations must sync at the end to prevent invalid state
};
public SyncHub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
{
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)
{
try
{
await base.OnDisconnectedAsync(exception);
}
finally
{
var id = Context.User.FindFirstValue("sub");
GlobalSync.ClearPushOperations(id, Context.ConnectionId);
}
}
private Action<SyncItem, string, long> MapTypeToUpsertAction(string type)
{
return type switch
{
"attachment" => Repositories.Attachments.Upsert,
"note" => Repositories.Notes.Upsert,
"notebook" => Repositories.Notebooks.Upsert,
"content" => Repositories.Contents.Upsert,
"shortcut" => Repositories.Shortcuts.Upsert,
"reminder" => Repositories.Reminders.Upsert,
"relation" => Repositories.Relations.Upsert,
_ => null,
};
}
public async Task<long> InitializePush(SyncMetadata syncMetadata)
{
if (syncMetadata.LastSynced <= 0) throw new HubException("Last synced time cannot be zero or less than zero.");
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) return 0;
UserSettings userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
long dateSynced = Math.Max(syncMetadata.LastSynced, userSettings.LastSynced);
GlobalSync.StartPush(userId, Context.ConnectionId);
if (
(userSettings.VaultKey != null &&
syncMetadata.VaultKey != null &&
!userSettings.VaultKey.Equals(syncMetadata.VaultKey) &&
!syncMetadata.VaultKey.IsEmpty()) ||
(userSettings.VaultKey == null &&
syncMetadata.VaultKey != null &&
!syncMetadata.VaultKey.IsEmpty()))
{
userSettings.VaultKey = syncMetadata.VaultKey;
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
}
return dateSynced;
}
public async Task<int> PushItems(SyncTransferItemV2 pushItem, long dateSynced)
{
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) return 0;
SyncEventCounterSource.Log.Push();
try
{
var others = Clients.OthersInGroup(userId);
others.PushItems(pushItem);
GlobalSync.ExtendPush(userId, Context.ConnectionId);
if (pushItem.Type == "settings")
{
var settings = pushItem.Items.First();
if (settings == null) return 0;
settings.Id = MongoDB.Bson.ObjectId.Parse(userId);
settings.ItemId = userId;
Repositories.LegacySettings.Upsert(settings, userId, dateSynced);
}
else
{
var UpsertItem = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception("Invalid item type.");
foreach (var item in pushItem.Items)
{
UpsertItem(item, userId, dateSynced);
}
}
return await unit.Commit() ? 1 : 0;
}
catch (Exception ex)
{
GlobalSync.ClearPushOperations(userId, Context.ConnectionId);
throw ex;
}
}
public async Task<bool> SyncCompleted(long dateSynced)
{
var userId = Context.User.FindFirstValue("sub");
try
{
UserSettings userSettings = await this.Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
long lastSynced = Math.Max(dateSynced, userSettings.LastSynced);
userSettings.LastSynced = lastSynced;
await this.Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
await Clients.OthersInGroup(userId).PushCompleted(lastSynced);
return true;
}
finally
{
GlobalSync.ClearPushOperations(userId, Context.ConnectionId);
}
}
private static async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(Func<string, long, int, Task<IAsyncCursor<SyncItem>>>[] collections, string[] types, string userId, long lastSyncedTimestamp, int size, long maxBytes, int skipChunks)
{
var chunksProcessed = 0;
for (int i = 0; i < collections.Length; i++)
{
var type = types[i];
using var cursor = await collections[i](userId, lastSyncedTimestamp, size);
var chunk = new List<SyncItem>();
long totalBytes = 0;
long METADATA_BYTES = 5 * 1024;
while (await cursor.MoveNextAsync())
{
if (chunksProcessed++ < skipChunks) continue;
foreach (var item in cursor.Current)
{
chunk.Add(item);
totalBytes += item.Length + METADATA_BYTES;
if (totalBytes >= maxBytes)
{
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = chunksProcessed
};
totalBytes = 0;
chunk.Clear();
}
}
}
if (chunk.Count > 0)
{
if (chunksProcessed++ < skipChunks) continue;
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = chunksProcessed
};
}
}
}
public Task<SyncMetadata> RequestFetch(long lastSyncedTimestamp)
{
return RequestResumableFetch(lastSyncedTimestamp);
}
public async Task<SyncMetadata> RequestResumableFetch(long lastSyncedTimestamp, int cursor = 0)
{
var userId = Context.User.FindFirstValue("sub");
if (GlobalSync.IsUserPushing(userId))
{
throw new HubException("Cannot fetch data while another sync is in progress. Please try again later.");
}
SyncEventCounterSource.Log.Fetch();
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
if (userSettings.LastSynced > 0 && lastSyncedTimestamp > userSettings.LastSynced)
{
throw new HubException($"Provided timestamp value is too large. Server timestamp: {userSettings.LastSynced} Sent timestamp: {lastSyncedTimestamp}. Please run a Force Sync to fix this issue.");
}
// var client = Clients.Caller;
if (lastSyncedTimestamp > 0 && userSettings.LastSynced == lastSyncedTimestamp)
{
return new SyncMetadata
{
LastSynced = userSettings.LastSynced,
};
}
var isResumable = lastSyncedTimestamp == 0;
if (!isResumable) cursor = 0;
var chunks = PrepareChunks(
collections: new[] {
Repositories.LegacySettings.FindItemsSyncedAfter,
Repositories.Attachments.FindItemsSyncedAfter,
Repositories.Notes.FindItemsSyncedAfter,
Repositories.Notebooks.FindItemsSyncedAfter,
Repositories.Contents.FindItemsSyncedAfter,
Repositories.Shortcuts.FindItemsSyncedAfter,
Repositories.Reminders.FindItemsSyncedAfter,
Repositories.Relations.FindItemsSyncedAfter,
},
types: CollectionKeys,
userId,
lastSyncedTimestamp,
size: 1000,
maxBytes: 7 * 1024 * 1024,
skipChunks: cursor
);
await foreach (var chunk in chunks)
{
_ = await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10));
}
return new SyncMetadata
{
VaultKey = userSettings.VaultKey,
LastSynced = userSettings.LastSynced,
};
}
}
[MessagePack.MessagePackObject]
public struct BatchedSyncTransferItem
{
[MessagePack.Key("lastSynced")]
public long LastSynced { get; set; }
[MessagePack.Key("items")]
public string[] Items { get; set; }
[MessagePack.Key("types")]
public string[] Types { get; set; }
[MessagePack.Key("total")]
public int Total { get; set; }
[MessagePack.Key("current")]
public int Current { get; set; }
}
[MessagePack.MessagePackObject]
public struct SyncTransferItem
{
[MessagePack.Key("synced")]
public bool Synced { get; set; }
[MessagePack.Key("lastSynced")]
public long LastSynced { get; set; }
[MessagePack.Key("item")]
public string Item { get; set; }
[MessagePack.Key("itemType")]
public string ItemType { get; set; }
[MessagePack.Key("total")]
public int Total { get; set; }
[MessagePack.Key("current")]
public int Current { get; set; }
}
[MessagePack.MessagePackObject]
public struct SyncTransferItemV2
{
[MessagePack.Key("items")]
[JsonPropertyName("items")]
public IEnumerable<SyncItem> Items { get; set; }
[MessagePack.Key("type")]
[JsonPropertyName("type")]
public string Type { get; set; }
[MessagePack.Key("count")]
[JsonPropertyName("count")]
public int Count { get; set; }
}
[MessagePack.MessagePackObject]
public struct SyncMetadata
{
[MessagePack.Key("vaultKey")]
[JsonPropertyName("vaultKey")]
public EncryptedData VaultKey { get; set; }
[MessagePack.Key("lastSynced")]
[JsonPropertyName("lastSynced")]
public long LastSynced { get; set; }
// [MessagePack.Key("total")]
// public long TotalItems { get; set; }
}
}

View File

@@ -18,18 +18,18 @@ 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.Diagnostics.Metrics;
using System.Linq;
using System.Linq.Expressions;
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Notesnook.API.Authorization;
using Notesnook.API.Interfaces;
@@ -43,15 +43,19 @@ namespace Notesnook.API.Hubs
{
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("Sync")]
[Authorize]
public class SyncV2Hub : Hub<ISyncV2HubClient>
{
private ISyncItemsRepositoryAccessor Repositories { get; }
private SyncDeviceService SyncDeviceService { get; }
private readonly IUnitOfWork unit;
private readonly string[] CollectionKeys = [
private static readonly string[] CollectionKeys = [
"settingitem",
"attachment",
"note",
@@ -64,11 +68,43 @@ namespace Notesnook.API.Hubs
"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)
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()
@@ -76,97 +112,65 @@ namespace Notesnook.API.Hubs
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2"));
if (!result.Succeeded)
{
var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault();
var reason = result.AuthorizationFailure?.FailureReasons.FirstOrDefault();
throw new HubException(reason?.Message ?? "Unauthorized");
}
var id = Context.User.FindFirstValue("sub");
var id = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
await Groups.AddToGroupAsync(Context.ConnectionId, id);
await base.OnConnectedAsync();
}
private Action<IEnumerable<SyncItem>, string, long> MapTypeToUpsertAction(string type)
{
return type switch
{
"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,
_ => null,
};
}
private Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> MapTypeToFindItemsAction(string type)
{
return type switch
{
"settingitem" => Repositories.Settings.FindItemsById,
"attachment" => Repositories.Attachments.FindItemsById,
"note" => Repositories.Notes.FindItemsById,
"notebook" => Repositories.Notebooks.FindItemsById,
"content" => Repositories.Contents.FindItemsById,
"shortcut" => Repositories.Shortcuts.FindItemsById,
"reminder" => Repositories.Reminders.FindItemsById,
"relation" => Repositories.Relations.FindItemsById,
"color" => Repositories.Colors.FindItemsById,
"vault" => Repositories.Vaults.FindItemsById,
"tag" => Repositories.Tags.FindItemsById,
_ => null,
};
}
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
{
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync.");
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
SyncEventCounterSource.Log.PushV2();
var stopwatch = new Stopwatch();
stopwatch.Start();
var stopwatch = Stopwatch.StartNew();
try
{
var UpsertItems = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
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 new SyncDeviceService(new SyncDevice(ref userId, ref deviceId)).AddIdsToOtherDevicesAsync(pushItem.Items.Select((i) => $"{i.ItemId}:{pushItem.Type}").ToList());
await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => new ItemKey(i.ItemId, pushItem.Type)));
return 1;
}
finally
{
stopwatch.Stop();
SyncEventCounterSource.Log.RecordPushDuration(stopwatch.ElapsedMilliseconds);
}
}
public async Task<bool> PushCompleted()
{
var userId = Context.User.FindFirstValue("sub");
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
await Clients.OthersInGroup(userId).PushCompleted();
return true;
}
private static async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(Func<string, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] collections, string[] types, string userId, string[] ids, int size, bool resetSync, long maxBytes)
public async Task<bool> PushCompletedV2(string deviceId)
{
var chunksProcessed = 0;
for (int i = 0; i < collections.Length; i++)
{
var type = types[i];
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;
}
var filteredIds = ids.Where((id) => id.EndsWith($":{type}")).Select((id) => id.Split(":")[0]).ToArray();
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);
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
var chunk = new List<SyncItem>();
long totalBytes = 0;
@@ -180,11 +184,12 @@ namespace Notesnook.API.Hubs
totalBytes += item.Length + METADATA_BYTES;
if (totalBytes >= maxBytes)
{
itemsProcessed += chunk.Count;
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = chunksProcessed
Count = itemsProcessed
};
totalBytes = 0;
@@ -194,11 +199,12 @@ namespace Notesnook.API.Hubs
}
if (chunk.Count > 0)
{
itemsProcessed += chunk.Count;
yield return new SyncTransferItemV2
{
Items = chunk,
Type = type,
Count = chunksProcessed
Count = itemsProcessed
};
}
}
@@ -206,67 +212,96 @@ namespace Notesnook.API.Hubs
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
{
var userId = Context.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync.");
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 deviceService = new SyncDeviceService(new SyncDevice(ref userId, ref deviceId));
if (!deviceService.IsDeviceRegistered()) deviceService.RegisterDevice();
var device = await SyncDeviceService.GetDeviceAsync(userId, deviceId);
if (device == null)
device = await SyncDeviceService.RegisterDeviceAsync(userId, deviceId);
else
await SyncDeviceService.UpdateLastAccessTimeAsync(userId, deviceId);
var isResetSync = deviceService.IsSyncReset();
if (!deviceService.IsUnsynced() &&
!deviceService.IsSyncPending() &&
!isResetSync)
return new SyncV2Metadata { Synced = true };
var stopwatch = new Stopwatch();
stopwatch.Start();
var stopwatch = Stopwatch.StartNew();
try
{
string[] ids = await deviceService.FetchUnsyncedIdsAsync();
var ids = await SyncDeviceService.FetchUnsyncedIdsAsync(userId, deviceId);
if (!device.IsSyncReset && ids.Count == 0)
return new SyncV2Metadata { Synced = true };
var chunks = PrepareChunks(
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,
],
types: CollectionKeys,
userId,
ids,
size: 1000,
resetSync: isResetSync,
resetSync: device.IsSyncReset,
maxBytes: 7 * 1024 * 1024
);
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId.Equals(userId));
if (userSettings.VaultKey != null)
{
if (!await Clients.Caller.SendVaultKey(userSettings.VaultKey).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected vault key.");
}
await foreach (var chunk in chunks)
{
if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items.");
if (!isResetSync)
if (!device.IsSyncReset)
{
var syncedIds = chunk.Items.Select((i) => $"{i.ItemId}:{chunk.Type}").ToHashSet();
ids = ids.Where((id) => !syncedIds.Contains(id)).ToArray();
await deviceService.WritePendingIdsAsync(ids);
ids.ExceptWith(chunk.Items.Select(i => new ItemKey(i.ItemId, chunk.Type)));
await SyncDeviceService.WritePendingIdsAsync(userId, deviceId, ids);
}
}
deviceService.Reset();
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
{
@@ -275,7 +310,6 @@ namespace Notesnook.API.Hubs
}
finally
{
stopwatch.Stop();
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
}
}
@@ -290,7 +324,7 @@ namespace Notesnook.API.Hubs
}
[MessagePack.MessagePackObject]
public struct SyncV2TransferItem
public struct SyncTransferItemV2
{
[MessagePack.Key("items")]
[JsonPropertyName("items")]
@@ -299,13 +333,8 @@ namespace Notesnook.API.Hubs
[MessagePack.Key("type")]
[JsonPropertyName("type")]
public string Type { get; set; }
[MessagePack.Key("final")]
[JsonPropertyName("final")]
public bool Final { get; set; }
[MessagePack.Key("vaultKey")]
[JsonPropertyName("vaultKey")]
public EncryptedData VaultKey { get; set; }
[MessagePack.Key("count")]
[JsonPropertyName("count")]
public int Count { get; set; }
}
}

View File

@@ -1,33 +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 Notesnook.API.Models;
using Streetwriters.Common.Interfaces;
namespace Notesnook.API.Interfaces
{
public interface IMonograph : IDocument
{
string Title { get; set; }
string UserId { get; set; }
byte[] CompressedContent { get; set; }
EncryptedData EncryptedContent { get; set; }
long DatePublished { get; set; }
}
}

View File

@@ -31,9 +31,10 @@ namespace Notesnook.API.Interfaces
Task DeleteObjectAsync(string userId, string name);
Task DeleteDirectoryAsync(string userId);
Task<long> GetObjectSizeAsync(string userId, string name);
string GetUploadObjectUrl(string userId, string name);
string GetDownloadObjectUrl(string userId, string name);
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null);
Task<string?> GetUploadObjectUrlAsync(string userId, string name);
Task<string?> GetInternalUploadObjectUrlAsync(string userId, string name);
Task<string?> GetDownloadObjectUrlAsync(string userId, string name);
Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null);
Task AbortMultipartUploadAsync(string userId, string name, string uploadId);
Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest);
}

View File

@@ -40,5 +40,9 @@ namespace Notesnook.API.Interfaces
SyncItemsRepository Tags { get; }
Repository<UserSettings> UsersSettings { get; }
Repository<Monograph> Monographs { get; }
Repository<InboxApiKey> InboxApiKey { get; }
Repository<InboxSyncItem> InboxItems { get; }
Repository<SyncDevice> SyncDevices { get; }
Repository<DeviceIdsChunk> DeviceIdsChunks { get; }
}
}

View File

@@ -1,36 +1,35 @@
/*
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 Notesnook.API.Models.Responses;
using Streetwriters.Common.Interfaces;
namespace Notesnook.API.Interfaces
{
public interface IUserService
{
Task CreateUserAsync();
Task DeleteUserAsync(string userId);
Task DeleteUserAsync(string userId, string jti, string password);
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
Task<UserResponse> GetUserAsync(string userId);
Task SetUserAttachmentsKeyAsync(string userId, IEncrypted key);
}
/*
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 Notesnook.API.Models;
using Notesnook.API.Models.Responses;
namespace Notesnook.API.Interfaces
{
public interface IUserService
{
Task CreateUserAsync();
Task DeleteUserAsync(string userId);
Task DeleteUserAsync(string userId, string? jti, string password);
Task<bool> ResetUserAsync(string userId, bool removeAttachments);
Task<UserResponse> GetUserAsync(string userId);
Task SetUserKeysAsync(string userId, UserKeys keys);
}
}

View File

@@ -0,0 +1,41 @@
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);
}
}
}

View File

@@ -1,26 +1,27 @@
/*
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/>.
*/
namespace Notesnook.API.Models
{
public class Algorithms
{
public static string Default => "xcha-argon2i13-7";
}
/*
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/>.
*/
namespace Notesnook.API.Models
{
public class Algorithms
{
public static string Default => "xcha-argon2i13-7";
public static string XSAL_X25519_7 => "xsal-x25519-7";
}
}

View File

@@ -1,159 +1,159 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Notesnook.API.Models
{
public class Announcement
{
public Announcement()
{
this.Id = ObjectId.GenerateNewId().ToString();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[BsonElement("id")]
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
[JsonPropertyName("timestamp")]
[BsonElement("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
[JsonPropertyName("isActive")]
[BsonElement("isActive")]
public bool IsActive { get; set; }
[JsonPropertyName("userTypes")]
[BsonElement("userTypes")]
public string[] UserTypes { get; set; }
[JsonPropertyName("appVersion")]
[BsonElement("appVersion")]
public int AppVersion { get; set; }
[JsonPropertyName("body")]
[BsonElement("body")]
public BodyComponent[] Body { get; set; }
[JsonIgnore]
[BsonElement("userIds")]
public string[] UserIds { get; set; }
[Obsolete]
[JsonPropertyName("title")]
[DataMember(Name = "title")]
[BsonElement("title")]
public string Title { get; set; }
[Obsolete]
[JsonPropertyName("description")]
[BsonElement("description")]
public string Description { get; set; }
[Obsolete]
[JsonPropertyName("callToActions")]
[BsonElement("callToActions")]
public CallToAction[] CallToActions { get; set; }
}
public class BodyComponent
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
[JsonPropertyName("style")]
[BsonElement("style")]
public Style Style { get; set; }
[JsonPropertyName("src")]
[BsonElement("src")]
public string Src { get; set; }
[JsonPropertyName("text")]
[BsonElement("text")]
public string Text { get; set; }
[JsonPropertyName("value")]
[BsonElement("value")]
public string Value { get; set; }
[JsonPropertyName("items")]
[BsonElement("items")]
public BodyComponent[] Items { get; set; }
[JsonPropertyName("actions")]
[BsonElement("actions")]
public CallToAction[] Actions { get; set; }
}
public class Style
{
[JsonPropertyName("marginTop")]
[BsonElement("marginTop")]
public int MarginTop { get; set; }
[JsonPropertyName("marginBottom")]
[BsonElement("marginBottom")]
public int MarginBottom { get; set; }
[JsonPropertyName("textAlign")]
[BsonElement("textAlign")]
public string TextAlign { get; set; }
}
public class CallToAction
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
[JsonPropertyName("data")]
[BsonElement("data")]
public string Data { get; set; }
[JsonPropertyName("title")]
[BsonElement("title")]
public string Title { get; set; }
}
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Notesnook.API.Models
{
public class Announcement
{
public Announcement()
{
this.Id = ObjectId.GenerateNewId().ToString();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[BsonElement("id")]
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
[BsonElement("type")]
public required string Type { get; set; }
[JsonPropertyName("timestamp")]
[BsonElement("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public required string[] Platforms { get; set; }
[JsonPropertyName("isActive")]
[BsonElement("isActive")]
public bool IsActive { get; set; }
[JsonPropertyName("userTypes")]
[BsonElement("userTypes")]
public required string[] UserTypes { get; set; }
[JsonPropertyName("appVersion")]
[BsonElement("appVersion")]
public int AppVersion { get; set; }
[JsonPropertyName("body")]
[BsonElement("body")]
public required BodyComponent[] Body { get; set; }
[JsonIgnore]
[BsonElement("userIds")]
public string[]? UserIds { get; set; }
[Obsolete]
[JsonPropertyName("title")]
[DataMember(Name = "title")]
[BsonElement("title")]
public string? Title { get; set; }
[Obsolete]
[JsonPropertyName("description")]
[BsonElement("description")]
public string? Description { get; set; }
[Obsolete]
[JsonPropertyName("callToActions")]
[BsonElement("callToActions")]
public CallToAction[]? CallToActions { get; set; }
}
public class BodyComponent
{
[JsonPropertyName("type")]
[BsonElement("type")]
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[]? Platforms { get; set; }
[JsonPropertyName("style")]
[BsonElement("style")]
public Style? Style { get; set; }
[JsonPropertyName("src")]
[BsonElement("src")]
public string? Src { get; set; }
[JsonPropertyName("text")]
[BsonElement("text")]
public string? Text { get; set; }
[JsonPropertyName("value")]
[BsonElement("value")]
public string? Value { get; set; }
[JsonPropertyName("items")]
[BsonElement("items")]
public BodyComponent[]? Items { get; set; }
[JsonPropertyName("actions")]
[BsonElement("actions")]
public required CallToAction[] Actions { get; set; }
}
public class Style
{
[JsonPropertyName("marginTop")]
[BsonElement("marginTop")]
public int MarginTop { get; set; }
[JsonPropertyName("marginBottom")]
[BsonElement("marginBottom")]
public int MarginBottom { get; set; }
[JsonPropertyName("textAlign")]
[BsonElement("textAlign")]
public string? TextAlign { get; set; }
}
public class CallToAction
{
[JsonPropertyName("type")]
[BsonElement("type")]
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[]? Platforms { get; set; }
[JsonPropertyName("data")]
[BsonElement("data")]
public string? Data { get; set; }
[JsonPropertyName("title")]
[BsonElement("title")]
public string? Title { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
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;
}
}

View File

@@ -5,7 +5,7 @@ namespace Notesnook.API.Models
public class DeleteAccountForm
{
[Required]
public string Password
public required string Password
{
get; set;
}

View File

@@ -0,0 +1,16 @@
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; } = [];
}
}

View File

@@ -1,75 +1,69 @@
/*
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 MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Notesnook.API.Interfaces;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class EncryptedData : IEncrypted
{
[MessagePack.Key("iv")]
[JsonPropertyName("iv")]
[BsonElement("iv")]
[DataMember(Name = "iv")]
public string IV
{
get; set;
}
[MessagePack.Key("cipher")]
[JsonPropertyName("cipher")]
[BsonElement("cipher")]
[DataMember(Name = "cipher")]
public string Cipher
{
get; set;
}
[MessagePack.Key("length")]
[JsonPropertyName("length")]
[BsonElement("length")]
[DataMember(Name = "length")]
public long Length { get; set; }
[MessagePack.Key("salt")]
[JsonPropertyName("salt")]
[BsonElement("salt")]
[DataMember(Name = "salt")]
public string Salt { get; set; }
public override bool Equals(object obj)
{
if (obj is EncryptedData encryptedData)
{
return IV == encryptedData.IV && Salt == encryptedData.Salt && Cipher == encryptedData.Cipher && Length == encryptedData.Length;
}
return base.Equals(obj);
}
public bool IsEmpty()
{
return this.Cipher == null && this.IV == null && this.Length == 0 && this.Salt == null;
}
}
}
/*
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 MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Notesnook.API.Interfaces;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class EncryptedData
{
[MessagePack.Key("iv")]
[JsonPropertyName("iv")]
[BsonElement("iv")]
[DataMember(Name = "iv")]
public required string IV { get; set; }
[MessagePack.Key("cipher")]
[JsonPropertyName("cipher")]
[BsonElement("cipher")]
[DataMember(Name = "cipher")]
public required string Cipher { get; set; }
[MessagePack.Key("length")]
[JsonPropertyName("length")]
[BsonElement("length")]
[DataMember(Name = "length")]
public long Length { get; set; }
[MessagePack.Key("salt")]
[JsonPropertyName("salt")]
[BsonElement("salt")]
[DataMember(Name = "salt")]
public required 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;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
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; }
}
}

View File

@@ -0,0 +1,67 @@
/*
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;
}
}
}

View File

@@ -1,67 +1,87 @@
/*
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 Notesnook.API.Interfaces;
namespace Notesnook.API.Models
{
public class ObjectWithId
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
}
public class Monograph : IMonograph
{
public Monograph()
{
Id = ObjectId.GenerateNewId().ToString();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
[JsonPropertyName("encryptedContent")]
public EncryptedData EncryptedContent { get; set; }
[JsonPropertyName("datePublished")]
public long DatePublished { get; set; }
[JsonPropertyName("content")]
[BsonIgnore]
public string Content { get; set; }
[JsonIgnore]
public byte[] CompressedContent { get; set; }
}
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Runtime.Serialization;
namespace Notesnook.API.Models
{
public class ObjectWithId
{
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public required string Id { get; set; }
public required string ItemId { get; set; }
}
public class Monograph
{
public Monograph()
{
Id = ObjectId.GenerateNewId().ToString();
}
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string? ItemId { get; set; }
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
[JsonPropertyName("encryptedContent")]
public EncryptedData? EncryptedContent { get; set; }
[JsonPropertyName("datePublished")]
public long DatePublished { get; set; }
[JsonPropertyName("content")]
[BsonIgnore]
public string? Content { get; set; }
[JsonIgnore]
public byte[]? CompressedContent { get; set; }
[JsonPropertyName("password")]
public EncryptedData? Password { get; set; }
[JsonPropertyName("deleted")]
public bool Deleted { get; set; }
[JsonPropertyName("viewCount")]
public int ViewCount { get; set; }
}
}

View File

@@ -1,33 +1,35 @@
/*
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.Serialization;
namespace Streetwriters.Common.Attributes
{
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
public JsonInterfaceConverterAttribute(Type converterType)
: base(converterType)
{
}
}
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Runtime.Serialization;
namespace Notesnook.API.Models
{
public class MonographContent
{
[JsonPropertyName("data")]
public required string Data { get; set; }
[JsonPropertyName("type")]
public required string Type { get; set; }
}
}

View File

@@ -1,42 +1,52 @@
/*
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; }
}
}
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Notesnook.API.Models
{
public class MonographMetadata
{
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public required string ItemId
{
get; set;
}
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
[JsonPropertyName("datePublished")]
public long DatePublished { get; set; }
[JsonPropertyName("password")]
public EncryptedData? Password { get; set; }
[JsonPropertyName("deleted")]
public bool Deleted { get; set; }
}
}

View File

@@ -1,27 +1,29 @@
/*
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/>.
*/
namespace Notesnook.API.Models
{
public class MultipartUploadMeta
{
public string UploadId { get; set; }
public string[] Parts { get; set; }
}
/*
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;
namespace Notesnook.API.Models
{
public class MultipartUploadMeta
{
public string UploadId { get; set; } = string.Empty;
public string[] Parts { get; set; } = Array.Empty<string>();
}
}

View File

@@ -0,0 +1,7 @@
namespace Notesnook.API.Models;
public class PartETagWrapper
{
public int PartNumber { get; set; }
public string ETag { get; set; } = string.Empty;
}

View File

@@ -1,14 +1,14 @@
using System.Text.Json.Serialization;
using Streetwriters.Common.Models;
namespace Notesnook.API.Models.Responses
{
public class SignupResponse : Response
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("errors")]
public string[] Errors { get; set; }
}
}
using System.Text.Json.Serialization;
using Streetwriters.Common.Models;
namespace Notesnook.API.Models.Responses
{
public class SignupResponse : Response
{
[JsonPropertyName("userId")]
public string? UserId { get; set; }
[JsonPropertyName("errors")]
public string[]? Errors { get; set; }
}
}

View File

@@ -1,25 +1,37 @@
using System.Text.Json.Serialization;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Models.Responses
{
public class UserResponse : UserModel, IResponse
{
[JsonPropertyName("salt")]
public string Salt { get; set; }
[JsonPropertyName("attachmentsKey")]
public EncryptedData AttachmentsKey { get; set; }
[JsonPropertyName("subscription")]
public ISubscription Subscription { get; set; }
[JsonPropertyName("profile")]
public EncryptedData Profile { get; set; }
[JsonIgnore]
public bool Success { get; set; }
public int StatusCode { get; set; }
}
}
using System.Net.Http;
using System.Text.Json.Serialization;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Models.Responses
{
public class UserResponse : UserModel, IResponse
{
[JsonPropertyName("salt")]
public string? Salt { get; set; }
[JsonPropertyName("attachmentsKey")]
public EncryptedData? AttachmentsKey { get; set; }
[JsonPropertyName("monographPasswordsKey")]
public EncryptedData? MonographPasswordsKey { get; set; }
[JsonPropertyName("inboxKeys")]
public InboxKeys? InboxKeys { 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; }
}
}

View File

@@ -1,29 +1,29 @@
/*
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/>.
*/
namespace Notesnook.API.Models
{
public class S3Options
{
public string ServiceUrl { get; set; }
public string Region { get; set; }
public string AccessKeyId { get; set; }
public string SecretAccessKey { get; set; }
}
/*
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/>.
*/
namespace Notesnook.API.Models
{
public class S3Options
{
public string ServiceUrl { get; set; } = string.Empty;
public string Region { get; set; } = string.Empty;
public string AccessKeyId { get; set; } = string.Empty;
public string SecretAccessKey { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,19 @@
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; }
}
}

View File

@@ -44,7 +44,7 @@ namespace Notesnook.API.Models
[DataMember(Name = "userId")]
[JsonPropertyName("userId")]
[MessagePack.Key("userId")]
public string UserId
public string? UserId
{
get; set;
}
@@ -53,25 +53,19 @@ namespace Notesnook.API.Models
[DataMember(Name = "iv")]
[MessagePack.Key("iv")]
[Required]
public string IV
{
get; set;
}
public string IV { get; set; } = string.Empty;
[JsonPropertyName("cipher")]
[DataMember(Name = "cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
public string Cipher { get; set; } = string.Empty;
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string ItemId
public string? ItemId
{
get; set;
}
@@ -108,10 +102,7 @@ namespace Notesnook.API.Models
[DataMember(Name = "alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
} = Algorithms.Default;
public string Algorithm { get; set; } = string.Empty;
}
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
@@ -156,58 +147,50 @@ namespace Notesnook.API.Models
public override SyncItem Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var syncItem = new SyncItem();
var bsonReader = context.Reader;
bsonReader.ReadStartDocument();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var id = bsonReader.ReadObjectId();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var dateSynced = bsonReader.ReadInt64();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var userId = bsonReader.ReadString();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var iv = bsonReader.ReadString();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var cipher = bsonReader.ReadString();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var itemId = bsonReader.ReadString();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var length = bsonReader.ReadInt64();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var version = bsonReader.ReadDouble();
bsonReader.ReadBsonType();
bsonReader.SkipName();
var algorithm = bsonReader.ReadString();
bsonReader.ReadEndDocument();
return new SyncItem
while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
{
Id = id,
DateSynced = dateSynced,
UserId = userId,
IV = iv,
Cipher = cipher,
ItemId = itemId,
Length = length,
Version = version,
Algorithm = algorithm
};
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;
}
}
}

View File

@@ -17,16 +17,18 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Interfaces
namespace Notesnook.API.Models
{
public interface IOffer : IDocument
public class UserKeys
{
ApplicationType AppId { get; set; }
string PromoCode { get; set; }
PromoCode[] Codes { get; set; }
public EncryptedData? AttachmentsKey { get; set; }
public EncryptedData? MonographPasswordsKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
}
public class InboxKeys
{
public string? Public { get; set; }
public EncryptedData? Private { get; set; }
}
}

View File

@@ -1,42 +1,65 @@
/*
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 MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Notesnook.API.Interfaces;
namespace Notesnook.API.Models
{
public class UserSettings : IUserSettings
{
public UserSettings()
{
this.Id = ObjectId.GenerateNewId().ToString();
}
public string UserId { get; set; }
public long LastSynced { get; set; }
public string Salt { get; set; }
public EncryptedData VaultKey { get; set; }
public EncryptedData AttachmentsKey { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
}
}
/*
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 MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Notesnook.API.Interfaces;
namespace Notesnook.API.Models
{
public class Limit
{
private long _value = 0;
public long Value
{
get => _value;
set
{
_value = value;
UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
public long UpdatedAt
{
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; }
}
}

View File

@@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<StartupObject>Notesnook.API.Program</StartupObject>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" />
<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="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
@@ -15,8 +18,11 @@
<PackageReference Include="AWSSDK.S3" Version="3.7.310.8" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.2.0" />
<PackageReference Include="Nanoid" Version="3.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-alpha.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="Quartz" Version="3.5.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,68 +1,69 @@
/*
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/>.
*/
#if !DEBUG
using System.Net;
#endif
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Streetwriters.Common;
using System.Linq;
using Microsoft.Extensions.Logging;
using System.Net;
namespace Notesnook.API
{
public class Program
{
public static async Task Main(string[] args)
{
#if DEBUG
DotNetEnv.Env.TraversePath().Load(".env.local");
#else
DotNetEnv.Env.TraversePath().Load(".env");
#endif
IHost host = CreateHostBuilder(args).Build();
await host.RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel((options) =>
{
options.Limits.MaxRequestBodySize = long.MaxValue;
options.ListenAnyIP(Servers.NotesnookAPI.Port);
if (Servers.NotesnookAPI.IsSecure)
{
options.ListenAnyIP(443, listenerOptions =>
{
listenerOptions.UseHttps(Servers.NotesnookAPI.SSLCertificate);
});
}
options.Listen(IPAddress.Parse("127.0.0.1"), 5067);
});
});
}
}
/*
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.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Streetwriters.Common;
namespace Notesnook.API
{
public class Program
{
public static async Task Main(string[] args)
{
#if (DEBUG || STAGING)
DotNetEnv.Env.TraversePath().Load(".env.local");
#else
DotNetEnv.Env.TraversePath().Load(".env");
#endif
IHost host = CreateHostBuilder(args).Build();
await host.RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddSystemdConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel((options) =>
{
options.Limits.MaxRequestBodySize = long.MaxValue;
options.ListenAnyIP(Servers.NotesnookAPI.Port);
if (Servers.NotesnookAPI.IsSecure && Servers.NotesnookAPI.SSLCertificate != null)
{
options.ListenAnyIP(443, listenerOptions =>
{
listenerOptions.UseHttps(Servers.NotesnookAPI.SSLCertificate);
});
}
options.Listen(IPAddress.Parse("127.0.0.1"), 5067);
});
});
}
}

View File

@@ -26,6 +26,7 @@ using System.Threading;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.VisualBasic;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using Notesnook.API.Hubs;
@@ -41,19 +42,14 @@ namespace Notesnook.API.Repositories
public class SyncItemsRepository : Repository<SyncItem>
{
private readonly string collectionName;
public SyncItemsRepository(IDbContext dbContext, IMongoCollection<SyncItem> collection) : base(dbContext, collection)
private readonly ILogger<SyncItemsRepository> logger;
public SyncItemsRepository(IDbContext dbContext, IMongoCollection<SyncItem> collection, ILogger<SyncItemsRepository> logger) : base(dbContext, collection)
{
this.collectionName = collection.CollectionNamespace.CollectionName;
#if DEBUG
Collection.Indexes.CreateMany([
new CreateIndexModel<SyncItem>(Builders<SyncItem>.IndexKeys.Ascending("UserId").Descending("DateSynced")),
new CreateIndexModel<SyncItem>(Builders<SyncItem>.IndexKeys.Ascending("UserId").Ascending("ItemId")),
new CreateIndexModel<SyncItem>(Builders<SyncItem>.IndexKeys.Ascending("UserId"))
]);
#endif
this.logger = logger;
}
private readonly List<string> ALGORITHMS = [Algorithms.Default];
private readonly List<string> ALGORITHMS = [Algorithms.Default, Algorithms.XSAL_X25519_7];
private bool IsValidAlgorithm(string algorithm)
{
return ALGORITHMS.Contains(algorithm);
@@ -117,10 +113,14 @@ namespace Notesnook.API.Repositories
// Handle case where the cipher is corrupted.
if (!IsBase64String(item.Cipher))
{
Slogger<SyncHub>.Error("Upsert", "Corrupted", item.ItemId, item.Length.ToString(), 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;
@@ -151,10 +151,14 @@ namespace Notesnook.API.Repositories
// Handle case where the cipher is corrupted.
if (!IsBase64String(item.Cipher))
{
Slogger<SyncHub>.Error("Upsert", "Corrupted", item.ItemId, item.Length.ToString(), 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)

View File

@@ -27,10 +27,18 @@ using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using Notesnook.API.Accessors;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Services
{
@@ -42,9 +50,11 @@ namespace Notesnook.API.Services
public class S3Service : IS3Service
{
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME ?? "";
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? "";
private AmazonS3Client S3Client { get; }
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME;
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? Constants.S3_BUCKET_NAME;
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
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
@@ -54,52 +64,49 @@ namespace Notesnook.API.Services
// URLs generated by S3 are host specific. Changing their hostname on the fly causes
// SignatureDoesNotMatch error.
// That is why we create 2 separate S3 clients. One for internal traffic and one for external.
private AmazonS3Client S3InternalClient { get; }
private HttpClient httpClient = new HttpClient();
private readonly S3FailoverHelper S3InternalClient;
private readonly HttpClient httpClient = new();
public S3Service()
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, WampServiceAccessor wampServiceAccessor, ILogger<S3Service> logger)
{
var config = new AmazonS3Config
{
#if DEBUG
ServiceURL = Servers.S3Server.ToString(),
#else
ServiceURL = Constants.S3_SERVICE_URL,
AuthenticationRegion = Constants.S3_REGION,
#endif
ForcePathStyle = true,
SignatureMethod = SigningAlgorithm.HmacSHA256,
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
Repositories = syncItemsRepositoryAccessor;
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))
if (!string.IsNullOrEmpty(Constants.S3_INTERNAL_SERVICE_URL) && !string.IsNullOrEmpty(Constants.S3_INTERNAL_BUCKET_NAME))
{
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"
});
S3InternalClient = new S3FailoverHelper(
S3ClientFactory.CreateS3Clients(
Constants.S3_INTERNAL_SERVICE_URL,
Constants.S3_REGION,
Constants.S3_ACCESS_KEY_ID,
Constants.S3_ACCESS_KEY,
forcePathStyle: true
),
logger: logger
);
}
else S3InternalClient = S3Client;
AWSConfigsS3.UseSignatureVersion4 = true;
}
public async Task DeleteObjectAsync(string userId, string name)
{
var objectName = GetFullObjectName(userId, name);
if (objectName == null) throw new Exception("Invalid object name."); ;
var objectName = GetFullObjectName(userId, name) ?? throw new Exception("Invalid object name.");
var response = await GetS3Client(S3ClientMode.INTERNAL).DeleteObjectAsync(GetBucketName(S3ClientMode.INTERNAL), objectName);
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.DeleteObjectAsync(INTERNAL_BUCKET_NAME, objectName), operationName: "DeleteObject", isWriteOperation: true);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode)))
if (!IsSuccessStatusCode((int)response.HttpStatusCode))
throw new Exception("Could not delete object.");
}
@@ -107,7 +114,7 @@ namespace Notesnook.API.Services
{
var request = new ListObjectsV2Request
{
BucketName = GetBucketName(S3ClientMode.INTERNAL),
BucketName = INTERNAL_BUCKET_NAME,
Prefix = userId,
};
@@ -115,7 +122,7 @@ namespace Notesnook.API.Services
var keys = new List<KeyVersion>();
do
{
response = await GetS3Client(S3ClientMode.INTERNAL).ListObjectsV2Async(request);
response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.ListObjectsV2Async(request), operationName: "ListObjectsV2");
response.S3Objects.ForEach(obj => keys.Add(new KeyVersion
{
Key = obj.Key,
@@ -127,12 +134,11 @@ namespace Notesnook.API.Services
if (keys.Count <= 0) return;
var deleteObjectsResponse = await GetS3Client(S3ClientMode.INTERNAL)
.DeleteObjectsAsync(new DeleteObjectsRequest
var deleteObjectsResponse = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.DeleteObjectsAsync(new DeleteObjectsRequest
{
BucketName = GetBucketName(S3ClientMode.INTERNAL),
BucketName = INTERNAL_BUCKET_NAME,
Objects = keys,
});
}), operationName: "DeleteObjects", isWriteOperation: true);
if (!IsSuccessStatusCode((int)deleteObjectsResponse.HttpStatusCode))
throw new Exception("Could not delete directory.");
@@ -140,7 +146,7 @@ namespace Notesnook.API.Services
public async Task<long> GetObjectSizeAsync(string userId, string name)
{
var url = this.GetPresignedURL(userId, name, HttpVerb.HEAD, S3ClientMode.INTERNAL);
var url = await this.GetPresignedURLAsync(userId, name, HttpVerb.HEAD, S3ClientMode.INTERNAL);
if (url == null) return 0;
var request = new HttpRequestMessage(HttpMethod.Head, url);
@@ -149,29 +155,42 @@ namespace Notesnook.API.Services
}
public string GetUploadObjectUrl(string userId, string name)
public async Task<string?> GetUploadObjectUrlAsync(string userId, string name)
{
var url = this.GetPresignedURL(userId, name, HttpVerb.PUT);
return await this.GetPresignedURLAsync(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;
return url;
}
public string GetDownloadObjectUrl(string userId, string name)
{
var url = this.GetPresignedURL(userId, name, HttpVerb.GET);
if (url == null) return null;
return url;
}
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null)
public async Task<MultipartUploadMeta> StartMultipartUploadAsync(string userId, string name, int parts, string? uploadId = null)
{
var objectName = GetFullObjectName(userId, name);
if (userId == null || objectName == null) throw new Exception("Could not initiate multipart upload.");
if (string.IsNullOrEmpty(uploadId))
{
var response = await GetS3Client(S3ClientMode.INTERNAL).InitiateMultipartUploadAsync(GetBucketName(S3ClientMode.INTERNAL), objectName);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to initiate multipart upload.");
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.InitiateMultipartUploadAsync(INTERNAL_BUCKET_NAME, objectName), operationName: "InitiateMultipartUpload", isWriteOperation: true);
if (!IsSuccessStatusCode((int)response.HttpStatusCode)) throw new Exception("Failed to initiate multipart upload.");
uploadId = response.UploadId;
}
@@ -179,7 +198,7 @@ namespace Notesnook.API.Services
var signedUrls = new string[parts];
for (var i = 0; i < parts; ++i)
{
signedUrls[i] = GetPresignedURLForUploadPart(objectName, uploadId, i + 1);
signedUrls[i] = await GetPresignedURLForUploadPartAsync(objectName, uploadId, i + 1);
}
return new MultipartUploadMeta
@@ -194,81 +213,126 @@ namespace Notesnook.API.Services
var objectName = GetFullObjectName(userId, name);
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
var response = await GetS3Client(S3ClientMode.INTERNAL).AbortMultipartUploadAsync(GetBucketName(S3ClientMode.INTERNAL), objectName, uploadId);
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.AbortMultipartUploadAsync(INTERNAL_BUCKET_NAME, objectName, uploadId), operationName: "AbortMultipartUpload", isWriteOperation: true);
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)
{
var objectName = GetFullObjectName(userId, uploadRequest.Key);
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
if (userSettings == null)
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("User settings not found.");
}
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.BucketName = GetBucketName(S3ClientMode.INTERNAL);
var response = await GetS3Client(S3ClientMode.INTERNAL).CompleteMultipartUploadAsync(uploadRequest);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
uploadRequest.BucketName = INTERNAL_BUCKET_NAME;
var response = await S3InternalClient.ExecuteWithFailoverAsync((client) => client.CompleteMultipartUploadAsync(uploadRequest), operationName: "CompleteMultipartUpload", isWriteOperation: true);
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 string GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
private async Task<string?> GetPresignedURLAsync(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
{
var objectName = GetFullObjectName(userId, name);
if (userId == null || objectName == null) return null;
var request = new GetPreSignedUrlRequest
var client = mode == S3ClientMode.INTERNAL ? S3InternalClient : S3Client;
var bucketName = mode == S3ClientMode.INTERNAL ? INTERNAL_BUCKET_NAME : BUCKET_NAME;
return await client.ExecuteWithFailoverAsync(client =>
{
BucketName = GetBucketName(mode),
Expires = System.DateTime.Now.AddHours(1),
Verb = httpVerb,
Key = objectName,
#if DEBUG
Protocol = Protocol.HTTP,
var request = new GetPreSignedUrlRequest
{
BucketName = bucketName,
Expires = System.DateTime.Now.AddHours(1),
Verb = httpVerb,
Key = objectName,
#if (DEBUG || STAGING)
Protocol = Protocol.HTTP,
#else
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
Protocol = client.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
#endif
};
return GetS3Client(mode).GetPreSignedURL(request);
};
return client.GetPreSignedURLAsync(request);
}, operationName: "GetPreSignedURL");
}
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber)
private Task<string> GetPresignedURLForUploadPartAsync(string objectName, string uploadId, int partNumber, S3ClientMode mode = S3ClientMode.EXTERNAL)
{
return GetS3Client(S3ClientMode.INTERNAL).GetPreSignedURL(new GetPreSignedUrlRequest
var client = mode == S3ClientMode.INTERNAL ? S3InternalClient : S3Client;
var bucketName = mode == S3ClientMode.INTERNAL ? INTERNAL_BUCKET_NAME : BUCKET_NAME;
return client.ExecuteWithFailoverAsync(c => c.GetPreSignedURLAsync(new GetPreSignedUrlRequest
{
BucketName = GetBucketName(S3ClientMode.INTERNAL),
BucketName = bucketName,
Expires = System.DateTime.Now.AddHours(1),
Verb = HttpVerb.PUT,
Key = objectName,
PartNumber = partNumber,
UploadId = uploadId,
#if DEBUG
#if (DEBUG || STAGING)
Protocol = Protocol.HTTP,
#else
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
Protocol = c.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
#endif
});
}), operationName: "GetPreSignedURL");
}
private string GetFullObjectName(string userId, string name)
private static string? GetFullObjectName(string userId, string name)
{
if (userId == null || !Regex.IsMatch(name, "[0-9a-zA-Z!" + Regex.Escape("-") + "_.*'()]")) return null;
return $"{userId}/{name}";
}
bool IsSuccessStatusCode(int statusCode)
static bool IsSuccessStatusCode(int statusCode)
{
return ((int)statusCode >= 200) && ((int)statusCode <= 299);
}
AmazonS3Client GetS3Client(S3ClientMode mode = S3ClientMode.EXTERNAL)
{
if (mode == S3ClientMode.INTERNAL && S3InternalClient != null) return S3InternalClient;
return S3Client;
}
string GetBucketName(S3ClientMode mode = S3ClientMode.EXTERNAL)
{
if (mode == S3ClientMode.INTERNAL && S3InternalClient != null) return INTERNAL_BUCKET_NAME;
return BUCKET_NAME;
}
}
}

View File

@@ -24,200 +24,201 @@ 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 struct SyncDevice(ref string userId, ref string deviceId)
public readonly record struct ItemKey(string ItemId, string Type)
{
public readonly string DeviceId = deviceId;
public readonly string UserId = userId;
private string userSyncDirectoryPath = null;
public string UserSyncDirectoryPath
{
get
{
userSyncDirectoryPath ??= Path.Join("sync", UserId);
return userSyncDirectoryPath;
}
}
private string userDeviceDirectoryPath = null;
public string UserDeviceDirectoryPath
{
get
{
userDeviceDirectoryPath ??= Path.Join(UserSyncDirectoryPath, DeviceId);
return userDeviceDirectoryPath;
}
}
private string pendingIdsFilePath = null;
public string PendingIdsFilePath
{
get
{
pendingIdsFilePath ??= Path.Join(UserDeviceDirectoryPath, "pending");
return pendingIdsFilePath;
}
}
private string unsyncedIdsFilePath = null;
public string UnsyncedIdsFilePath
{
get
{
unsyncedIdsFilePath ??= Path.Join(UserDeviceDirectoryPath, "unsynced");
return unsyncedIdsFilePath;
}
}
private string resetSyncFilePath = null;
public string ResetSyncFilePath
{
get
{
resetSyncFilePath ??= Path.Join(UserDeviceDirectoryPath, "reset-sync");
return resetSyncFilePath;
}
}
public override string ToString() => $"{ItemId}:{Type}";
}
public class SyncDeviceService(SyncDevice device)
public class SyncDeviceService(ISyncItemsRepositoryAccessor repositories, ILogger<SyncDeviceService> logger)
{
public async Task<string[]> GetUnsyncedIdsAsync()
{
try
{
return await File.ReadAllLinesAsync(device.UnsyncedIdsFilePath);
}
catch { return []; }
}
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);
public async Task<string[]> GetUnsyncedIdsAsync(string deviceId)
{
try
{
return await File.ReadAllLinesAsync(Path.Join(device.UserSyncDirectoryPath, deviceId, "unsynced"));
}
catch { return []; }
}
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);
public async Task<string[]> FetchUnsyncedIdsAsync()
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)
{
if (IsSyncReset()) return Array.Empty<string>();
if (UnsyncedIdsFileLocks.TryGetValue(device.DeviceId, out SemaphoreSlim fileLock) && fileLock.CurrentCount == 0)
await fileLock.WaitAsync();
try
var cursor = await repositories.DeviceIdsChunks.Collection.FindAsync(DeviceIdsChunkFilter(userId, deviceId, key));
var result = new HashSet<ItemKey>();
while (await cursor.MoveNextAsync())
{
var unsyncedIds = await GetUnsyncedIdsAsync();
if (IsSyncPending())
foreach (var chunk in cursor.Current)
{
unsyncedIds = unsyncedIds.Union(await File.ReadAllLinesAsync(device.PendingIdsFilePath)).ToArray();
foreach (var id in chunk.Ids)
{
var parts = id.Split(':', 2);
result.Add(new ItemKey(parts[0], parts[1]));
}
}
if (unsyncedIds.Length == 0) return [];
File.Delete(device.UnsyncedIdsFilePath);
await File.WriteAllLinesAsync(device.PendingIdsFilePath, unsyncedIds);
return unsyncedIds;
}
catch
return result;
}
const int MaxIdsPerChunk = 400_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)
{
return Array.Empty<string>();
var update = Builders<DeviceIdsChunk>.Update.PushEach(x => x.Ids, ids.Select(i => i.ToString()));
await repositories.DeviceIdsChunks.Collection.UpdateOneAsync(
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Id, chunk.Id),
update
);
}
finally
else
{
if (fileLock != null && fileLock.CurrentCount == 0) fileLock.Release();
}
}
public async Task WritePendingIdsAsync(IEnumerable<string> ids)
{
await File.WriteAllLinesAsync(device.PendingIdsFilePath, ids);
}
public bool IsSyncReset()
{
return File.Exists(device.ResetSyncFilePath);
}
public bool IsSyncReset(string deviceId)
{
return File.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId, "reset-sync"));
}
public bool IsSyncPending()
{
return File.Exists(device.PendingIdsFilePath);
}
public bool IsUnsynced()
{
return File.Exists(device.UnsyncedIdsFilePath);
}
public void Reset()
{
File.Delete(device.ResetSyncFilePath);
File.Delete(device.PendingIdsFilePath);
}
public bool IsDeviceRegistered()
{
return Directory.Exists(device.UserDeviceDirectoryPath);
}
public bool IsDeviceRegistered(string deviceId)
{
return Directory.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId));
}
public string[] ListDevices()
{
return Directory.GetDirectories(device.UserSyncDirectoryPath).Select((path) => path[(path.LastIndexOf(Path.DirectorySeparatorChar) + 1)..]).ToArray();
}
public void ResetDevices()
{
if (File.Exists(device.UserSyncDirectoryPath)) File.Delete(device.UserSyncDirectoryPath);
Directory.CreateDirectory(device.UserSyncDirectoryPath);
}
private readonly ConcurrentDictionary<string, SemaphoreSlim> UnsyncedIdsFileLocks = [];
public async Task AddIdsToOtherDevicesAsync(List<string> ids)
{
await Parallel.ForEachAsync(ListDevices(), async (id, ct) =>
{
if (id == device.DeviceId || IsSyncReset(id)) return;
if (!UnsyncedIdsFileLocks.TryGetValue(id, out SemaphoreSlim fileLock))
var newChunk = new DeviceIdsChunk
{
fileLock = UnsyncedIdsFileLocks.AddOrUpdate(id, (id) => new SemaphoreSlim(1, 1), (id, old) => new SemaphoreSlim(1, 1));
}
await fileLock.WaitAsync(ct);
try
{
if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id));
var oldIds = await GetUnsyncedIdsAsync(id);
await File.WriteAllLinesAsync(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds), ct);
}
finally
{
fileLock.Release();
}
});
}
public void RegisterDevice()
{
Directory.CreateDirectory(device.UserDeviceDirectoryPath);
File.Create(device.ResetSyncFilePath).Close();
}
public void UnregisterDevice()
{
try
{
Directory.Delete(device.UserDeviceDirectoryPath, true);
UserId = userId,
DeviceId = deviceId,
Key = key,
Ids = [.. ids.Select(i => i.ToString())]
};
await repositories.DeviceIdsChunks.Collection.InsertOneAsync(newChunk);
}
catch { }
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));
}
}
}

View File

@@ -18,53 +18,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
using Streetwriters.Common;
using Streetwriters.Common.Accessors;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
using Streetwriters.Data.Interfaces;
namespace Notesnook.API.Services
{
public class UserService : IUserService
public class UserService(IHttpContextAccessor accessor,
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 readonly HttpClient httpClient;
private IHttpContextAccessor HttpContextAccessor { get; }
private ISyncItemsRepositoryAccessor Repositories { get; }
private IS3Service S3Service { get; set; }
private readonly IUnitOfWork unit;
public UserService(IHttpContextAccessor accessor,
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor,
IUnitOfWork unitOfWork, IS3Service s3Service)
{
httpClient = new HttpClient();
Repositories = syncItemsRepositoryAccessor;
HttpContextAccessor = accessor;
unit = unitOfWork;
S3Service = s3Service;
}
private readonly HttpClient httpClient = new();
private IHttpContextAccessor HttpContextAccessor { get; } = accessor;
private ISyncItemsRepositoryAccessor Repositories { get; } = syncItemsRepositoryAccessor;
private IS3Service S3Service { get; set; } = s3Service;
private readonly IUnitOfWork unit = unitOfWork;
public async Task CreateUserAsync()
{
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
if (!response.Success || (response.Errors != null && response.Errors.Length > 0))
if (!response.Success || (response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
{
await Slogger<UserService>.Error(nameof(CreateUserAsync), "Couldn't sign up.", JsonSerializer.Serialize(response));
logger.LogError("Failed to sign up user: {Response}", JsonSerializer.Serialize(response));
if (response.Errors != null && response.Errors.Length > 0)
throw new Exception(string.Join(" ", response.Errors));
else throw new Exception("Could not create a new account. Error code: " + response.StatusCode);
@@ -73,39 +64,41 @@ namespace Notesnook.API.Services
await Repositories.UsersSettings.InsertAsync(new UserSettings
{
UserId = response.UserId,
StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 },
LastSynced = 0,
Salt = GetSalt()
});
if (!Constants.IS_SELF_HOSTED)
{
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionTopic, new CreateSubscriptionMessage
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionV2Topic, new CreateSubscriptionMessageV2
{
AppId = ApplicationType.NOTESNOOK,
Provider = SubscriptionProvider.STREETWRITERS,
Type = SubscriptionType.BASIC,
Status = SubscriptionStatus.ACTIVE,
Plan = SubscriptionPlan.FREE,
UserId = response.UserId,
StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});
}
await Slogger<UserService>.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response));
logger.LogInformation("New user created: {Response}", JsonSerializer.Serialize(response));
}
public async Task<UserResponse> GetUserAsync(string userId)
{
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
var user = await serviceAccessor.UserAccountService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
ISubscription subscription = null;
Subscription? subscription = null;
if (Constants.IS_SELF_HOSTED)
{
subscription = new Subscription
{
AppId = ApplicationType.NOTESNOOK,
Provider = SubscriptionProvider.STREETWRITERS,
Type = SubscriptionType.PREMIUM,
Plan = SubscriptionPlan.BELIEVER,
Status = SubscriptionStatus.ACTIVE,
UserId = user.UserId,
StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
// this date doesn't matter as the subscription is static.
@@ -114,11 +107,22 @@ namespace Notesnook.API.Services
}
else
{
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
subscription = await serviceAccessor.UserSubscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
// reset user's attachment limit every month
var limit = StorageHelper.RolloverStorageLimit(userSettings.StorageLimit);
if (userSettings.StorageLimit == null || limit.UpdatedAt != userSettings.StorageLimit?.UpdatedAt)
{
userSettings.StorageLimit = limit;
await Repositories.UsersSettings.Collection.UpdateOneAsync(
Builders<UserSettings>.Filter.Eq(u => u.UserId, user.UserId),
Builders<UserSettings>.Update.Set(u => u.StorageLimit, userSettings.StorageLimit)
);
}
return new UserResponse
{
UserId = user.UserId,
@@ -128,24 +132,56 @@ namespace Notesnook.API.Services
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 SetUserAttachmentsKeyAsync(string userId, IEncrypted key)
public async Task SetUserKeysAsync(string userId, UserKeys keys)
{
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId) ?? throw new Exception("User not found.");
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);
}
public async Task DeleteUserAsync(string userId)
{
new SyncDeviceService(new SyncDevice(ref userId, ref userId)).ResetDevices();
var cc = new CancellationTokenSource();
Repositories.Notes.DeleteByUserId(userId);
@@ -162,11 +198,14 @@ namespace Notesnook.API.Services
Repositories.Vaults.DeleteByUserId(userId);
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
var result = await unit.Commit();
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString());
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)
{
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
@@ -179,18 +218,17 @@ namespace Notesnook.API.Services
await S3Service.DeleteDirectoryAsync(userId);
}
public async Task DeleteUserAsync(string userId, string jti, string password)
public async Task DeleteUserAsync(string userId, string? jti, string password)
{
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "Deleting user account", userId);
logger.LogInformation("Deleting user account: {UserId}", userId);
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
await serviceAccessor.UserAccountService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
await DeleteUserAsync(userId);
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
SendToAll = false,
SendToAll = jti == null,
OriginTokenId = jti,
UserId = userId,
Message = new Message
@@ -204,7 +242,6 @@ namespace Notesnook.API.Services
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
{
new SyncDeviceService(new SyncDevice(ref userId, ref userId)).ResetDevices();
var cc = new CancellationTokenSource();
@@ -221,12 +258,17 @@ namespace Notesnook.API.Services
Repositories.Tags.DeleteByUserId(userId);
Repositories.Vaults.DeleteByUserId(userId);
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
if (!await unit.Commit()) return false;
await syncDeviceService.ResetDevicesAsync(userId);
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
userSettings.AttachmentsKey = null;
userSettings.MonographPasswordsKey = null;
userSettings.VaultKey = null;
userSettings.InboxKeys = null;
userSettings.LastSynced = 0;
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);

View File

@@ -24,6 +24,7 @@ using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Amazon.Runtime;
using IdentityModel.AspNetCore.OAuth2Introspection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -39,6 +40,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -48,15 +50,19 @@ using Notesnook.API.Authorization;
using Notesnook.API.Extensions;
using Notesnook.API.Hubs;
using Notesnook.API.Interfaces;
using Notesnook.API.Jobs;
using Notesnook.API.Models;
using Notesnook.API.Repositories;
using Notesnook.API.Services;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using Quartz;
using Streetwriters.Common;
using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
using Streetwriters.Common.Services;
using Streetwriters.Data;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Interfaces;
@@ -108,16 +114,15 @@ namespace Notesnook.API
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new SyncRequirement());
});
options.AddPolicy("Pro", policy =>
options.AddPolicy(InboxApiKeyAuthenticationDefaults.AuthenticationScheme, policy =>
{
policy.AuthenticationSchemes.Add("introspection");
policy.AuthenticationSchemes.Add(InboxApiKeyAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new SyncRequirement());
policy.Requirements.Add(new ProUserRequirement());
});
options.DefaultPolicy = options.GetPolicy("Notesnook");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
options.DefaultPolicy = options.GetPolicy("Notesnook") ?? throw new Exception("Notesnook policy not found");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddOAuth2Introspection("introspection", options =>
@@ -135,22 +140,26 @@ namespace Notesnook.API
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.AllowRefresh = true;
context.Properties.IsPersistent = true;
context.HttpContext.User = context.Principal;
context.HttpContext.User = context.Principal ?? throw new Exception("No principal found in token.");
return Task.CompletedTask;
};
options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256();
options.SaveToken = true;
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(30);
});
})
.AddScheme<InboxApiKeyAuthenticationSchemeOptions, InboxApiKeyAuthenticationHandler>(
InboxApiKeyAuthenticationDefaults.AuthenticationScheme,
options => { }
);
BsonSerializer.RegisterSerializer(new SyncItemBsonSerializer());
// Serializer.RegisterSerializer(new SyncItemBsonSerializer());
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
BsonClassMap.RegisterClassMap<UserSettings>();
@@ -160,12 +169,19 @@ namespace Notesnook.API
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
BsonClassMap.RegisterClassMap<CallToAction>();
if (!BsonClassMap.IsClassMapRegistered(typeof(SyncDevice)))
BsonClassMap.RegisterClassMap<SyncDevice>();
services.AddScoped<IDbContext, MongoDbContext>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddRepository<UserSettings>("user_settings", "notesnook")
.AddRepository<Monograph>("monographs", "notesnook")
.AddRepository<Announcement>("announcements", "notesnook");
.AddRepository<Announcement>("announcements", "notesnook")
.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)
@@ -178,15 +194,22 @@ namespace Notesnook.API
.AddMongoCollection(Collections.ShortcutsKey)
.AddMongoCollection(Collections.TagsKey)
.AddMongoCollection(Collections.ColorsKey)
.AddMongoCollection(Collections.VaultsKey);
.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.AddHealthChecks(); // .AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
services.AddHealthChecks();
services.AddSignalR((hub) =>
{
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
@@ -216,24 +239,36 @@ namespace 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.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
}
app.UseForwardedHeadersWithKnownProxies(env);
app.UseOpenTelemetryPrometheusScrapingEndpoint((context) => context.Request.Path == "/metrics" && context.Connection.LocalPort == 5067);
app.UseResponseCompression();
app.UseCors("notesnook");
app.UseVersion();
app.UseVersion(Servers.NotesnookAPI);
app.UseWamp(WampServers.NotesnookServer, (realm, server) =>
{
@@ -257,14 +292,8 @@ namespace Notesnook.API
app.UseEndpoints(endpoints =>
{
endpoints.MapPrometheusScrapingEndpoint();
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
{
options.CloseOnAuthenticationExpiration = false;
options.Transports = HttpTransportType.WebSockets;
});
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
{
options.CloseOnAuthenticationExpiration = false;
@@ -278,7 +307,7 @@ namespace Notesnook.API
{
public static IServiceCollection AddMongoCollection(this IServiceCollection services, string collectionName, string database = "notesnook")
{
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
return services;
}
}

View File

@@ -1,15 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Trace",
"Microsoft.AspNetCore.Http.Connections": "Trace"
}
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017/notesnook",
"DatabaseName": "notesnook"
}
}
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Trace",
"Microsoft.AspNetCore.Http.Connections": "Trace"
}
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017/notesnook",
"DatabaseName": "notesnook"
}
}

View File

@@ -1,7 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

View File

@@ -0,0 +1,2 @@
PORT=5181
NOTESNOOK_API_SERVER_URL=http://localhost:5264/

3
Notesnook.Inbox.API/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.env

View File

@@ -0,0 +1,19 @@
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"]

View File

@@ -0,0 +1,197 @@
{
"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=="],
}
}

View File

@@ -0,0 +1,34 @@
{
"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"
}
}

View File

@@ -0,0 +1,193 @@
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;

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"outDir": "./dist",
"strict": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -8,7 +8,7 @@ This repo contains the full source code of the Notesnook Sync Server licensed un
Requirements:
1. [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
1. [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. [git](https://git-scm.com/downloads)
The first step is to `clone` the repository:
@@ -55,35 +55,30 @@ dotnet run --project Streetwriters.Identity/Streetwriters.Identity.csproj
The sync server can easily be started using Docker.
The first step is to `clone` the repository:
```bash
git clone https://github.com/streetwriters/notesnook-sync-server.git
# change directory
cd notesnook-sync-server
wget https://raw.githubusercontent.com/streetwriters/notesnook-sync-server/master/docker-compose.yml
```
And then use Docker Compose to start the servers:
```bash
docker-compose up
docker compose up
```
This takes care of setting up everything including MongoDB, Minio etc.
## TODO Self-hosting
**Note: Self-hosting the Notesnook Sync Server is not yet possible. We are working to enable full on-premise self hosting so stay tuned!**
**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!**
- [x] Open source the Sync server
- [x] Open source the Identity server
- [x] Open source the SSE Messaging infrastructure
- [x] Fully Dockerize all services
- [x] Use self-hosted Minio for S3 storage
- [ ] Publish on DockerHub
- [x] Publish on DockerHub
- [x] Add settings to change server URLs in Notesnook client apps (starting from v3.0.18)
- [ ] Write self hosting docs
- [ ] Add settings to change server URLs in Notesnook client apps
## License

View File

@@ -0,0 +1,48 @@
/*
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;
}
}

View File

@@ -1,82 +1,83 @@
/*
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 Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
namespace Streetwriters.Common
{
public class Clients
{
public static readonly Client Notesnook = new()
{
Id = "notesnook",
Name = "Notesnook",
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL,
SenderName = Constants.NOTESNOOK_SENDER_NAME,
Type = ApplicationType.NOTESNOOK,
AppId = ApplicationType.NOTESNOOK,
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified",
OnEmailConfirmed = async (userId) =>
{
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
UserId = userId,
Message = new Message
{
Type = "emailConfirmed",
Data = null
}
});
}
};
public static Dictionary<string, Client> ClientsMap = new()
{
{ "notesnook", Notesnook }
};
public static Client FindClientById(string id)
{
if (!IsValidClient(id)) return null;
return ClientsMap[id];
}
public static Client FindClientByAppId(ApplicationType appId)
{
switch (appId)
{
case ApplicationType.NOTESNOOK:
return ClientsMap["notesnook"];
}
return null;
}
public static bool IsValidClient(string id)
{
return ClientsMap.ContainsKey(id);
}
}
/*
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 Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
namespace Streetwriters.Common
{
public class Clients
{
public static readonly Client Notesnook = new()
{
Id = "notesnook",
Name = "Notesnook",
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL ?? "noreply@notesnook.com",
SenderName = "Notesnook",
Type = ApplicationType.NOTESNOOK,
AppId = ApplicationType.NOTESNOOK,
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified",
OnEmailConfirmed = async (userId) =>
{
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
UserId = userId,
Message = new Message
{
Type = "emailConfirmed",
Data = null
}
});
}
};
public static Dictionary<string, Client> ClientsMap = new()
{
{ "notesnook", Notesnook }
};
public static Client? FindClientById(string? id)
{
if (string.IsNullOrEmpty(id) || !IsValidClient(id)) return null;
return ClientsMap[id];
}
public static Client? FindClientByAppId(ApplicationType? appId)
{
if (appId is null) return null;
switch (appId)
{
case ApplicationType.NOTESNOOK:
return ClientsMap["notesnook"];
}
return null;
}
public static bool IsValidClient(string id)
{
return ClientsMap.ContainsKey(id);
}
}
}

View File

@@ -1,83 +1,96 @@
/*
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;
namespace Streetwriters.Common
{
public class Constants
{
public static bool IS_SELF_HOSTED => Environment.GetEnvironmentVariable("SELF_HOSTED") == "1";
// S3 related
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
public static string S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
public static string S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
// SMTP settings
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
public static string SMTP_REPLYTO_NAME => Environment.GetEnvironmentVariable("SMTP_REPLYTO_NAME");
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL");
public static string NOTESNOOK_SENDER_NAME => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_NAME");
public static string NOTESNOOK_APP_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_APP_HOST");
public static string NOTESNOOK_API_SECRET => Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET");
// MessageBird is used for SMS sending
public static string TWILIO_ACCOUNT_SID => Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
public static string TWILIO_AUTH_TOKEN => Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
public static string TWILIO_SERVICE_SID => Environment.GetEnvironmentVariable("TWILIO_SERVICE_SID");
// Server discovery
public static int NOTESNOOK_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_PORT") ?? "80");
public static string NOTESNOOK_SERVER_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_HOST");
public static string NOTESNOOK_SERVER_DOMAIN => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_DOMAIN");
public static string NOTESNOOK_CERT_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_PATH");
public static string NOTESNOOK_CERT_KEY_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_KEY_PATH");
public static int IDENTITY_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("IDENTITY_SERVER_PORT") ?? "80");
public static string IDENTITY_SERVER_HOST => Environment.GetEnvironmentVariable("IDENTITY_SERVER_HOST");
public static string IDENTITY_SERVER_DOMAIN => Environment.GetEnvironmentVariable("IDENTITY_SERVER_DOMAIN");
public static string IDENTITY_CERT_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_PATH");
public static string IDENTITY_CERT_KEY_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_KEY_PATH");
public static int SSE_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SSE_SERVER_PORT") ?? "80");
public static string SSE_SERVER_HOST => Environment.GetEnvironmentVariable("SSE_SERVER_HOST");
public static string SSE_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SSE_SERVER_DOMAIN");
public static string SSE_CERT_PATH => Environment.GetEnvironmentVariable("SSE_CERT_PATH");
public static string SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
// internal
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME");
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
public static string SUBSCRIPTIONS_SERVER_HOST => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_HOST");
public static string SUBSCRIPTIONS_SERVER_DOMAIN => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_DOMAIN");
public static string SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH");
public static string SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH");
public static string[] NOTESNOOK_CORS_ORIGINS => Environment.GetEnvironmentVariable("NOTESNOOK_CORS")?.Split(",") ?? new string[] { };
}
}
/*
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;
namespace Streetwriters.Common
{
public class Constants
{
public static int COMPATIBILITY_VERSION = 1;
public static bool IS_SELF_HOSTED => ReadSecret("SELF_HOSTED") == "1";
public static bool DISABLE_SIGNUPS => ReadSecret("DISABLE_SIGNUPS") == "true";
public static string INSTANCE_NAME => ReadSecret("INSTANCE_NAME") ?? "default";
// S3 related
public static string S3_ACCESS_KEY => ReadSecret("S3_ACCESS_KEY") ?? throw new InvalidOperationException("S3_ACCESS_KEY is required");
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");
public static string S3_REGION => ReadSecret("S3_REGION") ?? throw new InvalidOperationException("S3_REGION is required");
public static string S3_BUCKET_NAME => ReadSecret("S3_BUCKET_NAME") ?? throw new InvalidOperationException("S3_BUCKET_NAME is required");
public static string? S3_INTERNAL_BUCKET_NAME => ReadSecret("S3_INTERNAL_BUCKET_NAME");
public static string? S3_INTERNAL_SERVICE_URL => ReadSecret("S3_INTERNAL_SERVICE_URL");
// SMTP settings
public static string? SMTP_USERNAME => ReadSecret("SMTP_USERNAME");
public static string? SMTP_PASSWORD => ReadSecret("SMTP_PASSWORD");
public static string? SMTP_HOST => ReadSecret("SMTP_HOST");
public static string? SMTP_PORT => ReadSecret("SMTP_PORT");
public static string? SMTP_REPLYTO_EMAIL => ReadSecret("SMTP_REPLYTO_EMAIL");
public static string? NOTESNOOK_SENDER_EMAIL => ReadSecret("NOTESNOOK_SENDER_EMAIL") ?? ReadSecret("SMTP_USERNAME");
public static string? NOTESNOOK_APP_HOST => ReadSecret("NOTESNOOK_APP_HOST");
public static string NOTESNOOK_API_SECRET => ReadSecret("NOTESNOOK_API_SECRET") ?? throw new InvalidOperationException("NOTESNOOK_API_SECRET is required");
// MessageBird is used for SMS sending
public static string? TWILIO_ACCOUNT_SID => ReadSecret("TWILIO_ACCOUNT_SID");
public static string? TWILIO_AUTH_TOKEN => ReadSecret("TWILIO_AUTH_TOKEN");
public static string? TWILIO_SERVICE_SID => ReadSecret("TWILIO_SERVICE_SID");
// Server discovery
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 string? NOTESNOOK_CERT_PATH => ReadSecret("NOTESNOOK_CERT_PATH");
public static string? NOTESNOOK_CERT_KEY_PATH => ReadSecret("NOTESNOOK_CERT_KEY_PATH");
public static string[] KNOWN_PROXIES => (ReadSecret("KNOWN_PROXIES") ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
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 Uri? IDENTITY_SERVER_URL => ReadSecret("IDENTITY_SERVER_URL") is string url ? new Uri(url) : null;
public static string? IDENTITY_CERT_PATH => ReadSecret("IDENTITY_CERT_PATH");
public static string? IDENTITY_CERT_KEY_PATH => ReadSecret("IDENTITY_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");
public static string? SSE_CERT_PATH => ReadSecret("SSE_CERT_PATH");
public static string? SSE_CERT_KEY_PATH => ReadSecret("SSE_CERT_KEY_PATH");
// internal
public static string? WEBRISK_API_URI => ReadSecret("WEBRISK_API_URI");
public static string MONGODB_CONNECTION_STRING => ReadSecret("MONGODB_CONNECTION_STRING") ?? throw new ArgumentNullException("MONGODB_CONNECTION_STRING environment variable is not set");
public static string MONGODB_DATABASE_NAME => ReadSecret("MONGODB_DATABASE_NAME") ?? throw new ArgumentNullException("MONGODB_DATABASE_NAME environment variable is not set");
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(ReadSecret("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
public static string? SUBSCRIPTIONS_SERVER_HOST => ReadSecret("SUBSCRIPTIONS_SERVER_HOST");
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;
}
}
}

View File

@@ -1,54 +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.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;
}
}
}
}
}

View File

@@ -1,26 +1,26 @@
/*
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/>.
*/
namespace Streetwriters.Common.Enums
{
public enum ApplicationType
{
NOTESNOOK = 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/>.
*/
namespace Streetwriters.Common.Enums
{
public enum ApplicationType
{
NOTESNOOK = 0
}
}

View File

@@ -1,29 +1,29 @@
/*
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/>.
*/
namespace Streetwriters.Common.Enums
{
public class MFAMethods
{
public static string Email => "email";
public static string SMS => "sms";
public static string App => "app";
public static string RecoveryCode => "recoveryCode";
}
/*
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/>.
*/
namespace Streetwriters.Common.Enums
{
public class MFAMethods
{
public static string Email => "email";
public static string SMS => "sms";
public static string App => "app";
public static string RecoveryCode => "recoveryCode";
}
}

View File

@@ -17,16 +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/>.
*/
namespace Streetwriters.Common
using System;
namespace Streetwriters.Common.Enums
{
public class Version
public enum SubscriptionPlan
{
public const int MAJOR = 2;
public const int MINOR = 3;
public const int PATCH = 0;
public static string AsString()
{
return $"{Version.MAJOR}.{Version.MINOR}.{Version.PATCH}";
}
FREE = 0,
ESSENTIAL = 1,
PRO = 2,
BELIEVER = 3,
EDUCATION = 4,
LEGACY_PRO = 5
}
}

View File

@@ -1,29 +1,30 @@
/*
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/>.
*/
namespace Streetwriters.Common.Enums
{
public enum SubscriptionProvider
{
STREETWRITERS = 0,
APPLE = 1,
GOOGLE = 2,
PADDLE = 3
}
}
/*
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/>.
*/
namespace Streetwriters.Common.Enums
{
public enum SubscriptionProvider
{
STREETWRITERS = 0,
APPLE = 1,
GOOGLE = 2,
PADDLE = 3,
GIFT_CARD = 4,
}
}

View File

@@ -17,13 +17,14 @@ 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/>.
*/
namespace Notesnook.API.Interfaces
namespace Streetwriters.Common.Enums
{
public interface IEncrypted
public enum SubscriptionStatus
{
string Cipher { get; set; }
string IV { get; set; }
long Length { get; set; }
string Salt { get; set; }
ACTIVE,
TRIAL,
CANCELED,
PAUSED,
EXPIRED
}
}
}

View File

@@ -27,6 +27,6 @@ namespace Streetwriters.Common.Enums
PREMIUM = 5,
PREMIUM_EXPIRED = 6,
PREMIUM_CANCELED = 7,
PREMIUM_PAUSED = 8
PREMIUM_PAUSED = 8,
}
}

View File

@@ -1,77 +1,113 @@
/*
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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using WampSharp.AspNetCore.WebSockets.Server;
using WampSharp.Binding;
using WampSharp.V2;
using WampSharp.V2.Realm;
namespace Streetwriters.Common.Extensions
{
public static class AppBuilderExtensions
{
public static IApplicationBuilder UseVersion(this IApplicationBuilder app)
{
app.Map("/version", (app) =>
{
app.Run(async context =>
{
await context.Response.WriteAsync(Version.AsString());
});
});
return app;
}
public static IApplicationBuilder UseWamp<T>(this IApplicationBuilder app, WampServer<T> server, Action<IWampHostedRealm, WampServer<T>> action) where T : new()
{
WampHost host = new WampHost();
app.Map(server.Endpoint, builder =>
{
builder.UseWebSockets();
host.RegisterTransport(new AspNetCoreWebSocketTransport(builder),
new JTokenJsonBinding(),
new JTokenMsgpackBinding());
});
host.Open();
action.Invoke(host.RealmContainer.GetRealmByName(server.Realm), server);
return app;
}
public static T GetService<T>(this IApplicationBuilder app)
{
return app.ApplicationServices.GetRequiredService<T>();
}
public static T GetScopedService<T>(this IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.CreateScope())
{
return scope.ServiceProvider.GetRequiredService<T>();
}
}
}
/*
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.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WampSharp.AspNetCore.WebSockets.Server;
using WampSharp.Binding;
using WampSharp.V2;
using WampSharp.V2.Realm;
namespace Streetwriters.Common.Extensions
{
public static class AppBuilderExtensions
{
public static IApplicationBuilder UseVersion(this IApplicationBuilder app, Server server)
{
app.Map("/version", (app) =>
{
app.Run(async context =>
{
context.Response.ContentType = "application/json";
var data = new Dictionary<string, object>
{
{ "version", Constants.COMPATIBILITY_VERSION },
{ "id", server.Id ?? "unknown" },
{ "instance", Constants.INSTANCE_NAME }
};
await context.Response.WriteAsync(JsonSerializer.Serialize(data));
});
});
return app;
}
public static IApplicationBuilder UseWamp(this IApplicationBuilder app, WampServer server, Action<IWampHostedRealm, WampServer> action)
{
WampHost host = new();
app.Map(server.Endpoint, builder =>
{
builder.UseWebSockets();
host.RegisterTransport(new AspNetCoreWebSocketTransport(builder),
new JTokenJsonBinding(),
new JTokenMsgpackBinding());
});
host.Open();
action.Invoke(host.RealmContainer.GetRealmByName(server.Realm), server);
return app;
}
public static T GetService<T>(this IApplicationBuilder app) 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;
}
}
}

View File

@@ -1,72 +1,76 @@
/*
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.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Extensions
{
public static class HttpClientExtensions
{
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary headers, HttpMethod method, HttpContent content = null) where T : IResponse, new()
{
var request = new HttpRequestMessage(method, url);
if (method != HttpMethod.Get && method != HttpMethod.Delete)
{
request.Content = content;
}
foreach (var header in headers)
{
if (header.Key == "Content-Type" || header.Key == "Content-Length")
{
if (request.Content != null)
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
continue;
}
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
}
var response = await httpClient.SendAsync(request);
if (response.Content.Headers.ContentLength > 0)
{
var res = await response.Content.ReadFromJsonAsync<T>();
res.Success = response.IsSuccessStatusCode;
res.StatusCode = (int)response.StatusCode;
return res;
}
else
{
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode };
}
}
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
{
var httpContext = accessor.HttpContext;
var content = new StreamContent(httpContext.Request.BodyReader.AsStream());
return httpClient.SendRequestAsync<T>(url, httpContext.Request.Headers, method, content);
}
}
/*
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.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Extensions
{
public static class HttpClientExtensions
{
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary? headers, HttpMethod method, HttpContent? content = null) where T : IResponse, new()
{
var request = new HttpRequestMessage(method, url);
if (method != HttpMethod.Get && method != HttpMethod.Delete)
{
request.Content = content;
}
if (headers != null)
{
foreach (var header in headers)
{
if (header.Key == "Content-Type" || header.Key == "Content-Length")
{
request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
continue;
}
request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
}
}
var response = await httpClient.SendAsync(request);
if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType?.ToString()?.Contains("application/json") == true)
{
var res = await response.Content.ReadFromJsonAsync<T>();
if (res != null)
{
res.Success = response.IsSuccessStatusCode;
res.StatusCode = (int)response.StatusCode;
return res;
}
}
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
}
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
{
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);
}
}
}

View File

@@ -1,53 +1,61 @@
/*
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 Microsoft.Extensions.DependencyInjection;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Repositories;
namespace Streetwriters.Common.Extensions
{
public static class ServiceCollectionServiceExtensions
{
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database) where T : class
{
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddScoped<Repository<T>>();
return services;
}
public static IServiceCollection AddDefaultCors(this IServiceCollection services)
{
services.AddCors(options =>
{
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;
}
}
}
/*
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 Microsoft.Extensions.DependencyInjection;
using Streetwriters.Common.Accessors;
using Streetwriters.Data.DbContexts;
using Streetwriters.Data.Repositories;
namespace Streetwriters.Common.Extensions
{
public static class ServiceCollectionServiceExtensions
{
public static IServiceCollection AddWampServiceAccessor(this IServiceCollection services, Server server)
{
services.AddSingleton<WampServiceAccessor>((provider) => new WampServiceAccessor(server));
services.AddHostedService(provider => provider.GetRequiredService<WampServiceAccessor>());
return services;
}
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database) where T : class
{
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddScoped<Repository<T>>();
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;
}
}
}

View File

@@ -1,87 +1,87 @@
/*
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.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
namespace System
{
public static class StringExtensions
{
public static string Sha256(this string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return Convert.ToBase64String(hash);
}
public static byte[] CompressBrotli(this string input)
{
var raw = Encoding.Default.GetBytes(input);
using (MemoryStream memory = new MemoryStream())
{
using (BrotliStream brotli = new BrotliStream(memory, CompressionLevel.Optimal))
{
brotli.Write(raw, 0, raw.Length);
}
return memory.ToArray();
}
}
public static string DecompressBrotli(this byte[] compressed)
{
using (BrotliStream stream = new BrotliStream(new MemoryStream(compressed), CompressionMode.Decompress))
{
const int size = 4096;
byte[] buffer = new byte[size];
using (MemoryStream memory = new MemoryStream())
{
int count = 0;
do
{
count = stream.Read(buffer, 0, size);
if (count > 0)
{
memory.Write(buffer, 0, count);
}
}
while (count > 0);
return Encoding.Default.GetString(memory.ToArray());
}
}
}
private static string ToHex(byte[] bytes, int startIndex, int length)
{
char[] c = new char[length * 2];
byte b;
for (int bx = startIndex, cx = startIndex; bx < length; ++bx, ++cx)
{
b = ((byte)(bytes[bx] >> 4));
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
b = ((byte)(bytes[bx] & 0x0F));
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
}
return new string(c);
}
}
/*
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.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
namespace System
{
public static class StringExtensions
{
public static string Sha256(this string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return Convert.ToBase64String(hash);
}
public static byte[] CompressBrotli(this string input)
{
var raw = Encoding.Default.GetBytes(input);
using (MemoryStream memory = new MemoryStream())
{
using (BrotliStream brotli = new BrotliStream(memory, CompressionLevel.Optimal))
{
brotli.Write(raw, 0, raw.Length);
}
return memory.ToArray();
}
}
public static string DecompressBrotli(this byte[] compressed)
{
using (BrotliStream stream = new BrotliStream(new MemoryStream(compressed), CompressionMode.Decompress))
{
const int size = 4096;
byte[] buffer = new byte[size];
using (MemoryStream memory = new MemoryStream())
{
int count = 0;
do
{
count = stream.Read(buffer, 0, size);
if (count > 0)
{
memory.Write(buffer, 0, count);
}
}
while (count > 0);
return Encoding.Default.GetString(memory.ToArray());
}
}
}
private static string ToHex(byte[] bytes, int startIndex, int length)
{
char[] c = new char[length * 2];
byte b;
for (int bx = startIndex, cx = startIndex; bx < length; ++bx, ++cx)
{
b = ((byte)(bytes[bx] >> 4));
c[cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
b = ((byte)(bytes[bx] & 0x0F));
c[++cx] = (char)(b > 9 ? b + 0x37 + 0x20 : b + 0x30);
}
return new string(c);
}
}
}

View File

@@ -1,42 +1,66 @@
/*
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 Microsoft.AspNetCore.Builder;
using Streetwriters.Common.Interfaces;
using WampSharp.AspNetCore.WebSockets.Server;
using WampSharp.Binding;
using WampSharp.V2;
using WampSharp.V2.Realm;
namespace Streetwriters.Common.Extensions
{
public static class WampRealmExtensions
{
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, Action<T> onNext)
{
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(onNext);
}
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
{
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
}
}
/*
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.Reactive.Disposables;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Streetwriters.Common.Interfaces;
using WampSharp.AspNetCore.WebSockets.Server;
using WampSharp.Binding;
using WampSharp.V2;
using WampSharp.V2.Realm;
namespace Streetwriters.Common.Extensions
{
public static class WampRealmExtensions
{
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, Action<T> onNext)
{
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(onNext);
}
public static IDisposable Subscribe<T>(this IWampHostedRealm realm, string topicName, IMessageHandler<T> handler)
{
return realm.Services.GetSubject<T>(topicName).Subscribe<T>(async (message) => await handler.Process(message));
}
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();
});
}
}
}

View File

@@ -0,0 +1,62 @@
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;
}
}
}
}

View File

@@ -0,0 +1,23 @@
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;
}
}
}

View File

@@ -17,6 +17,7 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Streetwriters.Common.Messages;
@@ -27,15 +28,28 @@ namespace Streetwriters.Common.Helpers
{
public class WampHelper
{
public static async Task<IWampRealmProxy> OpenWampChannelAsync(string server, string realmName)
public static async Task<IWampChannel> OpenWampChannelAsync(string server, string realmName)
{
DefaultWampChannelFactory channelFactory = new();
IWampChannel channel = channelFactory.CreateJsonChannel(server, realmName);
await channel.Open();
var isConnected = false;
while (!isConnected)
{
try
{
await channel.Open();
isConnected = true;
}
catch
{
await Task.Delay(5000);
continue;
}
}
return channel.RealmProxy;
return channel;
}
public static void PublishMessage<T>(IWampRealmProxy realm, string topicName, T message)
@@ -43,5 +57,12 @@ namespace Streetwriters.Common.Helpers
var subject = realm.Services.GetSubject<T>(topicName);
subject.OnNext(message);
}
public static void PublishMessages<T>(IWampRealmProxy realm, string topicName, IEnumerable<T> messages)
{
var subject = realm.Services.GetSubject<T>(topicName);
foreach (var message in messages)
subject.OnNext(message);
}
}
}

View File

@@ -1,38 +1,38 @@
/*
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.Threading.Tasks;
using Streetwriters.Common.Enums;
namespace Streetwriters.Common.Interfaces
{
public interface IClient
{
string Id { get; set; }
string Name { get; set; }
ApplicationType Type { get; set; }
ApplicationType AppId { get; set; }
string SenderEmail { get; set; }
string SenderName { get; set; }
string EmailConfirmedRedirectURL { get; }
string AccountRecoveryRedirectURL { get; }
Func<string, Task> OnEmailConfirmed { get; set; }
}
}
/*
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.Threading.Tasks;
using Streetwriters.Common.Enums;
namespace Streetwriters.Common.Interfaces
{
public interface IClient
{
string Id { get; set; }
string Name { get; set; }
ApplicationType Type { get; set; }
ApplicationType AppId { get; set; }
string SenderEmail { get; set; }
string SenderName { get; set; }
string EmailConfirmedRedirectURL { get; }
string AccountRecoveryRedirectURL { get; }
Func<string, Task>? OnEmailConfirmed { get; set; }
}
}

View File

@@ -1,29 +1,31 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Streetwriters.Common.Interfaces
{
public interface IDocument
{
string Id
{
get; set;
}
}
}
/*
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 MongoDB.Bson;
namespace Streetwriters.Common.Interfaces
{
public interface IDocument
{
ObjectId Id
{
get; set;
}
}
}

View File

@@ -0,0 +1,19 @@
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
);
}
}

View File

@@ -1,29 +1,29 @@
/*
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 Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Interfaces
{
public interface IMessageHandler<T>
{
Task Process(T message);
}
/*
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 Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Interfaces
{
public interface IMessageHandler<T>
{
Task Process(T message);
}
}

View File

@@ -1,27 +1,30 @@
/*
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/>.
*/
namespace Streetwriters.Common.Interfaces
{
public interface IResponse
{
bool Success { get; set; }
int StatusCode { get; set; }
}
/*
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.Net.Http;
namespace Streetwriters.Common.Interfaces
{
public interface IResponse
{
bool Success { get; set; }
int StatusCode { get; set; }
HttpContent? Content { get; set; }
}
}

View File

@@ -1,41 +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 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; }
}
}

View File

@@ -0,0 +1,13 @@
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);
}
}

View File

@@ -7,7 +7,7 @@ namespace Streetwriters.Common.Interfaces
public interface IUserAccountService
{
[WampProcedure("co.streetwriters.identity.users.get_user")]
Task<UserModel> GetUserAsync(string clientId, string userId);
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")]

View File

@@ -8,6 +8,7 @@ namespace Streetwriters.Common.Interfaces
public interface IUserSubscriptionService
{
[WampProcedure("co.streetwriters.subscriptions.subscriptions.get_user_subscription")]
Task<Subscription> GetUserSubscriptionAsync(string clientId, string userId);
Task<Subscription?> GetUserSubscriptionAsync(string clientId, string userId);
Subscription TransformUserSubscription(Subscription subscription);
}
}

View File

@@ -1,52 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Threading.Tasks;
namespace Streetwriters.Common
{
public class Slogger<T>
{
public static Task Info(string scope, params string[] messages)
{
return Write(Format("info", scope, messages));
}
public static Task Error(string scope, params string[] messages)
{
return Write(Format("error", scope, messages));
}
private static string Format(string level, string scope, params string[] messages)
{
var date = DateTime.UtcNow.ToString("MM-dd-yyyy HH:mm:ss");
var messageText = string.Join(" ", messages);
return $"[{date}] | {level} | <{scope}> {messageText}";
}
private static Task Write(string line)
{
var logDirectory = Path.GetFullPath("./logs");
if (!Directory.Exists(logDirectory))
Directory.CreateDirectory(logDirectory);
var path = Path.Join(logDirectory, typeof(T).FullName + "-" + DateTime.UtcNow.ToString("MM-dd-yyyy") + ".log");
return File.AppendAllLinesAsync(path, new string[1] { line });
}
}
}

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