From 9338d8c106c0372f8c89a018ade098204cebeda0 Mon Sep 17 00:00:00 2001 From: Max <51083570+DRdrProfessor@users.noreply.github.com> Date: Mon, 9 Oct 2023 01:58:53 +0200 Subject: [PATCH] Implemented groups, sessions & authorization basics. --- .../{DTO => DTOs/Groups}/Group.cs | 4 +- .../DTOs/Groups/GroupItem.cs | 9 + .../DTOs/Groups/InsertGroup.cs | 9 + .../{DTO => DTOs/Sessions}/Session.cs | 2 +- .../Users}/AuthenticateUser.cs | 4 +- .../Users/InsertUser.cs} | 6 +- .../{DTO => DTOs/Users}/User.cs | 2 +- SharpRSS.API.Contracts/DTOs/Users/UserItem.cs | 11 + SharpRSS.API.Contracts/Models/ResultOr.cs | 2 +- .../SharpRSS.API.Contracts.csproj | 4 - .../Auth/SessionAuthorizeAttribute.cs | 43 ++- SharpRSS.API/Controllers/AuthController.cs | 116 ++++++-- SharpRSS.API/Data/AuthService.cs | 251 ++++++++++++++---- SharpRSS.API/Models/AuthorizationSet.cs | 10 + SharpRSS.API/Models/DbGroup.cs | 11 +- SharpRSS.API/Models/DbSession.cs | 10 +- SharpRSS.API/Models/DbUser.cs | 21 +- 17 files changed, 403 insertions(+), 112 deletions(-) rename SharpRSS.API.Contracts/{DTO => DTOs/Groups}/Group.cs (66%) create mode 100644 SharpRSS.API.Contracts/DTOs/Groups/GroupItem.cs create mode 100644 SharpRSS.API.Contracts/DTOs/Groups/InsertGroup.cs rename SharpRSS.API.Contracts/{DTO => DTOs/Sessions}/Session.cs (80%) rename SharpRSS.API.Contracts/{Payloads => DTOs/Users}/AuthenticateUser.cs (61%) rename SharpRSS.API.Contracts/{Payloads/ModifyUser.cs => DTOs/Users/InsertUser.cs} (69%) rename SharpRSS.API.Contracts/{DTO => DTOs/Users}/User.cs (89%) create mode 100644 SharpRSS.API.Contracts/DTOs/Users/UserItem.cs create mode 100644 SharpRSS.API/Models/AuthorizationSet.cs diff --git a/SharpRSS.API.Contracts/DTO/Group.cs b/SharpRSS.API.Contracts/DTOs/Groups/Group.cs similarity index 66% rename from SharpRSS.API.Contracts/DTO/Group.cs rename to SharpRSS.API.Contracts/DTOs/Groups/Group.cs index 773c487..149b64c 100644 --- a/SharpRSS.API.Contracts/DTO/Group.cs +++ b/SharpRSS.API.Contracts/DTOs/Groups/Group.cs @@ -1,10 +1,10 @@ using System; -namespace SharpRSS.API.Contracts.DTO +namespace SharpRSS.API.Contracts.DTOs.Groups { public class Group { - public string Gid { get; set; } = string.Empty; + public string Gid { get; set; } = Guid.NewGuid().ToString(); public string DisplayName { get; set; } = string.Empty; public bool Administrator { get; set; } public DateTime DateCreated { get; set; } = DateTime.Now; diff --git a/SharpRSS.API.Contracts/DTOs/Groups/GroupItem.cs b/SharpRSS.API.Contracts/DTOs/Groups/GroupItem.cs new file mode 100644 index 0000000..b47d1cd --- /dev/null +++ b/SharpRSS.API.Contracts/DTOs/Groups/GroupItem.cs @@ -0,0 +1,9 @@ +namespace SharpRSS.API.Contracts.DTOs.Groups +{ + public class GroupItem + { + public string Gid { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public bool Administrator { get; set; } + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTOs/Groups/InsertGroup.cs b/SharpRSS.API.Contracts/DTOs/Groups/InsertGroup.cs new file mode 100644 index 0000000..8404ac5 --- /dev/null +++ b/SharpRSS.API.Contracts/DTOs/Groups/InsertGroup.cs @@ -0,0 +1,9 @@ +namespace SharpRSS.API.Contracts.DTOs.Groups +{ + public class InsertGroup + { + public string Gid { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public bool Administrator { get; set; } + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/DTO/Session.cs b/SharpRSS.API.Contracts/DTOs/Sessions/Session.cs similarity index 80% rename from SharpRSS.API.Contracts/DTO/Session.cs rename to SharpRSS.API.Contracts/DTOs/Sessions/Session.cs index 484dd0f..c9e6114 100644 --- a/SharpRSS.API.Contracts/DTO/Session.cs +++ b/SharpRSS.API.Contracts/DTOs/Sessions/Session.cs @@ -1,6 +1,6 @@ using System; -namespace SharpRSS.API.Contracts.DTO +namespace SharpRSS.API.Contracts.DTOs.Sessions { public class Session { diff --git a/SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs b/SharpRSS.API.Contracts/DTOs/Users/AuthenticateUser.cs similarity index 61% rename from SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs rename to SharpRSS.API.Contracts/DTOs/Users/AuthenticateUser.cs index 8be9b35..8f9b8a3 100644 --- a/SharpRSS.API.Contracts/Payloads/AuthenticateUser.cs +++ b/SharpRSS.API.Contracts/DTOs/Users/AuthenticateUser.cs @@ -1,6 +1,6 @@ -namespace SharpRSS.API.Contracts.Payloads +namespace SharpRSS.API.Contracts.DTOs.Users { - public abstract class AuthenticateUser + public 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/DTOs/Users/InsertUser.cs similarity index 69% rename from SharpRSS.API.Contracts/Payloads/ModifyUser.cs rename to SharpRSS.API.Contracts/DTOs/Users/InsertUser.cs index 296a0ef..2bff249 100644 --- a/SharpRSS.API.Contracts/Payloads/ModifyUser.cs +++ b/SharpRSS.API.Contracts/DTOs/Users/InsertUser.cs @@ -1,8 +1,8 @@ -namespace SharpRSS.API.Contracts.Payloads +namespace SharpRSS.API.Contracts.DTOs.Users { - public class ModifyUser + public class InsertUser { - public string UserName { get; set; } = string.Empty; + public string Uid { 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; diff --git a/SharpRSS.API.Contracts/DTO/User.cs b/SharpRSS.API.Contracts/DTOs/Users/User.cs similarity index 89% rename from SharpRSS.API.Contracts/DTO/User.cs rename to SharpRSS.API.Contracts/DTOs/Users/User.cs index 55a19b4..7278249 100644 --- a/SharpRSS.API.Contracts/DTO/User.cs +++ b/SharpRSS.API.Contracts/DTOs/Users/User.cs @@ -1,6 +1,6 @@ using System; -namespace SharpRSS.API.Contracts.DTO +namespace SharpRSS.API.Contracts.DTOs.Users { public class User { diff --git a/SharpRSS.API.Contracts/DTOs/Users/UserItem.cs b/SharpRSS.API.Contracts/DTOs/Users/UserItem.cs new file mode 100644 index 0000000..55c678f --- /dev/null +++ b/SharpRSS.API.Contracts/DTOs/Users/UserItem.cs @@ -0,0 +1,11 @@ +namespace SharpRSS.API.Contracts.DTOs.Users +{ + // Used for returning users in a list. + public class UserItem + { + public string Uid { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public bool Active { get; set; } + } +} \ No newline at end of file diff --git a/SharpRSS.API.Contracts/Models/ResultOr.cs b/SharpRSS.API.Contracts/Models/ResultOr.cs index 7e0629e..dc970c9 100644 --- a/SharpRSS.API.Contracts/Models/ResultOr.cs +++ b/SharpRSS.API.Contracts/Models/ResultOr.cs @@ -11,7 +11,7 @@ namespace SharpRSS.API.Contracts.Models public TValue? Value { get; } - public static ResultOr Ok(TValue value) => new ResultOr(value, "", ResultStatus.Ok); + 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/SharpRSS.API.Contracts.csproj b/SharpRSS.API.Contracts/SharpRSS.API.Contracts.csproj index 5cf2042..bede7b6 100644 --- a/SharpRSS.API.Contracts/SharpRSS.API.Contracts.csproj +++ b/SharpRSS.API.Contracts/SharpRSS.API.Contracts.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs b/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs index f949f2b..942d98e 100644 --- a/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs +++ b/SharpRSS.API/Auth/SessionAuthorizeAttribute.cs @@ -4,7 +4,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Primitives; +using SharpRSS.API.Contracts.Models; +using SharpRSS.API.Data; using ToolQit; +using ToolQit.Extensions; using ToolQit.Logging; namespace SharpRSS.API.Auth @@ -12,31 +15,49 @@ namespace SharpRSS.API.Auth [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class SessionAuthorizeAttribute : Attribute, IAuthorizationFilter { - public SessionAuthorizeAttribute(string group = "") + public SessionAuthorizeAttribute(bool admin = true) { + _admin = admin; _log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute)); - _group = group; } - + private readonly ILog _log; - private readonly string _group; + private readonly bool _admin; - public void OnAuthorization(AuthorizationFilterContext context) + public async void OnAuthorization(AuthorizationFilterContext context) { if (context.ActionDescriptor.EndpointMetadata.Any(obj => obj.GetType() == typeof(AllowAnonymousAttribute))) + return; + var authService = context.HttpContext.RequestServices.GetService(typeof(AuthService)) as AuthService; + if (authService == null) { - //context.Result = new OkResult(); + context.Result = new UnauthorizedObjectResult(new Result("Failed to initialize service!", ResultStatus.InternalFail)); return; } - return; - if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out StringValues val)) + if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out var val)) { - //TODO: if no permission check for valid session, permission check if session has access! + string? headerVal = val.ToString(); + if (headerVal == null || headerVal.IsNullEmptyWhiteSpace()) + { + context.Result = new UnauthorizedObjectResult(new Result("Invalid session ID")); + return; + } + var authSet = await authService.ValidateSession(headerVal); + if (!authSet.Success || authSet.Value == null) + { + context.Result = new UnauthorizedResult(); + return; + } + if (authSet.Value.Session is { Expired: true }) + { + context.Result = new UnauthorizedObjectResult(new Result("Session expired", ResultStatus.Failed)); + return; + } + authSet.Value.AdminRequired = _admin; + context.HttpContext.Items["auth"] = authSet.Value; return; } - - //TODO: Check session ID! context.Result = new UnauthorizedResult(); } } diff --git a/SharpRSS.API/Controllers/AuthController.cs b/SharpRSS.API/Controllers/AuthController.cs index 5299904..b41bce7 100644 --- a/SharpRSS.API/Controllers/AuthController.cs +++ b/SharpRSS.API/Controllers/AuthController.cs @@ -1,18 +1,21 @@ using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SharpRSS.API.Auth; -using SharpRSS.API.Contracts.DTO; +using SharpRSS.API.Contracts.DTOs.Groups; +using SharpRSS.API.Contracts.DTOs.Sessions; +using SharpRSS.API.Contracts.DTOs.Users; using SharpRSS.API.Contracts.Models; -using SharpRSS.API.Contracts.Payloads; using SharpRSS.API.Data; +using SharpRSS.API.Models; using ToolQit; using ToolQit.Logging; namespace SharpRSS.API.Controllers { [ApiController] - [SessionAuthorize] + [SessionAuthorize(false)] [Route("api/[controller]")] public class AuthController : ControllerBase { @@ -25,29 +28,37 @@ namespace SharpRSS.API.Controllers private readonly AuthService _authService; - [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("updateuser")] + [HttpPost("authenticate")] + [AllowAnonymous] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> UpdateUser(ModifyUser payload) + public async Task>> Authenticate(AuthenticateUser auth) { - var updatedUserResult = await _authService.UpdateUser(payload); - return updatedUserResult.Success ? Ok(updatedUserResult) : updatedUserResult.Status == ResultStatus.Failed ? BadRequest(updatedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, updatedUserResult); + var sessionResult = await _authService.Authenticate(auth); + return sessionResult.Success ? Ok(sessionResult) : + sessionResult.Status == ResultStatus.Failed ? BadRequest(sessionResult) : + StatusCode(StatusCodes.Status500InternalServerError, sessionResult); + } + + // To update only fill the values that need to be updated. + [HttpPost("user")] + [SessionAuthorize] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> InsertUser(InsertUser payload) + { + var createdUserResult = await _authService.InsertUserAsync(payload); + return createdUserResult.Success ? Created("", createdUserResult) : + createdUserResult.Status == ResultStatus.Failed ? BadRequest(createdUserResult) : + StatusCode(StatusCodes.Status500InternalServerError, createdUserResult); } - [HttpDelete("deleteuser")] + [HttpDelete("user")] + [SessionAuthorize] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -55,10 +66,13 @@ namespace SharpRSS.API.Controllers 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); + return removedUserResult.Success ? Ok(removedUserResult) : + removedUserResult.Status == ResultStatus.Failed ? BadRequest(removedUserResult) : + StatusCode(StatusCodes.Status500InternalServerError, removedUserResult); } [HttpGet("user")] + [SessionAuthorize] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -69,14 +83,72 @@ namespace SharpRSS.API.Controllers } [HttpGet("users")] + [SessionAuthorize] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetUsers(string search = "", int results = 20, int skip = 0) + public async Task>> GetUsers(string search = "", int results = 20, int skip = 0) { + var authSet = HttpContext.Items["auth"] as AuthorizationSet; var usersResult = await _authService.GetUsersAsync(results, skip, search); - return usersResult.Success ? Ok(usersResult) : usersResult.Status == ResultStatus.Failed ? BadRequest(usersResult) : StatusCode(StatusCodes.Status500InternalServerError, usersResult); + return usersResult.Success ? Ok(usersResult) : + usersResult.Status == ResultStatus.Failed ? BadRequest(usersResult) : + StatusCode(StatusCodes.Status500InternalServerError, usersResult); + } + + [HttpPost("group")] + [SessionAuthorize] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> InsertGroup(InsertGroup @group) + { + var groupInsertResult = await _authService.InsertGroupAsync(group); + return groupInsertResult.Success ? Ok(groupInsertResult) : + groupInsertResult.Status == ResultStatus.Failed ? BadRequest(groupInsertResult) : + StatusCode(StatusCodes.Status500InternalServerError, groupInsertResult); + } + + [HttpDelete("group")] + [SessionAuthorize] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> RemoveGroup(string groupId) + { + var removeResult = await _authService.RemoveGroup(groupId); + return removeResult.Success ? Ok(removeResult) : + removeResult.Status == ResultStatus.Failed ? BadRequest(removeResult) : + StatusCode(StatusCodes.Status500InternalServerError, removeResult); + } + + [HttpGet("groups")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetGroups(string search = "", int results = 20, int skip = 0) + { //TODO: Change DTO to GroupItem! + var groupsResult = await _authService.GetGroupsAsync(results, skip, search); + return groupsResult.Success ? Ok(groupsResult) : + groupsResult.Status == ResultStatus.Failed ? BadRequest(groupsResult) : + StatusCode(StatusCodes.Status500InternalServerError, groupsResult); + } + + [HttpGet("group")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetGroup(string groupId) + { + var groupResult = await _authService.GetGroupAsync(groupId); + return groupResult.Success ? Ok(groupResult) : + groupResult.Status == ResultStatus.Failed ? BadRequest(groupResult) : + StatusCode(StatusCodes.Status500InternalServerError, groupResult); } } } \ No newline at end of file diff --git a/SharpRSS.API/Data/AuthService.cs b/SharpRSS.API/Data/AuthService.cs index 3256df9..fdcc1cc 100644 --- a/SharpRSS.API/Data/AuthService.cs +++ b/SharpRSS.API/Data/AuthService.cs @@ -1,11 +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.DTO; +using SharpRSS.API.Contracts.DTOs.Groups; +using SharpRSS.API.Contracts.DTOs.Sessions; +using SharpRSS.API.Contracts.DTOs.Users; using SharpRSS.API.Contracts.Models; -using SharpRSS.API.Contracts.Payloads; +using SharpRSS.API.Cryptography; using SharpRSS.API.Models; using ToolQit; using ToolQit.Extensions; @@ -25,61 +28,98 @@ namespace SharpRSS.API.Data private readonly IConfiguration _configuration; private readonly ILog _log; - public async Task> CreateUser(ModifyUser user) + public async Task> Authenticate(AuthenticateUser authUser) { - if (user.UserName.IsNullEmptyWhiteSpace() || user.Password.IsNullEmptyWhiteSpace()) - return ResultOr.Failed("Username or password cannot be empty!"); - DbUser newUser; + if (authUser.UserName.IsNullEmptyWhiteSpace() || authUser.Password.IsNullEmptyWhiteSpace()) + return ResultOr.Failed("Username/Password cannot be empty!"); try { 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); + var user = await access.Users.Where(u => u.Uid == authUser.UserName).FirstOrDefaultAsync(); + if (user == null) + return ResultOr.Failed("Unknown user"); + if (!user.Active) return ResultOr.Failed("User is not active, contact your administrator to enable this account!"); + if (!Hasher.ComparePasswords(authUser.Password, user.PswHash, user.Salt)) + return ResultOr.Failed($"Failed to authenticate user: '{user.Uid}'"); + DbSession session = DbSession.CreateSession(user.Uid); + access.Sessions.Add(session); + bool saved = await access.SaveChangesAsync() > 0; + return saved ? ResultOr.Ok(Utilities.ConvertFrom(session)) : ResultOr.Failed($"Could create session for user: '{user.Uid}'"); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return ResultOr.Failed(e.Message, ResultStatus.InternalFail); + } + } + + internal async Task> ValidateSession(string sessionId) + { + if (sessionId.IsNullEmptyWhiteSpace()) return ResultOr.Failed("Session ID is empty!"); + try + { + DbAccess access = new DbAccess(_configuration); + var dbSession = await access.Sessions.Where(s => s.Sid == sessionId).FirstOrDefaultAsync(); + if (dbSession == null) return ResultOr.Failed($"Could not get session: '{sessionId}'"); + var dbUser = await access.Users.Where(u => u.Uid == dbSession.Uid).FirstOrDefaultAsync(); + if (dbUser == null) return ResultOr.Failed($"Could not get user: '{dbSession.Uid}'"); + var dbGroup = await access.Groups.Where(g => g.Gid == dbUser.Gid).FirstOrDefaultAsync(); + var set = new AuthorizationSet() + { + Session = dbSession, + User = dbUser, + Group = dbGroup + }; + return ResultOr.Ok(set); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return ResultOr.Failed(e.Message, ResultStatus.InternalFail); + } + } + + public async Task> InsertUserAsync(InsertUser user) + { + User? retUser; + try + { + var dbNew = Utilities.ConvertFrom(user); + if (dbNew == null) return ResultOr.Failed("Failed to map user!", ResultStatus.InternalFail); + + DbAccess access = new DbAccess(_configuration); + + if (dbNew.Uid.IsNullEmptyWhiteSpace()) + { + if (user.Password.IsNullEmptyWhiteSpace()) return ResultOr.Failed("To create a new user, create a password!"); + dbNew.Uid = Guid.NewGuid().ToString(); + retUser = Utilities.ConvertFrom(dbNew); + access.Users.Add(dbNew); + } + else + { + var dbUser = await access.Users.Where(u => u.Uid == user.Uid).FirstOrDefaultAsync(); + if (dbUser == null) + return ResultOr.Failed($"Failed to get user by id: '{user.Uid}'"); + dbUser.Update(user); + retUser = Utilities.ConvertFrom(dbUser); + access.Users.Update(dbUser); + } bool saved = await access.SaveChangesAsync() > 0; if (!saved) { - _log.Warning("Failed to save user: {UserName} to database", user.UserName); + _log.Warning("Failed to save user: {UserName} to database", user.Uid); return ResultOr.Failed("Could not save user to database!"); } } catch (Exception e) { - _log.Error(e, "Failed to create user: {UserName}", user.UserName); - return ResultOr.Failed($"Failed to create user: {user.UserName}", ResultStatus.InternalFail); + _log.Error(e, "Failed to insert user: {UserName}", user.Uid); + return ResultOr.Failed($"Failed to insert user: {user.Uid}", ResultStatus.InternalFail); } - return ResultOr.Ok(newUser.ToDto()); + return retUser == null ? ResultOr.Failed("Could not return user!") : ResultOr.Ok(retUser); } - - public async Task> UpdateUser(ModifyUser user) - { - 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 RemoveUserAsync(string userId) { if (userId.IsNullEmptyWhiteSpace()) @@ -101,11 +141,13 @@ namespace SharpRSS.API.Data } } - public async Task> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "") + public async Task> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "") { if (results is 0 or > 20) results = 20; try { + int totalCount; + IEnumerable items; DbAccess access = new DbAccess(_configuration); if (!searchQuery.IsNullEmptyWhiteSpace()) { @@ -113,16 +155,20 @@ namespace SharpRSS.API.Data 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()); + items = await queryResult.Skip(skip).Take(results).ToListAsync(); + totalCount = queryResult.Count(); } - var users = await access.Users.Skip(skip).Take(results).ToListAsync(); - return new ListResult(users, access.Users.Count()); + else + { + items = await access.Users.Skip(skip).Take(results).ToListAsync(); + totalCount = access.Users.Count(); + } + return new ListResult(items.Select(du => Utilities.ConvertFrom(du) ?? new UserItem() { Uid = "NULL" }).Where(ui => ui.Uid != "NULL"), totalCount); } 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); + _log.Error(e, e.Message); + return new ListResult(null, 0, e.Message, ResultStatus.InternalFail); } } @@ -132,13 +178,114 @@ namespace SharpRSS.API.Data try { DbAccess access = new DbAccess(_configuration); - User? user = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync(); + var dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync(); + var user = dbUser?.ToDto(); 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); + return new ResultOr(null, e.Message, ResultStatus.InternalFail); + } + } + + public async Task> InsertGroupAsync(InsertGroup group) + { + if (group.DisplayName.IsNullEmptyWhiteSpace()) return ResultOr.Failed("Display name is empty!"); + try + { + var dbNew = Utilities.ConvertFrom(group); + if (dbNew == null) return ResultOr.Failed("Failed to map group!"); + + DbAccess access = new DbAccess(_configuration); + if (dbNew.Gid.IsNullEmptyWhiteSpace()) + { + dbNew.Gid = Guid.NewGuid().ToString(); + access.Groups.Add(dbNew); + } + else + { + var existingGroup = await access.Groups.Where(g => g.Gid == group.Gid).FirstOrDefaultAsync(); + if (existingGroup == null) + return ResultOr.Failed($"Could not find a group with id: '{group.Gid}'"); + access.Groups.Update(dbNew); + } + bool saved = await access.SaveChangesAsync() > 0; + return saved ? ResultOr.Ok(Utilities.ConvertFrom(dbNew)) : ResultOr.Failed($"Failed to save group: '{@group.DisplayName}'"); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return ResultOr.Failed(e.Message, ResultStatus.InternalFail); + } + } + + public async Task RemoveGroup(string groupId) + { + if (groupId.IsNullEmptyWhiteSpace()) return new Result($"'{nameof(groupId)} is empty!'", ResultStatus.Failed); + try + { + DbAccess access = new DbAccess(_configuration); + DbGroup? dbGroup = await access.Groups.Where(g => g.Gid == groupId).FirstOrDefaultAsync(); + if (dbGroup == null) + return new Result($"Group with id: '{groupId}' does not exists!", ResultStatus.Failed); + access.Groups.Remove(dbGroup); + bool removed = await access.SaveChangesAsync() > 0; + if (removed) + return new Result(); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return new Result(e.Message, ResultStatus.InternalFail); + } + + return new Result($"Failed to remove group: '{groupId}'", ResultStatus.Failed); + } + + public async Task> GetGroupsAsync(int results = 20, int skip = 0, string searchQuery = "") + { + try + { + int totalCount; + IEnumerable items; + DbAccess access = new DbAccess(_configuration); + if (!searchQuery.IsNullEmptyWhiteSpace()) + { + IQueryable queryResult = access.Groups.Where(u => + EF.Functions.Like(u.Gid, $"%{searchQuery}%") || + EF.Functions.Like(u.DateCreated, $"%{searchQuery}%") || + EF.Functions.Like(u.DisplayName, $"%{searchQuery}%")); + items = await queryResult.Skip(skip).Take(results).ToListAsync(); + totalCount = queryResult.Count(); + } + else + { + items = await access.Groups.Skip(skip).Take(results).ToListAsync(); + totalCount = access.Groups.Count(); + } + return new ListResult(items.Select(dg => Utilities.ConvertFrom(dg) ?? new GroupItem() { Gid = "NULL" }).Where(gi => gi.Gid != "NULL"), totalCount); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return new ListResult(null, 0, e.Message, ResultStatus.InternalFail); + } + } + + public async Task> GetGroupAsync(string groupId) + { + if (groupId.IsNullEmptyWhiteSpace()) return ResultOr.Failed($"Parameter '{nameof(groupId)}' is empty!"); + try + { + DbAccess access = new DbAccess(_configuration); + var group = Utilities.ConvertFrom(await access.Groups.Where(g => g.Gid == groupId).FirstOrDefaultAsync()); + return group == null ? ResultOr.Failed($"Failed to get group from id: {groupId}") : ResultOr.Ok(group); + } + catch (Exception e) + { + _log.Error(e, e.Message); + return ResultOr.Failed(e.Message, ResultStatus.InternalFail); } } } diff --git a/SharpRSS.API/Models/AuthorizationSet.cs b/SharpRSS.API/Models/AuthorizationSet.cs new file mode 100644 index 0000000..76ce812 --- /dev/null +++ b/SharpRSS.API/Models/AuthorizationSet.cs @@ -0,0 +1,10 @@ +namespace SharpRSS.API.Models +{ + internal class AuthorizationSet + { + public DbSession? Session { get; set; } + public DbUser? User { get; set; } + public DbGroup? Group { get; set; } + public bool AdminRequired { get; set; } + } +} \ No newline at end of file diff --git a/SharpRSS.API/Models/DbGroup.cs b/SharpRSS.API/Models/DbGroup.cs index 665f98d..f87bbaf 100644 --- a/SharpRSS.API/Models/DbGroup.cs +++ b/SharpRSS.API/Models/DbGroup.cs @@ -1,9 +1,14 @@ -using SharpRSS.API.Contracts.DTO; +using System; +using Newtonsoft.Json; +using SharpRSS.API.Contracts.DTOs.Groups; namespace SharpRSS.API.Models { - internal class DbGroup : Group + internal class DbGroup { - + public string Gid { get; set; } = Guid.NewGuid().ToString(); + 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/Models/DbSession.cs b/SharpRSS.API/Models/DbSession.cs index 000b66a..1d3b567 100644 --- a/SharpRSS.API/Models/DbSession.cs +++ b/SharpRSS.API/Models/DbSession.cs @@ -2,11 +2,12 @@ using System; using System.Security.Cryptography; using System.Text; using Serilog; -using SharpRSS.API.Contracts.DTO; +using SharpRSS.API.Contracts.DTOs; +using SharpRSS.API.Contracts.DTOs.Sessions; namespace SharpRSS.API.Models { - internal class DbSession : Session + internal class DbSession { public DbSession() { } private DbSession(string uid, double expiresMinutes) @@ -24,6 +25,11 @@ namespace SharpRSS.API.Models Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}"; } public string Uid { get; set; } + public string Sid { get; set; } = string.Empty; + public DateTime Created { get; set; } + public DateTime Expires { get; set; } + + public bool Expired => Expires < DateTime.Now; public static DbSession CreateSession(string uid, double expiresMinutes = 10080) { diff --git a/SharpRSS.API/Models/DbUser.cs b/SharpRSS.API/Models/DbUser.cs index 9a06331..7999214 100644 --- a/SharpRSS.API/Models/DbUser.cs +++ b/SharpRSS.API/Models/DbUser.cs @@ -1,18 +1,18 @@ +using System; using Newtonsoft.Json; -using SharpRSS.API.Contracts.DTO; -using SharpRSS.API.Contracts.Payloads; +using SharpRSS.API.Contracts.DTOs.Users; using SharpRSS.API.Cryptography; using ToolQit.Extensions; namespace SharpRSS.API.Models { // Database model - internal class DbUser : User + internal class DbUser { public DbUser() { } - public DbUser(ModifyUser user) + public DbUser(InsertUser user) { - Uid = user.UserName; + Uid = user.Uid; DisplayName = user.DisplayName; Email = user.Email; Gid = user.GroupId; @@ -20,6 +20,12 @@ namespace SharpRSS.API.Models PswHash = Hasher.HashPassword(user.Password, out byte[] salt); Salt = salt; } + 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; public byte[] PswHash { get; set; } public byte[] Salt { get; set; } @@ -29,9 +35,9 @@ namespace SharpRSS.API.Models return JsonConvert.DeserializeObject(json) ?? new User(); } - public bool Update(ModifyUser user) + public void Update(InsertUser user) { - if (user == null) return false; + if (user == null) return; if (!user.DisplayName.IsNullEmptyWhiteSpace()) DisplayName = user.DisplayName; if (!user.Email.IsNullEmptyWhiteSpace()) @@ -44,7 +50,6 @@ namespace SharpRSS.API.Models PswHash = Hasher.HashPassword(user.Password, out byte[] salt); Salt = salt; } - return true; } } } \ No newline at end of file