diff --git a/BTCPayServer.Client/Models/ApplicationUserData.cs b/BTCPayServer.Client/Models/ApplicationUserData.cs index 25715ae12..f11f0f9a9 100644 --- a/BTCPayServer.Client/Models/ApplicationUserData.cs +++ b/BTCPayServer.Client/Models/ApplicationUserData.cs @@ -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 /// public string[] Roles { get; set; } + + /// + /// the date the user was created. Null if created before v1.0.5.6. + /// + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] + public DateTimeOffset? Created { get; set; } } } diff --git a/BTCPayServer.Data/Data/ApplicationUser.cs b/BTCPayServer.Data/Data/ApplicationUser.cs index 8f853392a..80c86dd43 100644 --- a/BTCPayServer.Data/Data/ApplicationUser.cs +++ b/BTCPayServer.Data/Data/ApplicationUser.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Microsoft.AspNetCore.Identity; @@ -26,5 +27,6 @@ namespace BTCPayServer.Data public List U2FDevices { get; set; } public List APIKeys { get; set; } + public DateTimeOffset? Created { get; set; } } } diff --git a/BTCPayServer.Data/Migrations/20201002145033_AddCreateDateToUser.cs b/BTCPayServer.Data/Migrations/20201002145033_AddCreateDateToUser.cs new file mode 100644 index 000000000..55397f2f2 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20201002145033_AddCreateDateToUser.cs @@ -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( + name: "Created", + table: "AspNetUsers", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.DropColumn( + name: "Created", + table: "AspNetUsers"); + + } + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index cbc070198..0473d9fa6 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -108,6 +108,9 @@ namespace BTCPayServer.Migrations .IsConcurrencyToken() .HasColumnType("TEXT"); + b.Property("Created") + .HasColumnType("TEXT"); + b.Property("Email") .HasColumnType("TEXT") .HasMaxLength(256); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ac1d0054c..62f73fab6 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -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 diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index ae5520317..adc95f775 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -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) { diff --git a/BTCPayServer/Controllers/GreenField/UsersController.cs b/BTCPayServer/Controllers/GreenField/UsersController.cs index dba0672c0..e76cd784c 100644 --- a/BTCPayServer/Controllers/GreenField/UsersController.cs +++ b/BTCPayServer/Controllers/GreenField/UsersController.cs @@ -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 }; } } diff --git a/BTCPayServer/Controllers/ServerController.Users.cs b/BTCPayServer/Controllers/ServerController.Users.cs index a3020e2a9..a5fc5e26c 100644 --- a/BTCPayServer/Controllers/ServerController.Users.cs +++ b/BTCPayServer/Controllers/ServerController.Users.cs @@ -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 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 roles) { return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal); @@ -55,7 +60,7 @@ namespace BTCPayServer.Controllers [Route("server/users/{userId}")] [HttpPost] - public new async Task User(string userId, UserViewModel viewModel) + public new async Task 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)) { diff --git a/BTCPayServer/Extensions/ControllerBaseExtensions.cs b/BTCPayServer/Extensions/ControllerBaseExtensions.cs index 47520c950..774608036 100644 --- a/BTCPayServer/Extensions/ControllerBaseExtensions.cs +++ b/BTCPayServer/Extensions/ControllerBaseExtensions.cs @@ -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 diff --git a/BTCPayServer/Models/ServerViewModels/UserViewModel.cs b/BTCPayServer/Models/ServerViewModels/UserViewModel.cs deleted file mode 100644 index a1d39f3e1..000000000 --- a/BTCPayServer/Models/ServerViewModels/UserViewModel.cs +++ /dev/null @@ -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; } - } -} diff --git a/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs b/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs index d6944d1ce..ff779883b 100644 --- a/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs @@ -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 Users { get; set; } = new List(); } diff --git a/BTCPayServer/Views/Server/ListUsers.cshtml b/BTCPayServer/Views/Server/ListUsers.cshtml index 77c5d71f1..00afcc7cf 100644 --- a/BTCPayServer/Views/Server/ListUsers.cshtml +++ b/BTCPayServer/Views/Server/ListUsers.cshtml @@ -3,16 +3,17 @@ ViewData.SetActivePageAndTitle(ServerNavPages.Users); } - +
-
- Total Users: @Model.Total - - - Add User - - + + +
+
@@ -22,6 +23,8 @@ Email + Created + Verified Actions @@ -30,63 +33,26 @@ { @user.Email - Edit - Remove + @user.Created?.ToBrowserDate() + + @if (user.Verified) + { + + } + else + { + + } + + + Edit - Remove + } - - @{ - 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; - } - } + +
diff --git a/BTCPayServer/Views/Server/User.cshtml b/BTCPayServer/Views/Server/User.cshtml index 67da9dfce..4a50ec4c4 100644 --- a/BTCPayServer/Views/Server/User.cshtml +++ b/BTCPayServer/Views/Server/User.cshtml @@ -1,4 +1,4 @@ -@model UserViewModel +@model UsersViewModel.UserViewModel @{ ViewData.SetActivePageAndTitle(ServerNavPages.Users); } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json index ce2180dd8..a027c79cc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json @@ -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,