The tree had not compiled since its first commit: Group, DmGroup and
IGroupUsersService were referenced and never written, an IDE rename had
turned the user-settings DTO into the ViewAvatarServer enum, and the settings
were saved as an entity they no longer were.
Built now:
- Group (an ActivityPub Group actor with its own keys, members, invitation
code and optional password) and DmGroup (a conversation), with
/clientapi/group/{list,insert,update,join,leave,approve}.
- Posts and DMs: /clientapi/post/{list,insert,delete}, /clientapi/dm/{list,insert};
DM recipients are local usernames or user@host handles resolved by WebFinger.
- Invitation sign-up and login against the group's invitation code, checking
the password before any account is created.
- Federation: WebFinger, NodeInfo 2.0, actors at /peasants/{name} (Person,
Group, and an Application instance actor) with SPKI keys, draft-cavage
RSA-SHA256 HTTP signatures both ways, an inbox handling Follow (+Accept),
Undo, Create, Delete and Update, an outbox, notes at /posts/{id}, and a
persisted, retried, signed delivery queue. A post to a group is announced
by the group to its followers (FEP-1b12).
- The unused, broken typed ActivityPub models are replaced by a renderer;
NSign's HMAC setup, which could not federate, is gone.
Upgrade: net10.0, MongoDB.Entities 25.1 (instance DB API, Standard GUIDs),
Swashbuckle 10 / OpenApi 2, Serilog.AspNetCore 10, MailKit 4.18,
PasswordGenerator 3. The JWT keys are 64 bytes (IdentityModel 8 refuses
shorter for HS512). Production runs its own mongod on 127.0.0.1:27022, as
Sintopia's apps do, and deploys to privapub.thepra.dev from the build runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
575 lines
20 KiB
C#
575 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);
|
|
}
|
|
|
|
var cultureLanguage = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
|
|
var language = await DbEntities.Languages.Match(l => l.International2Code == cultureLanguage).ExecuteFirstAsync();
|
|
newUser.Settings = new RootUserSettings
|
|
{
|
|
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 DB.Default.SaveAsync(newUser);
|
|
|
|
//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, ToViewSettings(newUser.Settings));
|
|
|
|
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.DeletedAt == 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 = (ViewUserSettings)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.Default.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(ViewUserSettings 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.Default.Update<RootUser>()
|
|
.MatchID(userId)
|
|
.Modify(u => u.Settings.LanguageCode, language.International2Code)
|
|
.Modify(u => u.Settings.LightThemeIndexColour, userSettings.LightThemeIndexColour)
|
|
.Modify(u => u.Settings.DarkThemeIndexColour, userSettings.DarkThemeIndexColour)
|
|
.Modify(u => u.Settings.PreferSystemTheming, userSettings.PreferSystemTheming)
|
|
.Modify(u => u.Settings.ThemeIsDarkMode, userSettings.ThemeIsDarkMode)
|
|
.Modify(u => u.Settings.ThemeIsDarkGray, userSettings.ThemeIsDarkGray)
|
|
.Modify(u => u.Settings.ThemeIsLightGray, userSettings.ThemeIsLightGray)
|
|
.Modify(u => u.Settings.IconsThemeIndexColour, userSettings.IconsThemeIndexColour)
|
|
.Modify(u => u.UpdatedAt, DateTime.UtcNow)
|
|
.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.DeletedAt == 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;
|
|
user.UpdatedAt = DateTime.UtcNow;
|
|
await DB.Default.SaveAsync(user);
|
|
|
|
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.DeletedAt == 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.DeletedAt = DateTime.UtcNow;
|
|
|
|
await DB.Default.SaveAsync(user);
|
|
}
|
|
|
|
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.Default.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.Default.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 user = await DbEntities.RootUsers.MatchID(userId).ExecuteFirstAsync();
|
|
if (user == default)
|
|
return result.Invalidate(Localizer["User not found."], StatusCodes.Status404NotFound);
|
|
|
|
if (loginForm != default && loginForm.ThemeIsDarkMode != user.Settings.ThemeIsDarkMode)
|
|
{
|
|
user.Settings.ThemeIsDarkMode = loginForm.ThemeIsDarkMode;
|
|
await DB.Default.Update<RootUser>()
|
|
.MatchID(userId)
|
|
.Modify(u => u.Settings.ThemeIsDarkMode, loginForm.ThemeIsDarkMode)
|
|
.ExecuteAsync();
|
|
}
|
|
|
|
result.Data = ToViewSettings(user.Settings);
|
|
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.DeletedAt == 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.DeletedAt == 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 DB.Default.SaveAsync(emailRecovery);
|
|
}
|
|
|
|
using var recoveryCodeGenerator = new Password(true, true, true, false, 127);
|
|
emailRecovery.RecoveryCode = recoveryCodeGenerator.Next();
|
|
await DB.Default.SaveAsync(emailRecovery);
|
|
|
|
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.Default.Update<RootUser>()
|
|
.Match(u => u.ID == emailRecovery.RootUserId && u.DeletedAt == null)
|
|
.Modify(u => u.HashedPassword, newHashedPassword)
|
|
.ExecuteAsync();
|
|
|
|
_ = await DB.Default.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);
|
|
}
|
|
}
|
|
|
|
static ViewUserSettings ToViewSettings(RootUserSettings settings) => new()
|
|
{
|
|
LanguageCode = settings.LanguageCode,
|
|
LightThemeIndexColour = settings.LightThemeIndexColour,
|
|
DarkThemeIndexColour = settings.DarkThemeIndexColour,
|
|
PreferSystemTheming = settings.PreferSystemTheming,
|
|
ThemeIsDarkMode = settings.ThemeIsDarkMode,
|
|
IconsThemeIndexColour = settings.IconsThemeIndexColour,
|
|
ThemeIsDarkGray = settings.ThemeIsDarkGray,
|
|
ThemeIsLightGray = settings.ThemeIsLightGray
|
|
};
|
|
}
|
|
}
|