/* 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 . */ using System; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Notesnook.API.Helpers; using Notesnook.API.Interfaces; using Notesnook.API.Models; using Notesnook.API.Models.Responses; using Streetwriters.Common; using Streetwriters.Common.Enums; using Streetwriters.Common.Extensions; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; using Streetwriters.Common.Models; using Streetwriters.Data.Interfaces; namespace Notesnook.API.Services { public class UserService : IUserService { private static readonly System.Security.Cryptography.RandomNumberGenerator Rng = System.Security.Cryptography.RandomNumberGenerator.Create(); private readonly HttpClient httpClient; private IHttpContextAccessor HttpContextAccessor { get; } private ISyncItemsRepositoryAccessor Repositories { get; } private IS3Service S3Service { get; set; } private readonly IUnitOfWork unit; public UserService(IHttpContextAccessor accessor, ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, IS3Service s3Service) { httpClient = new HttpClient(); Repositories = syncItemsRepositoryAccessor; HttpContextAccessor = accessor; unit = unitOfWork; S3Service = s3Service; } public async Task CreateUserAsync() { SignupResponse response = await httpClient.ForwardAsync(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post); if (!response.Success || (response.Errors != null && response.Errors.Length > 0)) { await Slogger.Error(nameof(CreateUserAsync), "Couldn't sign up.", JsonSerializer.Serialize(response)); if (response.Errors != null && response.Errors.Length > 0) throw new Exception(string.Join(" ", response.Errors)); else throw new Exception("Could not create a new account. Error code: " + response.StatusCode); } await Repositories.UsersSettings.InsertAsync(new UserSettings { UserId = response.UserId, StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 }, LastSynced = 0, Salt = GetSalt() }); if (!Constants.IS_SELF_HOSTED) { await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionV2Topic, new CreateSubscriptionMessageV2 { AppId = ApplicationType.NOTESNOOK, Provider = SubscriptionProvider.STREETWRITERS, Status = SubscriptionStatus.ACTIVE, Plan = SubscriptionPlan.FREE, UserId = response.UserId, StartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }); } await Slogger.Info(nameof(CreateUserAsync), "New user created.", JsonSerializer.Serialize(response)); } public async Task GetUserAsync(string userId) { var userService = await WampServers.IdentityServer.GetServiceAsync(IdentityServerTopics.UserAccountServiceTopic); var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found."); Subscription? subscription = null; if (Constants.IS_SELF_HOSTED) { subscription = new Subscription { AppId = ApplicationType.NOTESNOOK, Provider = SubscriptionProvider.STREETWRITERS, Plan = SubscriptionPlan.BELIEVER, Status = SubscriptionStatus.ACTIVE, UserId = user.UserId, StartDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), // this date doesn't matter as the subscription is static. ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds() }; } else { var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync(SubscriptionServerTopics.UserSubscriptionServiceTopic); subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found."); } var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found."); // reset user's attachment limit every month if (userSettings.StorageLimit == null || DateTimeOffset.UtcNow.Month > DateTimeOffset.FromUnixTimeMilliseconds(userSettings.StorageLimit.UpdatedAt).Month) { userSettings.StorageLimit ??= new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 }; await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == user.UserId); } return new UserResponse { UserId = user.UserId, Email = user.Email, IsEmailConfirmed = user.IsEmailConfirmed, MarketingConsent = user.MarketingConsent, MFA = user.MFA, PhoneNumber = user.PhoneNumber, AttachmentsKey = userSettings.AttachmentsKey, MonographPasswordsKey = userSettings.MonographPasswordsKey, InboxKeys = userSettings.InboxKeys, Salt = userSettings.Salt, Subscription = subscription, StorageUsed = userSettings.StorageLimit.Value, TotalStorage = StorageHelper.GetStorageLimitForPlan(subscription), Success = true, StatusCode = 200 }; } public async Task SetUserKeysAsync(string userId, UserKeys keys) { var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId) ?? throw new Exception("User not found."); if (keys.AttachmentsKey != null) { userSettings.AttachmentsKey = keys.AttachmentsKey; } if (keys.MonographPasswordsKey != null) { userSettings.MonographPasswordsKey = keys.MonographPasswordsKey; } if (keys.InboxKeys != null) { if (keys.InboxKeys.Public == null || keys.InboxKeys.Private == null) { userSettings.InboxKeys = null; await Repositories.InboxApiKey.DeleteManyAsync(t => t.UserId == userId); } else { userSettings.InboxKeys = keys.InboxKeys; var defaultInboxKey = new InboxApiKey { UserId = userId, Name = "Default", DateCreated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), ExpiryDate = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds(), LastUsedAt = 0 }; await Repositories.InboxApiKey.InsertAsync(defaultInboxKey); } } await Repositories.UsersSettings.UpdateAsync(userSettings.Id, userSettings); } public async Task DeleteUserAsync(string userId) { new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices(); var cc = new CancellationTokenSource(); Repositories.Notes.DeleteByUserId(userId); Repositories.Notebooks.DeleteByUserId(userId); Repositories.Shortcuts.DeleteByUserId(userId); Repositories.Contents.DeleteByUserId(userId); Repositories.Settings.DeleteByUserId(userId); Repositories.LegacySettings.DeleteByUserId(userId); Repositories.Attachments.DeleteByUserId(userId); Repositories.Reminders.DeleteByUserId(userId); Repositories.Relations.DeleteByUserId(userId); Repositories.Colors.DeleteByUserId(userId); Repositories.Tags.DeleteByUserId(userId); Repositories.Vaults.DeleteByUserId(userId); Repositories.UsersSettings.Delete((u) => u.UserId == userId); Repositories.Monographs.DeleteMany((m) => m.UserId == userId); Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId); var result = await unit.Commit(); await Slogger.Info(nameof(DeleteUserAsync), "User data deleted", userId, result.ToString()); if (!result) throw new Exception("Could not delete user data."); if (!Constants.IS_SELF_HOSTED) { await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage { AppId = ApplicationType.NOTESNOOK, UserId = userId }); } await S3Service.DeleteDirectoryAsync(userId); } public async Task DeleteUserAsync(string userId, string jti, string password) { await Slogger.Info(nameof(DeleteUserAsync), "Deleting user account", userId); var userService = await WampServers.IdentityServer.GetServiceAsync(IdentityServerTopics.UserAccountServiceTopic); await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password); await DeleteUserAsync(userId); await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage { SendToAll = false, OriginTokenId = jti, UserId = userId, Message = new Message { Type = "logout", Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) } }); } public async Task ResetUserAsync(string userId, bool removeAttachments) { new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices(); var cc = new CancellationTokenSource(); Repositories.Notes.DeleteByUserId(userId); Repositories.Notebooks.DeleteByUserId(userId); Repositories.Shortcuts.DeleteByUserId(userId); Repositories.Contents.DeleteByUserId(userId); Repositories.Settings.DeleteByUserId(userId); Repositories.LegacySettings.DeleteByUserId(userId); Repositories.Attachments.DeleteByUserId(userId); Repositories.Reminders.DeleteByUserId(userId); Repositories.Relations.DeleteByUserId(userId); Repositories.Colors.DeleteByUserId(userId); Repositories.Tags.DeleteByUserId(userId); Repositories.Vaults.DeleteByUserId(userId); Repositories.Monographs.DeleteMany((m) => m.UserId == userId); Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId); if (!await unit.Commit()) return false; var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId); userSettings.AttachmentsKey = null; userSettings.MonographPasswordsKey = null; userSettings.VaultKey = null; userSettings.InboxKeys = null; userSettings.LastSynced = 0; await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId); if (removeAttachments) await S3Service.DeleteDirectoryAsync(userId); return true; } private static string GetSalt() { byte[] salt = new byte[16]; Rng.GetNonZeroBytes(salt); return Convert.ToBase64String(salt).TrimEnd('=').Replace('+', '-').Replace('/', '_'); } } }