diff --git a/API/API.csproj b/API/API.csproj index fb104f05e..784cff6b4 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -5,6 +5,7 @@ + diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index d483617a7..91273a368 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using API.Data; @@ -15,12 +16,14 @@ namespace API.Controllers { private readonly DataContext _context; private readonly ITokenService _tokenService; + private readonly IUserRepository _userRepository; private readonly ILogger _logger; - public AccountController(DataContext context, ITokenService tokenService, ILogger logger) + public AccountController(DataContext context, ITokenService tokenService, IUserRepository userRepository, ILogger logger) { _context = context; _tokenService = tokenService; + _userRepository = userRepository; _logger = logger; } @@ -39,7 +42,8 @@ namespace API.Controllers UserName = registerDto.Username.ToLower(), PasswordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(registerDto.Password)), PasswordSalt = hmac.Key, - IsAdmin = registerDto.IsAdmin + IsAdmin = registerDto.IsAdmin, + LastActive = DateTime.Now }; _context.Users.Add(user); @@ -68,11 +72,17 @@ namespace API.Controllers { if (computedHash[i] != user.PasswordHash[i]) return Unauthorized("Invalid password"); } + + // Update LastActive on account + user.LastActive = DateTime.Now; + _userRepository.Update(user); + await _userRepository.SaveAllAsync(); return new UserDto() { Username = user.UserName, - Token = _tokenService.CreateToken(user) + Token = _tokenService.CreateToken(user), + IsAdmin = user.IsAdmin }; } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs new file mode 100644 index 000000000..dd27d3f15 --- /dev/null +++ b/API/Controllers/UsersController.cs @@ -0,0 +1,28 @@ +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers +{ + public class UsersController : BaseApiController + { + private readonly DataContext _context; + private readonly IUserRepository _userRepository; + + public UsersController(DataContext context, IUserRepository userRepository) + { + _context = context; + _userRepository = userRepository; + } + + [HttpGet] + public async Task>> GetUsers() + { + return Ok(await _userRepository.GetMembersAsync()); + } + } +} \ No newline at end of file diff --git a/API/Converters/JsonBoolNumberConverter.cs b/API/Converters/JsonBoolNumberConverter.cs deleted file mode 100644 index c370fb9fd..000000000 --- a/API/Converters/JsonBoolNumberConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace API.Converters -{ - /// - /// Converts a number to a boolean. - /// This is needed for HDHomerun. - /// - public class JsonBoolNumberConverter : JsonConverter - { - /// - public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Number) - { - return Convert.ToBoolean(reader.GetInt32()); - } - - return reader.GetBoolean(); - } - - /// - public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) - { - writer.WriteBooleanValue(value); - } - } -} \ No newline at end of file diff --git a/API/Converters/JsonBoolStringConverter.cs b/API/Converters/JsonBoolStringConverter.cs deleted file mode 100644 index d17a6537a..000000000 --- a/API/Converters/JsonBoolStringConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace API.Converters -{ - public class JsonBoolStringConverter : JsonConverter - { - /// - public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - return reader.GetString().ToLower() == "true"; - } - - return reader.GetBoolean(); - } - - /// - public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) - { - writer.WriteBooleanValue(value); - } - } -} \ No newline at end of file diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs new file mode 100644 index 000000000..a1f8b377b --- /dev/null +++ b/API/DTOs/MemberDto.cs @@ -0,0 +1,16 @@ +using System; + +namespace API.DTOs +{ + /// + /// Represents a member of a Kavita server. + /// + public class MemberDto + { + public int Id { get; set; } + public string Username { get; set; } + public DateTime Created { get; set; } + public DateTime LastActive { get; set; } + public bool IsAdmin { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index 34e1a7a60..61fd26f96 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -1,6 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using API.Converters; namespace API.DTOs { @@ -11,7 +9,6 @@ namespace API.DTOs [Required] [StringLength(8, MinimumLength = 4)] public string Password { get; set; } - [JsonConverter(typeof(JsonBoolNumberConverter))] public bool IsAdmin { get; set; } } } \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs new file mode 100644 index 000000000..2308525a4 --- /dev/null +++ b/API/Data/UserRepository.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; +using API.Interfaces; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class UserRepository : IUserRepository + { + private readonly DataContext _context; + private readonly IMapper _mapper; + + public UserRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Update(AppUser user) + { + _context.Entry(user).State = EntityState.Modified; + } + + public async Task SaveAllAsync() + { + return await _context.SaveChangesAsync() > 0; + } + + public async Task> GetUsersAsync() + { + return await _context.Users.ToListAsync(); + } + + public async Task GetUserByIdAsync(int id) + { + return await _context.Users.FindAsync(id); + } + + public async Task GetUserByUsernameAsync(string username) + { + return await _context.Users.SingleOrDefaultAsync(x => x.UserName == username); + } + + public async Task> GetMembersAsync() + { + return await _context.Users.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + public async Task GetMemberAsync(string username) + { + return await _context.Users.Where(x => x.UserName == username) + .ProjectTo(_mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } + } +} \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 778eb8c6e..07b096c95 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -1,6 +1,8 @@ using API.Data; +using API.Helpers; using API.Interfaces; using API.Services; +using AutoMapper; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +13,8 @@ namespace API.Extensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config) { + services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); + services.AddScoped(); services.AddScoped(); services.AddDbContext(options => { diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/API/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 000000000..3dacfc854 --- /dev/null +++ b/API/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,12 @@ +using System.Security.Claims; + +namespace API.Extensions +{ + public static class ClaimsPrincipalExtensions + { + public static string GetUsername(this ClaimsPrincipal user) + { + return user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } + } +} \ No newline at end of file diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs new file mode 100644 index 000000000..49da92f21 --- /dev/null +++ b/API/Helpers/AutoMapperProfiles.cs @@ -0,0 +1,14 @@ +using API.DTOs; +using API.Entities; +using AutoMapper; + +namespace API.Helpers +{ + public class AutoMapperProfiles : Profile + { + public AutoMapperProfiles() + { + CreateMap(); + } + } +} \ No newline at end of file diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs new file mode 100644 index 000000000..69b872821 --- /dev/null +++ b/API/Interfaces/IUserRepository.cs @@ -0,0 +1,19 @@ +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; + +namespace API.Interfaces +{ + public interface IUserRepository + { + void Update(AppUser user); + Task SaveAllAsync(); + Task> GetUsersAsync(); + Task GetUserByIdAsync(int id); + Task GetUserByUsernameAsync(string username); + Task> GetMembersAsync(); + Task GetMemberAsync(string username); + } +} \ No newline at end of file diff --git a/API/Middleware/ExceptionMiddleware.cs b/API/Middleware/ExceptionMiddleware.cs index 5219bf138..413fbdb16 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/API/Middleware/ExceptionMiddleware.cs @@ -14,6 +14,7 @@ namespace API.Middleware private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly IHostEnvironment _env; + public ExceptionMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment env) { diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json index 92f840e5c..677d81685 100644 --- a/API/Properties/launchSettings.json +++ b/API/Properties/launchSettings.json @@ -11,7 +11,7 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -20,7 +20,7 @@ "API": { "commandName": "Project", "dotnetRunMessages": "true", - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": {