Merge pull request #1934 from btcpayserver/better-users

Add Created date to user, add verified column in list and make user l…
This commit is contained in:
Nicolas Dorier 2020-10-08 12:08:16 +09:00 committed by GitHub
commit 55eec06e77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 118 additions and 100 deletions

View File

@ -1,3 +1,6 @@
using System;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class ApplicationUserData
@ -26,5 +29,11 @@ namespace BTCPayServer.Client.Models
/// the roles of the user
/// </summary>
public string[] Roles { get; set; }
/// <summary>
/// the date the user was created. Null if created before v1.0.5.6.
/// </summary>
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? Created { get; set; }
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
@ -26,5 +27,6 @@ namespace BTCPayServer.Data
public List<U2FDevice> U2FDevices { get; set; }
public List<APIKeyData> APIKeys { get; set; }
public DateTimeOffset? Created { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20201002145033_AddCreateDateToUser")]
public partial class AddCreateDateToUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "AspNetUsers",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Created",
table: "AspNetUsers");
}
}
}
}

View File

@ -108,6 +108,9 @@ namespace BTCPayServer.Migrations
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Created")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasMaxLength(256);

View File

@ -168,6 +168,8 @@ namespace BTCPayServer.Tests
IsAdministrator = true
});
Assert.Contains("ServerAdmin", admin.Roles);
Assert.NotNull(admin.Created);
Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10);
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection

View File

@ -426,7 +426,8 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(HomeController.Index), "Home");
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
Created = DateTimeOffset.UtcNow };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{

View File

@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -110,7 +111,8 @@ namespace BTCPayServer.Controllers.GreenField
{
UserName = request.Email,
Email = request.Email,
RequiresEmailConfirmation = policies.RequiresConfirmedEmail
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
Created = DateTimeOffset.UtcNow,
};
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
if (!passwordValidation.Succeeded)
@ -165,7 +167,8 @@ namespace BTCPayServer.Controllers.GreenField
Email = data.Email,
EmailConfirmed = data.EmailConfirmed,
RequiresEmailConfirmation = data.RequiresEmailConfirmation,
Roles = roles
Roles = roles,
Created = data.Created
};
}
}

View File

@ -10,26 +10,30 @@ using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
public partial class ServerController
{
[Route("server/users")]
public IActionResult ListUsers(int skip = 0, int count = 50)
public async Task<IActionResult> ListUsers(UsersViewModel model)
{
var users = new UsersViewModel();
users.Users = _UserManager.Users.Skip(skip).Take(count)
model = this.ParseListQuery(model ?? new UsersViewModel());
var users = _UserManager.Users;
model.Total = await users.CountAsync();
model.Users = await users
.Skip(model.Skip).Take(model.Count)
.Select(u => new UsersViewModel.UserViewModel
{
Name = u.UserName,
Email = u.Email,
Id = u.Id
}).ToList();
users.Skip = skip;
users.Count = count;
users.Total = _UserManager.Users.Count();
return View(users);
Id = u.Id,
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
Created = u.Created
}).ToListAsync();
return View(model);
}
[Route("server/users/{userId}")]
@ -39,15 +43,16 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UserViewModel();
userVM.Id = user.Id;
userVM.Email = user.Email;
userVM.IsAdmin = IsAdmin(roles);
var userVM = new UsersViewModel.UserViewModel
{
Id = user.Id,
Email = user.Email,
Verified = user.EmailConfirmed || !user.RequiresEmailConfirmation,
IsAdmin = IsAdmin(roles)
};
return View(userVM);
}
private static bool IsAdmin(IList<string> roles)
{
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
@ -55,7 +60,7 @@ namespace BTCPayServer.Controllers
[Route("server/users/{userId}")]
[HttpPost]
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
@ -104,7 +109,8 @@ namespace BTCPayServer.Controllers
if (ModelState.IsValid)
{
IdentityResult result;
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = model.EmailConfirmed, RequiresEmailConfirmation = _cssThemeManager.Policies.RequiresConfirmedEmail };
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = model.EmailConfirmed, RequiresEmailConfirmation = _cssThemeManager.Policies.RequiresConfirmedEmail,
Created = DateTimeOffset.UtcNow };
if (!string.IsNullOrEmpty(model.Password))
{

View File

@ -3,6 +3,7 @@ using System.Reflection;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Models.ServerViewModels;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
@ -18,6 +19,8 @@ namespace BTCPayServer
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.InvoicesQuery));
else if (model is ListPaymentRequestsViewModel)
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.PaymentRequestsQuery));
else if (model is UsersViewModel)
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.UsersQuery));
else
throw new Exception("Unsupported BasePagingViewModel for cookie user preferences saving");
@ -74,6 +77,7 @@ namespace BTCPayServer
public ListQueryDataHolder InvoicesQuery { get; set; }
public ListQueryDataHolder PaymentRequestsQuery { get; set; }
public ListQueryDataHolder UsersQuery { get; set; }
}
class ListQueryDataHolder

View File

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Models.ServerViewModels
{
public class UserViewModel
{
public string Id { get; set; }
public string Email { get; set; }
[Display(Name = "Is admin")]
public bool IsAdmin { get; set; }
}
}

View File

@ -1,20 +1,19 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Models.ServerViewModels
{
public class UsersViewModel
public class UsersViewModel: BasePagingViewModel
{
public class UserViewModel
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public bool Verified { get; set; }
public bool IsAdmin { get; set; }
public DateTimeOffset? Created { get; set; }
}
public int Skip { get; set; }
public int Count { get; set; }
public int Total { get; set; }
public List<UserViewModel> Users { get; set; } = new List<UserViewModel>();
}

View File

@ -3,16 +3,17 @@
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
}
<partial name="_StatusMessage" />
<partial name="_StatusMessage"/>
<div class="row button-row">
<div class="col-lg-9 col-xl-8">
<span>Total Users: @Model.Total</span>
<span class="pull-right">
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
<span class="fa fa-plus"></span> Add User
</a>
</span>
<div class="col-12 col-sm-4 col-lg-6 mb-3">
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
<span class="fa fa-plus"></span> Add User
</a>
</div>
<div class="col-12 col-sm-8 col-lg-6 mb-3">
</div>
</div>
@ -22,6 +23,8 @@
<thead>
<tr>
<th>Email</th>
<th>Created</th>
<th>Verified</th>
<th class="text-right">Actions</th>
</tr>
</thead>
@ -30,63 +33,26 @@
{
<tr>
<td>@user.Email</td>
<td class="text-right"><a asp-action="User" asp-route-userId="@user.Id">Edit</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
<td>@user.Created?.ToBrowserDate()</td>
<td class="text-center">
@if (user.Verified)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
</td>
<td class="text-right">
<a asp-action="User" asp-route-userId="@user.Id">Edit</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
<nav aria-label="..." class="w-100">
<ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@listUsers(-1, Model.Count)">&laquo;</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Users.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Users.Count) ? null : "disabled")">
<a class="page-link" href="@listUsers(1, Model.Count)">&raquo;</a>
</li>
</ul>
<ul class="pagination float-right">
<li class="page-item disabled">
<span class="page-link">Page Size:</span>
</li>
<li class="page-item @(Model.Count == 50 ? "active" : null)">
<a class="page-link" href="@listUsers(0, 50)">50</a>
</li>
<li class="page-item @(Model.Count == 100 ? "active" : null)">
<a class="page-link" href="@listUsers(0, 100)">100</a>
</li>
<li class="page-item @(Model.Count == 250 ? "active" : null)">
<a class="page-link" href="@listUsers(0, 250)">250</a>
</li>
<li class="page-item @(Model.Count == 500 ? "active" : null)">
<a class="page-link" href="@listUsers(0, 500)">500</a>
</li>
</ul>
</nav>
@{
string listUsers(int prevNext, int count)
{
var skip = Model.Skip;
if (prevNext == -1)
{
skip = Math.Max(0, Model.Skip - Model.Count);
}
else if (prevNext == 1)
{
skip = Model.Skip + count;
}
var act = Url.Action("ListUsers", new
{
skip = skip,
count = count,
});
return act;
}
}
<vc:pager view-model="Model"></vc:pager>
</div>
</div>

View File

@ -1,4 +1,4 @@
@model UserViewModel
@model UsersViewModel.UserViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
}

View File

@ -133,6 +133,10 @@
"type": "boolean",
"description": "True if the email requires email confirmation to log in"
},
"created": {
"type": "string",
"description": "The creation date of the user as a unix timestamp. Null if created before v1.0.5.6"
},
"roles": {
"type": "array",
"nullable": false,