From 4bb2697f5a3e177e37c29a5fa79e68571d2eeb15 Mon Sep 17 00:00:00 2001 From: Hubert Date: Thu, 7 Dec 2023 19:42:00 +0100 Subject: [PATCH 1/4] recommit everything after rebasing --- .../Services/v2/AccountService.cs | 29 +++++++++++ .../Services/v2/IAccountService.cs | 7 +++ .../v2/User/SearchUserRequest.cs | 24 +++++++++ .../v2/User/UserSearchResponse.cs | 50 +++++++++++++++++++ .../Controllers/v2/AccountController.cs | 35 +++++++++++++ 5 files changed, 145 insertions(+) create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs diff --git a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs index 0b72d071..979e6f21 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs @@ -210,6 +210,35 @@ public async Task UpdateUserGroup(UserGroup userGroup, int userId) await _context.SaveChangesAsync(); } + public async Task> SearchUsers(String search, int pageNum, int pageLength) + { + List users = new List(); + + int skip = pageNum * pageLength; + + if (int.TryParse(search, out int id)) + { + var user = await _context.Users.Skip(skip).Take(pageLength).FirstOrDefaultAsync(u => u.Id == id); + if (user != null) + { + users.Add(user); + } + } + + users = await _context.Users.Skip(0).Take(pageLength) + .Where(u => u.Email.ToLower().StartsWith(search.ToLower()) || u.Name.ToLower().StartsWith(search.ToLower())) + .ToListAsync(); + + + if (users.Count == 0) + { + throw new EntityNotFoundException("No users match the search"); + } + + return users; + } + + private async Task GetUserByIdAsync(int id) { var user = await _context.Users diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs index 9a35b4f2..5dc79404 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; @@ -63,5 +64,11 @@ public interface IAccountService /// The user group that will be updated /// id of the user Task UpdateUserGroup(UserGroup userGroup, int id); + + /// + /// Search a user from the database + /// + /// The request to search a user by id, name, or email + Task> SearchUsers(String search, int pageNum, int pageLength); } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs new file mode 100644 index 00000000..55f96409 --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace CoffeeCard.Models.DataTransferObjects.v2.User +{ + /// + /// Search a User in the database + /// + /// + /// { + /// "Search": "john.doe" + /// } + /// + public class SearchUserRequest + { + /// + /// The string that is submitted in a search bar + /// + /// Search of a user + /// john.doe@gmail.com + [Required] + [MinLength(3)] + public string Search { get; set; } + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs new file mode 100644 index 00000000..dd62e08d --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace CoffeeCard.Models.DataTransferObjects.v2.User +{ + /// + /// User information that is returned when searching + /// + /// + /// { + /// "id": 123, + /// "name": "John Doe", + /// "email": "john@doe.com", + /// "role": "Barista" + /// } + /// + public class UserSearchResponse + { + /// + /// User Id + /// + /// User Id + /// 123 + [Required] + public int Id { get; set; } + + /// + /// Full Name of user + /// + /// Full Name + /// John Doe + [Required] + public string Name { get; set; } + + /// + /// Email of user + /// + /// Email + /// john@doe.com + [Required] + public string Email { get; set; } + + /// + /// User's role + /// + /// Role + /// Barista + [Required] + public UserRole Role { get; set; } = UserRole.Customer; + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs index 3751d641..f6ca9621 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using CoffeeCard.Common.Errors; using CoffeeCard.Library.Utils; @@ -201,5 +204,37 @@ private async Task UserWithRanking(User user) PrivacyActivated = user.PrivacyActivated, }; } + + /// + /// Searches a user in the database + /// + /// Update User Group information request + /// no content result + /// The user(s) were found + /// Invalid credentials + /// User(s) not found + [HttpGet] + //[AuthorizeRoles(UserGroup.Board)] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status200OK)] + [Route("search")] + public async Task>> SearchUsers([FromQuery] String search, [FromQuery] int pageNum, [FromQuery] int pageLength) + { + List users = await _accountService.SearchUsers(search, pageNum, pageLength); + return Ok(users.Select(MapSearchedUserToDto).ToList()); + } + + private static UserSearchResponse MapSearchedUserToDto(User user) + { + return new UserSearchResponse + { + Id = user.Id, + Email = user.Email, + Name = user.Name, + Role = user.UserGroup.toUserRole(), + }; + } } } \ No newline at end of file From 3ab7b21855d06cd40ab3f4ce4996c8d362d0ee0f Mon Sep 17 00:00:00 2001 From: Hubert Date: Sun, 10 Dec 2023 09:58:30 +0100 Subject: [PATCH 2/4] finish up pagination feature of search --- .../Services/v2/AccountService.cs | 14 +++++++---- .../Services/v2/IAccountService.cs | 5 +++- .../v2/User/SearchUserRequest.cs | 24 ------------------- .../Controllers/v2/AccountController.cs | 8 ++++--- 4 files changed, 18 insertions(+), 33 deletions(-) delete mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs diff --git a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs index 979e6f21..c21a7810 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs @@ -213,22 +213,26 @@ public async Task UpdateUserGroup(UserGroup userGroup, int userId) public async Task> SearchUsers(String search, int pageNum, int pageLength) { List users = new List(); - + int skip = pageNum * pageLength; if (int.TryParse(search, out int id)) { - var user = await _context.Users.Skip(skip).Take(pageLength).FirstOrDefaultAsync(u => u.Id == id); + Log.Information("this is an integer"); + var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id); if (user != null) { users.Add(user); + return users; } } - - users = await _context.Users.Skip(0).Take(pageLength) + + users = await _context.Users .Where(u => u.Email.ToLower().StartsWith(search.ToLower()) || u.Name.ToLower().StartsWith(search.ToLower())) + .OrderBy(u => u.Id) + .Skip(skip).Take(pageLength) .ToListAsync(); - + if (users.Count == 0) { diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs index 5dc79404..c64a554f 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs @@ -68,7 +68,10 @@ public interface IAccountService /// /// Search a user from the database /// - /// The request to search a user by id, name, or email + /// The search string from a search bar + /// The page number + /// The length of a page + /// No user found Task> SearchUsers(String search, int pageNum, int pageLength); } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs deleted file mode 100644 index 55f96409..00000000 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SearchUserRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace CoffeeCard.Models.DataTransferObjects.v2.User -{ - /// - /// Search a User in the database - /// - /// - /// { - /// "Search": "john.doe" - /// } - /// - public class SearchUserRequest - { - /// - /// The string that is submitted in a search bar - /// - /// Search of a user - /// john.doe@gmail.com - [Required] - [MinLength(3)] - public string Search { get; set; } - } -} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs index f6ca9621..8c46e631 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs @@ -208,13 +208,15 @@ private async Task UserWithRanking(User user) /// /// Searches a user in the database /// - /// Update User Group information request - /// no content result + /// The search string from a search bar + /// The page number + /// The length of a page + /// A collection of User objects that match the search criteria /// The user(s) were found /// Invalid credentials /// User(s) not found [HttpGet] - //[AuthorizeRoles(UserGroup.Board)] + [AuthorizeRoles(UserGroup.Board)] [AllowAnonymous] [ProducesResponseType(typeof(ApiError), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] From 149cc9bf94f6370fbde335d862920a7745bc26e8 Mon Sep 17 00:00:00 2001 From: Hubert Date: Sun, 10 Dec 2023 10:02:01 +0100 Subject: [PATCH 3/4] remove unnecessary log --- coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs index c21a7810..5dca690e 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs @@ -218,7 +218,6 @@ public async Task> SearchUsers(String search, int pageNum, int pageLe if (int.TryParse(search, out int id)) { - Log.Information("this is an integer"); var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id); if (user != null) { From a5c54939699c6ecc6cf4f9f1c47bda4ae4c026bf Mon Sep 17 00:00:00 2001 From: Jonas Anker Rasmussen Date: Thu, 18 Jan 2024 18:51:55 +0100 Subject: [PATCH 4/4] Suggested changes for Admin Endpoint PR (#245) Co-authored-by: Hubert --- .../20240111163619_NewIndexName.Designer.cs | 574 ++++++++++++++++++ .../Migrations/20240111163619_NewIndexName.cs | 44 ++ .../CoffeeCardContextModelSnapshot.cs | 6 +- .../Services/v2/AccountService.cs | 46 +- .../Services/v2/IAccountService.cs | 3 +- .../v2/User/RegisterAccountRequest.cs | 2 +- .../v2/User/SimpleUserResponse.cs | 13 + .../v2/User/UserSearchResponse.cs | 84 +-- coffeecard/CoffeeCard.Models/Entities/User.cs | 1 + .../Controllers/v2/AccountController.cs | 30 +- coffeecard/CoffeeCard.WebApi/appsettings.json | 2 +- 11 files changed, 716 insertions(+), 89 deletions(-) create mode 100644 coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.Designer.cs create mode 100644 coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.cs create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SimpleUserResponse.cs diff --git a/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.Designer.cs b/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.Designer.cs new file mode 100644 index 00000000..2ba45670 --- /dev/null +++ b/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.Designer.cs @@ -0,0 +1,574 @@ +// +using System; +using CoffeeCard.Library.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CoffeeCard.Library.Migrations +{ + [DbContext(typeof(CoffeeCardContext))] + [Migration("20240111163619_NewIndexName")] + partial class NewIndexName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dbo") + .HasAnnotation("ProductVersion", "6.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("CoffeeCard.Models.Entities.LoginAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Time") + .HasColumnType("datetime2"); + + b.Property("User_Id") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("User_Id"); + + b.ToTable("LoginAttempts", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.PosPurhase", b => + { + b.Property("PurchaseId") + .HasColumnType("int"); + + b.Property("BaristaInitials") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("PurchaseId"); + + b.ToTable("PosPurchases", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExperienceWorth") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfTickets") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("Visible") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Products", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.ProductUserGroup", b => + { + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("UserGroup") + .HasColumnType("int"); + + b.HasKey("ProductId", "UserGroup"); + + b.ToTable("ProductUserGroups", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("FullName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SortPriority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Programmes", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Purchase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("ExternalTransactionId") + .HasColumnType("nvarchar(450)"); + + b.Property("NumberOfTickets") + .HasColumnType("int"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Price") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PurchasedById") + .HasColumnType("int") + .HasColumnName("PurchasedBy_Id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ProductId"); + + b.HasIndex("PurchasedById"); + + b.ToTable("Purchases", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Statistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ExpiryDate") + .HasColumnType("datetime2"); + + b.Property("LastSwipe") + .HasColumnType("datetime2"); + + b.Property("Preset") + .HasColumnType("int"); + + b.Property("SwipeCount") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("User_Id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Preset", "ExpiryDate"); + + b.ToTable("Statistics", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("DateUsed") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.Property("OwnerId") + .HasColumnType("int") + .HasColumnName("Owner_Id"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("PurchaseId") + .HasColumnType("int") + .HasColumnName("Purchase_Id"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("PurchaseId"); + + b.ToTable("Tickets", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Token", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("User_Id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("DateUpdated") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Experience") + .HasColumnType("int"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PrivacyActivated") + .HasColumnType("bit"); + + b.Property("ProgrammeId") + .HasColumnType("int") + .HasColumnName("Programme_Id"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserGroup") + .HasColumnType("int"); + + b.Property("UserState") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("Name"); + + b.HasIndex("ProgrammeId"); + + b.ToTable("Users", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Voucher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("DateUsed") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductId") + .HasColumnType("int") + .HasColumnName("Product_Id"); + + b.Property("PurchaseId") + .HasColumnType("int"); + + b.Property("Requester") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("User_Id"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProductId"); + + b.HasIndex("PurchaseId"); + + b.HasIndex("UserId"); + + b.ToTable("Vouchers", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.WebhookConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("SignatureKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WebhookConfigurations", "dbo"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.LoginAttempt", b => + { + b.HasOne("CoffeeCard.Models.Entities.User", "User") + .WithMany("LoginAttempts") + .HasForeignKey("User_Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.PosPurhase", b => + { + b.HasOne("CoffeeCard.Models.Entities.Purchase", "Purchase") + .WithMany() + .HasForeignKey("PurchaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Purchase"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.ProductUserGroup", b => + { + b.HasOne("CoffeeCard.Models.Entities.Product", "Product") + .WithMany("ProductUserGroup") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Purchase", b => + { + b.HasOne("CoffeeCard.Models.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CoffeeCard.Models.Entities.User", "PurchasedBy") + .WithMany("Purchases") + .HasForeignKey("PurchasedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("PurchasedBy"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Statistic", b => + { + b.HasOne("CoffeeCard.Models.Entities.User", "User") + .WithMany("Statistics") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Ticket", b => + { + b.HasOne("CoffeeCard.Models.Entities.User", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("CoffeeCard.Models.Entities.Purchase", "Purchase") + .WithMany("Tickets") + .HasForeignKey("PurchaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Purchase"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Token", b => + { + b.HasOne("CoffeeCard.Models.Entities.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.User", b => + { + b.HasOne("CoffeeCard.Models.Entities.Programme", "Programme") + .WithMany("Users") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Programme"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Voucher", b => + { + b.HasOne("CoffeeCard.Models.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CoffeeCard.Models.Entities.Purchase", "Purchase") + .WithMany() + .HasForeignKey("PurchaseId"); + + b.HasOne("CoffeeCard.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Product"); + + b.Navigation("Purchase"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Product", b => + { + b.Navigation("ProductUserGroup"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Programme", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.Purchase", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("CoffeeCard.Models.Entities.User", b => + { + b.Navigation("LoginAttempts"); + + b.Navigation("Purchases"); + + b.Navigation("Statistics"); + + b.Navigation("Tickets"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.cs b/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.cs new file mode 100644 index 00000000..46fbab57 --- /dev/null +++ b/coffeecard/CoffeeCard.Library/Migrations/20240111163619_NewIndexName.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CoffeeCard.Library.Migrations +{ + public partial class NewIndexName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + schema: "dbo", + table: "Users", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Name", + schema: "dbo", + table: "Users", + column: "Name"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Users_Name", + schema: "dbo", + table: "Users"); + + migrationBuilder.AlterColumn( + name: "Name", + schema: "dbo", + table: "Users", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + } + } +} diff --git a/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs b/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs index 558a6e23..863f3a34 100644 --- a/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs +++ b/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using CoffeeCard.Library.Persistence; using Microsoft.EntityFrameworkCore; @@ -304,7 +304,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("nvarchar(450)"); b.Property("Password") .IsRequired() @@ -332,6 +332,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Email"); + b.HasIndex("Name"); + b.HasIndex("ProgrammeId"); b.ToTable("Users", "dbo"); diff --git a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs index 5dca690e..3a308c4d 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs @@ -8,7 +8,6 @@ using CoffeeCard.Models.DataTransferObjects.v2.User; using CoffeeCard.Models.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Serilog; @@ -210,35 +209,44 @@ public async Task UpdateUserGroup(UserGroup userGroup, int userId) await _context.SaveChangesAsync(); } - public async Task> SearchUsers(String search, int pageNum, int pageLength) - { - List users = new List(); + public async Task SearchUsers(String search, int pageNum, int pageLength) + { int skip = pageNum * pageLength; - if (int.TryParse(search, out int id)) + IQueryable query; + if (string.IsNullOrEmpty(search)) { - var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id); - if (user != null) - { - users.Add(user); - return users; - } + query = _context.Users; } + else + { + query = _context.Users + .Where(u => EF.Functions.Like(u.Id.ToString(), $"%{search}%") || + EF.Functions.Like(u.Name, $"%{search}%") || + EF.Functions.Like(u.Email, $"%{search}%")); + } + + var totalUsers = await query.CountAsync(); - users = await _context.Users - .Where(u => u.Email.ToLower().StartsWith(search.ToLower()) || u.Name.ToLower().StartsWith(search.ToLower())) + var userByPage = await query .OrderBy(u => u.Id) .Skip(skip).Take(pageLength) + .Select(u => new SimpleUserResponse + { + Id = u.Id, + Name = u.Name, + Email = u.Email, + UserGroup = u.UserGroup, + State = u.UserState + }) .ToListAsync(); - - if (users.Count == 0) + return new UserSearchResponse { - throw new EntityNotFoundException("No users match the search"); - } - - return users; + TotalUsers = totalUsers, + Users = userByPage + }; } diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs index c64a554f..42166a66 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs @@ -71,7 +71,6 @@ public interface IAccountService /// The search string from a search bar /// The page number /// The length of a page - /// No user found - Task> SearchUsers(String search, int pageNum, int pageLength); + Task SearchUsers(String search, int pageNum, int pageLength); } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/RegisterAccountRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/RegisterAccountRequest.cs index 44f2f124..b39499a4 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/RegisterAccountRequest.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/RegisterAccountRequest.cs @@ -10,7 +10,7 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.User /// "name": "John Doe", /// "email": "john@doe.com", /// "password": "[no example provided]", - /// "programme": 1 + /// "programmeId": 1 /// } /// public class RegisterAccountRequest diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SimpleUserResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SimpleUserResponse.cs new file mode 100644 index 00000000..d033bb34 --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/SimpleUserResponse.cs @@ -0,0 +1,13 @@ +using CoffeeCard.Models.Entities; + +namespace CoffeeCard.Models.DataTransferObjects.v2.User +{ + public class SimpleUserResponse + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public UserGroup UserGroup { get; set; } + public UserState State { get; set; } + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs index dd62e08d..bdf65ca7 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs @@ -1,50 +1,50 @@ +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace CoffeeCard.Models.DataTransferObjects.v2.User +namespace CoffeeCard.Models.DataTransferObjects.v2.User; + +/// +/// Represents a search result +/// +/// +/// { +/// "users": [ +/// { +/// "id": 12232, +/// "name": "John Doe", +/// "email": "johndoe@itu.dk", +/// "userGroup": "Barista", +/// "state": "Active" +/// } +/// ], +/// "totalUsers": 1 +/// } +/// +public class UserSearchResponse { /// - /// User information that is returned when searching + /// The number of users that match the query + /// + /// Users number + /// 1 + [Required] + public int TotalUsers { get; set; } + + /// + /// The users that match the query /// + /// Users List /// - /// { - /// "id": 123, - /// "name": "John Doe", - /// "email": "john@doe.com", - /// "role": "Barista" - /// } + /// [ + /// { + /// "id": 12232, + /// "name": "John Doe", + /// "email": "johndoe@itu.dk", + /// "userGroup": "Barista", + /// "state": "Active" + /// } + /// ], /// - public class UserSearchResponse - { - /// - /// User Id - /// - /// User Id - /// 123 - [Required] - public int Id { get; set; } - - /// - /// Full Name of user - /// - /// Full Name - /// John Doe - [Required] - public string Name { get; set; } - - /// - /// Email of user - /// - /// Email - /// john@doe.com - [Required] - public string Email { get; set; } - - /// - /// User's role - /// - /// Role - /// Barista - [Required] - public UserRole Role { get; set; } = UserRole.Customer; - } + [Required] + public IEnumerable Users; } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/Entities/User.cs b/coffeecard/CoffeeCard.Models/Entities/User.cs index 3a557d20..0bdeb613 100644 --- a/coffeecard/CoffeeCard.Models/Entities/User.cs +++ b/coffeecard/CoffeeCard.Models/Entities/User.cs @@ -8,6 +8,7 @@ namespace CoffeeCard.Models.Entities { [Index(nameof(Email))] + [Index(nameof(Name))] public class User { public int Id { get; set; } diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs index 8c46e631..bfdefd8f 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs @@ -13,6 +13,7 @@ using CoffeeCard.Library.Services.v2; using CoffeeCard.Models.Entities; using CoffeeCard.WebApi.Helpers; +using System.ComponentModel.DataAnnotations; namespace CoffeeCard.WebApi.Controllers.v2 { @@ -208,35 +209,20 @@ private async Task UserWithRanking(User user) /// /// Searches a user in the database /// - /// The search string from a search bar - /// The page number - /// The length of a page + /// A filter to search by Id, Name or Email. When an empty string is given, all users will be returned + /// The page number + /// The length of a page /// A collection of User objects that match the search criteria - /// The user(s) were found + /// Users, possible with filter applied /// Invalid credentials - /// User(s) not found [HttpGet] [AuthorizeRoles(UserGroup.Board)] - [AllowAnonymous] [ProducesResponseType(typeof(ApiError), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiError), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(SimpleUserResponse), StatusCodes.Status200OK)] [Route("search")] - public async Task>> SearchUsers([FromQuery] String search, [FromQuery] int pageNum, [FromQuery] int pageLength) - { - List users = await _accountService.SearchUsers(search, pageNum, pageLength); - return Ok(users.Select(MapSearchedUserToDto).ToList()); - } - - private static UserSearchResponse MapSearchedUserToDto(User user) + public async Task>> SearchUsers([FromQuery][Range(0, int.MaxValue)] int pageNum, [FromQuery] string filter = "", [FromQuery][Range(1, 100)] int pageLength = 30) { - return new UserSearchResponse - { - Id = user.Id, - Email = user.Email, - Name = user.Name, - Role = user.UserGroup.toUserRole(), - }; + return Ok(await _accountService.SearchUsers(filter, pageNum, pageLength)); } } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.WebApi/appsettings.json b/coffeecard/CoffeeCard.WebApi/appsettings.json index d65081e1..a5aaf876 100644 --- a/coffeecard/CoffeeCard.WebApi/appsettings.json +++ b/coffeecard/CoffeeCard.WebApi/appsettings.json @@ -9,7 +9,7 @@ "DeploymentUrl": "https://localhost:8080/" }, "DatabaseSettings": { - "ConnectionString": "Server=localhost;Initial Catalog=master;User=sa;Password=Your_password123;TrustServerCertificate=True;", + "ConnectionString": "Server=mssql;Initial Catalog=master;User=sa;Password=Your_password123;TrustServerCertificate=True;", "SchemaName": "dbo" }, "IdentitySettings": {