Skip to content

Commit

Permalink
217 new admin endpoints: search user (#233)
Browse files Browse the repository at this point in the history
Create an endpoint to search for user(s) based on their id, name, or
email.

---------

Co-authored-by: Omid Marfavi <[email protected]>
Co-authored-by: Jonas Anker Rasmussen <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2024
1 parent ee7d26c commit 46a9e66
Show file tree
Hide file tree
Showing 11 changed files with 761 additions and 5 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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<string>(
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<string>(
name: "Name",
schema: "dbo",
table: "Users",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using CoffeeCard.Library.Persistence;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -304,7 +304,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)

b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
.HasColumnType("nvarchar(450)");

b.Property<string>("Password")
.IsRequired()
Expand Down Expand Up @@ -332,6 +332,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)

b.HasIndex("Email");

b.HasIndex("Name");

b.HasIndex("ProgrammeId");

b.ToTable("Users", "dbo");
Expand Down
42 changes: 41 additions & 1 deletion coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -210,6 +209,47 @@ public async Task UpdateUserGroup(UserGroup userGroup, int userId)
await _context.SaveChangesAsync();
}


public async Task<UserSearchResponse> SearchUsers(String search, int pageNum, int pageLength)
{
int skip = pageNum * pageLength;

IQueryable<User> query;
if (string.IsNullOrEmpty(search))
{
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();

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();

return new UserSearchResponse
{
TotalUsers = totalUsers,
Users = userByPage
};
}


private async Task<User> GetUserByIdAsync(int id)
{
var user = await _context.Users
Expand Down
9 changes: 9 additions & 0 deletions coffeecard/CoffeeCard.Library/Services/v2/IAccountService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
Expand Down Expand Up @@ -63,5 +64,13 @@ public interface IAccountService
/// <param name="userGroup"> The user group that will be updated </param>
/// <param name="id"> id of the user </param>
Task UpdateUserGroup(UserGroup userGroup, int id);

/// <summary>
/// Search a user from the database
/// </summary>
/// <param name="search"> The search string from a search bar </param>
/// <param name="pageNum"> The page number </param>
/// <param name="pageLength"> The length of a page </param>
Task<UserSearchResponse> SearchUsers(String search, int pageNum, int pageLength);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.User
/// "name": "John Doe",
/// "email": "[email protected]",
/// "password": "[no example provided]",
/// "programme": 1
/// "programmeId": 1
/// }
/// </example>
public class RegisterAccountRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace CoffeeCard.Models.DataTransferObjects.v2.User;

/// <summary>
/// Represents a search result
/// </summary>
/// <example>
/// {
/// "users": [
/// {
/// "id": 12232,
/// "name": "John Doe",
/// "email": "[email protected]",
/// "userGroup": "Barista",
/// "state": "Active"
/// }
/// ],
/// "totalUsers": 1
/// }
/// </example>
public class UserSearchResponse
{
/// <summary>
/// The number of users that match the query
/// </summary>
/// <value> Users number </value>
/// <example>1</example>
[Required]
public int TotalUsers { get; set; }

/// <summary>
/// The users that match the query
/// </summary>
/// <value> Users List </value>
/// <example>
/// [
/// {
/// "id": 12232,
/// "name": "John Doe",
/// "email": "[email protected]",
/// "userGroup": "Barista",
/// "state": "Active"
/// }
/// ],
/// </example>
[Required]
public IEnumerable<SimpleUserResponse> Users;

Check warning on line 49 in coffeecard/CoffeeCard.Models/DataTransferObjects/v2/User/UserSearchResponse.cs

View workflow job for this annotation

GitHub Actions / dev-deploy / Build codebase / Build codebase / Build and test analog-core

Non-nullable field 'Users' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.
}
1 change: 1 addition & 0 deletions coffeecard/CoffeeCard.Models/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace CoffeeCard.Models.Entities
{
[Index(nameof(Email))]
[Index(nameof(Name))]
public class User
{
public int Id { get; set; }
Expand Down
23 changes: 23 additions & 0 deletions coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,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
{
Expand Down Expand Up @@ -201,5 +205,24 @@ private async Task<UserResponse> UserWithRanking(User user)
PrivacyActivated = user.PrivacyActivated,
};
}

/// <summary>
/// Searches a user in the database
/// </summary>
/// <param name="filter">A filter to search by Id, Name or Email. When an empty string is given, all users will be returned</param>
/// <param name="pageNum">The page number</param>
/// <param name="pageLength">The length of a page</param>
/// <returns> A collection of User objects that match the search criteria </returns>
/// <response code="200">Users, possible with filter applied</response>
/// <response code="401"> Invalid credentials </response>
[HttpGet]
[AuthorizeRoles(UserGroup.Board)]
[ProducesResponseType(typeof(ApiError), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(SimpleUserResponse), StatusCodes.Status200OK)]
[Route("search")]
public async Task<ActionResult<IEnumerable<SimpleUserResponse>>> SearchUsers([FromQuery][Range(0, int.MaxValue)] int pageNum, [FromQuery] string filter = "", [FromQuery][Range(1, 100)] int pageLength = 30)
{
return Ok(await _accountService.SearchUsers(filter, pageNum, pageLength));
}
}
}
2 changes: 1 addition & 1 deletion coffeecard/CoffeeCard.WebApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit 46a9e66

Please sign in to comment.