mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-08-13 12:00:20 +02:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b953f756e | ||
|
|
d27ab68735 | ||
|
|
294d885dbf | ||
|
|
bdd5017394 | ||
|
|
7ad70c63ee | ||
|
|
0367ab6f80 | ||
|
|
f3bfe0957b | ||
|
|
7f614f6954 | ||
|
|
6663778e3e | ||
|
|
14f0a3b37e | ||
|
|
82a1152f9f | ||
|
|
580524b855 | ||
|
|
159fe0e376 | ||
|
|
30fdaae36c | ||
|
|
815c8fb84c | ||
|
|
04b0c305ed |
@@ -42,6 +42,7 @@ namespace Notesnook.API.Accessors
|
||||
public SyncItemsRepository Colors { get; }
|
||||
public SyncItemsRepository Vaults { get; }
|
||||
public SyncItemsRepository Tags { get; }
|
||||
public SyncItemsRepository InboxItemsHistory { get; }
|
||||
public Repository<UserSettings> UsersSettings { get; }
|
||||
public Repository<Monograph> Monographs { get; }
|
||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
||||
@@ -75,6 +76,8 @@ namespace Notesnook.API.Accessors
|
||||
IMongoCollection<SyncItem> vaults,
|
||||
[FromKeyedServices(Collections.TagsKey)]
|
||||
IMongoCollection<SyncItem> tags,
|
||||
[FromKeyedServices(Collections.InboxItemsHistoryKey)]
|
||||
IMongoCollection<SyncItem> inboxItemsHistory,
|
||||
|
||||
Repository<UserSettings> usersSettings,
|
||||
Repository<Monograph> monographs,
|
||||
@@ -102,6 +105,7 @@ namespace Notesnook.API.Accessors
|
||||
Colors = new SyncItemsRepository(dbContext, colors, logger);
|
||||
Vaults = new SyncItemsRepository(dbContext, vaults, logger);
|
||||
Tags = new SyncItemsRepository(dbContext, tags, logger);
|
||||
InboxItemsHistory = new SyncItemsRepository(dbContext, inboxItemsHistory, logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,6 @@ namespace Notesnook.API
|
||||
public const string InboxApiKeysKey = "inbox_api_keys";
|
||||
public const string SyncDevicesKey = "sync_devices";
|
||||
public const string DeviceIdsChunksKey = "device_ids_chunks";
|
||||
public const string InboxItemsHistoryKey = "inbox_items_history";
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
@@ -72,9 +73,9 @@ namespace Notesnook.API.Controllers
|
||||
{
|
||||
return BadRequest(new { error = "Api key name is required." });
|
||||
}
|
||||
if (request.ExpiryDate <= -1)
|
||||
if (request.ExpiryDate == null)
|
||||
{
|
||||
return BadRequest(new { error = "Valid expiry date is required." });
|
||||
return BadRequest(new { error = "Expiry date is required." });
|
||||
}
|
||||
|
||||
var count = await inboxApiKeysRepository.CountAsync(t => t.UserId == userId);
|
||||
@@ -133,7 +134,7 @@ namespace Notesnook.API.Controllers
|
||||
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
|
||||
if (string.IsNullOrWhiteSpace(userSetting?.InboxKeys?.Public))
|
||||
{
|
||||
return BadRequest(new { error = "Inbox public key is not configured." });
|
||||
return NotFound(new { error = "Inbox public key is not configured." });
|
||||
}
|
||||
return Ok(new { key = userSetting.InboxKeys.Public });
|
||||
}
|
||||
|
||||
@@ -183,8 +183,8 @@ namespace Notesnook.API.Controllers
|
||||
try
|
||||
{
|
||||
var userId = this.User.GetUserId();
|
||||
var size = await s3Service.GetObjectSizeAsync(userId, name);
|
||||
HttpContext.Response.Headers.ContentLength = size;
|
||||
var size = await s3Service.GetObjectSizeAsync(userId, name); Response.Headers.ContentLength = size;
|
||||
Response.Headers["X-Object-Size"] = size.ToString();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -57,21 +57,9 @@ namespace Notesnook.API.Hubs
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private SyncDeviceService SyncDeviceService { get; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private static readonly string[] CollectionKeys = [
|
||||
"settingitem",
|
||||
"attachment",
|
||||
"note",
|
||||
"notebook",
|
||||
"content",
|
||||
"shortcut",
|
||||
"reminder",
|
||||
"color",
|
||||
"tag",
|
||||
"vault",
|
||||
"relation", // relations must sync at the end to prevent invalid state
|
||||
];
|
||||
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
|
||||
private readonly Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
||||
private readonly CollectionDef[] BaseCollectionDefs;
|
||||
private readonly CollectionDef[] V4CollectionDefs;
|
||||
ILogger<SyncV2Hub> Logger { get; }
|
||||
|
||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
|
||||
@@ -81,18 +69,32 @@ namespace Notesnook.API.Hubs
|
||||
unit = unitOfWork;
|
||||
SyncDeviceService = syncDeviceService;
|
||||
|
||||
Collections = [
|
||||
Repositories.Settings.FindItemsById,
|
||||
Repositories.Attachments.FindItemsById,
|
||||
Repositories.Notes.FindItemsById,
|
||||
Repositories.Notebooks.FindItemsById,
|
||||
Repositories.Contents.FindItemsById,
|
||||
Repositories.Shortcuts.FindItemsById,
|
||||
Repositories.Reminders.FindItemsById,
|
||||
Repositories.Colors.FindItemsById,
|
||||
Repositories.Tags.FindItemsById,
|
||||
Repositories.Vaults.FindItemsById,
|
||||
Repositories.Relations.FindItemsById,
|
||||
BaseCollectionDefs = [
|
||||
new("settingitem", Repositories.Settings.FindItemsById),
|
||||
new("attachment", Repositories.Attachments.FindItemsById),
|
||||
new("note", Repositories.Notes.FindItemsById),
|
||||
new("notebook", Repositories.Notebooks.FindItemsById),
|
||||
new("content", Repositories.Contents.FindItemsById),
|
||||
new("shortcut", Repositories.Shortcuts.FindItemsById),
|
||||
new("reminder", Repositories.Reminders.FindItemsById),
|
||||
new("color", Repositories.Colors.FindItemsById),
|
||||
new("tag", Repositories.Tags.FindItemsById),
|
||||
new("vault", Repositories.Vaults.FindItemsById),
|
||||
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
|
||||
];
|
||||
V4CollectionDefs = [
|
||||
new("settingitem", Repositories.Settings.FindItemsById),
|
||||
new("attachment", Repositories.Attachments.FindItemsById),
|
||||
new("note", Repositories.Notes.FindItemsById),
|
||||
new("notebook", Repositories.Notebooks.FindItemsById),
|
||||
new("content", Repositories.Contents.FindItemsById),
|
||||
new("shortcut", Repositories.Shortcuts.FindItemsById),
|
||||
new("reminder", Repositories.Reminders.FindItemsById),
|
||||
new("color", Repositories.Colors.FindItemsById),
|
||||
new("tag", Repositories.Tags.FindItemsById),
|
||||
new("vault", Repositories.Vaults.FindItemsById),
|
||||
new("inboxitemhistory", Repositories.InboxItemsHistory.FindItemsById),
|
||||
new("relation", Repositories.Relations.FindItemsById), // relations must sync at the end to prevent invalid state
|
||||
];
|
||||
UpsertActionsMap = new Dictionary<string, Action<IEnumerable<SyncItem>, string, long>> {
|
||||
{ "settingitem", Repositories.Settings.UpsertMany },
|
||||
@@ -106,6 +108,7 @@ namespace Notesnook.API.Hubs
|
||||
{ "color", Repositories.Colors.UpsertMany },
|
||||
{ "vault", Repositories.Vaults.UpsertMany },
|
||||
{ "tag", Repositories.Tags.UpsertMany },
|
||||
{ "inboxitemhistory", Repositories.InboxItemsHistory.UpsertMany },
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
@@ -122,6 +125,19 @@ namespace Notesnook.API.Hubs
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Logger.LogWarning(exception, "Connection {ConnectionId} disconnected with error (server-side drop)", Context.ConnectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("Connection {ConnectionId} disconnected cleanly (client-initiated)", Context.ConnectionId);
|
||||
}
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
|
||||
{
|
||||
@@ -168,17 +184,17 @@ namespace Notesnook.API.Hubs
|
||||
return true;
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes)
|
||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes, CollectionDef[] collectionDefs)
|
||||
{
|
||||
var itemsProcessed = 0;
|
||||
for (int i = 0; i < Collections.Length; i++)
|
||||
foreach (var def in collectionDefs)
|
||||
{
|
||||
var type = CollectionKeys[i];
|
||||
var type = def.Key;
|
||||
|
||||
var filteredIds = ids.Where((id) => id.Type == type).Select((id) => id.ItemId).ToArray();
|
||||
if (!resetSync && filteredIds.Length == 0) continue;
|
||||
|
||||
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
|
||||
using var cursor = await def.FindItems(userId, filteredIds, resetSync, size);
|
||||
|
||||
var chunk = new List<SyncItem>();
|
||||
long totalBytes = 0;
|
||||
@@ -220,20 +236,25 @@ namespace Notesnook.API.Hubs
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
|
||||
{
|
||||
return await HandleRequestFetch(deviceId, false, false);
|
||||
return await HandleRequestFetch(deviceId, false, false, BaseCollectionDefs);
|
||||
}
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetchV2(string deviceId)
|
||||
{
|
||||
return await HandleRequestFetch(deviceId, true, false);
|
||||
return await HandleRequestFetch(deviceId, true, false, BaseCollectionDefs);
|
||||
}
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetchV3(string deviceId)
|
||||
{
|
||||
return await HandleRequestFetch(deviceId, true, true);
|
||||
return await HandleRequestFetch(deviceId, true, true, BaseCollectionDefs);
|
||||
}
|
||||
|
||||
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems)
|
||||
public async Task<SyncV2Metadata> RequestFetchV4(string deviceId)
|
||||
{
|
||||
return await HandleRequestFetch(deviceId, true, true, V4CollectionDefs);
|
||||
}
|
||||
|
||||
private async Task<SyncV2Metadata> HandleRequestFetch(string deviceId, bool includeMonographs, bool includeInboxItems, CollectionDef[] collectionDefs)
|
||||
{
|
||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync.");
|
||||
|
||||
@@ -257,7 +278,8 @@ namespace Notesnook.API.Hubs
|
||||
ids,
|
||||
size: 100,
|
||||
resetSync: device.IsSyncReset,
|
||||
maxBytes: 3 * 1024 * 1024
|
||||
maxBytes: 3 * 1024 * 1024,
|
||||
collectionDefs
|
||||
);
|
||||
|
||||
await foreach (var chunk in chunks)
|
||||
@@ -312,7 +334,7 @@ namespace Notesnook.API.Hubs
|
||||
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
|
||||
var userInboxItems = device.IsSyncReset
|
||||
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
|
||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId ?? m.Id.ToString()));
|
||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId));
|
||||
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||
{
|
||||
throw new HubException("Client rejected inbox items.");
|
||||
@@ -331,6 +353,11 @@ namespace Notesnook.API.Hubs
|
||||
SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private record CollectionDef(
|
||||
string Key,
|
||||
Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> FindItems
|
||||
);
|
||||
}
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Notesnook.API.Interfaces
|
||||
SyncItemsRepository Colors { get; }
|
||||
SyncItemsRepository Vaults { get; }
|
||||
SyncItemsRepository Tags { get; }
|
||||
SyncItemsRepository InboxItemsHistory { get; }
|
||||
Repository<UserSettings> UsersSettings { get; }
|
||||
Repository<Monograph> Monographs { get; }
|
||||
Repository<InboxApiKey> InboxApiKey { get; }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Aws.S3" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
|
||||
<PackageReference Include="AWSSDK.Core" Version="3.7.304.31" />
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
||||
|
||||
@@ -174,18 +174,19 @@ namespace Notesnook.API.Services
|
||||
else
|
||||
{
|
||||
userSettings.InboxKeys = keys.InboxKeys;
|
||||
var defaultInboxKey = new InboxApiKey
|
||||
{
|
||||
UserId = userId,
|
||||
Name = "Default",
|
||||
DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(),
|
||||
LastUsedAt = 0
|
||||
};
|
||||
await Repositories.InboxApiKey.InsertAsync(defaultInboxKey);
|
||||
}
|
||||
|
||||
await Repositories.InboxItems.DeleteManyAsync(t => t.UserId == userId);
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
OriginTokenId = null,
|
||||
UserId = userId,
|
||||
Message = new Message
|
||||
{
|
||||
Type = "inboxUpdated",
|
||||
Data = JsonSerializer.Serialize(new { reason = "Inbox PGP keys added, updated, or removed." })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings);
|
||||
@@ -208,6 +209,7 @@ namespace Notesnook.API.Services
|
||||
Repositories.Colors.DeleteByUserId(userId);
|
||||
Repositories.Tags.DeleteByUserId(userId);
|
||||
Repositories.Vaults.DeleteByUserId(userId);
|
||||
Repositories.InboxItemsHistory.DeleteByUserId(userId);
|
||||
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
|
||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
||||
@@ -269,6 +271,7 @@ namespace Notesnook.API.Services
|
||||
Repositories.Colors.DeleteByUserId(userId);
|
||||
Repositories.Tags.DeleteByUserId(userId);
|
||||
Repositories.Vaults.DeleteByUserId(userId);
|
||||
Repositories.InboxItemsHistory.DeleteByUserId(userId);
|
||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
||||
if (!await unit.Commit()) return false;
|
||||
|
||||
@@ -25,6 +25,7 @@ using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Runtime;
|
||||
using StackExchange.Redis;
|
||||
using IdentityModel.AspNetCore.OAuth2Introspection;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
@@ -196,7 +197,8 @@ namespace Notesnook.API
|
||||
.AddMongoCollection(Collections.ColorsKey)
|
||||
.AddMongoCollection(Collections.VaultsKey)
|
||||
.AddMongoCollection(Collections.InboxItemsKey)
|
||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
||||
.AddMongoCollection(Collections.InboxApiKeysKey)
|
||||
.AddMongoCollection(Collections.InboxItemsHistoryKey);
|
||||
|
||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||
services.AddScoped<SyncDeviceService>();
|
||||
@@ -219,7 +221,20 @@ namespace Notesnook.API
|
||||
}).AddMessagePackProtocol().AddJsonProtocol();
|
||||
|
||||
if (!string.IsNullOrEmpty(Constants.SIGNALR_REDIS_CONNECTION_STRING))
|
||||
signalR.AddStackExchangeRedis(Constants.SIGNALR_REDIS_CONNECTION_STRING);
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddRedis(Constants.SIGNALR_REDIS_CONNECTION_STRING, tags: ["ready"]);
|
||||
signalR.AddStackExchangeRedis(options =>
|
||||
{
|
||||
options.Configuration = ConfigurationOptions.Parse(Constants.SIGNALR_REDIS_CONNECTION_STRING);
|
||||
options.Configuration.AbortOnConnectFail = false;
|
||||
options.Configuration.ConnectRetry = 5;
|
||||
options.Configuration.ReconnectRetryPolicy = new ExponentialRetry(5000, 30000);
|
||||
options.Configuration.KeepAlive = 60;
|
||||
options.Configuration.ConnectTimeout = 5000;
|
||||
options.Configuration.SyncTimeout = 5000;
|
||||
});
|
||||
}
|
||||
|
||||
services.AddResponseCompression(options =>
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ const RawInboxItemSchema = z.object({
|
||||
notebookIds: z.array(z.string()).optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
type: z.enum(["note"]),
|
||||
source: z.string(),
|
||||
source: z.string().min(1, "Source is required"),
|
||||
version: z.literal(1),
|
||||
content: z
|
||||
.object({
|
||||
@@ -56,7 +56,9 @@ async function encrypt(
|
||||
};
|
||||
}
|
||||
|
||||
async function getInboxPublicEncryptionKey(apiKey: string) {
|
||||
async function getInboxPublicEncryptionKey(
|
||||
apiKey: string,
|
||||
): Promise<{ status: "unauthorized" } | { status: "ok"; key: string | null }> {
|
||||
const response = await fetch(
|
||||
`${NOTESNOOK_API_SERVER_URL}/inbox/public-encryption-key`,
|
||||
{
|
||||
@@ -65,6 +67,9 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.status === 401) {
|
||||
return { status: "unauthorized" };
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`failed to fetch inbox public encryption key: ${await response.text()}`,
|
||||
@@ -72,7 +77,7 @@ async function getInboxPublicEncryptionKey(apiKey: string) {
|
||||
}
|
||||
|
||||
const data = (await response.json()) as unknown as any;
|
||||
return (data?.key as string) || null;
|
||||
return { status: "ok", key: (data?.key as string) || null };
|
||||
}
|
||||
|
||||
async function postEncryptedInboxItem(
|
||||
@@ -110,10 +115,14 @@ app.post("/", async (req, res) => {
|
||||
return res.status(401).json({ error: "unauthorized" });
|
||||
}
|
||||
|
||||
const inboxPublicKey = await getInboxPublicEncryptionKey(apiKey);
|
||||
if (!inboxPublicKey) {
|
||||
return res.status(403).json({ error: "inbox public key not found" });
|
||||
const encryptionKeyResult = await getInboxPublicEncryptionKey(apiKey);
|
||||
if (encryptionKeyResult.status === "unauthorized") {
|
||||
return res.status(401).json({ error: "unauthorized" });
|
||||
}
|
||||
if (!encryptionKeyResult.key) {
|
||||
return res.status(404).json({ error: "inbox public key not found" });
|
||||
}
|
||||
const inboxPublicKey = encryptionKeyResult.key;
|
||||
console.log("[info] fetched inbox public key");
|
||||
|
||||
const validationResult = RawInboxItemSchema.safeParse(req.body);
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace Streetwriters.Common.Extensions
|
||||
b.WithOrigins(Constants.NOTESNOOK_CORS_ORIGINS);
|
||||
|
||||
b.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders(["X-Object-Size", "Content-Length"]);
|
||||
});
|
||||
});
|
||||
return services;
|
||||
|
||||
@@ -190,8 +190,8 @@ namespace Streetwriters.Identity.Controllers
|
||||
var client = Clients.FindClientById(form.ClientId);
|
||||
if (client == null) return BadRequest("Invalid client_id.");
|
||||
|
||||
var user = await UserManager.FindByEmailAsync(form.Email) ?? throw new Exception("User not found.");
|
||||
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
var user = await UserManager.FindByEmailAsync(form.Email);
|
||||
if (user == null || !await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
|
||||
|
||||
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
|
||||
var callbackUrl = UrlExtensions.TokenLink(user.Id.ToString(), code, client.Id, TokenType.RESET_PASSWORD);
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Streetwriters.Identity.Interfaces
|
||||
{
|
||||
public interface ISMSSender
|
||||
{
|
||||
Task<string> SendOTPAsync(string number, IClient client);
|
||||
Task<string?> SendOTPAsync(string number, IClient client);
|
||||
Task<bool> VerifyOTPAsync(string id, string code);
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,8 @@ namespace Streetwriters.Identity.Services
|
||||
ArgumentNullException.ThrowIfNull(form.PhoneNumber);
|
||||
await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
|
||||
var id = await SMSSender.SendOTPAsync(form.PhoneNumber, client);
|
||||
if (string.IsNullOrEmpty(id)) throw new Exception("Failed to send SMS. Please try again.");
|
||||
|
||||
logger.LogInformation("SMS OTP sent for user: {UserId}, SMS ID: {SmsId}", user.Id, id);
|
||||
await this.ReplaceClaimAsync(user, MFAService.SMS_ID_CLAIM, id);
|
||||
break;
|
||||
|
||||
@@ -23,36 +23,56 @@ using Streetwriters.Common;
|
||||
using Twilio.Rest.Verify.V2.Service;
|
||||
using Twilio;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Streetwriters.Identity.Services
|
||||
{
|
||||
public class SMSSender : ISMSSender
|
||||
{
|
||||
public SMSSender()
|
||||
private readonly ILogger<SMSSender> Logger;
|
||||
public SMSSender(ILogger<SMSSender> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
if (!string.IsNullOrEmpty(Constants.TWILIO_ACCOUNT_SID) && !string.IsNullOrEmpty(Constants.TWILIO_AUTH_TOKEN))
|
||||
{
|
||||
TwilioClient.Init(Constants.TWILIO_ACCOUNT_SID, Constants.TWILIO_AUTH_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> SendOTPAsync(string number, IClient app)
|
||||
public async Task<string?> SendOTPAsync(string number, IClient app)
|
||||
{
|
||||
var verification = await VerificationResource.CreateAsync(
|
||||
to: number,
|
||||
channel: "sms",
|
||||
pathServiceSid: Constants.TWILIO_SERVICE_SID
|
||||
);
|
||||
return verification.Sid;
|
||||
try
|
||||
{
|
||||
var verification = await VerificationResource.CreateAsync(
|
||||
to: number,
|
||||
channel: "sms",
|
||||
pathServiceSid: Constants.TWILIO_SERVICE_SID
|
||||
);
|
||||
return verification.Sid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error sending OTP with Twilio");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyOTPAsync(string id, string code)
|
||||
{
|
||||
return (await VerificationCheckResource.CreateAsync(
|
||||
verificationSid: id,
|
||||
pathServiceSid: Constants.TWILIO_SERVICE_SID,
|
||||
code: code
|
||||
)).Status == "approved";
|
||||
try
|
||||
{
|
||||
return (await VerificationCheckResource.CreateAsync(
|
||||
verificationSid: id,
|
||||
pathServiceSid: Constants.TWILIO_SERVICE_SID,
|
||||
code: code
|
||||
)).Status == "approved";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error verifying OTP with Twilio");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,13 @@ namespace Streetwriters.Identity.Services
|
||||
|
||||
await mfaService.ResetMFAAsync(user);
|
||||
result = await userManager.AddPasswordAsync(user, newPassword);
|
||||
|
||||
// force change email to lowercase if it is not already
|
||||
if (user.Email != null && user.Email != user.Email.ToLower())
|
||||
{
|
||||
var token = await userManager.GenerateChangeEmailTokenAsync(user, user.Email.ToLower());
|
||||
result = await userManager.ChangeEmailAsync(user, user.Email.ToLower(), token);
|
||||
}
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
|
||||
@@ -504,7 +504,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
widows: 2;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
@@ -536,7 +535,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
widows: 2;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
@@ -567,7 +565,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
widows: 2;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
widows: 2;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
|
||||
@@ -401,7 +401,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
background-color: rgb(
|
||||
|
||||
@@ -467,7 +467,6 @@
|
||||
text-align: start;
|
||||
text-indent: 0px;
|
||||
text-transform: none;
|
||||
white-space: pre-wrap;
|
||||
widows: 2;
|
||||
word-spacing: 0px;
|
||||
-webkit-text-stroke-width: 0px;
|
||||
@@ -483,9 +482,7 @@
|
||||
display: inline;
|
||||
"
|
||||
><em
|
||||
>If you did not request to reset
|
||||
your account password, you can
|
||||
safely ignore this email.</em
|
||||
>If you did not request to reset your account password, you can safely ignore this email.</em
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
@@ -554,7 +551,6 @@
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
.
|
||||
<td
|
||||
style="
|
||||
padding: 18px 0px 18px 0px;
|
||||
|
||||
Reference in New Issue
Block a user