using MailKit.Net.Smtp; using Microsoft.Extensions.Localization; using MimeKit; using MongoDB.Driver; using MongoDB.Entities; using PasswordGenerator; using PrivaPub.ClientModels; using PrivaPub.ClientModels.User; using PrivaPub.Models; using PrivaPub.Models.User; using PrivaPub.Resources; using PrivaPub.StaticServices; using System.Globalization; #pragma warning disable 8603 #pragma warning disable 8625 namespace PrivaPub.Services { public class RootUsersService : IRootUsersService { readonly DbEntities DbEntities; readonly IPasswordHasher PasswordHasher; readonly IStringLocalizer Localizer; readonly ILogger Logger; readonly AppConfigurationService AppConfigurationService; readonly AuthTokenManager AuthTokenManager; public RootUsersService( IStringLocalizer localizer, ILogger logger, IPasswordHasher passwordHasher, DbEntities dbEntities, AppConfigurationService appConfigurationService, AuthTokenManager authTokenManager) { DbEntities = dbEntities; AuthTokenManager = authTokenManager; PasswordHasher = passwordHasher; Localizer = localizer; Logger = logger; AppConfigurationService = appConfigurationService; } public async Task SignUpAsync(LoginForm signUpForm, string invitationCode = default, bool isPasswordRequired = false) { var result = new WebResult(); try { signUpForm.UserName = signUpForm.UserName.ToLower(); if (await DbEntities.RootUsers.Match(u => u.UserName == signUpForm.UserName).ExecuteAnyAsync()) return result.Invalidate(Localizer["Username '{0}' already taken.", signUpForm.UserName]); var signUpPasswordHashed = PasswordHasher.Hash(signUpForm.Password); var newUser = new RootUser { UserName = signUpForm.UserName, HashedPassword = signUpPasswordHashed }; if (signUpForm.UserName == "admin") { newUser.Policies.Clear(); newUser.Policies.Add(Policies.IsAdmin); newUser.Policies.Add(Policies.IsUser); newUser.Policies.Add(Policies.IsModerator); } await newUser.SaveAsync(); var cultureLanguage = CultureInfo.CurrentCulture.TwoLetterISOLanguageName; var language = await DbEntities.Languages.Match(l => l.International2Code == cultureLanguage).ExecuteFirstAsync(); var newRootUserSettings = new RootUserSettings { RootUserId = newUser.ID, LanguageCode = language?.International2Code ?? "en", LightThemeIndexColour = signUpForm.LightThemeIndexColour, DarkThemeIndexColour = signUpForm.DarkThemeIndexColour, IconsThemeIndexColour = signUpForm.IconsThemeIndexColour, ThemeIsDarkGray = signUpForm.ThemeIsDarkGray, ThemeIsLightGray = signUpForm.ThemeIsLightGray, PreferSystemTheming = signUpForm.PreferSystemTheming, ThemeIsDarkMode = signUpForm.ThemeIsDarkMode }; await newRootUserSettings.SaveAsync(); //if (!string.IsNullOrEmpty(invitationCode)) //{ // if (isPasswordRequired) // result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm // { // InvitationCode = invitationCode, // Password = signUpForm.InvitationPassword // }, newUser.ID); // else // result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm // { // InvitationCode = invitationCode, // }, newUser.ID); // if (!result.IsValid) // return result; //} result.Data = (newUser, new ViewAvatarServer { LanguageCode = newRootUserSettings.LanguageCode, LightThemeIndexColour = newRootUserSettings.LightThemeIndexColour, ThemeIsDarkMode = newRootUserSettings.ThemeIsDarkMode, IconsThemeIndexColour = newRootUserSettings.IconsThemeIndexColour, ThemeIsDarkGray = newRootUserSettings.ThemeIsDarkGray, ThemeIsLightGray = newRootUserSettings.ThemeIsLightGray, DarkThemeIndexColour = newRootUserSettings.DarkThemeIndexColour, PreferSystemTheming = newRootUserSettings.PreferSystemTheming }); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(SignUpAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task LoginAsync(LoginForm loginForm, string invitationCode = default, bool isPasswordRequired = false) { var result = new WebResult(); try { loginForm.UserName = loginForm.UserName.ToLower(); if (!await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName && u.DeletionDate == null) .ExecuteAnyAsync()) return result.Invalidate(Localizer["Username '{0}' not found.", loginForm.UserName]); var user = await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName).ExecuteFirstAsync(); if (user.IsBanned) return result.Invalidate(Localizer["User '{0}' banned.", user.UserName]); var (verified, needsUpgrade) = PasswordHasher.Check(user.HashedPassword, loginForm.Password); if (!verified) return result.Invalidate(Localizer["Wrong password."]); if (needsUpgrade) result.ErrorMessage = Localizer["Needs upgrade!"]; var userSettingsResult = await GetUserSettingsAsync(user.ID, loginForm); var userSettings = (ViewAvatarServer)userSettingsResult.Data; //if (!string.IsNullOrEmpty(invitationCode)) //{ // if (isPasswordRequired) // result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm // { // InvitationCode = invitationCode, // Password = loginForm.InvitationPassword // }, user.ID); // else // result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm // { // InvitationCode = invitationCode, // }, user.ID); // if (!result.IsValid) // return result; //} result.Data = (user, userSettings); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(LoginAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task UpdateUserAsync(UserForm userForm, string userId) { var result = new WebResult(); try { var currentUser = await DbEntities.RootUsers.Match(u => u.ID == userId).ExecuteFirstAsync(); if (!string.IsNullOrEmpty(userForm.Email) && currentUser.Email != userForm.Email) { var emailAlreadyUsed = await DbEntities.RootUsers.Match(u => u.Email == userForm.Email).ExecuteAnyAsync(); if (emailAlreadyUsed) return result.Invalidate(Localizer["Email '{0}' already taken.", userForm.Email]); } await DB.Update() .Match(u => u.ID == userId) .Modify(u => u.Email, userForm.Email) .ExecuteAsync(); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task UpdateUserSettingsAsync(ViewAvatarServer userSettings, string userId) { var result = new WebResult(); try { var isSupportedLanguage = AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(userSettings.LanguageCode); if (!isSupportedLanguage) return result.Invalidate(Localizer["Language code '{0}' unsupported.", userSettings.LanguageCode]); var languageCodeExists = await DbEntities.Languages .Match(l => l.International2Code == userSettings.LanguageCode).ExecuteAnyAsync(); if (!languageCodeExists) return result.Invalidate(Localizer["Language code '{0}' doesn't exist.", userSettings.LanguageCode]); var language = await DbEntities.Languages.Match(l => l.International2Code == userSettings.LanguageCode) .ExecuteFirstAsync(); _ = await DB.Update() .Match(u => u.RootUserId == userId) .Modify(us => us.LanguageCode, language.International2Code) .Modify(us => us.LightThemeIndexColour, userSettings.LightThemeIndexColour) .Modify(us => us.DarkThemeIndexColour, userSettings.DarkThemeIndexColour) .Modify(us => us.PreferSystemTheming, userSettings.PreferSystemTheming) .Modify(us => us.ThemeIsDarkMode, userSettings.ThemeIsDarkMode) .Modify(us => us.ThemeIsDarkGray, userSettings.ThemeIsDarkGray) .Modify(us => us.ThemeIsLightGray, userSettings.ThemeIsLightGray) .Modify(us => us.IconsThemeIndexColour, userSettings.IconsThemeIndexColour) .ExecuteAsync(); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserSettingsAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task UpdateUserPasswordAsync(UserPasswordForm userPasswordForm, string userId) { var result = new WebResult(); try { var user = await DbEntities.RootUsers.Match(u => u.ID == userId && u.DeletionDate == null).ExecuteFirstAsync(); if (user == null) return result.Invalidate(Localizer["Username '{0}' not found.", userId]); var (verified, needsUpgrade) = PasswordHasher.Check(user.HashedPassword, userPasswordForm.OldPassword); if (!verified) return result.Invalidate(Localizer["Wrong password."]); var newPasswordHashed = PasswordHasher.Hash(userPasswordForm.NewPassword); user.HashedPassword = newPasswordHashed; await user.SaveAsync(); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserPasswordAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task RemoveUserAsync(UsersIds usersIds) { var result = new WebResult(); try { var users = await DbEntities.RootUsers.Match(u => usersIds.UserIdList.Contains(u.ID) && u.DeletionDate == default).ExecuteAsync(); if (users == null || users.Count == 0) return result.Invalidate(Localizer["User already deleted."]); foreach (var user in users) { user.Email = Localizer["Deleted user"]; user.HashedPassword = null; user.Policies.Clear(); user.IsBanned = false; user.IsEmailValidated = false; //user.TempSecret = null; user.UserName = Localizer["Deleted user"]; user.DeletionDate = DateTime.UtcNow; await user.SaveAsync(); } return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(RemoveUserAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task BanUserAsync(UsersIds usersIds) { var result = new WebResult(); try { await DB.Update() .Match(u => usersIds.UserIdList.Contains(u.ID)) .Modify(u => u.IsBanned, true) .ExecuteAsync(); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(BanUserAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task UnbanUserAsync(UsersIds usersIds) { var result = new WebResult(); try { await DB.Update() .Match(u => usersIds.UserIdList.Contains(u.ID)) .Modify(u => u.IsBanned, false) .ExecuteAsync(); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UnbanUserAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task GetUserSettingsAsync(string userId, LoginForm loginForm = default) { var result = new WebResult(); try { var userSettings = await DbEntities.RootUsersSettings.Match(u => u.RootUserId == userId).ExecuteFirstAsync(); if (loginForm != default && (loginForm.ThemeIsDarkMode != userSettings.ThemeIsDarkMode)) { userSettings.ThemeIsDarkMode = loginForm.ThemeIsDarkMode; await userSettings.SaveAsync(); } result.Data = new ViewAvatarServer { LanguageCode = userSettings.LanguageCode, LightThemeIndexColour = userSettings.LightThemeIndexColour, DarkThemeIndexColour = userSettings.DarkThemeIndexColour, PreferSystemTheming = userSettings.PreferSystemTheming, ThemeIsDarkMode = userSettings.ThemeIsDarkMode, IconsThemeIndexColour = userSettings.IconsThemeIndexColour, ThemeIsDarkGray = userSettings.ThemeIsDarkGray, ThemeIsLightGray = userSettings.ThemeIsLightGray }; return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(GetUserSettingsAsync)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task SetupAndSendRecoveryEmail(PasswordRecoveryForm passwordRecoveryForm, string host) { var result = new WebResult(); try { var usernameExists = false; if (passwordRecoveryForm.IsEmailDisabled) { if (!await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName && u.DeletionDate == null) .ExecuteAnyAsync()) return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName], StatusCodes.Status404NotFound); usernameExists = true; } else { if (!await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email && u.DeletionDate == null) .ExecuteAnyAsync()) return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName], StatusCodes.Status404NotFound); } var user = default(RootUser); if (usernameExists) user = await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName).ExecuteFirstAsync(); else user = await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email).ExecuteFirstAsync(); if (string.IsNullOrEmpty(user.Email)) return result.Invalidate(Localizer["This User doesn't have an email, no way to recover."], StatusCodes.Status423Locked); var emailRecovery = await DbEntities.EmailRecoveries .Match(er => er.RootUserId == user.ID).ExecuteFirstAsync(); if (emailRecovery is null) { emailRecovery = new() { RootUserId = user.ID }; await emailRecovery.SaveAsync(); } var recoveryCodeGenerator = new Password(true, true, true, false, 127); emailRecovery.RecoveryCode = recoveryCodeGenerator.Next(); await emailRecovery.SaveAsync(); using (var smtpClient = new SmtpClient()) { try { await smtpClient.ConnectAsync(AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer, AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpPort, AppConfigurationService.AppConfiguration.EmailConfiguration.UseSSL); if (!smtpClient.IsConnected) { Logger.LogError($"Failed to connect to the SMTP server({AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer})."); return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.ServiceNotAvailable); } } catch (Exception ex) { Logger.LogError(ex, $"Error at connection to the SMTP server({AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer})."); return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.ServiceNotAvailable, exception: ex); } try { await smtpClient.AuthenticateAsync(AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername, AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpPassword); if (!smtpClient.IsAuthenticated) { Logger.LogError($"Failed SMTP authentication of {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}."); return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.TemporaryAuthenticationFailure); } } catch (Exception ex) { Logger.LogError(ex, $"Failed SMTP authentication of {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}."); return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.TemporaryAuthenticationFailure, exception: ex); } try { var toParsed = await smtpClient.VerifyAsync(user.Email); if (toParsed == null) return result.Invalidate($"Invalid email of {user.Email}.", (int)SmtpStatusCode.MailboxUnavailable); } catch (OperationCanceledException ex) { Logger.LogWarning( $"SMTP operation canceled exception at email verification of {user.Email}. Exception=[{ex.Message}]"); } catch (SmtpCommandException ex) { Logger.LogWarning( $"SMTP command exception at email verification of {user.Email}. Exception=[{ex.Message}]"); } catch (SmtpProtocolException ex) { Logger.LogWarning( $"SMTP protocol exception at email verification of {user.Email}. Exception=[{ex.Message}]"); } catch (Exception ex) { Logger.LogWarning($"General exception at email verification of {user.Email}. Exception=[{ex.Message}]"); } var message = new MimeMessage(); message.From.Add(new MailboxAddress(Localizer["Eugene - collAnon support"], AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername)); message.To.Add(MailboxAddress.Parse(user.Email)); message.Subject = Localizer["PrivaPub - Password recovery link"]; message.Body = new TextPart("plain") { Text = string.Format(Localizer[@"Hey {0}, Eugene from collAnon, following is the password recovery link: {1} -- Eugene"], user.UserName, $"{host}/password-recovery?rc={emailRecovery.RecoveryCode}") }; try { await smtpClient.SendAsync(message); } catch (Exception ex) { Logger.LogError(ex, $"Error at email sending to {user.Email} from {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}."); return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.TransactionFailed, exception: ex); } await smtpClient.DisconnectAsync(quit: true); } return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(SetupAndSendRecoveryEmail)}()"); return result.Invalidate(ex.Message, exception: ex); } } public async Task IsValidRecoveryCode(string recoveryCode) { var result = new WebResult(); try { var isValidRecoveryCode = await DbEntities.EmailRecoveries .Match(er => er.RecoveryCode == recoveryCode).ExecuteAnyAsync(); result.Data = isValidRecoveryCode; return result; } catch (Exception ex) { return result.Invalidate(ex.Message, exception: ex); } } public async Task ChangePassword(NewPasswordForm newPasswordForm) { var result = new WebResult(); try { var isValidRecoveryCode = await DbEntities.EmailRecoveries .Match(er => er.RecoveryCode == newPasswordForm.RecoveryCode).ExecuteAnyAsync(); if (!isValidRecoveryCode) return result.Invalidate(Localizer["Invalid recovery code."], StatusCodes.Status404NotFound); var emailRecovery = await DbEntities.EmailRecoveries .Match(er => er.RecoveryCode == newPasswordForm.RecoveryCode) .Project(er => er.Include(nameof(EmailRecovery.RootUserId))) .ExecuteFirstAsync(); if (emailRecovery is null || emailRecovery.RootUserId is null) return result.Invalidate(Localizer["User not found."]); var newHashedPassword = PasswordHasher.Hash(newPasswordForm.NewPassword); _ = await DB.Update() .Match(u => u.ID == emailRecovery.RootUserId && u.DeletionDate == null) .Modify(u => u.HashedPassword, newHashedPassword) .ExecuteAsync(); _ = await DB.DeleteAsync(er => er.RecoveryCode == newPasswordForm.RecoveryCode); return result; } catch (Exception ex) { Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(ChangePassword)}()"); return result.Invalidate(ex.Message, exception: ex); } } } }