Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account delete user flow #4977

Merged
merged 6 commits into from
Nov 16, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/NuGetGallery/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ public static void RegisterUIRoutes(RouteCollection routes)
"account/delete/{accountName}",
new { controller = "Users", action = "Delete" });

routes.MapRoute(
RouteName.UserDeleteAccount,
"account/delete",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spacing looks off here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

new { controller = "Users", action = "DeleteRequest" });

routes.MapRoute(
RouteName.Account,
"account/{action}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public interface ISupportRequestService
/// <returns>Returns a <see cref="IReadOnlyCollection{Issue}"/> that matches the provided filter parameters.</returns>
IReadOnlyCollection<Issue> GetIssues(int? assignedTo = null, string reason = null, int? issueStatusId = null, string galleryUsername = null);

Task AddNewSupportRequestAsync(string subject, string message, string requestorEmailAddress, string reason,
Task<bool> AddNewSupportRequestAsync(string subject, string message, string requestorEmailAddress, string reason,
User user, Package package = null);
Task UpdateIssueAsync(int issueId, int? assignedToId, int issueStatusId, string comment, string editedBy);
int GetIssueCount(int? assignedToId, string reason, int? issueStatusId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,11 @@ public async Task UpdateIssueAsync(int issueId, int? assignedToId, int issueStat
}
}

public async Task AddNewSupportRequestAsync(string subject, string message, string requestorEmailAddress, string reason,
public async Task<bool> AddNewSupportRequestAsync(string subject, string message, string requestorEmailAddress, string reason,
User user, Package package = null)
{
var loggedInUser = user?.Username ?? "Anonymous";
bool result = true;

try
{
Expand Down Expand Up @@ -236,6 +237,7 @@ public async Task AddNewSupportRequestAsync(string subject, string message, stri
catch (SqlException sqlException)
{
QuietLog.LogHandledException(sqlException);
result = false;

var packageInfo = "N/A";
if (package != null)
Expand All @@ -249,8 +251,10 @@ public async Task AddNewSupportRequestAsync(string subject, string message, stri
}
catch (Exception e) //In case getting data from PagerDuty has failed
{
result = false;
QuietLog.LogHandledException(e);
}
return result;
}

private async Task AddIssueAsync(Issue issue)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace NuGetGallery.Areas.Admin.ViewModels
{
public class DeleteUserAccountViewModel
public class DeleteUserAccountViewModel : DeleteAccountViewModel
{
public DeleteUserAccountViewModel()
{
ShouldUnlist = true;
}

public List<ListPackageItemViewModel> Packages { get; set; }

public User User { get; set; }

public string AccountName { get; set; }

[Required(ErrorMessage = "Please sign using your name.")]
[StringLength(1000)]
[Display(Name = "Signature")]
public string Signature { get; set; }

[Display(Name = "Unlist the packages with no other owners.")]
public bool ShouldUnlist { get; set; }

public bool HasOrphanPackages { get; set; }
}

}
1 change: 1 addition & 0 deletions src/NuGetGallery/Controllers/PackagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ public virtual ActionResult ContactOwners(string id)
CopySender = true,
};

ViewData["HasOwners"] = package.PackageRegistration.Owners.Count > 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.Owners.Count > 0 -> .Owners.Any()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

return View(model);
}

Expand Down
59 changes: 58 additions & 1 deletion src/NuGetGallery/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net.Mail;
using System.Threading.Tasks;
using System.Web.Mvc;
using NuGetGallery.Areas.Admin;
using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Authentication;
using NuGetGallery.Configuration;
Expand All @@ -28,6 +29,7 @@ public partial class UsersController
private readonly AuthenticationService _authService;
private readonly ICredentialBuilder _credentialBuilder;
private readonly IDeleteAccountService _deleteAccountService;
private readonly ISupportRequestService _supportRequestService;

public UsersController(
ICuratedFeedService feedsQuery,
Expand All @@ -38,7 +40,8 @@ public UsersController(
IAppConfiguration config,
AuthenticationService authService,
ICredentialBuilder credentialBuilder,
IDeleteAccountService deleteAccountService)
IDeleteAccountService deleteAccountService,
ISupportRequestService supportRequestService)
{
_curatedFeedService = feedsQuery ?? throw new ArgumentNullException(nameof(feedsQuery));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
Expand All @@ -49,6 +52,7 @@ public UsersController(
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
_credentialBuilder = credentialBuilder ?? throw new ArgumentNullException(nameof(credentialBuilder));
_deleteAccountService = deleteAccountService ?? throw new ArgumentNullException(nameof(deleteAccountService));
_supportRequestService = supportRequestService ?? throw new ArgumentNullException(nameof(supportRequestService));
}

[HttpGet]
Expand Down Expand Up @@ -94,6 +98,59 @@ public virtual ActionResult Account()
return AccountView(new AccountViewModel());
}

[HttpGet]
[Authorize]
public virtual ActionResult DeleteRequest()
{
var user = GetCurrentUser();

if (user == null || user.IsDeleted)
{
return HttpNotFound("User not found.");
}
bool pendingRequest = _supportRequestService.GetIssues(assignedTo: null, reason: null, issueStatusId: null, galleryUsername: user.Username)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone cancels a request to delete their account, won't this still find the existing request?

It seems a bit awkward to query the support request service for whether or not a user requested deletion. Additionally, when we move off our support request system, this will need to be changed. I would rather store this in the database somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the first question - no they will not find it because it was not created.
For the second question, the result of the user action to submit the request to have the package deleted is that a new support request is created.
As discussed offline updated to use the issuekey.

.Any(issue => string.Equals(issue.IssueTitle, Strings.AccountDelete_SupportRequestTitle, StringComparison.CurrentCultureIgnoreCase) &&
issue.IssueStatus.Key != 3 &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does issue.IssueStatus.Key != 3 mean? A constant for 3 could help explain this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code was updated. It represents the status for a specific issue. ( 3 == resolved)

issue.OwnerEmail == user.EmailAddress);
ViewData["HasPendingRequest"] = pendingRequest;

var listPackageItems = _packageService
.FindPackagesByOwner(user, includeUnlisted: true)
.Select(p => new ListPackageItemViewModel(p))
.ToList();

var model = new DeleteAccountViewModel()
{
Packages = listPackageItems,
User = user,
AccountName = user.Username,
HasOrphanPackages = listPackageItems.Any(p => p.Owners.Count <= 1),
};

return View("DeleteAccount", model);
}

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public virtual async Task<ActionResult> SendAccountDeleteNotificationAsync()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestAccountDeletionAsync?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we use Async in controller action names

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async suffix was added to follow the naming convention for the async methods. The observation that @scottbommarito made is correct. The name was changed to RequestAccountDeletion to be in sync with the current pattern.

{
var user = GetCurrentUser();
bool createSupportRequestStatus = await _supportRequestService.AddNewSupportRequestAsync(Strings.AccountDelete_SupportRequestTitle,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no checks here:
if (user == null || user.IsDeleted)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think bool isSupportRequestCreated is a better name for this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Strings.AccountDelete_SupportRequestTitle,
user.EmailAddress,
"The user requested to have the account deleted.",
user);
if (!createSupportRequestStatus)
{
TempData["RequestFailedMessage"] = Strings.AccountDelete_CreateSupportRequestFails;
return RedirectToAction("DeleteRequest");
}
_messageService.SendAccountDeleteNotice(user.ToMailAddress(), user.Username);

return RedirectToAction("DeleteRequest");
}

[HttpGet]
[Authorize(Roles = "Admins")]
public virtual ActionResult Delete(string accountName)
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,7 @@
<Compile Include="Services\ReflowPackageService.cs" />
<Compile Include="Services\TelemetryService.cs" />
<Compile Include="Areas\Admin\ViewModels\DeleteUserAccountViewModel.cs" />
<Compile Include="ViewModels\DeleteAccountViewModel.cs" />
<Compile Include="ViewModels\PackageOwnersResultViewModel.cs" />
<Compile Include="ViewModels\ManagePackagesListViewModel.cs" />
<Compile Include="ViewModels\ApiKeyListViewModel.cs" />
Expand Down Expand Up @@ -1955,6 +1956,7 @@
<Content Include="Areas\Admin\Views\DeleteAccount\DeleteUserAccount.cshtml" />
<Content Include="Views\Users\_UserPackagesListForDeletedAccount.cshtml" />
<Content Include="Views\Users\_ReservedNamespacesList.cshtml" />
<Content Include="Views\Users\DeleteAccount.cshtml" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="Properties\CodeAnalysisDictionary.xml" />
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/RouteNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ public static class RouteName
public const string JsonApi = "JsonApi";
public const string Downloads = "Downloads";
public const string AdminDeleteAccount = "AdminDeleteAccount";
public const string UserDeleteAccount = "DeleteAccount";
}
}
1 change: 1 addition & 0 deletions src/NuGetGallery/Services/IMessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public interface IMessageService
void SendCredentialAddedNotice(User user, Credential added);
void SendContactSupportEmail(ContactSupportRequest request);
void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl);
void SendAccountDeleteNotice(MailAddress mailAddress, string userName);
}
}
26 changes: 26 additions & 0 deletions src/NuGetGallery/Services/MessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,32 @@ [change your email notification settings]({5}).
}
}

public void SendAccountDeleteNotice(MailAddress mailAddress, string account)
{
string body = @"We received a request to delete your account {0}. If you did not initiate this request please contact the {1} team.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting here could be much better. Also there's a couple oddly worded phrases in here.

You should also make similar modifications to the page itself.

We received a request to delete your account {0}. If you did not initiate this request please contact the {1} team immediately.

When your account will be deleted, we will:
- revoke your API key(s)
- remove your ownership from any packages you own
- remove your ownership from any ID prefix reservations and delete any ID prefix reservations that you were the only owner of

We will not delete any packages associated with your account.

Thanks,
The {1} Team

For comparison, it is currently:

We received a request to delete your account {0}. If you did not initiate this request please contact the {1} team.
When your account will be deleted, we will:revoke your API key(s), 
remove you as the owner for any package you own, dissociate all previously existing ID prefix reservation with this account. If you were the only account tied to a particular ID prefix reservation the prefix will be available to anyone.
We will not delete the NuGet packages associated with the account.

Thanks,
The {1} Team

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

When your account will be deleted, we will:revoke your API key(s),
remove you as the owner for any package you own, dissociate all previously existing ID prefix reservation with this account. If you were the only account tied to a particular ID prefix reservation the prefix will be available to anyone.
We will not delete the NuGet packages associated with the account.

Thanks,
The {1} Team";

body = String.Format(
CultureInfo.CurrentCulture,
body,
account,
Config.GalleryOwner.DisplayName);

using (var mailMessage = new MailMessage())
{
mailMessage.Subject = Strings.AccountDelete_SupportRequestTitle;
mailMessage.Body = body;
mailMessage.From = Config.GalleryNoReplyAddress;

mailMessage.To.Add(mailAddress.Address);
SendMessage(mailMessage);
}
}
private static void AddOwnersToMailMessage(PackageRegistration packageRegistration, MailMessage mailMessage)
{
foreach (var owner in packageRegistration.Owners.Where(o => o.EmailAllowed))
Expand Down
27 changes: 27 additions & 0 deletions src/NuGetGallery/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/NuGetGallery/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -618,4 +618,13 @@ For more information, please contact '{2}'.</value>
<data name="AccountDelete_Success" xml:space="preserve">
<value>The account:{0} was deleted succesfully.</value>
</data>
<data name="AccountDelete_CreateSupportRequestFails" xml:space="preserve">
<value>The request failed to be submitted. Please try again or contact nuget support.</value>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things
1 - nuget -> NuGet
2 - This should use IAppConfiguration.Brand so that if somebody deploys the gallery themselves and changes the brand, it doesn't say NuGet support anyway

An easy alternative may be just remove the use of nuget and just show contact support.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to not use NuGet in the notification.

</data>
<data name="AccountDelete_SupportRequestTitle" xml:space="preserve">
<value>DeleteAccountRequest</value>
</data>
<data name="PackageHasNoOwnersMessage" xml:space="preserve">
<value>This package has no owners and is not being actively maintained.</value>
</data>
</root>
18 changes: 18 additions & 0 deletions src/NuGetGallery/ViewModels/DeleteAccountViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace NuGetGallery
{
public class DeleteAccountViewModel
{
public List<ListPackageItemViewModel> Packages { get; set; }

public User User { get; set; }

public string AccountName { get; set; }

public bool HasOrphanPackages { get; set; }
}
}
37 changes: 24 additions & 13 deletions src/NuGetGallery/Views/Packages/ContactOwners.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,31 @@
}
else
{
@ViewHelpers.AlertWarning(
@<text>
Sorry, the owners of this package do not allow contacting them through this form.
if ((bool)ViewData["HasOwners"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in the Model, don't use ViewData.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

{
@ViewHelpers.AlertWarning(
@<text>
Sorry, the owners of this package do not allow contacting them through this form.

@if (string.IsNullOrEmpty(Model.ProjectUrl))
{
@:You can try contacting the package owners of "@Model.PackageId" through its <a href="@Model.ProjectUrl">Project Site</a>
}
else
{
@:You'll have to find some other way to contact them.
}
</text>
);
@if (string.IsNullOrEmpty(Model.ProjectUrl))
{
@:You can try contacting the package owners of "@Model.PackageId" through its <a href="@Model.ProjectUrl">Project Site</a>
}
else
{
@:You'll have to find some other way to contact them.
}
</text>
);
}
else
{
@ViewHelpers.AlertWarning(
@<text>
Sorry, this package has no owners and is not being actively maintained. You can try to find a contact for "@Model.PackageId" through its <a href="@Model.ProjectUrl">Project Site</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check that the package has a ProjectUrl before you ask them to go to it like above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

</text>
);
}
}
</div>
</div>
Expand Down
Loading