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

[Organizations]: UI for creating migration request #5241

Merged
merged 12 commits into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 6 additions & 1 deletion src/NuGetGallery/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,12 @@ public static void RegisterUIRoutes(RouteCollection routes)
new { controller = "Users", action = "Account" });

routes.MapRoute(
RouteName.TransformAccountConfirmation,
RouteName.TransformToOrganization,
"account/transform",
new { controller = "Users", action = "Transform" });

routes.MapRoute(
RouteName.TransformToOrganizationConfirmation,
"account/transform/confirm/{accountNameToTransform}/{token}",
new { controller = "Users", action = "ConfirmTransform" });

Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class Constants

public const int GravatarElementSize = 32;
public const int GravatarImageSize = GravatarElementSize * 2;
public const int GravatarImageSizeLarge = 332;

/// <summary>
/// Parameters for calculating account lockout period after
Expand Down
73 changes: 66 additions & 7 deletions src/NuGetGallery/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,69 @@ public virtual ActionResult Account()
{
return AccountView(new AccountViewModel());
}


[HttpGet]
[Authorize]
[ActionName("Transform")]
public virtual ActionResult TransformToOrganization()
{
var accountToTransform = GetCurrentUser();
string errorReason;
if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason))
{
TempData["TransformError"] = errorReason;
return View("TransformFailed");
}

var transformRequest = accountToTransform.OrganizationMigrationRequest;
if (transformRequest != null)
{
TempData["Message"] = String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_RequestExists,
transformRequest.RequestDate.ToNuGetShortDateString(), transformRequest.AdminUser.Username);
}

return View(new TransformAccountViewModel());
}

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
[ActionName("Transform")]
public virtual async Task<ActionResult> TransformToOrganization(TransformAccountViewModel transformViewModel)
{
var accountToTransform = GetCurrentUser();
string errorReason;
if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason))
{
TempData["TransformError"] = errorReason;
return View("TransformFailed");
}

var adminUser = _userService.FindByUsername(transformViewModel.AdminUsername);
if (adminUser == null)
{
ModelState.AddModelError("AdminUsername", String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_AdminAccountDoesNotExist, transformViewModel.AdminUsername));
return View(transformViewModel);
}

if (!adminUser.Confirmed)
{
ModelState.AddModelError("AdminUsername", Strings.TransformAccount_AdminAccountNotConfirmed);
return View(transformViewModel);
}

await _userService.RequestTransformToOrganizationAccount(accountToTransform, adminUser);

// prompt for admin sign-on to confirm transformation
OwinContext.Authentication.SignOut();
return Redirect(Url.ConfirmTransformAccount(accountToTransform));
}

[HttpGet]
[Authorize]
public virtual async Task<ActionResult> ConfirmTransform(string accountNameToTransform, string token)
[ActionName("ConfirmTransform")]
public virtual async Task<ActionResult> ConfirmTransformToOrganization(string accountNameToTransform, string token)
{
var adminUser = GetCurrentUser();
if (!adminUser.Confirmed)
Expand All @@ -116,23 +175,23 @@ public virtual async Task<ActionResult> ConfirmTransform(string accountNameToTra
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_OrganizationAccountDoesNotExist, accountNameToTransform);
return View("AccountTransformFailed");
return View("TransformFailed");
}

string errorReason;
if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason))
{
TempData["TransformError"] = errorReason;
return View("AccountTransformFailed");
return View("TransformFailed");
}

if (!await _userService.TransformUserToOrganization(accountToTransform, adminUser, token))
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_Failed, accountNameToTransform);
return View("AccountTransformFailed");
return View("TransformFailed");
}

TempData["Message"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_Success, accountNameToTransform);

Expand Down
6 changes: 5 additions & 1 deletion src/NuGetGallery/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,13 @@ public static HtmlString ShowPasswordFor<TModel, TProperty>(this HtmlHelper<TMod
return html.PasswordFor(expression, htmlAttributes);
}

public static HtmlString ShowTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
public static HtmlString ShowTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, bool enabled = true)
{
var htmlAttributes = GetHtmlAttributes(html, expression);
if (!enabled)
{
htmlAttributes.Add("disabled", "true");
}
return html.TextBoxFor(expression, htmlAttributes);
}

Expand Down
6 changes: 4 additions & 2 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,7 @@
<Compile Include="ViewModels\ScopeViewModel.cs" />
<Compile Include="ViewModels\PackageListSearchViewModel.cs" />
<Compile Include="ViewModels\ThirdPartyPackageManagerViewModel.cs" />
<Compile Include="ViewModels\TransformAccountViewModel.cs" />
<Compile Include="WebApi\PlainTextResult.cs" />
<Compile Include="WebApi\QueryResult.cs" />
<Compile Include="WebApi\QueryResultDefaults.cs" />
Expand Down Expand Up @@ -2001,7 +2002,8 @@
<Content Include="Views\Authentication\SignInNuGetAccount.cshtml" />
<Content Include="Views\Packages\_VerifyForm.cshtml" />
<Content Include="Views\Packages\_VerifyMetadata.cshtml" />
<Content Include="Views\Users\AccountTransformFailed.cshtml" />
<Content Include="Views\Users\TransformFailed.cshtml" />
<Content Include="Views\Users\Transform.cshtml" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="Properties\CodeAnalysisDictionary.xml" />
Expand Down Expand Up @@ -2341,7 +2343,7 @@
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>True</UseIIS>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>80</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
Expand Down
3 changes: 2 additions & 1 deletion src/NuGetGallery/RouteNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public static class RouteName
public const string V2ApiFeed = "V2ApiFeed";
public const string ApiFeed = "ApiFeed";
public const string Account = "Account";
public const string TransformAccountConfirmation = "ConfirmTransformAccount";
public const string TransformToOrganization = "TransformToOrganization";
public const string TransformToOrganizationConfirmation = "ConfirmTransformToOrganization";
public const string ApiKeys = "ApiKeys";
public const string Profile = "Profile";
public const string DisplayPackage = "package-route";
Expand Down
4 changes: 3 additions & 1 deletion src/NuGetGallery/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public interface IUserService
Task<IDictionary<int, string>> GetEmailAddressesForUserKeysAsync(IReadOnlyCollection<int> distinctUserKeys);

bool CanTransformUserToOrganization(User accountToTransform, out string errorReason);


Task RequestTransformToOrganizationAccount(User accountToTransform, User adminUser);

Task<bool> TransformUserToOrganization(User accountToTransform, User adminUser, string token);
}
}
19 changes: 19 additions & 0 deletions src/NuGetGallery/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ public async Task<bool> ConfirmEmailAddress(User user, string token)
await UserRepository.CommitChangesAsync();
return true;
}

public async Task RequestTransformToOrganizationAccount(User accountToTransform, User adminUser)
{
accountToTransform = accountToTransform ?? throw new ArgumentNullException(nameof(accountToTransform));
Copy link
Contributor

Choose a reason for hiding this comment

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

Will the proposed admin requested to accept the request of becoming an admin?

Copy link
Member Author

Choose a reason for hiding this comment

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

see PR #5228 for the confirmation

The proposed/pending organization admin must log in and confirm using the generated confirmationToken.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we handle the cases when the accountToTransform is already an organization ?

Copy link
Member Author

Choose a reason for hiding this comment

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

adminUser = adminUser ?? throw new ArgumentNullException(nameof(adminUser));

// create new or update existing request
if (accountToTransform.OrganizationMigrationRequest == null)
{
accountToTransform.OrganizationMigrationRequest = new OrganizationMigrationRequest();
};

accountToTransform.OrganizationMigrationRequest.NewOrganization = accountToTransform;
accountToTransform.OrganizationMigrationRequest.AdminUser = adminUser;
accountToTransform.OrganizationMigrationRequest.ConfirmationToken = Crypto.GenerateToken();
accountToTransform.OrganizationMigrationRequest.RequestDate = DateTime.UtcNow;

await UserRepository.CommitChangesAsync();
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense as the requests to have a Status? Will they be ever completed?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the confirmation logic is very similar to email confirmation. The user must be authenticated and verify against the token we generate. Nothing fancy is needed for status.

We considered whether to expire the confirmation after 24hr, but chose to do the same as email confirmation which doesn't expire. We do store RequestDate in case we want to change this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to add auditing for this scenario?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, there's a separate work item to track auditing which will be done at lower priority.

}

public bool CanTransformUserToOrganization(User accountToTransform, out string errorReason)
{
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 @@ -712,4 +712,13 @@ For more information, please contact '{2}'.</value>
<data name="TransformAccount_FailedReasonIsOrganization" xml:space="preserve">
<value>Account '{0}' is already an organization.</value>
</data>
<data name="TransformAccount_AdminAccountDoesNotExist" xml:space="preserve">
<value>Administrator account '{0}' does not exist.</value>
</data>
<data name="TransformAccount_AdminAccountNotConfirmed" xml:space="preserve">
<value>Administrator account '{0}' has not confirmed their email address.</value>
</data>
<data name="TransformAccount_RequestExists" xml:space="preserve">
<value>Another tranform request was created on {0} with organization admin '{1}'.</value>
</data>
</root>
20 changes: 19 additions & 1 deletion src/NuGetGallery/UrlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,11 @@ public static string ConfirmationRequired(this UrlHelper url, bool relativeUrl =

public static string LogOff(this UrlHelper url, bool relativeUrl = true)
{
string returnUrl = url.Current();
return LogOff(url, url.Current(), relativeUrl);
}

public static string LogOff(this UrlHelper url, string returnUrl, bool relativeUrl = true)
{
// If we're logging off from the Admin Area, don't set a return url
if (string.Equals(url.RequestContext.RouteData.DataTokens[Area].ToStringOrNull(), AdminAreaRegistration.Name, StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -971,6 +975,20 @@ public static string GenerateApiKey(this UrlHelper url, bool relativeUrl = true)
return GetActionLink(url, "GenerateApiKey", "Users", relativeUrl);
}

public static string ConfirmTransformAccount(this UrlHelper url, User accountToTransform, bool relativeUrl = true)
{
return GetActionLink(
url,
"ConfirmTransform",
"Users",
relativeUrl,
routeValues: new RouteValueDictionary
{
{ "accountNameToTransform", accountToTransform.Username },
{ "token", accountToTransform.OrganizationMigrationRequest.ConfirmationToken }
});
}

private static UriBuilder GetCanonicalUrl(UrlHelper url)
{
var builder = new UriBuilder(url.RequestContext.HttpContext.Request.Url);
Expand Down
15 changes: 15 additions & 0 deletions src/NuGetGallery/ViewModels/TransformAccountViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.ComponentModel.DataAnnotations;

namespace NuGetGallery
{
public class TransformAccountViewModel
{
[Required]
[StringLength(255)]
[Display(Name = "Administrator")]
public string AdminUsername { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/NuGetGallery/Views/Users/Profiles.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<section role="main" class="container main-container page-profile">
<section role="main" class="row">
<aside class="col-md-3 col-md-push-9 profile-details">
@ViewHelpers.GravatarImage(Model.EmailAddress ?? Model.UnconfirmedEmailAddress, Model.Username, 332, responsive: true)
@ViewHelpers.GravatarImage(Model.EmailAddress ?? Model.UnconfirmedEmailAddress, Model.Username, Constants.GravatarImageSizeLarge, responsive: true)
<div class="statistics">
<div class="statistic">
<div class="value">@Model.AllPackages.Count.ToNuGetNumberString()</div>
Expand Down
57 changes: 57 additions & 0 deletions src/NuGetGallery/Views/Users/Transform.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@model TransformAccountViewModel
@{
ViewBag.Title = "Transform Account";
ViewBag.MdPageColumns = Constants.ColumnsFormMd;
Layout = "~/Views/Shared/Gallery/Layout.cshtml";
}

<section role="main" class="container main-container page-account-settings">
<div class="row">
<div class="@ViewHelpers.GetColumnClasses(ViewBag)">
<h1 class="text-center">Transform Account</h1>
<div class="text-center ms-font-xxl">
<a href="@Url.User(CurrentUser)">@CurrentUser.Username</a>
</div>

<div>
<aside class="col-md-3 col-md-push-9">
@Html.Label("Logo")
@ViewHelpers.GravatarImage(CurrentUser.EmailAddress, CurrentUser.Username, Constants.GravatarImageSizeLarge, responsive: true)
</aside>

<div class="row form-group col-md-9 col-md-pull-3">
@Html.Label("Organization")
<p>@CurrentUser.Username</p>
</div>

<div class="row form-group col-md-9 col-md-pull-3">
@Html.Label("Email")
<p>@CurrentUser.EmailAddress</p>
</div>

@using (Html.BeginForm("Transform", "Users"))
{
@Html.AntiForgeryToken()

<div class="row form-group col-md-9 col-md-pull-3 @Html.HasErrorFor(m => m.AdminUsername)">
@Html.ShowLabelFor(m => m.AdminUsername)
@Html.ShowTextBoxFor(m => m.AdminUsername)
@Html.ShowValidationMessagesFor(m => m.AdminUsername)
</div>

<div class="row form-group">
<div class="col-md-6">
<input type="submit" class="btn btn-primary form-control" value="Transform" />
</div>
<div class="col-md-6">
<a href="#" role="button" class="btn btn-default form-control" id="cancel-transform">
Cancel
</a>
</div>
</div>
}
</div>

</div>
</div>
</section>
Loading