SocialPub/PrivaPub/Services/RootUsersService.cs

577 lines
20 KiB
C#

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<RootUsersService> Logger;
readonly AppConfigurationService AppConfigurationService;
readonly AuthTokenManager AuthTokenManager;
public RootUsersService(
IStringLocalizer<GenericRes> localizer,
ILogger<RootUsersService> logger,
IPasswordHasher passwordHasher,
DbEntities dbEntities,
AppConfigurationService appConfigurationService,
AuthTokenManager authTokenManager)
{
DbEntities = dbEntities;
AuthTokenManager = authTokenManager;
PasswordHasher = passwordHasher;
Localizer = localizer;
Logger = logger;
AppConfigurationService = appConfigurationService;
}
public async Task<WebResult> 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<WebResult> 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<WebResult> 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<RootUser>()
.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<WebResult> 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<RootUserSettings>()
.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<WebResult> 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<WebResult> 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<WebResult> BanUserAsync(UsersIds usersIds)
{
var result = new WebResult();
try
{
await DB.Update<RootUser>()
.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<WebResult> UnbanUserAsync(UsersIds usersIds)
{
var result = new WebResult();
try
{
await DB.Update<RootUser>()
.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<WebResult> 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<WebResult> 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<WebResult> 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<WebResult> 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<RootUser>()
.Match(u => u.ID == emailRecovery.RootUserId && u.DeletionDate == null)
.Modify(u => u.HashedPassword, newHashedPassword)
.ExecuteAsync();
_ = await DB.DeleteAsync<EmailRecovery>(er => er.RecoveryCode == newPasswordForm.RecoveryCode);
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(ChangePassword)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
}
}