feat(backend): add add email address endpoint

This commit is contained in:
sam 2024-10-02 00:52:49 +02:00
parent 7f971e8549
commit 5b17c716cb
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
6 changed files with 114 additions and 3 deletions

View file

@ -183,10 +183,40 @@ public class EmailAuthController(
[HttpPost("add")] [HttpPost("add")]
[Authorize("*")] [Authorize("*")]
public async Task<IActionResult> AddEmailAddressAsync() public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
{ {
_logger.Information("beep"); var emails = await db
.AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email)
.ToListAsync();
if (emails.Count > AuthUtils.MaxAuthMethodsPerType)
{
throw new ApiError.BadRequest(
"Too many email addresses, maximum of 3 per account.",
"email",
null
);
}
var validPassword = await authService.ValidatePasswordAsync(CurrentUser!, req.Password);
if (!validPassword)
{
throw new ApiError.Forbidden("Invalid password");
}
var state = await keyCacheService.GenerateRegisterEmailStateAsync(
req.Email,
userId: CurrentUser!.Id
);
var emailExists = await db
.AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email)
.AnyAsync();
if (emailExists)
{
return NoContent();
}
mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username);
return NoContent(); return NoContent();
} }

View file

@ -42,7 +42,7 @@ public class ApiError(
IReadOnlyDictionary<string, IEnumerable<ValidationError>>? errors = null IReadOnlyDictionary<string, IEnumerable<ValidationError>>? errors = null
) : ApiError(message, statusCode: HttpStatusCode.BadRequest) ) : ApiError(message, statusCode: HttpStatusCode.BadRequest)
{ {
public BadRequest(string message, string field, object actualValue) public BadRequest(string message, string field, object? actualValue)
: this( : this(
"Error validating input", "Error validating input",
new Dictionary<string, IEnumerable<ValidationError>> new Dictionary<string, IEnumerable<ValidationError>>

View file

@ -0,0 +1,18 @@
using Coravel.Mailer.Mail;
namespace Foxnouns.Backend.Mailables;
public class AddEmailMailable(Config config, AddEmailMailableView view)
: Mailable<AddEmailMailableView>
{
public override void Build()
{
To(view.To).From(config.EmailAuth.From!).View("~/Views/Mail/AddEmail.cshtml", view);
}
}
public class AddEmailMailableView : BaseView
{
public required string Code { get; init; }
public required string Username { get; init; }
}

View file

@ -129,6 +129,30 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
return (user, EmailAuthenticationResult.AuthSuccessful); return (user, EmailAuthenticationResult.AuthSuccessful);
} }
/// <summary>
/// Validates a user's password outside an authentication context, for when a password is required for changing
/// a setting, such as adding a new email address or changing passwords.
/// </summary>
public async Task<bool> ValidatePasswordAsync(
User user,
string password,
CancellationToken ct = default
)
{
if (user.Password == null)
{
throw new FoxnounsError("Password for user supplied to ValidatePasswordAsync was null");
}
var pwResult = await Task.Run(
() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password),
ct
);
return pwResult
is PasswordVerificationResult.SuccessRehashNeeded
or PasswordVerificationResult.Success;
}
public enum EmailAuthenticationResult public enum EmailAuthenticationResult
{ {
AuthSuccessful, AuthSuccessful,

View file

@ -33,4 +33,31 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co
} }
}); });
} }
public void QueueAddEmailAddressEmail(string to, string code, string username)
{
_logger.Debug("Sending add email address email to {ToEmail}", to);
queue.QueueAsyncTask(async () =>
{
try
{
await mailer.SendAsync(
new AddEmailMailable(
config,
new AddEmailMailableView
{
BaseUrl = config.BaseUrl,
To = to,
Code = code,
Username = username,
}
)
);
}
catch (Exception exc)
{
_logger.Error(exc, "Sending add email address email");
}
});
}
} }

View file

@ -0,0 +1,12 @@
@model Foxnouns.Backend.Mailables.AddEmailMailableView
<p>
Hello @@@Model.Username, please confirm adding this email address to your account by using the following link:
<br/>
<a href="@Model.BaseUrl/settings/auth/confirm-email/@Model.Code">Confirm your email address</a>
<br/>
Note that this link will expire in one hour.
</p>
<p>
If you didn't mean to link this email address to @@@Model.Username, feel free to ignore this email.
</p>