mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
Compare commits
28 Commits
v1.0-alpha
...
v1.0-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0447ab6e55 | ||
|
|
682d904dc9 | ||
|
|
d8ee28389a | ||
|
|
309dcafa02 | ||
|
|
1b97ba77da | ||
|
|
6d19112fb6 | ||
|
|
1c68942a6d | ||
|
|
3cc84d7603 | ||
|
|
7f94a647c7 | ||
|
|
ba006974a0 | ||
|
|
c3772c86ee | ||
|
|
53695174b5 | ||
|
|
356488beab | ||
|
|
b12eb39797 | ||
|
|
962b805054 | ||
|
|
f3216330a1 | ||
|
|
63069ae573 | ||
|
|
cd06a31d1b | ||
|
|
21a9b4c203 | ||
|
|
a1003ffdd5 | ||
|
|
c66a084ed6 | ||
|
|
dfabfcbc23 | ||
|
|
e324b588a1 | ||
|
|
15b6947ff0 | ||
|
|
c441a1750c | ||
|
|
9f1f3e14d7 | ||
|
|
90118488cb | ||
|
|
e99f0f33d2 |
107
.env
107
.env
@@ -1,51 +1,80 @@
|
||||
# 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: 0 for false; 1 for true
|
||||
DISABLE_ACCOUNT_CREATION=0
|
||||
|
||||
### 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
|
||||
# Description: The FROM email address when sending out emails. Must be an email address under your control otherwise sending will fail. Most times it is the same email address as the SMTP_USERNAME.
|
||||
# Required: no
|
||||
# Example: support@notesnook.com
|
||||
NOTESNOOK_SENDER_EMAIL=
|
||||
# Description: The reply-to email is used whenever a user is replying to the email you sent. You can use this to set a different reply-to email address than the one you used to send the email.
|
||||
# Required: no
|
||||
# Example: support@notesnook.com
|
||||
SMTP_REPLYTO_EMAIL=
|
||||
|
||||
# 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: This is the public URL for the web app, and is used by the backend for creating redirect URLs (e.g. after email confirmation etc).
|
||||
# Note: the URL has no slashes at the end
|
||||
# Required: yes
|
||||
# Example: https://app.notesnook.com
|
||||
NOTESNOOK_APP_PUBLIC_URL=https://app.notesnook.com
|
||||
# Description: This is the public URL for the monograph frontend.
|
||||
# Required: yes
|
||||
# Example: https://monogr.ph
|
||||
MONOGRAPH_PUBLIC_URL=http://localhost:6264
|
||||
# Description: This is the public URL for the Authentication server. Used for generating email confirmation & password reset URLs.
|
||||
# Required: yes
|
||||
# Example: https://auth.streetwriters.co
|
||||
AUTH_SERVER_PUBLIC_URL=http://localhost:8264
|
||||
# Description: This is the public URL for the S3 attachments server (minio). It'll be used by the Notesnook clients for uploading/downloading attachments.
|
||||
# Required: yes
|
||||
# Example: https://attachments.notesnook.com
|
||||
ATTACHMENTS_SERVER_PUBLIC_URL=http://localhost:9000
|
||||
|
||||
# url of the web app instance you want to use
|
||||
# e.g. https://app.notesnook.com
|
||||
# Note: no slashes at the end
|
||||
NOTESNOOK_APP_HOST=
|
||||
|
||||
# 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=
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace Notesnook.API.Controllers
|
||||
[Authorize("Sync")]
|
||||
public class MonographsController : ControllerBase
|
||||
{
|
||||
const string SVG_PIXEL = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><circle r='9'/></svg>";
|
||||
private Repository<Monograph> Monographs { get; set; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private const int MAX_DOC_SIZE = 15 * 1024 * 1024;
|
||||
@@ -127,24 +128,17 @@ namespace Notesnook.API.Controllers
|
||||
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."
|
||||
});
|
||||
}
|
||||
if (monograph == null) return Content(SVG_PIXEL, "image/svg+xml");
|
||||
|
||||
if (monograph.SelfDestruct)
|
||||
await Monographs.DeleteByIdAsync(monograph.Id);
|
||||
|
||||
return Ok();
|
||||
return Content(SVG_PIXEL, "image/svg+xml");
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -156,58 +156,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,12 @@ namespace Notesnook.API.Services
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
const long MAX_SIZE = 513 * 1024 * 1024; // 512 MB
|
||||
if (!Constants.IS_SELF_HOSTED && response.Content.Headers.ContentLength >= MAX_SIZE)
|
||||
{
|
||||
await this.DeleteObjectAsync(userId, name);
|
||||
throw new Exception("File size exceeds the maximum allowed size.");
|
||||
}
|
||||
return response.Content.Headers.ContentLength ?? 0;
|
||||
}
|
||||
|
||||
@@ -214,6 +220,7 @@ namespace Notesnook.API.Services
|
||||
var objectName = GetFullObjectName(userId, name);
|
||||
if (userId == null || objectName == null) return null;
|
||||
|
||||
var client = GetS3Client(mode);
|
||||
var request = new GetPreSignedUrlRequest
|
||||
{
|
||||
BucketName = GetBucketName(mode),
|
||||
@@ -223,16 +230,17 @@ namespace Notesnook.API.Services
|
||||
#if DEBUG
|
||||
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.GetPreSignedURL(request);
|
||||
}
|
||||
|
||||
private string GetPresignedURLForUploadPart(string objectName, string uploadId, int partNumber)
|
||||
{
|
||||
|
||||
return GetS3Client(S3ClientMode.INTERNAL).GetPreSignedURL(new GetPreSignedUrlRequest
|
||||
var client = GetS3Client(S3ClientMode.INTERNAL);
|
||||
return client.GetPreSignedURL(new GetPreSignedUrlRequest
|
||||
{
|
||||
BucketName = GetBucketName(S3ClientMode.INTERNAL),
|
||||
Expires = System.DateTime.Now.AddHours(1),
|
||||
@@ -243,7 +251,7 @@ namespace Notesnook.API.Services
|
||||
#if DEBUG
|
||||
Protocol = Protocol.HTTP,
|
||||
#else
|
||||
Protocol = Constants.IS_SELF_HOSTED ? Protocol.HTTP : Protocol.HTTPS,
|
||||
Protocol = client.Config.ServiceURL.StartsWith("http://") ? Protocol.HTTP : Protocol.HTTPS,
|
||||
#endif
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ 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
|
||||
@@ -77,8 +77,8 @@ This takes care of setting up everything including MongoDB, Minio etc.
|
||||
- [x] Fully Dockerize all services
|
||||
- [x] Use self-hosted Minio for S3 storage
|
||||
- [x] Publish on DockerHub
|
||||
- [x] Add settings to change server URLs in Notesnook client apps (starting from v3.0.18)
|
||||
- [ ] Write self hosting docs
|
||||
- [ ] Add settings to change server URLs in Notesnook client apps
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Streetwriters.Common
|
||||
Id = "notesnook",
|
||||
Name = "Notesnook",
|
||||
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL,
|
||||
SenderName = Constants.NOTESNOOK_SENDER_NAME,
|
||||
SenderName = "Notesnook",
|
||||
Type = ApplicationType.NOTESNOOK,
|
||||
AppId = ApplicationType.NOTESNOOK,
|
||||
AccountRecoveryRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/recovery",
|
||||
|
||||
@@ -1,85 +1,81 @@
|
||||
/*
|
||||
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";
|
||||
public static bool DISABLE_ACCOUNT_CREATION => Environment.GetEnvironmentVariable("DISABLE_ACCOUNT_CREATION") == "1";
|
||||
public static string INSTANCE_NAME => Environment.GetEnvironmentVariable("INSTANCE_NAME") ?? "default";
|
||||
|
||||
// S3 related
|
||||
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
|
||||
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
|
||||
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
|
||||
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
|
||||
public static string S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
|
||||
public static string S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
|
||||
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
|
||||
|
||||
// SMTP settings
|
||||
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
|
||||
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
|
||||
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
|
||||
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
|
||||
public static string SMTP_REPLYTO_NAME => Environment.GetEnvironmentVariable("SMTP_REPLYTO_NAME");
|
||||
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
|
||||
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL");
|
||||
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 => Environment.GetEnvironmentVariable("SELF_HOSTED") == "1";
|
||||
public static bool DISABLE_ACCOUNT_CREATION => Environment.GetEnvironmentVariable("DISABLE_ACCOUNT_CREATION") == "1";
|
||||
public static string INSTANCE_NAME => Environment.GetEnvironmentVariable("INSTANCE_NAME") ?? "default";
|
||||
|
||||
// S3 related
|
||||
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
|
||||
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
|
||||
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
|
||||
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
|
||||
public static string S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
|
||||
public static string S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
|
||||
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
|
||||
|
||||
// SMTP settings
|
||||
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
|
||||
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
|
||||
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
|
||||
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
|
||||
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
|
||||
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL") ?? Environment.GetEnvironmentVariable("SMTP_USERNAME");
|
||||
|
||||
public static string NOTESNOOK_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_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 Uri IDENTITY_SERVER_URL => new(Environment.GetEnvironmentVariable("IDENTITY_SERVER_URL"));
|
||||
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_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_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[] { };
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ namespace Streetwriters.Common.Extensions
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
var data = new Dictionary<string, string>
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "version", Version.AsString() },
|
||||
{ "version", Constants.COMPATIBILITY_VERSION },
|
||||
{ "id", server.Id },
|
||||
{ "instance", Constants.INSTANCE_NAME }
|
||||
};
|
||||
|
||||
@@ -1,125 +1,122 @@
|
||||
/*
|
||||
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.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#if !DEBUG
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#endif
|
||||
|
||||
namespace Streetwriters.Common
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
public Server(string originCertPath = null, string originCertKeyPath = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(originCertPath) && !string.IsNullOrEmpty(originCertKeyPath))
|
||||
this.SSLCertificate = X509Certificate2.CreateFromPemFile(originCertPath, originCertKeyPath);
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Hostname { get; set; }
|
||||
public string Domain { get; set; }
|
||||
public X509Certificate2 SSLCertificate { get; }
|
||||
public bool IsSecure { get => this.SSLCertificate != null; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var url = "";
|
||||
url += "http";
|
||||
url += $"://{Hostname}";
|
||||
url += Port == 80 || Port == 443 ? "" : $":{Port}";
|
||||
return url;
|
||||
}
|
||||
|
||||
public string WS()
|
||||
{
|
||||
var url = "";
|
||||
url += IsSecure ? "ws" : "ws";
|
||||
url += $"://{Hostname}";
|
||||
url += Port == 80 ? "" : $":{Port}";
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public class Servers
|
||||
{
|
||||
#if DEBUG
|
||||
public static string GetLocalIPv4()
|
||||
{
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
string output = "";
|
||||
foreach (NetworkInterface item in interfaces)
|
||||
{
|
||||
if ((item.NetworkInterfaceType == NetworkInterfaceType.Ethernet || item.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) && item.OperationalStatus == OperationalStatus.Up)
|
||||
{
|
||||
foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
output = ip.Address.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
public readonly static string HOST = GetLocalIPv4();
|
||||
public static Server S3Server { get; } = new()
|
||||
{
|
||||
Port = 4568,
|
||||
Hostname = HOST,
|
||||
Domain = HOST
|
||||
};
|
||||
#endif
|
||||
public static Server NotesnookAPI { get; } = new(Constants.NOTESNOOK_CERT_PATH, Constants.NOTESNOOK_CERT_KEY_PATH)
|
||||
{
|
||||
Domain = Constants.NOTESNOOK_SERVER_DOMAIN,
|
||||
Port = Constants.NOTESNOOK_SERVER_PORT,
|
||||
Hostname = Constants.NOTESNOOK_SERVER_HOST,
|
||||
Id = "notesnook-sync"
|
||||
};
|
||||
|
||||
public static Server MessengerServer { get; } = new(Constants.SSE_CERT_PATH, Constants.SSE_CERT_KEY_PATH)
|
||||
{
|
||||
Domain = Constants.SSE_SERVER_DOMAIN,
|
||||
Port = Constants.SSE_SERVER_PORT,
|
||||
Hostname = Constants.SSE_SERVER_HOST,
|
||||
Id = "sse"
|
||||
};
|
||||
|
||||
public static Server IdentityServer { get; } = new(Constants.IDENTITY_CERT_PATH, Constants.IDENTITY_CERT_KEY_PATH)
|
||||
{
|
||||
Domain = Constants.IDENTITY_SERVER_DOMAIN,
|
||||
Port = Constants.IDENTITY_SERVER_PORT,
|
||||
Hostname = Constants.IDENTITY_SERVER_HOST,
|
||||
Id = "auth"
|
||||
};
|
||||
|
||||
public static Server SubscriptionServer { get; } = new(Constants.SUBSCRIPTIONS_CERT_PATH, Constants.SUBSCRIPTIONS_CERT_KEY_PATH)
|
||||
{
|
||||
Domain = Constants.SUBSCRIPTIONS_SERVER_DOMAIN,
|
||||
Port = Constants.SUBSCRIPTIONS_SERVER_PORT,
|
||||
Hostname = Constants.SUBSCRIPTIONS_SERVER_HOST,
|
||||
Id = "subscription"
|
||||
};
|
||||
}
|
||||
}
|
||||
/*
|
||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#if !DEBUG
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#endif
|
||||
|
||||
namespace Streetwriters.Common
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
public Server(string originCertPath = null, string originCertKeyPath = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(originCertPath) && !string.IsNullOrEmpty(originCertKeyPath))
|
||||
this.SSLCertificate = X509Certificate2.CreateFromPemFile(originCertPath, originCertKeyPath);
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Hostname { get; set; }
|
||||
public Uri PublicURL { get; set; }
|
||||
public X509Certificate2 SSLCertificate { get; }
|
||||
public bool IsSecure { get => this.SSLCertificate != null; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var url = "";
|
||||
url += "http";
|
||||
url += $"://{Hostname}";
|
||||
url += Port == 80 || Port == 443 ? "" : $":{Port}";
|
||||
return url;
|
||||
}
|
||||
|
||||
public string WS()
|
||||
{
|
||||
var url = "";
|
||||
url += IsSecure ? "ws" : "ws";
|
||||
url += $"://{Hostname}";
|
||||
url += Port == 80 ? "" : $":{Port}";
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public class Servers
|
||||
{
|
||||
#if DEBUG
|
||||
public static string GetLocalIPv4()
|
||||
{
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
string output = "";
|
||||
foreach (NetworkInterface item in interfaces)
|
||||
{
|
||||
if ((item.NetworkInterfaceType == NetworkInterfaceType.Ethernet || item.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) && item.OperationalStatus == OperationalStatus.Up)
|
||||
{
|
||||
foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses)
|
||||
{
|
||||
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
output = ip.Address.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
public readonly static string HOST = GetLocalIPv4();
|
||||
public static Server S3Server { get; } = new()
|
||||
{
|
||||
Port = 4568,
|
||||
Hostname = HOST
|
||||
};
|
||||
#endif
|
||||
public static Server NotesnookAPI { get; } = new(Constants.NOTESNOOK_CERT_PATH, Constants.NOTESNOOK_CERT_KEY_PATH)
|
||||
{
|
||||
Port = Constants.NOTESNOOK_SERVER_PORT,
|
||||
Hostname = Constants.NOTESNOOK_SERVER_HOST,
|
||||
Id = "notesnook-sync"
|
||||
};
|
||||
|
||||
public static Server MessengerServer { get; } = new(Constants.SSE_CERT_PATH, Constants.SSE_CERT_KEY_PATH)
|
||||
{
|
||||
Port = Constants.SSE_SERVER_PORT,
|
||||
Hostname = Constants.SSE_SERVER_HOST,
|
||||
Id = "sse"
|
||||
};
|
||||
|
||||
public static Server IdentityServer { get; } = new(Constants.IDENTITY_CERT_PATH, Constants.IDENTITY_CERT_KEY_PATH)
|
||||
{
|
||||
PublicURL = Constants.IDENTITY_SERVER_URL,
|
||||
Port = Constants.IDENTITY_SERVER_PORT,
|
||||
Hostname = Constants.IDENTITY_SERVER_HOST,
|
||||
Id = "auth"
|
||||
};
|
||||
|
||||
public static Server SubscriptionServer { get; } = new(Constants.SUBSCRIPTIONS_CERT_PATH, Constants.SUBSCRIPTIONS_CERT_KEY_PATH)
|
||||
{
|
||||
Port = Constants.SUBSCRIPTIONS_SERVER_PORT,
|
||||
Hostname = Constants.SUBSCRIPTIONS_SERVER_HOST,
|
||||
Id = "subscription"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +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/>.
|
||||
*/
|
||||
|
||||
namespace Streetwriters.Common
|
||||
{
|
||||
public class Version
|
||||
{
|
||||
public const int MAJOR = 1;
|
||||
public const int MINOR = 0;
|
||||
public const int PATCH = 0;
|
||||
public static string AsString()
|
||||
{
|
||||
return $"{Version.MAJOR}.{Version.MINOR}.{Version.PATCH}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,344 +1,344 @@
|
||||
/*
|
||||
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.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Identity.Services;
|
||||
using static IdentityServer4.IdentityServerConstants;
|
||||
|
||||
namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[DisplayName("Account")]
|
||||
[Route("account")]
|
||||
[Authorize(LocalApi.PolicyName)]
|
||||
public class AccountController : IdentityControllerBase
|
||||
{
|
||||
private IPersistedGrantStore PersistedGrantStore { get; set; }
|
||||
private ITokenGenerationService TokenGenerationService { get; set; }
|
||||
private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; }
|
||||
private IdentityServerOptions ISOptions { get; set; }
|
||||
private IUserAccountService UserAccountService { get; set; }
|
||||
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IPersistedGrantStore store,
|
||||
ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{
|
||||
PersistedGrantStore = store;
|
||||
TokenGenerationService = tokenGenerationService;
|
||||
UserAccountService = userAccountService;
|
||||
}
|
||||
|
||||
[HttpGet("confirm")]
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(NoStore = true)]
|
||||
public async Task<IActionResult> ConfirmToken(string userId, string code, string clientId, TokenType type)
|
||||
{
|
||||
var client = Clients.FindClientById(clientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByIdAsync(userId);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case TokenType.CONFRIM_EMAIL:
|
||||
{
|
||||
if (await UserManager.IsEmailConfirmedAsync(user)) return Ok("Email already verified.");
|
||||
|
||||
var result = await UserManager.ConfirmEmailAsync(user, code);
|
||||
if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
|
||||
|
||||
if (await UserManager.IsInRoleAsync(user, client.Id))
|
||||
{
|
||||
await client.OnEmailConfirmed(userId);
|
||||
}
|
||||
|
||||
if (!await UserManager.GetTwoFactorEnabledAsync(user))
|
||||
{
|
||||
await MFAService.EnableMFAAsync(user, MFAMethods.Email);
|
||||
user = await UserManager.GetUserAsync(User);
|
||||
}
|
||||
|
||||
var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}";
|
||||
return RedirectPermanent(redirectUrl);
|
||||
}
|
||||
case TokenType.RESET_PASSWORD:
|
||||
{
|
||||
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
|
||||
return BadRequest("Invalid token.");
|
||||
|
||||
var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
|
||||
var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
|
||||
return RedirectPermanent(redirectUrl);
|
||||
}
|
||||
default:
|
||||
return BadRequest("Invalid type.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("verify")]
|
||||
public async Task<IActionResult> SendVerificationEmail([FromForm] string newEmail)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
if (string.IsNullOrEmpty(newEmail))
|
||||
{
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
else
|
||||
{
|
||||
var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail);
|
||||
await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetUserAccount()
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
|
||||
}
|
||||
|
||||
[HttpPost("recover")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ResetUserPassword([FromForm] ResetPasswordForm form)
|
||||
{
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
|
||||
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD, Request.Scheme);
|
||||
#if DEBUG
|
||||
return Ok(callbackUrl);
|
||||
#else
|
||||
await Slogger<AccountController>.Info("ResetUserPassword", user.Email, callbackUrl);
|
||||
await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
|
||||
return Ok();
|
||||
#endif
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
var subjectId = User.FindFirstValue("sub");
|
||||
var jti = User.FindFirstValue("jti");
|
||||
|
||||
var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
|
||||
{
|
||||
ClientId = client.Id,
|
||||
SubjectId = subjectId
|
||||
});
|
||||
grants = grants.Where((grant) => grant.Data.Contains(jti));
|
||||
if (grants.Any())
|
||||
{
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
await PersistedGrantStore.RemoveAsync(grant.Key);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("token")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
|
||||
{
|
||||
if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
|
||||
var user = await UserManager.FindByIdAsync(form.UserId);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId))
|
||||
return BadRequest($"Unable to find user with ID '{form.UserId}'.");
|
||||
|
||||
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode", form.Code))
|
||||
return BadRequest("Invalid authorization_code.");
|
||||
var token = await TokenGenerationService.CreateAccessTokenAsync(user, form.ClientId);
|
||||
return Ok(new
|
||||
{
|
||||
access_token = token,
|
||||
scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)),
|
||||
expires_in = 18000
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
public async Task<IActionResult> UpdateAccount([FromForm] UpdateUserForm form)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id))
|
||||
return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
switch (form.Type)
|
||||
{
|
||||
case "change_email":
|
||||
{
|
||||
var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await UserManager.RemovePasswordAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await UserManager.AddPasswordAsync(user, form.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await UserManager.SetUserNameAsync(user, form.NewEmail);
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Email changed.");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "change_password":
|
||||
{
|
||||
var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
|
||||
return Ok();
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "reset_password":
|
||||
{
|
||||
var result = await UserManager.RemovePasswordAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await MFAService.ResetMFAAsync(user);
|
||||
result = await UserManager.AddPasswordAsync(user, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "change_marketing_consent":
|
||||
{
|
||||
var claimType = $"{client.Id}:marketing_consent";
|
||||
var claims = await UserManager.GetClaimsAsync(user);
|
||||
var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType);
|
||||
if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim);
|
||||
if (!form.Enabled)
|
||||
await UserManager.AddClaimAsync(user, new Claim(claimType, "false"));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
return BadRequest("Invalid type.");
|
||||
}
|
||||
|
||||
[HttpPost("sessions/clear")]
|
||||
public async Task<IActionResult> ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'.");
|
||||
|
||||
var jti = User.FindFirstValue("jti");
|
||||
|
||||
var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
|
||||
{
|
||||
ClientId = client.Id,
|
||||
SubjectId = user.Id.ToString()
|
||||
});
|
||||
var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken);
|
||||
var removedKeys = new List<string>();
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue;
|
||||
await PersistedGrantStore.RemoveAsync(grant.Key);
|
||||
removedKeys.Add(grant.Key);
|
||||
}
|
||||
|
||||
await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked.");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private static string GetHashedKey(string value, string grantType)
|
||||
{
|
||||
return (value + ":" + grantType).Sha256();
|
||||
}
|
||||
|
||||
private async Task SendLogoutMessageAsync(string userId, string reason)
|
||||
{
|
||||
await SendMessageAsync(userId, new Message
|
||||
{
|
||||
Type = "logout",
|
||||
Data = JsonSerializer.Serialize(new { reason })
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(string userId, Message message)
|
||||
{
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
UserId = userId,
|
||||
OriginTokenId = User.FindFirstValue("jti"),
|
||||
Message = 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.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Configuration;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Identity.Services;
|
||||
using static IdentityServer4.IdentityServerConstants;
|
||||
|
||||
namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[DisplayName("Account")]
|
||||
[Route("account")]
|
||||
[Authorize(LocalApi.PolicyName)]
|
||||
public class AccountController : IdentityControllerBase
|
||||
{
|
||||
private IPersistedGrantStore PersistedGrantStore { get; set; }
|
||||
private ITokenGenerationService TokenGenerationService { get; set; }
|
||||
private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; }
|
||||
private IdentityServerOptions ISOptions { get; set; }
|
||||
private IUserAccountService UserAccountService { get; set; }
|
||||
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IPersistedGrantStore store,
|
||||
ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{
|
||||
PersistedGrantStore = store;
|
||||
TokenGenerationService = tokenGenerationService;
|
||||
UserAccountService = userAccountService;
|
||||
}
|
||||
|
||||
[HttpGet("confirm")]
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(NoStore = true)]
|
||||
public async Task<IActionResult> ConfirmToken(string userId, string code, string clientId, TokenType type)
|
||||
{
|
||||
var client = Clients.FindClientById(clientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByIdAsync(userId);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case TokenType.CONFRIM_EMAIL:
|
||||
{
|
||||
if (await UserManager.IsEmailConfirmedAsync(user)) return Ok("Email already verified.");
|
||||
|
||||
var result = await UserManager.ConfirmEmailAsync(user, code);
|
||||
if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
|
||||
|
||||
if (await UserManager.IsInRoleAsync(user, client.Id))
|
||||
{
|
||||
await client.OnEmailConfirmed(userId);
|
||||
}
|
||||
|
||||
if (!await UserManager.GetTwoFactorEnabledAsync(user))
|
||||
{
|
||||
await MFAService.EnableMFAAsync(user, MFAMethods.Email);
|
||||
user = await UserManager.GetUserAsync(User);
|
||||
}
|
||||
|
||||
var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}";
|
||||
return RedirectPermanent(redirectUrl);
|
||||
}
|
||||
case TokenType.RESET_PASSWORD:
|
||||
{
|
||||
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", code))
|
||||
return BadRequest("Invalid token.");
|
||||
|
||||
var authorizationCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode");
|
||||
var redirectUrl = $"{client.AccountRecoveryRedirectURL}?userId={userId}&code={authorizationCode}";
|
||||
return RedirectPermanent(redirectUrl);
|
||||
}
|
||||
default:
|
||||
return BadRequest("Invalid type.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("verify")]
|
||||
public async Task<IActionResult> SendVerificationEmail([FromForm] string newEmail)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
if (string.IsNullOrEmpty(newEmail))
|
||||
{
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
else
|
||||
{
|
||||
var code = await UserManager.GenerateChangeEmailTokenAsync(user, newEmail);
|
||||
await EmailSender.SendChangeEmailConfirmationAsync(newEmail, code, client);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetUserAccount()
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
|
||||
}
|
||||
|
||||
[HttpPost("recover")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ResetUserPassword([FromForm] ResetPasswordForm form)
|
||||
{
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
|
||||
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||
#if DEBUG
|
||||
return Ok(callbackUrl);
|
||||
#else
|
||||
await Slogger<AccountController>.Info("ResetUserPassword", user.Email, callbackUrl);
|
||||
await EmailSender.SendPasswordResetEmailAsync(user.Email, callbackUrl, client);
|
||||
return Ok();
|
||||
#endif
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
var subjectId = User.FindFirstValue("sub");
|
||||
var jti = User.FindFirstValue("jti");
|
||||
|
||||
var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
|
||||
{
|
||||
ClientId = client.Id,
|
||||
SubjectId = subjectId
|
||||
});
|
||||
grants = grants.Where((grant) => grant.Data.Contains(jti));
|
||||
if (grants.Any())
|
||||
{
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
await PersistedGrantStore.RemoveAsync(grant.Key);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("token")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
|
||||
{
|
||||
if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
|
||||
var user = await UserManager.FindByIdAsync(form.UserId);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId))
|
||||
return BadRequest($"Unable to find user with ID '{form.UserId}'.");
|
||||
|
||||
if (!await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "PasswordResetAuthorizationCode", form.Code))
|
||||
return BadRequest("Invalid authorization_code.");
|
||||
var token = await TokenGenerationService.CreateAccessTokenAsync(user, form.ClientId);
|
||||
return Ok(new
|
||||
{
|
||||
access_token = token,
|
||||
scope = string.Join(' ', Config.ApiScopes.Select(s => s.Name)),
|
||||
expires_in = 18000
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
public async Task<IActionResult> UpdateAccount([FromForm] UpdateUserForm form)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id))
|
||||
return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
|
||||
|
||||
switch (form.Type)
|
||||
{
|
||||
case "change_email":
|
||||
{
|
||||
var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await UserManager.RemovePasswordAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await UserManager.AddPasswordAsync(user, form.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await UserManager.SetUserNameAsync(user, form.NewEmail);
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Email changed.");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "change_password":
|
||||
{
|
||||
var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password changed.");
|
||||
return Ok();
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "reset_password":
|
||||
{
|
||||
var result = await UserManager.RemovePasswordAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await MFAService.ResetMFAAsync(user);
|
||||
result = await UserManager.AddPasswordAsync(user, form.NewPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Password reset.");
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
case "change_marketing_consent":
|
||||
{
|
||||
var claimType = $"{client.Id}:marketing_consent";
|
||||
var claims = await UserManager.GetClaimsAsync(user);
|
||||
var marketingConsentClaim = claims.FirstOrDefault((claim) => claim.Type == claimType);
|
||||
if (marketingConsentClaim != null) await UserManager.RemoveClaimAsync(user, marketingConsentClaim);
|
||||
if (!form.Enabled)
|
||||
await UserManager.AddClaimAsync(user, new Claim(claimType, "false"));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
return BadRequest("Invalid type.");
|
||||
}
|
||||
|
||||
[HttpPost("sessions/clear")]
|
||||
public async Task<IActionResult> ClearUserSessions([FromQuery] bool all, [FromForm] string refresh_token)
|
||||
{
|
||||
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'.");
|
||||
|
||||
var jti = User.FindFirstValue("jti");
|
||||
|
||||
var grants = await PersistedGrantStore.GetAllAsync(new PersistedGrantFilter
|
||||
{
|
||||
ClientId = client.Id,
|
||||
SubjectId = user.Id.ToString()
|
||||
});
|
||||
var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken);
|
||||
var removedKeys = new List<string>();
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue;
|
||||
await PersistedGrantStore.RemoveAsync(grant.Key);
|
||||
removedKeys.Add(grant.Key);
|
||||
}
|
||||
|
||||
await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys));
|
||||
await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked.");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private static string GetHashedKey(string value, string grantType)
|
||||
{
|
||||
return (value + ":" + grantType).Sha256();
|
||||
}
|
||||
|
||||
private async Task SendLogoutMessageAsync(string userId, string reason)
|
||||
{
|
||||
await SendMessageAsync(userId, new Message
|
||||
{
|
||||
Type = "logout",
|
||||
Data = JsonSerializer.Serialize(new { reason })
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(string userId, Message message)
|
||||
{
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
UserId = userId,
|
||||
OriginTokenId = User.FindFirstValue("jti"),
|
||||
Message = message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +1,139 @@
|
||||
/*
|
||||
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.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Identity.Services;
|
||||
|
||||
namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("signup")]
|
||||
public class SignupController : IdentityControllerBase
|
||||
{
|
||||
public SignupController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{ }
|
||||
|
||||
private async Task AddClientRoleAsync(string clientId)
|
||||
{
|
||||
if (await RoleManager.FindByNameAsync(clientId) == null)
|
||||
await RoleManager.CreateAsync(new MongoRole(clientId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Signup([FromForm] SignupForm form)
|
||||
{
|
||||
if (Constants.DISABLE_ACCOUNT_CREATION)
|
||||
return BadRequest(new string[] { "Creating new accounts is not allowed." });
|
||||
try
|
||||
{
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest(new string[] { "Invalid client id." });
|
||||
|
||||
await AddClientRoleAsync(client.Id);
|
||||
|
||||
// email addresses must be case-insensitive
|
||||
form.Email = form.Email.ToLowerInvariant();
|
||||
form.Username = form.Username?.ToLowerInvariant();
|
||||
|
||||
if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." });
|
||||
|
||||
var result = await UserManager.CreateAsync(new User
|
||||
{
|
||||
Email = form.Email,
|
||||
EmailConfirmed = Constants.IS_SELF_HOSTED,
|
||||
UserName = form.Username ?? form.Email,
|
||||
}, form.Password);
|
||||
|
||||
if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
|
||||
if (!await UserManager.IsInRoleAsync(user, client.Id))
|
||||
{
|
||||
if (!await UserManager.CheckPasswordAsync(user, form.Password))
|
||||
{
|
||||
// TODO
|
||||
await UserManager.RemovePasswordAsync(user);
|
||||
await UserManager.AddPasswordAsync(user, form.Password);
|
||||
}
|
||||
await MFAService.DisableMFAAsync(user);
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(new string[] { "Invalid email address.." });
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
userId = user.Id.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM));
|
||||
}
|
||||
else
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent)));
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL, Request.Scheme);
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
return Ok(new
|
||||
{
|
||||
userId = user.Id.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await Slogger<SignupController>.Error("Signup", ex.ToString());
|
||||
return BadRequest("Failed to create an account.");
|
||||
}
|
||||
}
|
||||
|
||||
string PlatformFromUserAgent(string userAgent)
|
||||
{
|
||||
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
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.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCore.Identity.Mongo.Model;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Enums;
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Identity.Services;
|
||||
|
||||
namespace Streetwriters.Identity.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("signup")]
|
||||
public class SignupController : IdentityControllerBase
|
||||
{
|
||||
public SignupController(UserManager<User> _userManager, IEmailSender _emailSender,
|
||||
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
|
||||
{ }
|
||||
|
||||
private async Task AddClientRoleAsync(string clientId)
|
||||
{
|
||||
if (await RoleManager.FindByNameAsync(clientId) == null)
|
||||
await RoleManager.CreateAsync(new MongoRole(clientId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Signup([FromForm] SignupForm form)
|
||||
{
|
||||
if (Constants.DISABLE_ACCOUNT_CREATION)
|
||||
return BadRequest(new string[] { "Creating new accounts is not allowed." });
|
||||
try
|
||||
{
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest(new string[] { "Invalid client id." });
|
||||
|
||||
await AddClientRoleAsync(client.Id);
|
||||
|
||||
// email addresses must be case-insensitive
|
||||
form.Email = form.Email.ToLowerInvariant();
|
||||
form.Username = form.Username?.ToLowerInvariant();
|
||||
|
||||
if (!await EmailAddressValidator.IsEmailAddressValidAsync(form.Email)) return BadRequest(new string[] { "Invalid email address." });
|
||||
|
||||
var result = await UserManager.CreateAsync(new User
|
||||
{
|
||||
Email = form.Email,
|
||||
EmailConfirmed = Constants.IS_SELF_HOSTED,
|
||||
UserName = form.Username ?? form.Email,
|
||||
}, form.Password);
|
||||
|
||||
if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
|
||||
if (!await UserManager.IsInRoleAsync(user, client.Id))
|
||||
{
|
||||
if (!await UserManager.CheckPasswordAsync(user, form.Password))
|
||||
{
|
||||
// TODO
|
||||
await UserManager.RemovePasswordAsync(user);
|
||||
await UserManager.AddPasswordAsync(user, form.Password);
|
||||
}
|
||||
await MFAService.DisableMFAAsync(user);
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(new string[] { "Invalid email address.." });
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
userId = user.Id.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
await UserManager.AddToRoleAsync(user, client.Id);
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, UserService.SubscriptionTypeToClaim(client.Id, Common.Enums.SubscriptionType.PREMIUM));
|
||||
}
|
||||
else
|
||||
{
|
||||
await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent)));
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
|
||||
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
|
||||
}
|
||||
return Ok(new
|
||||
{
|
||||
userId = user.Id.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(result.Errors.ToErrors());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await Slogger<SignupController>.Error("Signup", ex.ToString());
|
||||
return BadRequest("Failed to create an account.");
|
||||
}
|
||||
}
|
||||
|
||||
string PlatformFromUserAgent(string userAgent)
|
||||
{
|
||||
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
/*
|
||||
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 Streetwriters.Common;
|
||||
using Streetwriters.Identity.Controllers;
|
||||
using Streetwriters.Identity.Enums;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type, string scheme)
|
||||
{
|
||||
|
||||
return urlHelper.ActionLink(
|
||||
#if DEBUG
|
||||
host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
|
||||
#else
|
||||
host: Servers.IdentityServer.Domain,
|
||||
#endif
|
||||
action: nameof(AccountController.ConfirmToken),
|
||||
controller: "Account",
|
||||
values: new { userId, code, clientId, type },
|
||||
protocol: scheme);
|
||||
|
||||
}
|
||||
}
|
||||
/*
|
||||
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 Streetwriters.Common;
|
||||
using Streetwriters.Identity.Controllers;
|
||||
using Streetwriters.Identity.Enums;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type)
|
||||
{
|
||||
|
||||
return urlHelper.ActionLink(
|
||||
#if DEBUG
|
||||
host: $"{Servers.IdentityServer.Hostname}:{Servers.IdentityServer.Port}",
|
||||
protocol: "http",
|
||||
#else
|
||||
host: Servers.IdentityServer.PublicURL.Host,
|
||||
protocol: Servers.IdentityServer.PublicURL.Scheme,
|
||||
#endif
|
||||
action: nameof(AccountController.ConfirmToken),
|
||||
controller: "Account",
|
||||
values: new { userId, code, clientId, type });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,40 @@
|
||||
/*
|
||||
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;
|
||||
using Sodium;
|
||||
|
||||
namespace Streetwriters.Identity.Helpers
|
||||
{
|
||||
internal class PasswordHelper
|
||||
{
|
||||
public static bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return PasswordHash.ArgonHashStringVerify(hash, password);
|
||||
}
|
||||
|
||||
public static string CreatePasswordHash(string password)
|
||||
{
|
||||
return PasswordHash.ArgonHashString(password, 3, 65536);
|
||||
}
|
||||
}
|
||||
/*
|
||||
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;
|
||||
using Geralt;
|
||||
|
||||
namespace Streetwriters.Identity.Helpers
|
||||
{
|
||||
internal class PasswordHelper
|
||||
{
|
||||
public static bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return Argon2id.VerifyHash(Encoding.UTF8.GetBytes(hash), Encoding.UTF8.GetBytes(password));
|
||||
}
|
||||
|
||||
public static string CreatePasswordHash(string password)
|
||||
{
|
||||
Span<byte> hash = new(new byte[128]);
|
||||
Argon2id.ComputeHash(hash, Encoding.UTF8.GetBytes(password), 3, 65536);
|
||||
return Encoding.UTF8.GetString(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ namespace Streetwriters.Identity.Services
|
||||
{
|
||||
if (int.TryParse(Constants.SMTP_PORT, out int port))
|
||||
{
|
||||
await mailClient.ConnectAsync(Constants.SMTP_HOST, port, MailKit.Security.SecureSocketOptions.StartTls);
|
||||
await mailClient.ConnectAsync(Constants.SMTP_HOST, port, MailKit.Security.SecureSocketOptions.Auto);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,8 +196,8 @@ namespace Streetwriters.Identity.Services
|
||||
message.To.Add(new MailboxAddress("", email));
|
||||
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
|
||||
|
||||
if (!string.IsNullOrEmpty(Constants.SMTP_REPLYTO_NAME) && !string.IsNullOrEmpty(Constants.SMTP_REPLYTO_EMAIL))
|
||||
message.ReplyTo.Add(new MailboxAddress(Constants.SMTP_REPLYTO_NAME, Constants.SMTP_REPLYTO_EMAIL));
|
||||
if (!string.IsNullOrEmpty(Constants.SMTP_REPLYTO_EMAIL))
|
||||
message.ReplyTo.Add(MailboxAddress.Parse(Constants.SMTP_REPLYTO_EMAIL));
|
||||
|
||||
message.Body = await GetEmailBodyAsync(template, client, sender);
|
||||
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
/*
|
||||
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.Identity;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Helpers;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class Argon2PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : User
|
||||
{
|
||||
public string HashPassword(TUser user, string password)
|
||||
{
|
||||
if (password == null)
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
|
||||
return PasswordHelper.CreatePasswordHash(password);
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
if (hashedPassword == null)
|
||||
throw new ArgumentNullException(nameof(hashedPassword));
|
||||
if (providedPassword == null)
|
||||
throw new ArgumentNullException(nameof(providedPassword));
|
||||
|
||||
return PasswordHelper.VerifyPassword(providedPassword, hashedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
|
||||
}
|
||||
}
|
||||
/*
|
||||
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.Identity;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Identity.Helpers;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class Argon2PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : User
|
||||
{
|
||||
public string HashPassword(TUser user, string password)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNullOrEmpty(password, nameof(password));
|
||||
return PasswordHelper.CreatePasswordHash(password);
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNullOrEmpty(hashedPassword, nameof(hashedPassword));
|
||||
ArgumentNullException.ThrowIfNullOrEmpty(providedPassword, nameof(providedPassword));
|
||||
|
||||
return PasswordHelper.VerifyPassword(providedPassword, hashedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using Streetwriters.Identity.Interfaces;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using MessageBird;
|
||||
using MessageBird.Objects;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Streetwriters.Identity.Models;
|
||||
using Streetwriters.Common;
|
||||
using Twilio.Rest.Verify.V2.Service;
|
||||
using Twilio;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class SMSSender : ISMSSender
|
||||
{
|
||||
private Client client;
|
||||
public SMSSender()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Constants.TWILIO_ACCOUNT_SID) && !string.IsNullOrEmpty(Constants.TWILIO_AUTH_TOKEN))
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<StartupObject>Streetwriters.Identity.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||
<PackageReference Include="MailKit" Version="3.4.3" />
|
||||
<PackageReference Include="MessageBird" Version="3.2.0" />
|
||||
<PackageReference Include="Ng.UserAgentService" Version="1.1.2" />
|
||||
<PackageReference Include="Quartz" Version="3.5.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
|
||||
<PackageReference Include="Scriban" Version="5.5.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.24.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Sodium.Core" Version="1.2.3" />
|
||||
<PackageReference Include="IdentityServer4.Contrib.MongoDB" Version="4.0.0-rc.2" />
|
||||
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
|
||||
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
|
||||
<PackageReference Include="Streetwriters.IdentityServer4.KeyRack" Version="0.2.0" />
|
||||
<PackageReference Include="Streetwriters.IdentityServer4.KeyRack.DataProtection" Version="0.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="7.0.4" />
|
||||
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
|
||||
<PackageReference Include="Twilio" Version="6.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.NUglify" Version="2.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Templates\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
|
||||
</ItemGroup>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<StartupObject>Streetwriters.Identity.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
||||
<PackageReference Include="MailKit" Version="3.4.3" />
|
||||
<PackageReference Include="MessageBird" Version="3.2.0" />
|
||||
<PackageReference Include="Ng.UserAgentService" Version="1.1.2" />
|
||||
<PackageReference Include="Quartz" Version="3.5.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
|
||||
<PackageReference Include="Scriban" Version="5.5.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.24.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Geralt" Version="3.1.0" />
|
||||
<PackageReference Include="IdentityServer4.Contrib.MongoDB" Version="4.0.0-rc.2" />
|
||||
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
|
||||
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
|
||||
<PackageReference Include="Streetwriters.IdentityServer4.KeyRack" Version="0.2.0" />
|
||||
<PackageReference Include="Streetwriters.IdentityServer4.KeyRack.DataProtection" Version="0.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="7.0.4" />
|
||||
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
|
||||
<PackageReference Include="Twilio" Version="6.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.13.0" />
|
||||
<PackageReference Include="WebMarkupMin.NUglify" Version="2.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Templates\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Streetwriters.Common\Streetwriters.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -1,150 +1,218 @@
|
||||
x-server-discovery: &server-discovery
|
||||
NOTESNOOK_SERVER_PORT: 80
|
||||
NOTESNOOK_SERVER_HOST: notesnook-server
|
||||
IDENTITY_SERVER_PORT: 80
|
||||
IDENTITY_SERVER_HOST: identity-server
|
||||
SSE_SERVER_PORT: 80
|
||||
SSE_SERVER_HOST: sse-server
|
||||
SELF_HOSTED: 1
|
||||
|
||||
x-env-files: &env-files
|
||||
- .env
|
||||
|
||||
services:
|
||||
notesnook-db:
|
||||
image: mongo:7.0.12
|
||||
networks:
|
||||
- notesnook
|
||||
command: --replSet rs0 --bind_ip_all
|
||||
|
||||
# the notesnook sync server requires transactions which only work
|
||||
# with a MongoDB replica set.
|
||||
# This job just runs `rs.initiate()` on our mongodb instance
|
||||
# upgrading it to a replica set. This is only required once but we running
|
||||
# it multiple times is no issue.
|
||||
initiate-rs0:
|
||||
image: mongo:7.0.12
|
||||
networks:
|
||||
- notesnook
|
||||
depends_on:
|
||||
- notesnook-db
|
||||
entrypoint: /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
mongosh mongodb://notesnook-db:27017 <<EOF
|
||||
rs.initiate();
|
||||
rs.status();
|
||||
EOF
|
||||
|
||||
notesnook-s3:
|
||||
image: minio/minio:RELEASE.2024-07-29T22-14-52Z
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 9090:9090
|
||||
networks:
|
||||
- notesnook
|
||||
volumes:
|
||||
- ${HOME}/.notesnook/s3:/data/s3
|
||||
environment:
|
||||
MINIO_BROWSER: "on"
|
||||
env_file: *env-files
|
||||
command: server /data/s3 --console-address :9090
|
||||
|
||||
# There's no way to specify a default bucket in Minio so we have to
|
||||
# set it up ourselves.
|
||||
setup-s3:
|
||||
image: minio/mc:RELEASE.2024-07-26T13-08-44Z
|
||||
depends_on:
|
||||
- notesnook-s3
|
||||
networks:
|
||||
- notesnook
|
||||
entrypoint: /bin/sh
|
||||
env_file: *env-files
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
until mc alias set minio http://notesnook-s3:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin}; do
|
||||
sleep 1;
|
||||
done;
|
||||
mc mb minio/$$S3_BUCKET_NAME -p
|
||||
|
||||
identity-server:
|
||||
image: streetwriters/identity:latest
|
||||
ports:
|
||||
- "8264:80"
|
||||
networks:
|
||||
- notesnook
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- notesnook-db
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:8264/health || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
MONGODB_CONNECTION_STRING: mongodb://notesnook-db:27017/identity?replSet=rs0
|
||||
MONGODB_DATABASE_NAME: identity
|
||||
|
||||
notesnook-server:
|
||||
image: streetwriters/notesnook-sync:latest
|
||||
ports:
|
||||
- "5264:80"
|
||||
networks:
|
||||
- notesnook
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- notesnook-s3
|
||||
- setup-s3
|
||||
- identity-server
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:5264/health || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
MONGODB_CONNECTION_STRING: mongodb://notesnook-db:27017/?replSet=rs0
|
||||
MONGODB_DATABASE_NAME: notesnook
|
||||
S3_INTERNAL_SERVICE_URL: "${S3_SERVICE_URL:-http://notesnook-s3:9000}"
|
||||
S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-${MINIO_ROOT_USER:-minioadmin}}"
|
||||
S3_ACCESS_KEY: "${S3_ACCESS_KEY:-${MINIO_ROOT_PASSWORD:-minioadmin}}"
|
||||
S3_SERVICE_URL: "${S3_SERVICE_URL:-http://localhost:9000}"
|
||||
S3_REGION: "${S3_REGION:-us-east-1}"
|
||||
S3_BUCKET_NAME: "${S3_BUCKET_NAME}"
|
||||
|
||||
sse-server:
|
||||
image: streetwriters/sse:latest
|
||||
ports:
|
||||
- "7264:80"
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- identity-server
|
||||
- notesnook-server
|
||||
networks:
|
||||
- notesnook
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:7264/health || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
|
||||
autoheal:
|
||||
image: willfarrell/autoheal:latest
|
||||
tty: true
|
||||
restart: always
|
||||
environment:
|
||||
- AUTOHEAL_INTERVAL=60
|
||||
- AUTOHEAL_START_PERIOD=300
|
||||
- AUTOHEAL_DEFAULT_STOP_TIMEOUT=10
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
notesnook:
|
||||
x-server-discovery: &server-discovery
|
||||
NOTESNOOK_SERVER_PORT: 5264
|
||||
NOTESNOOK_SERVER_HOST: notesnook-server
|
||||
IDENTITY_SERVER_PORT: 8264
|
||||
IDENTITY_SERVER_HOST: identity-server
|
||||
SSE_SERVER_PORT: 7264
|
||||
SSE_SERVER_HOST: sse-server
|
||||
SELF_HOSTED: 1
|
||||
|
||||
x-env-files: &env-files
|
||||
- .env
|
||||
|
||||
services:
|
||||
validate:
|
||||
image: vandot/alpine-bash
|
||||
entrypoint: /bin/bash
|
||||
env_file: *env-files
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
# List of required environment variables
|
||||
required_vars=(
|
||||
"INSTANCE_NAME"
|
||||
"NOTESNOOK_API_SECRET"
|
||||
"DISABLE_ACCOUNT_CREATION"
|
||||
"SMTP_USERNAME"
|
||||
"SMTP_PASSWORD"
|
||||
"SMTP_HOST"
|
||||
"SMTP_PORT"
|
||||
"AUTH_SERVER_PUBLIC_URL"
|
||||
"NOTESNOOK_APP_PUBLIC_URL"
|
||||
"MONOGRAPH_PUBLIC_URL"
|
||||
"ATTACHMENTS_SERVER_PUBLIC_URL"
|
||||
)
|
||||
|
||||
# Check each required environment variable
|
||||
for var in "$${required_vars[@]}"; do
|
||||
if [ -z "$${!var}" ]; then
|
||||
echo "Error: Required environment variable $$var is not set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All required environment variables are set."
|
||||
# Ensure the validate service runs first
|
||||
restart: "no"
|
||||
|
||||
notesnook-db:
|
||||
image: mongo:7.0.12
|
||||
hostname: notesnook-db
|
||||
volumes:
|
||||
- ${HOME}/.notesnook/db/data:/data/db
|
||||
- ${HOME}/.notesnook/db/configdb:/data/configdb
|
||||
networks:
|
||||
- notesnook
|
||||
command: --replSet rs0 --bind_ip_all
|
||||
depends_on:
|
||||
validate:
|
||||
condition: service_completed_successfully
|
||||
# the notesnook sync server requires transactions which only work
|
||||
# with a MongoDB replica set.
|
||||
# This job just runs `rs.initiate()` on our mongodb instance
|
||||
# upgrading it to a replica set. This is only required once but we running
|
||||
# it multiple times is no issue.
|
||||
initiate-rs0:
|
||||
image: mongo:7.0.12
|
||||
networks:
|
||||
- notesnook
|
||||
depends_on:
|
||||
- notesnook-db
|
||||
entrypoint: /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
mongosh mongodb://notesnook-db:27017 <<EOF
|
||||
rs.initiate();
|
||||
rs.status();
|
||||
EOF
|
||||
|
||||
notesnook-s3:
|
||||
image: minio/minio:RELEASE.2024-07-29T22-14-52Z
|
||||
ports:
|
||||
- 9000:9000
|
||||
networks:
|
||||
- notesnook
|
||||
volumes:
|
||||
- ${HOME}/.notesnook/s3:/data/s3
|
||||
environment:
|
||||
MINIO_BROWSER: "on"
|
||||
depends_on:
|
||||
validate:
|
||||
condition: service_completed_successfully
|
||||
env_file: *env-files
|
||||
command: server /data/s3 --console-address :9090
|
||||
|
||||
# There's no way to specify a default bucket in Minio so we have to
|
||||
# set it up ourselves.
|
||||
setup-s3:
|
||||
image: minio/mc:RELEASE.2024-07-26T13-08-44Z
|
||||
depends_on:
|
||||
- notesnook-s3
|
||||
networks:
|
||||
- notesnook
|
||||
entrypoint: /bin/bash
|
||||
env_file: *env-files
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
until mc alias set minio http://notesnook-s3:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin}; do
|
||||
sleep 1;
|
||||
done;
|
||||
mc mb minio/attachments -p
|
||||
|
||||
identity-server:
|
||||
image: streetwriters/identity:latest
|
||||
ports:
|
||||
- 8264:8264
|
||||
networks:
|
||||
- notesnook
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- notesnook-db
|
||||
healthcheck:
|
||||
test: wget --tries=1 -nv -q http://localhost:8264/health -O- || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
MONGODB_CONNECTION_STRING: mongodb://notesnook-db:27017/identity?replSet=rs0
|
||||
MONGODB_DATABASE_NAME: identity
|
||||
IDENTITY_SERVER_URL: ${AUTH_SERVER_PUBLIC_URL}
|
||||
NOTESNOOK_APP_HOST: ${NOTESNOOK_APP_PUBLIC_URL}
|
||||
|
||||
notesnook-server:
|
||||
image: streetwriters/notesnook-sync:latest
|
||||
ports:
|
||||
- 5264:5264
|
||||
networks:
|
||||
- notesnook
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- notesnook-s3
|
||||
- setup-s3
|
||||
- identity-server
|
||||
healthcheck:
|
||||
test: wget --tries=1 -nv -q http://localhost:5264/health -O- || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
MONGODB_CONNECTION_STRING: mongodb://notesnook-db:27017/?replSet=rs0
|
||||
MONGODB_DATABASE_NAME: notesnook
|
||||
S3_INTERNAL_SERVICE_URL: "http://notesnook-s3:9000"
|
||||
S3_INTERNAL_BUCKET_NAME: "attachments"
|
||||
S3_ACCESS_KEY_ID: "${MINIO_ROOT_USER:-minioadmin}"
|
||||
S3_ACCESS_KEY: "${MINIO_ROOT_PASSWORD:-minioadmin}"
|
||||
S3_SERVICE_URL: "${ATTACHMENTS_SERVER_PUBLIC_URL}"
|
||||
S3_REGION: "us-east-1"
|
||||
S3_BUCKET_NAME: "attachments"
|
||||
|
||||
sse-server:
|
||||
image: streetwriters/sse:latest
|
||||
ports:
|
||||
- 7264:7264
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- identity-server
|
||||
- notesnook-server
|
||||
networks:
|
||||
- notesnook
|
||||
healthcheck:
|
||||
test: wget --tries=1 -nv -q http://localhost:7264/health -O- || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
|
||||
monograph-server:
|
||||
image: streetwriters/monograph:latest
|
||||
ports:
|
||||
- 6264:3000
|
||||
env_file: *env-files
|
||||
depends_on:
|
||||
- notesnook-server
|
||||
networks:
|
||||
- notesnook
|
||||
healthcheck:
|
||||
test: wget --tries=1 -nv -q http://localhost:3000/api/health -O- || exit 1
|
||||
interval: 40s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
environment:
|
||||
<<: *server-discovery
|
||||
API_HOST: http://notesnook-server:5264
|
||||
PUBLIC_URL: ${MONOGRAPH_PUBLIC_URL}
|
||||
|
||||
autoheal:
|
||||
image: willfarrell/autoheal:latest
|
||||
tty: true
|
||||
restart: always
|
||||
environment:
|
||||
- AUTOHEAL_INTERVAL=60
|
||||
- AUTOHEAL_START_PERIOD=300
|
||||
- AUTOHEAL_DEFAULT_STOP_TIMEOUT=10
|
||||
depends_on:
|
||||
validate:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
notesnook:
|
||||
|
||||
Reference in New Issue
Block a user