-
Notifications
You must be signed in to change notification settings - Fork 860
Add Manage/Privacy-Delete/Download functionality #1559
Conversation
There won't be time for in person with @DamianEdwards is off having fun in London, so let's use @danroth27 and I as his proxy. |
@danroth27 who do you want to review this PR from the perspective of proper RazorPages usage/patterns etc? I mostly just copied what the existing pages did, I only intentionally deviated slightly using the NavPages constant instead of hardcoding the value in the ActivePages setting. |
Get Ryan and Pranav to look at it pls |
</div> | ||
|
||
@section Scripts { | ||
@await Html.PartialAsync("_ValidationScriptsPartial") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<partial name="_ValidationScriptsPartial" />
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pranavkm What's the advantage of <partial over Html.PartialAsync?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally prefer tag helpers to HtmlHelpers. It's cleaner, more idiomatic code.
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", _userManager.GetUserId(User)); | ||
|
||
// REVIEW: should this redirect to somewhere that says user was deleted? | ||
return RedirectToPage("./"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should redirect to the website root return Redirect("~/");
or a successfully deleted page. Redirecting to the page ./
is kinda weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the right thing to do here is to pass in a return url and do a local redirect, look at the logout action for how to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So most things in manage are doing some form of RedirectToPage... is one better than the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RedirectToPage expects to resolve a page. Since there isn't a page named ./
, it ends up resolving the empty string which ends up in the app root. But that's just a weird way of going about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so after they get deleted, manage will no longer be valid, so I really was just trying to redirect to home or whatever. Unless we think its worth adding a dedicated page for a Deleted confirmation
<h4>@ViewData["Title"]</h4> | ||
|
||
@section Scripts { | ||
@await Html.PartialAsync("_ValidationScriptsPartial") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<partial name="_ValidationScriptsPartial" />
|
||
public DownloadPersonalDataModel( | ||
UserManager<IdentityUser> userManager, | ||
ILogger<PrivacyModel> logger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ILogger<DownloadPersonalDataModel>
?
_logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); | ||
|
||
// REVIEW: do we want to set a force download header? | ||
return new FileContentResult(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(user)), "text/json"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You likely need Encoding.UTF8. That said, this kinda feels icky, but there really doesn't seem like a more definitive way to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably want to force the download header, but I would just do OK(user) and set the header before. We can add a file content result in MVC that takes an object. I've filed aspnet/Mvc#7232 to track this
public DeletePersonalDataModel( | ||
UserManager<IdentityUser> userManager, | ||
SignInManager<IdentityUser> signInManager, | ||
ILogger<PrivacyModel> logger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ILogger<DeletePersonalDataModel>
} | ||
|
||
var result = await _userManager.DeleteAsync(user); | ||
if (!result.Succeeded) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it still correct for a user to be signed in if this fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default implementation basically never will fail, it would only blow up with an exception, but this is a valid point, I guess we should only sign out if it succeeded
var result = await _userManager.DeleteAsync(user); | ||
if (!result.Succeeded) | ||
{ | ||
throw new ApplicationException($"Unexpected error occurred deleteing user with ID '{user.Id}'."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ApplicationException
-> InvalidOperationException
@model PrivacyModel | ||
@{ | ||
ViewData["Title"] = "Privacy"; | ||
ViewData["ActivePage"] = "Privacy"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ManageNavPages.Privacy
_logger = logger; | ||
} | ||
|
||
public async Task<IActionResult> OnGet() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When do we get to this handler? Privacy.cshtml
posts to this action and there isn't a link to this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we don't, I'll remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks promising.
<strong>This action will delete your user.</strong> | ||
</p> | ||
<p> | ||
This is unrecoverable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this sentence means
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"This action will delete your user."
"This is unrecoverable."
means you can't recover your user after you delete it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this elaborate and say, once deleted you cannot use your login credentials and would need to re-register?
Kinda unrelated, but is there a hook that let's the app developer also clean up other stored details about the user when this is deleted? For instance, if the user id is used to store my order history or posted comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't worry about the exact text yet, I'll update that shortly.
re hook, not really, ideally we'd have an easy way for them to just drop in a new DeletePersonalData.cshtml.cs to take over this work entirely to customize. Or I guess scaffold it all and change the code if that feature doesn't make it...
</div> | ||
|
||
@section Scripts { | ||
@await Html.PartialAsync("_ValidationScriptsPartial") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pranavkm What's the advantage of <partial over Html.PartialAsync?
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", _userManager.GetUserId(User)); | ||
|
||
// REVIEW: should this redirect to somewhere that says user was deleted? | ||
return RedirectToPage("./"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the right thing to do here is to pass in a return url and do a local redirect, look at the logout action for how to do it.
_logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); | ||
|
||
// REVIEW: do we want to set a force download header? | ||
return new FileContentResult(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(user)), "text/json"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably want to force the download header, but I would just do OK(user) and set the header before. We can add a file content result in MVC that takes an object. I've filed aspnet/Mvc#7232 to track this
|
||
await _signInManager.SignOutAsync(); | ||
|
||
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", _userManager.GetUserId(User)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ok to log the userId as is (as this can be the email address, which is PII)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UserName is the email, the user id is a guid by default, but this is the pattern the existing templates use for logging I believe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
</div> | ||
|
||
@section Scripts { | ||
@await Html.PartialAsync("_ValidationScriptsPartial") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the rest of the comments - prefer <partial ... /> tag syntax instead
Ok updated switching all default UI over to use the new partial and consistently use ManageNavClass constants in for the active page stuff |
Text has been updated, Privacy tab => Personal Data, will add one more change which will require users to reenter their password if they have a local password before being allowed to delete their account |
Updated flow with password confirmation on Delete page if the user has a password. Also added white list to only include Id/UserName/Email/Phone/and Confirmed properties) |
I think all feedback has been addressed now, added @pranavkm can you do a final review/sign off? |
{ | ||
if (!await _userManager.CheckPasswordAsync(user, Input.Password)) | ||
{ | ||
ModelState.AddModelError(string.Empty, "Password not correct."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AddModelError("Input.Password",
) if we'd like to show error along the password field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error shows up at the top in the summary and at the bottom (below the password field) with this change so I think i'll leave it as is, so its only shown at the top
If a user has two factor authentication enable would the second factor used for verification before deletion? |
No, we just check password right now, checking a second factor is significantly more involved, but its certainly doable if an app wants that, you basically just need to duplicate the two factor login flow |
based on changes in aspnet/Identity/pull/1559
based on changes in aspnet/Identity/pull/1559
based on changes in aspnet/Identity/pull/1559
based on changes in aspnet/Identity/pull/1559
Initial changes to identity default UI for Manage/Privacy/DownloadPersonalData and DeletePersonalData
Still need final tweaks to UI/text which we'll plan to get from @DamianEdwards review in person.
@blowdart @danroth27 @javiercn @ajcvickers @mkArtakMSFT