From f95e985c7bf6605566a92e5a38ff5cb33711a19a Mon Sep 17 00:00:00 2001 From: Max <51083570+DRdrProfessor@users.noreply.github.com> Date: Sun, 8 Oct 2023 00:46:42 +0200 Subject: [PATCH] Implemented users db & API side. --- SharpRSS.API.Contracts/ApiError.cs | 15 -- SharpRSS.API.Contracts/ApiResult.cs | 21 --- SharpRSS.API.Contracts/DTO/ApiListResult.cs | 16 -- SharpRSS.API.Contracts/DTO/Group.cs | 12 ++ SharpRSS.API.Contracts/DTO/Session.cs | 11 ++ SharpRSS.API.Contracts/DTO/User.cs | 14 ++ SharpRSS.API.Contracts/DTO/UserDto.cs | 22 --- SharpRSS.API.Contracts/Models/ListResult.cs | 16 ++ SharpRSS.API.Contracts/Models/Result.cs | 23 +++ SharpRSS.API.Contracts/Models/ResultOr.cs | 17 ++ .../User => Payloads}/AuthenticateUser.cs | 4 +- SharpRSS.API.Contracts/Payloads/ModifyUser.cs | 12 ++ .../Auth/SessionAuthorizeAttribute.cs | 9 +- SharpRSS.API/Controllers/AuthController.cs | 77 ++++++--- SharpRSS.API/Data/AuthService.cs | 146 +++++++++++++----- SharpRSS.API/Data/DbAccess.cs | 33 ++-- .../{SharpRssService.cs => RssService.cs} | 2 +- SharpRSS.API/Models/Auth/User.cs | 27 ---- SharpRSS.API/Models/DbGroup.cs | 9 ++ SharpRSS.API/Models/DbSession.cs | 34 ++++ SharpRSS.API/Models/DbUser.cs | 50 ++++++ SharpRSS.API/Models/Result.cs | 15 -- SharpRSS.API/Program.cs | 3 +- 23 files changed, 390 insertions(+), 198 deletions(-) delete mode 100644 SharpRSS.API.Contracts/ApiError.cs delete mode 100644 SharpRSS.API.Contracts/ApiResult.cs delete mode 100644 SharpRSS.API.Contracts/DTO/ApiListResult.cs create mode 100644 SharpRSS.API.Contracts/DTO/Group.cs create mode 100644 SharpRSS.API.Contracts/DTO/Session.cs create mode 100644 SharpRSS.API.Contracts/DTO/User.cs delete mode 100644 SharpRSS.API.Contracts/DTO/UserDto.cs create mode 100644 SharpRSS.API.Contracts/Models/ListResult.cs create mode 100644 SharpRSS.API.Contracts/Models/Result.cs create mode 100644 SharpRSS.API.Contracts/Models/ResultOr.cs rename SharpRSS.API.Contracts/{Models/User => Payloads}/AuthenticateUser.cs (61%) create mode 100644 SharpRSS.API.Contracts/Payloads/ModifyUser.cs rename SharpRSS.API/Data/{SharpRssService.cs => RssService.cs} (60%) delete mode 100644 SharpRSS.API/Models/Auth/User.cs create mode 100644 SharpRSS.API/Models/DbGroup.cs create mode 100644 SharpRSS.API/Models/DbSession.cs create mode 100644 SharpRSS.API/Models/DbUser.cs delete mode 100644 SharpRSS.API/Models/Result.cs diff --git a/SharpRSS.API.Contracts/ApiError.cs b/SharpRSS.API.Contracts/ApiError.cs deleted file mode 100644 index aa673fe..0000000 --- a/SharpRSS.API.Contracts/ApiError.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace SharpRSS.API.Contracts -{ - public class ApiError - { - public ApiError() - { - - } - public bool Logged { get; set; } - public string Message { get; set; } = string.Empty; - public DateTime Date { get; set; } - } -} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/ApiResult.cs b/SharpRSS.API.Contracts/ApiResult.cs deleted file mode 100644 index adf5af4..0000000 --- a/SharpRSS.API.Contracts/ApiResult.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace SharpRSS.API.Contracts -{ - public class ApiResult - { - public ApiResult(string message, ApiResults result) - { - Message = message; - Result = result; - } - public string Message { get; } - public ApiResults Result { get; } - } - - public enum ApiResults - { - Ok, - Warning, - Error, - Invalid - } -} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/ApiListResult.cs b/SharpRSS.API.Contracts/DTO/ApiListResult.cs deleted file mode 100644 index 52bfd0d..0000000 --- a/SharpRSS.API.Contracts/DTO/ApiListResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SharpRSS.API.Contracts.DTO -{ - public class ApiListResult - { - public ApiListResult(int hits, int total, TResultValue? data) - { - Hits = hits; - Total = total; - Data = data; - } - - public int Hits { get; } - public int Total { get; } - public TResultValue? Data { get; } - } -} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/Group.cs b/SharpRSS.API.Contracts/DTO/Group.cs new file mode 100644 index 0000000..773c487 --- /dev/null +++ b/SharpRSS.API.Contracts/DTO/Group.cs @@ -0,0 +1,12 @@ +using System; + +namespace SharpRSS.API.Contracts.DTO +{ + public class Group + { + public string Gid { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public bool Administrator { get; set; } + public DateTime DateCreated { get; set; } = DateTime.Now; + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/Session.cs b/SharpRSS.API.Contracts/DTO/Session.cs new file mode 100644 index 0000000..484dd0f --- /dev/null +++ b/SharpRSS.API.Contracts/DTO/Session.cs @@ -0,0 +1,11 @@ +using System; + +namespace SharpRSS.API.Contracts.DTO +{ + public class Session + { + public string Sid { get; set; } = string.Empty; + public DateTime Created { get; set; } + public DateTime Expires { get; set; } + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/User.cs b/SharpRSS.API.Contracts/DTO/User.cs new file mode 100644 index 0000000..55a19b4 --- /dev/null +++ b/SharpRSS.API.Contracts/DTO/User.cs @@ -0,0 +1,14 @@ +using System; + +namespace SharpRSS.API.Contracts.DTO +{ + public class User + { + public string Uid { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Gid { get; set; } = string.Empty; + public bool Active { get; set; } + public DateTime DateCreated { get; set; } = DateTime.Now; + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/UserDto.cs b/SharpRSS.API.Contracts/DTO/UserDto.cs deleted file mode 100644 index bfb3483..0000000 --- a/SharpRSS.API.Contracts/DTO/UserDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace SharpRSS.API.Contracts.DTO -{ - public class UserDto - { - public UserDto(string id, string userName, string mail, string role, DateTime dateCreated) - { - Id = id; - UserName = userName; - Mail = mail; - Role = role; - DateCreated = dateCreated; - } - - public string Id { get; } - public string UserName { get; } - public string Mail { get; } - public string Role { get; } - public DateTime DateCreated { get; } - } -} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/Models/ListResult.cs b/SharpRSS.API.Contracts/Models/ListResult.cs new file mode 100644 index 0000000..f426a60 --- /dev/null +++ b/SharpRSS.API.Contracts/Models/ListResult.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpRSS.API.Contracts.Models +{ + public class ListResult : ResultOr> + { + public ListResult(IEnumerable? value, long total, string message = "", ResultStatus status = ResultStatus.Ok) : base(value, message, status) + { + TotalFound = total; + } + public long TotalFound { get; } + public int Hits => Value?.Count() ?? 0; + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/Models/Result.cs b/SharpRSS.API.Contracts/Models/Result.cs new file mode 100644 index 0000000..02c863d --- /dev/null +++ b/SharpRSS.API.Contracts/Models/Result.cs @@ -0,0 +1,23 @@ +namespace SharpRSS.API.Contracts.Models +{ + public class Result + { + public Result(string message = "", ResultStatus status = ResultStatus.Ok) + { + Message = message; + Status = status; + } + + public bool Success => Status == ResultStatus.Ok; + public string Message { get; } + public ResultStatus Status { get; set; } + } + + public enum ResultStatus + { + Ok, + Failed, + InternalFail, + Unknown + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/Models/ResultOr.cs b/SharpRSS.API.Contracts/Models/ResultOr.cs new file mode 100644 index 0000000..7e0629e --- /dev/null +++ b/SharpRSS.API.Contracts/Models/ResultOr.cs @@ -0,0 +1,17 @@ +using System; + +namespace SharpRSS.API.Contracts.Models +{ + public class ResultOr : Result + { + public ResultOr(TValue? value, string message, ResultStatus status) : base(message, status) + { + Value = value; + } + + public TValue? Value { get; } + + public static ResultOr Ok(TValue value) => new ResultOr(value, "", ResultStatus.Ok); + public static ResultOr Failed(string message = "Failed", ResultStatus status = ResultStatus.Failed) => new ResultOr(default, message, status); + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/Models/User/AuthenticateUser.cs b/SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs similarity index 61% rename from SharpRSS.API.Contracts/Models/User/AuthenticateUser.cs rename to SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs index cb23739..8be9b35 100644 --- a/SharpRSS.API.Contracts/Models/User/AuthenticateUser.cs +++ b/SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs @@ -1,6 +1,6 @@ -namespace SharpRSS.API.Contracts.Models.User +namespace SharpRSS.API.Contracts.Payloads { - public class AuthenticateUser + public abstract class AuthenticateUser { public string UserName { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; diff --git a/SharpRSS.API.Contracts/Payloads/ModifyUser.cs b/SharpRSS.API.Contracts/Payloads/ModifyUser.cs new file mode 100644 index 0000000..296a0ef --- /dev/null +++ b/SharpRSS.API.Contracts/Payloads/ModifyUser.cs @@ -0,0 +1,12 @@ +namespace SharpRSS.API.Contracts.Payloads +{ + public class ModifyUser + { + public string UserName { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string GroupId { get; set; } = string.Empty; + public bool Active { get; set; } = true; + } +} \ No newline at end of file diff --git a/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs b/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs index ce29375..f949f2b 100644 --- a/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs +++ b/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs @@ -12,14 +12,14 @@ namespace SharpRSS.API.Auth [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class SessionAuthorizeAttribute : Attribute, IAuthorizationFilter { - public SessionAuthorizeAttribute(string permission = "") + public SessionAuthorizeAttribute(string group = "") { _log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute)); - _perm = permission; + _group = group; } private readonly ILog _log; - private readonly string _perm; + private readonly string _group; public void OnAuthorization(AuthorizationFilterContext context) { @@ -29,9 +29,10 @@ namespace SharpRSS.API.Auth return; } + return; if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out StringValues val)) { - //TODO: if no permission check for valid session, if permission check if session has access! + //TODO: if no permission check for valid session, permission check if session has access! return; } diff --git a/SharpRSS.API/Controllers/AuthController.cs b/SharpRSS.API/Controllers/AuthController.cs index 256390c..5299904 100644 --- a/SharpRSS.API/Controllers/AuthController.cs +++ b/SharpRSS.API/Controllers/AuthController.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SharpRSS.API.Auth; -using SharpRSS.API.Contracts; using SharpRSS.API.Contracts.DTO; -using SharpRSS.API.Contracts.Models.User; +using SharpRSS.API.Contracts.Models; +using SharpRSS.API.Contracts.Payloads; using SharpRSS.API.Data; -using SharpRSS.API.Models; -using SharpRSS.API.Models.Auth; +using ToolQit; +using ToolQit.Logging; namespace SharpRSS.API.Controllers { @@ -22,34 +19,64 @@ namespace SharpRSS.API.Controllers public AuthController(AuthService authService) { _authService = authService; + _log = LogManager.CreateLogger(typeof(AuthController)); } + private readonly ILog _log; private readonly AuthService _authService; - [HttpPost("[action]")] - [AllowAnonymous] - public async Task> Authenticate(AuthenticateUser authenticateUser) - { // Return test result - return Ok(new { Expires = DateTime.Now.Add(TimeSpan.FromDays(7)), SessionToken = Guid.NewGuid().ToString(), Released = DateTime.Now }); + [HttpPost("createuser")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> CreateUser(ModifyUser payload) + { + var createdUserResult = await _authService.CreateUser(payload); + return createdUserResult.Success ? Created("", createdUserResult) : createdUserResult.Status == ResultStatus.Failed ? BadRequest(createdUserResult) : StatusCode(StatusCodes.Status500InternalServerError, createdUserResult); } - [HttpPost("user")] - [SessionAuthorize("auth:user:create")] - public async Task> CreateUser(AuthenticateUser authenticateUser) + [HttpPost("updateuser")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> UpdateUser(ModifyUser payload) { - Result result = await _authService.CreateUser(authenticateUser); - if (result.Success) - return Ok(Models.Auth.User.ToDto(result.Value ?? new User())); - return BadRequest(new ApiResult(result.Message, ApiResults.Error)); + var updatedUserResult = await _authService.UpdateUser(payload); + return updatedUserResult.Success ? Ok(updatedUserResult) : updatedUserResult.Status == ResultStatus.Failed ? BadRequest(updatedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, updatedUserResult); + } + + [HttpDelete("deleteuser")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteUser(string userId) + { + var removedUserResult = await _authService.RemoveUserAsync(userId); + return removedUserResult.Success ? Ok(removedUserResult) : removedUserResult.Status == ResultStatus.Failed ? BadRequest(removedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, removedUserResult); } [HttpGet("user")] - [SessionAuthorize("auth:user:get")] - public async Task>>> GetUsers(int take, int skip) + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> GetUser(string userId) { - var usersAuth = await _authService.GetUsers(take, skip); - List users = usersAuth.Value?.Select(Models.Auth.User.ToDto).ToList() ?? new List(); - return Ok(new ApiListResult>(users.Count, await _authService.UserCount(), users)); + var userResult = await _authService.GetUserAsync(userId); + return userResult.Success ? Ok(userResult) : BadRequest(userResult); + } + + [HttpGet("users")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetUsers(string search = "", int results = 20, int skip = 0) + { + var usersResult = await _authService.GetUsersAsync(results, skip, search); + return usersResult.Success ? Ok(usersResult) : usersResult.Status == ResultStatus.Failed ? BadRequest(usersResult) : StatusCode(StatusCodes.Status500InternalServerError, usersResult); } } } \ No newline at end of file diff --git a/SharpRSS.API/Data/AuthService.cs b/SharpRSS.API/Data/AuthService.cs index 3a88e03..3256df9 100644 --- a/SharpRSS.API/Data/AuthService.cs +++ b/SharpRSS.API/Data/AuthService.cs @@ -1,13 +1,14 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using SharpRSS.API.Contracts.Models.User; -using SharpRSS.API.Cryptography; +using SharpRSS.API.Contracts.DTO; +using SharpRSS.API.Contracts.Models; +using SharpRSS.API.Contracts.Payloads; using SharpRSS.API.Models; -using SharpRSS.API.Models.Auth; using ToolQit; +using ToolQit.Extensions; using ToolQit.Logging; namespace SharpRSS.API.Data @@ -24,54 +25,121 @@ namespace SharpRSS.API.Data private readonly IConfiguration _configuration; private readonly ILog _log; - public async Task> CreateUser(AuthenticateUser authenticateUserRequest) + public async Task> CreateUser(ModifyUser user) { - bool result; - if (authenticateUserRequest.UserName.Any(char.IsWhiteSpace)) - return new Result(null, message: "Username should not contain space/whitespaces!"); - - await using DbAccess access = new DbAccess(_configuration); - var user = access.Users.FirstOrDefault(u => u.UserName == authenticateUserRequest.UserName); - if (user != null) - return new Result(user, message:"User name already exists!"); - - byte[] hashedPwdBytes = Hasher.HashPassword(authenticateUserRequest.Password, out byte[] salt); - user = new User() - { - UserName = authenticateUserRequest.UserName, - Mail = "", - Password = hashedPwdBytes, - Salt = salt - }; - access.Users.Add(user); + if (user.UserName.IsNullEmptyWhiteSpace() || user.Password.IsNullEmptyWhiteSpace()) + return ResultOr.Failed("Username or password cannot be empty!"); + DbUser newUser; try { - int entries = await access.SaveChangesAsync(); - result = entries > 0; + DbAccess access = new DbAccess(_configuration); + bool userExists = access.Users.Any(u => u.Uid == user.UserName); + if (userExists) + return ResultOr.Failed("Username already exists!"); + newUser = new DbUser(user); + access.Users.Add(newUser); + bool saved = await access.SaveChangesAsync() > 0; + if (!saved) + { + _log.Warning("Failed to save user: {UserName} to database", user.UserName); + return ResultOr.Failed("Could not save user to database!"); + } } catch (Exception e) { - _log.Error(e, "Error creating user: {UserName}", user.UserName); - return new Result(user, message: "Could not create user!"); + _log.Error(e, "Failed to create user: {UserName}", user.UserName); + return ResultOr.Failed($"Failed to create user: {user.UserName}", ResultStatus.InternalFail); } - return new Result(user, result, "Ok"); + return ResultOr.Ok(newUser.ToDto()); } - public async Task>> GetUsers(int take = 50, int skip = 0) + public async Task> UpdateUser(ModifyUser user) { - if (take is 0 or > 50) - take = 50; - await using DbAccess access = new DbAccess(_configuration); - IEnumerable users = access.Users.Skip(skip).Take(take).ToList(); - if (!users.Any()) - return new Result>(users, false, "No users found!"); - return new Result>(users, true, "Ok"); + if (user.UserName.IsNullEmptyWhiteSpace()) + return ResultOr.Failed("Username cannot be empty!"); + try + { + DbAccess access = new DbAccess(_configuration); + DbUser? dbUser = await access.Users.Where(u => u.Uid == user.UserName).FirstOrDefaultAsync(); + if (dbUser == null) + return ResultOr.Failed($"User{user.UserName} does not exists, first create an user then modify!"); + dbUser.Update(user); + access.Update(dbUser); + bool saved = await access.SaveChangesAsync() > 0; + if (!saved) + { + _log.Warning("Failed to save user: '{UserName}'", dbUser.Uid); + return ResultOr.Failed($"Could not save user: '{dbUser.Uid}'"); + } + return ResultOr.Ok(dbUser.ToDto()); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return ResultOr.Failed(e.Message, ResultStatus.InternalFail); + } } - public async Task UserCount() + public async Task RemoveUserAsync(string userId) { - await using DbAccess access = new DbAccess(_configuration); - return access.Users.Count(); + if (userId.IsNullEmptyWhiteSpace()) + return new Result("User id is empty!", ResultStatus.Failed); + try + { + DbAccess access = new DbAccess(_configuration); + DbUser? dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync(); + if (dbUser == null) + return new Result($"Could not find user: '{userId}'", ResultStatus.Failed); + access.Users.Remove(dbUser); + bool removed = await access.SaveChangesAsync() > 0; + return new Result(removed ? "" : $"Failed to remove user: '{dbUser.Uid}'!", removed ? ResultStatus.Ok : ResultStatus.Failed); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return new Result(e.Message, ResultStatus.InternalFail); + } + } + + public async Task> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "") + { + if (results is 0 or > 20) results = 20; + try + { + DbAccess access = new DbAccess(_configuration); + if (!searchQuery.IsNullEmptyWhiteSpace()) + { + IQueryable queryResult = access.Users.Where(u => + EF.Functions.Like(u.Uid, $"%{searchQuery}%") || + EF.Functions.Like(u.Email, $"%{searchQuery}%") || + EF.Functions.Like(u.DisplayName, $"%{searchQuery}%")); + var searchResults = await queryResult.Skip(skip).Take(results).ToListAsync(); + return new ListResult(searchResults, queryResult.Count()); + } + var users = await access.Users.Skip(skip).Take(results).ToListAsync(); + return new ListResult(users, access.Users.Count()); + } + catch (Exception e) + { + _log.Error(e, "Error while loading users from database!"); + return new ListResult(null, 0, "Error while loading users from database!", ResultStatus.Failed); + } + } + + public async Task> GetUserAsync(string userId) + { + if (userId.IsNullEmptyWhiteSpace()) return new ResultOr(null, "User ID is empty!", ResultStatus.Failed); + try + { + DbAccess access = new DbAccess(_configuration); + User? user = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync(); + return new ResultOr(user, user == null ? $"Could not find user with id: {userId}!" : "", user == null ? ResultStatus.Failed : ResultStatus.Ok); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return new ResultOr(null, e.Message, ResultStatus.Failed); + } } } } \ No newline at end of file diff --git a/SharpRSS.API/Data/DbAccess.cs b/SharpRSS.API/Data/DbAccess.cs index 281a7c9..f3a9258 100644 --- a/SharpRSS.API/Data/DbAccess.cs +++ b/SharpRSS.API/Data/DbAccess.cs @@ -1,32 +1,37 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using SharpRSS.API.Models.Auth; +using SharpRSS.API.Models; using ToolQit; using ToolQit.Logging; namespace SharpRSS.API.Data { - public sealed class DbAccess : DbContext + internal sealed class DbAccess : DbContext { public DbAccess(IConfiguration configuration) { _log = LogManager.CreateLogger(typeof(DbAccess)); + _log.Debug("Initializing new context..."); _configuration = configuration; + _dbServer = _configuration["DataBase:Server"] ?? "Unknown"; + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.LazyLoadingEnabled = false; Database.EnsureCreated(); } - public DbSet Users { get; set; } + public DbSet Users { get; set; } + public DbSet Groups { get; set; } + public DbSet Sessions { get; set; } private readonly IConfiguration _configuration; private readonly ILog _log; - - + private readonly string _dbServer; + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connection = _configuration["DataBase:Connection"] ?? throw new ArgumentNullException(nameof(_configuration), "No connection string in appsettings!"); - string server = _configuration["DataBase:Server"] ?? "Unknown"; - switch (server) + switch (_dbServer) { case "MariaDB": var dbSrvVersion = ServerVersion.AutoDetect(connection); @@ -34,14 +39,24 @@ namespace SharpRSS.API.Data optionsBuilder.UseMySql(connection, dbSrvVersion); break; default: // TODO: Add more database support. - _log.Warning("No valid db server: {Server}/nSupported db servers: 'MariaDB'", server); + _log.Warning("No valid db server: {Server}\nSupported db servers: 'MariaDB'", _dbServer); throw new Exception("Database server not specified!"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().ToTable("srss_user"); + switch (_dbServer) + { + case "MariaDB": + modelBuilder.UseCollation("utf8mb4_unicode_520_ci"); + break; + default: + break; + } + modelBuilder.Entity().ToTable("srss_users").HasKey(dbu => dbu.Uid); + modelBuilder.Entity().ToTable("srss_groups").HasKey(dbg => dbg.Gid); + modelBuilder.Entity().ToTable("srss_sessions").HasKey(dbs => dbs.Sid); base.OnModelCreating(modelBuilder); } } diff --git a/SharpRSS.API/Data/SharpRssService.cs b/SharpRSS.API/Data/RssService.cs similarity index 60% rename from SharpRSS.API/Data/SharpRssService.cs rename to SharpRSS.API/Data/RssService.cs index 51fd029..15d11ce 100644 --- a/SharpRSS.API/Data/SharpRssService.cs +++ b/SharpRSS.API/Data/RssService.cs @@ -1,6 +1,6 @@ namespace SharpRSS.API.Data { - public class SharpRssService + public class RssService { } diff --git a/SharpRSS.API/Models/Auth/User.cs b/SharpRSS.API/Models/Auth/User.cs deleted file mode 100644 index 7e54e5f..0000000 --- a/SharpRSS.API/Models/Auth/User.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using SharpRSS.API.Contracts.DTO; - -namespace SharpRSS.API.Models.Auth -{ - public class User - { - public static UserDto ToDto(User user) => new UserDto(user.Id, user.UserName, user.Mail, user.Role, user.DateCreated); - - [Key] - public string Id { get; set; } = Guid.NewGuid().ToString(); - [Required] - public string UserName { get; set; } - [Required] - [EmailAddress] - public string Mail { get; set; } - [Required] - public byte[] Password { get; set; } - [Required] - public byte[] Salt { get; set; } - [Required] - public string Role { get; set; } = "User"; - [Required] - public DateTime DateCreated { get; set; } = DateTime.Now; - } -} \ No newline at end of file diff --git a/SharpRSS.API/Models/DbGroup.cs b/SharpRSS.API/Models/DbGroup.cs new file mode 100644 index 0000000..665f98d --- /dev/null +++ b/SharpRSS.API/Models/DbGroup.cs @@ -0,0 +1,9 @@ +using SharpRSS.API.Contracts.DTO; + +namespace SharpRSS.API.Models +{ + internal class DbGroup : Group + { + + } +} \ No newline at end of file diff --git a/SharpRSS.API/Models/DbSession.cs b/SharpRSS.API/Models/DbSession.cs new file mode 100644 index 0000000..000b66a --- /dev/null +++ b/SharpRSS.API/Models/DbSession.cs @@ -0,0 +1,34 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Serilog; +using SharpRSS.API.Contracts.DTO; + +namespace SharpRSS.API.Models +{ + internal class DbSession : Session + { + public DbSession() { } + private DbSession(string uid, double expiresMinutes) + { + if (string.IsNullOrEmpty(uid) || string.IsNullOrWhiteSpace(uid)) + { + Log.Error("User id is null or empty cannot create session!"); + throw new Exception("User id cannot be null!"); + } + Uid = uid; + Created = DateTime.Now; + Expires = Created.AddMinutes(expiresMinutes); + using SHA1 sha1 = SHA1.Create(); + string uidHash = Convert.ToHexString(sha1.ComputeHash(Encoding.UTF8.GetBytes(Uid))); + Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}"; + } + public string Uid { get; set; } + + public static DbSession CreateSession(string uid, double expiresMinutes = 10080) + { + DbSession newSession = new DbSession(uid, expiresMinutes); + return newSession; + } + } +} \ No newline at end of file diff --git a/SharpRSS.API/Models/DbUser.cs b/SharpRSS.API/Models/DbUser.cs new file mode 100644 index 0000000..9a06331 --- /dev/null +++ b/SharpRSS.API/Models/DbUser.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using SharpRSS.API.Contracts.DTO; +using SharpRSS.API.Contracts.Payloads; +using SharpRSS.API.Cryptography; +using ToolQit.Extensions; + +namespace SharpRSS.API.Models +{ + // Database model + internal class DbUser : User + { + public DbUser() { } + public DbUser(ModifyUser user) + { + Uid = user.UserName; + DisplayName = user.DisplayName; + Email = user.Email; + Gid = user.GroupId; + Active = user.Active; + PswHash = Hasher.HashPassword(user.Password, out byte[] salt); + Salt = salt; + } + public byte[] PswHash { get; set; } + public byte[] Salt { get; set; } + + public User ToDto() + { + string json = JsonConvert.SerializeObject(this); + return JsonConvert.DeserializeObject(json) ?? new User(); + } + + public bool Update(ModifyUser user) + { + if (user == null) return false; + if (!user.DisplayName.IsNullEmptyWhiteSpace()) + DisplayName = user.DisplayName; + if (!user.Email.IsNullEmptyWhiteSpace()) + Email = user.Email; + if (!user.GroupId.IsNullEmptyWhiteSpace()) + Gid = user.GroupId; + Active = user.Active; + if (!user.Password.IsNullEmptyWhiteSpace()) + { + PswHash = Hasher.HashPassword(user.Password, out byte[] salt); + Salt = salt; + } + return true; + } + } +} \ No newline at end of file diff --git a/SharpRSS.API/Models/Result.cs b/SharpRSS.API/Models/Result.cs deleted file mode 100644 index e3cb5a4..0000000 --- a/SharpRSS.API/Models/Result.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace SharpRSS.API.Models -{ - public class Result - { - public Result(TValue? value, bool success = false, string message = "") - { - Value = value; - Success = success; - Message = message; - } - public TValue? Value { get; } - public bool Success { get; } - public string Message { get; } - } -} \ No newline at end of file diff --git a/SharpRSS.API/Program.cs b/SharpRSS.API/Program.cs index d89c7e6..8aa526b 100644 --- a/SharpRSS.API/Program.cs +++ b/SharpRSS.API/Program.cs @@ -16,7 +16,6 @@ SetupSerilog(); var builder = WebApplication.CreateBuilder(args); builder.Logging.AddSerilog(); // Add services to the container. - builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -26,7 +25,7 @@ builder.Services.AddSwaggerGen(con => con.OperationFilter(); }); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build();