Skip to content

Commit b7d8b63

Browse files
author
mergewhenready[bot]
authored
Merge pull request #36 from fossapps/activation
Activation
2 parents 814ba5d + 0b75657 commit b7d8b63

7 files changed

Lines changed: 298 additions & 1 deletion

File tree

Micro.Auth.Api/Measurements/UsersControllerMetrics.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,82 @@ public void MarkExceptionActivation(string exceptionType)
129129
Tags = new MetricTags("exceptionType", exceptionType)
130130
});
131131
}
132+
133+
public async Task RecordTimeToConfirmEmail(Func<Task> fn)
134+
{
135+
await MeasureTimeAsync(fn, new TimerOptions
136+
{
137+
Name = "UsersController.Activation.TimeToConfirmEmail",
138+
DurationUnit = TimeUnit.Milliseconds,
139+
RateUnit = TimeUnit.Milliseconds
140+
});
141+
}
142+
143+
public void MarkSuccessfulConfirmation()
144+
{
145+
MeterMark(new MeterOptions
146+
{
147+
Name = "UsersController.Activation.SuccessfulConfirmation",
148+
MeasurementUnit = Unit.Requests,
149+
RateUnit = TimeUnit.Seconds
150+
});
151+
}
152+
153+
public void MarkFailedToConfirmActivation()
154+
{
155+
MeterMark(new MeterOptions
156+
{
157+
Name = "UsersController.Activation.FailedToConfirm",
158+
MeasurementUnit = Unit.Errors,
159+
RateUnit = TimeUnit.Seconds
160+
});
161+
}
162+
public void MarkPasswordResetUserNotFound()
163+
{
164+
MeterMark(new MeterOptions
165+
{
166+
Name = "UsersController.RequestPasswordReset.UserNotFound",
167+
MeasurementUnit = Unit.Requests,
168+
RateUnit = TimeUnit.Seconds
169+
});
170+
}
171+
public async Task MeasureTimeToSendPasswordResetEmail(Func<Task> fn)
172+
{
173+
await MeasureTimeAsync(fn, new TimerOptions
174+
{
175+
Name = "UsersController.RequestPasswordReset.TimeToSendPasswordResetEmail",
176+
DurationUnit = TimeUnit.Milliseconds,
177+
RateUnit = TimeUnit.Milliseconds
178+
});
179+
}
180+
public async Task MeasureTimeToResetPassword(Func<Task> fn)
181+
{
182+
await MeasureTimeAsync(fn, new TimerOptions
183+
{
184+
Name = "UsersController.ResetPassword.TimeToSendPasswordResetEmail",
185+
DurationUnit = TimeUnit.Milliseconds,
186+
RateUnit = TimeUnit.Milliseconds
187+
});
188+
}
189+
public void MarkFailedToResetPassword()
190+
{
191+
MeterMark(new MeterOptions
192+
{
193+
Name = "UsersController.RequestPasswordReset.FailedToReset",
194+
MeasurementUnit = Unit.Requests,
195+
RateUnit = TimeUnit.Seconds
196+
});
197+
}
198+
public void MarkResetPasswordException(string exception)
199+
{
200+
MeterMark(new MeterOptions
201+
{
202+
Name = "UsersController.RequestPasswordReset.Exception",
203+
Tags = new MetricTags("name", exception),
204+
MeasurementUnit = Unit.Requests,
205+
RateUnit = TimeUnit.Seconds
206+
});
207+
}
208+
132209
}
133210
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
3+
namespace Micro.Auth.Api.Users.Exceptions
4+
{
5+
public class EmailConfirmationFailedException : Exception
6+
{
7+
public EmailConfirmationFailedException(string message): base(message)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.AspNetCore.Identity;
5+
6+
namespace Micro.Auth.Api.Users.Exceptions
7+
{
8+
public class PasswordResetFailedException : Exception
9+
{
10+
public PasswordResetFailedException(IEnumerable<IdentityError> resultErrors)
11+
:base(String.Join(':', resultErrors.Select(e => e.Description)))
12+
{
13+
}
14+
}
15+
}

Micro.Auth.Api/Users/UserService.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public interface IUserService
2020
Task SendActivationEmail(string login);
2121
Task SendActivationEmail(User user);
2222
Task<(SignInResult, LoginSuccessResponse)> Login(LoginRequest loginRequest);
23+
Task ConfirmEmail(ConfirmEmailRequest request);
24+
Task RequestPasswordReset(string login);
25+
Task ResetPassword(ResetPasswordRequest request);
2326
}
2427

2528
public class UserService : IUserService
@@ -110,6 +113,58 @@ public async Task SendActivationEmail(User user)
110113
return await AuthenticateUser(loginRequest);
111114
}
112115

116+
public async Task ConfirmEmail(ConfirmEmailRequest request)
117+
{
118+
var user = await GetUserByLogin(request.Login);
119+
await ConfirmEmail(user, request.Token);
120+
}
121+
122+
public async Task RequestPasswordReset(string login)
123+
{
124+
var user = await GetUserByLogin(login);
125+
if (user == null)
126+
{
127+
throw new UserNotFoundException();
128+
}
129+
130+
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
131+
var mailMessage = await _mailBuilder.ForgotPasswordEmail().Build(new ForgotPasswordEmailDetails
132+
{
133+
Name = user.UserName,
134+
PasswordResetUrl = _emailUrlBuilder.BuildPasswordResetFormUrl(token)
135+
},
136+
new MailAddress(user.Email, user.UserName));
137+
await _mailService.SendAsync(mailMessage);
138+
}
139+
140+
public async Task ResetPassword(ResetPasswordRequest request)
141+
{
142+
var user = await GetUserByLogin(request.Login);
143+
if (user == null)
144+
{
145+
throw new UserNotFoundException();
146+
}
147+
148+
var result = await _userManager.ResetPasswordAsync(user, request.Token, request.NewPassword);
149+
if (!result.Succeeded)
150+
{
151+
throw new PasswordResetFailedException(result.Errors);
152+
}
153+
}
154+
155+
private async Task ConfirmEmail(User user, string token)
156+
{
157+
if (user == null)
158+
{
159+
throw new UserNotFoundException();
160+
}
161+
var result = await _userManager.ConfirmEmailAsync(user, token);
162+
if (!result.Succeeded)
163+
{
164+
throw new EmailConfirmationFailedException(result.ToString());
165+
}
166+
}
167+
113168
private Task<User> GetUserByLogin(string usernameOrEmail)
114169
{
115170
return usernameOrEmail.Contains("@")

Micro.Auth.Api/Users/UsersController.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ public async Task<ActionResult<FindByUsernameResponse>> FindByEmail(string email
8383
[ProducesResponseType(typeof(IdentityResult), StatusCodes.Status200OK)]
8484
[ProducesResponseType(typeof(IdentityResult), StatusCodes.Status400BadRequest)]
8585
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
86-
8786
public async Task<IActionResult> Create(CreateUserRequest request)
8887
{
8988
try
@@ -122,6 +121,7 @@ public async Task<IActionResult> Create(CreateUserRequest request)
122121
}
123122
}
124123

124+
125125
[HttpPost("activation/sendEmail")]
126126
[ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)]
127127
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
@@ -159,5 +159,111 @@ await _metrics.UsersControllerMetrics()
159159
});
160160
}
161161
}
162+
163+
[HttpPost("activation/confirm")]
164+
[ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)]
165+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
166+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
167+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
168+
public async Task<IActionResult> ConfirmEmail(ConfirmEmailRequest request)
169+
{
170+
try
171+
{
172+
await _metrics.UsersControllerMetrics()
173+
.RecordTimeToConfirmEmail(async () => await _userService.ConfirmEmail(request));
174+
_metrics.UsersControllerMetrics().MarkSuccessfulConfirmation();
175+
return Accepted();
176+
}
177+
catch (UserNotFoundException)
178+
{
179+
_logger.LogInformation($"user not found {request.Login}");
180+
_metrics.UsersControllerMetrics().MarkUserNotFoundActivation();
181+
return NotFound(new ProblemDetails {Type = "NotFound", Title = "user not found"});
182+
}
183+
catch (EmailConfirmationFailedException e)
184+
{
185+
_logger.LogInformation("EmailConfirmationFailed", e);
186+
_metrics.UsersControllerMetrics().MarkFailedToConfirmActivation();
187+
return Unauthorized(new ProblemDetails {Title = "failed to confirm"});
188+
}
189+
catch (Exception e)
190+
{
191+
_metrics.UsersControllerMetrics().MarkExceptionActivation(e.GetType().FullName);
192+
_logger.LogCritical(e, "unexpected error during email confirmation");
193+
return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
194+
{
195+
Title = "error handling request"
196+
});
197+
}
198+
}
199+
200+
201+
[HttpPost("password/requestReset")]
202+
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
203+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
204+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
205+
public async Task<IActionResult> RequestPasswordReset(RequestPasswordReset request)
206+
{
207+
try
208+
{
209+
await _metrics.UsersControllerMetrics().MeasureTimeToSendPasswordResetEmail(async () =>
210+
await _userService.RequestPasswordReset(request.Login));
211+
return Accepted();
212+
}
213+
catch (UserNotFoundException)
214+
{
215+
_metrics.UsersControllerMetrics().MarkPasswordResetUserNotFound();
216+
return NotFound(new ProblemDetails
217+
{
218+
Title = "user not found"
219+
});
220+
}
221+
catch (EmailSendingFailureException e)
222+
{
223+
_logger.LogError("error sending email", e);
224+
_metrics.UsersControllerMetrics().MarkEmailSendingFailure();
225+
return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
226+
{
227+
Title = "error handling request"
228+
});
229+
}
230+
}
231+
232+
[HttpPost("password/reset")]
233+
[ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)]
234+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
235+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
236+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
237+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
238+
public async Task<IActionResult> ResetPassword(ResetPasswordRequest request)
239+
{
240+
try
241+
{
242+
await _metrics.UsersControllerMetrics().MeasureTimeToResetPassword(async () =>
243+
await _userService.ResetPassword(request));
244+
return Accepted();
245+
}
246+
catch (UserNotFoundException e)
247+
{
248+
_logger.LogWarning("user not found while resetting password: ", e);
249+
_metrics.UsersControllerMetrics().MarkPasswordResetUserNotFound();
250+
return NotFound(new ProblemDetails {Type = "NotFound", Title = "user not found"});
251+
}
252+
catch (PasswordResetFailedException e)
253+
{
254+
_logger.LogWarning("failed resetting password", e);
255+
_metrics.UsersControllerMetrics().MarkFailedToResetPassword();
256+
return Unauthorized(new ProblemDetails {Title = "failed to reset"});
257+
}
258+
catch (Exception e)
259+
{
260+
_logger.LogError("caught exception", e);
261+
_metrics.UsersControllerMetrics().MarkResetPasswordException(e.GetType().FullName);
262+
return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
263+
{
264+
Title = "error handling request"
265+
});
266+
}
267+
}
162268
}
163269
}

Micro.Auth.Api/Users/ViewModels/SendActivationEmailViewModel.cs renamed to Micro.Auth.Api/Users/ViewModels/ActivationViewModel.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ public class SendActivationEmailRequest
77
[Required]
88
public string Login { get; set; }
99
}
10+
public class ConfirmEmailRequest
11+
{
12+
[Required]
13+
public string Login { get; set; }
14+
15+
[Required]
16+
public string Token { get; set; }
17+
}
18+
1019
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Micro.Auth.Api.Users.ViewModels
4+
{
5+
public class RequestPasswordReset
6+
{
7+
[Required]
8+
public string Login { set; get; }
9+
}
10+
11+
public class ResetPasswordRequest
12+
{
13+
[Required]
14+
public string Login { set; get; }
15+
16+
[Required]
17+
public string Token { set; get; }
18+
19+
[Required]
20+
[MinLength(8)]
21+
public string NewPassword { set; get; }
22+
}
23+
}

0 commit comments

Comments
 (0)