Implemented groups, sessions & authorization basics.

This commit is contained in:
Max 2023-10-09 01:58:53 +02:00
parent f95e985c7b
commit 9338d8c106
17 changed files with 403 additions and 112 deletions

View File

@ -1,10 +1,10 @@
using System; using System;
namespace SharpRSS.API.Contracts.DTO namespace SharpRSS.API.Contracts.DTOs.Groups
{ {
public class Group 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 string DisplayName { get; set; } = string.Empty;
public bool Administrator { get; set; } public bool Administrator { get; set; }
public DateTime DateCreated { get; set; } = DateTime.Now; public DateTime DateCreated { get; set; } = DateTime.Now;

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -1,6 +1,6 @@
using System; using System;
namespace SharpRSS.API.Contracts.DTO namespace SharpRSS.API.Contracts.DTOs.Sessions
{ {
public class Session public class Session
{ {

View File

@ -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 UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;

View File

@ -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 DisplayName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;

View File

@ -1,6 +1,6 @@
using System; using System;
namespace SharpRSS.API.Contracts.DTO namespace SharpRSS.API.Contracts.DTOs.Users
{ {
public class User public class User
{ {

View File

@ -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; }
}
}

View File

@ -11,7 +11,7 @@ namespace SharpRSS.API.Contracts.Models
public TValue? Value { get; } public TValue? Value { get; }
public static ResultOr<TValue> Ok(TValue value) => new ResultOr<TValue>(value, "", ResultStatus.Ok); public static ResultOr<TValue> Ok(TValue? value) => new ResultOr<TValue>(value, "", ResultStatus.Ok);
public static ResultOr<TValue> Failed(string message = "Failed", ResultStatus status = ResultStatus.Failed) => new ResultOr<TValue>(default, message, status); public static ResultOr<TValue> Failed(string message = "Failed", ResultStatus status = ResultStatus.Failed) => new ResultOr<TValue>(default, message, status);
} }
} }

View File

@ -11,8 +11,4 @@
<ProjectReference Include="..\ToolQit\ToolQit\ToolQit.csproj" /> <ProjectReference Include="..\ToolQit\ToolQit\ToolQit.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Auth" />
</ItemGroup>
</Project> </Project>

View File

@ -4,7 +4,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using SharpRSS.API.Contracts.Models;
using SharpRSS.API.Data;
using ToolQit; using ToolQit;
using ToolQit.Extensions;
using ToolQit.Logging; using ToolQit.Logging;
namespace SharpRSS.API.Auth namespace SharpRSS.API.Auth
@ -12,31 +15,49 @@ namespace SharpRSS.API.Auth
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SessionAuthorizeAttribute : Attribute, IAuthorizationFilter public class SessionAuthorizeAttribute : Attribute, IAuthorizationFilter
{ {
public SessionAuthorizeAttribute(string group = "") public SessionAuthorizeAttribute(bool admin = true)
{ {
_admin = admin;
_log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute)); _log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute));
_group = group;
} }
private readonly ILog _log; 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))) 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;
} }
return; if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out var val))
if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out StringValues 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; return;
} }
//TODO: Check session ID!
context.Result = new UnauthorizedResult(); context.Result = new UnauthorizedResult();
} }
} }

View File

@ -1,18 +1,21 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharpRSS.API.Auth; 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.Models;
using SharpRSS.API.Contracts.Payloads;
using SharpRSS.API.Data; using SharpRSS.API.Data;
using SharpRSS.API.Models;
using ToolQit; using ToolQit;
using ToolQit.Logging; using ToolQit.Logging;
namespace SharpRSS.API.Controllers namespace SharpRSS.API.Controllers
{ {
[ApiController] [ApiController]
[SessionAuthorize] [SessionAuthorize(false)]
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
@ -25,29 +28,37 @@ namespace SharpRSS.API.Controllers
private readonly AuthService _authService; private readonly AuthService _authService;
[HttpPost("createuser")] [HttpPost("authenticate")]
[Produces("application/json")] [AllowAnonymous]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ResultOr<User>>> 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")]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ResultOr<User>>> UpdateUser(ModifyUser payload) public async Task<ActionResult<ResultOr<Session>>> Authenticate(AuthenticateUser auth)
{ {
var updatedUserResult = await _authService.UpdateUser(payload); var sessionResult = await _authService.Authenticate(auth);
return updatedUserResult.Success ? Ok(updatedUserResult) : updatedUserResult.Status == ResultStatus.Failed ? BadRequest(updatedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, updatedUserResult); return sessionResult.Success ? Ok(sessionResult) :
sessionResult.Status == ResultStatus.Failed ? BadRequest(sessionResult) :
StatusCode(StatusCodes.Status500InternalServerError, sessionResult);
} }
[HttpDelete("deleteuser")] // 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<ActionResult<ResultOr<User>>> 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("user")]
[SessionAuthorize]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -55,10 +66,13 @@ namespace SharpRSS.API.Controllers
public async Task<ActionResult<Result>> DeleteUser(string userId) public async Task<ActionResult<Result>> DeleteUser(string userId)
{ {
var removedUserResult = await _authService.RemoveUserAsync(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")] [HttpGet("user")]
[SessionAuthorize]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -69,14 +83,72 @@ namespace SharpRSS.API.Controllers
} }
[HttpGet("users")] [HttpGet("users")]
[SessionAuthorize]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ListResult<User>>> GetUsers(string search = "", int results = 20, int skip = 0) public async Task<ActionResult<ListResult<UserItem>>> GetUsers(string search = "", int results = 20, int skip = 0)
{ {
var authSet = HttpContext.Items["auth"] as AuthorizationSet;
var usersResult = await _authService.GetUsersAsync(results, skip, search); 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<ActionResult<ResultOr<Group>>> 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<ActionResult<Result>> 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<ActionResult<ListResult<Group>>> 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<ActionResult<ResultOr<Group>>> GetGroup(string groupId)
{
var groupResult = await _authService.GetGroupAsync(groupId);
return groupResult.Success ? Ok(groupResult) :
groupResult.Status == ResultStatus.Failed ? BadRequest(groupResult) :
StatusCode(StatusCodes.Status500InternalServerError, groupResult);
} }
} }
} }

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; 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.Models;
using SharpRSS.API.Contracts.Payloads; using SharpRSS.API.Cryptography;
using SharpRSS.API.Models; using SharpRSS.API.Models;
using ToolQit; using ToolQit;
using ToolQit.Extensions; using ToolQit.Extensions;
@ -25,59 +28,96 @@ namespace SharpRSS.API.Data
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly ILog _log; private readonly ILog _log;
public async Task<ResultOr<User>> CreateUser(ModifyUser user) public async Task<ResultOr<Session>> Authenticate(AuthenticateUser authUser)
{ {
if (user.UserName.IsNullEmptyWhiteSpace() || user.Password.IsNullEmptyWhiteSpace()) if (authUser.UserName.IsNullEmptyWhiteSpace() || authUser.Password.IsNullEmptyWhiteSpace())
return ResultOr<User>.Failed("Username or password cannot be empty!"); return ResultOr<Session>.Failed("Username/Password cannot be empty!");
DbUser newUser;
try try
{ {
DbAccess access = new DbAccess(_configuration); DbAccess access = new DbAccess(_configuration);
bool userExists = access.Users.Any(u => u.Uid == user.UserName); var user = await access.Users.Where(u => u.Uid == authUser.UserName).FirstOrDefaultAsync();
if (userExists) if (user == null)
return ResultOr<User>.Failed("Username already exists!"); return ResultOr<Session>.Failed("Unknown user");
newUser = new DbUser(user); if (!user.Active) return ResultOr<Session>.Failed("User is not active, contact your administrator to enable this account!");
access.Users.Add(newUser); if (!Hasher.ComparePasswords(authUser.Password, user.PswHash, user.Salt))
return ResultOr<Session>.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<Session>.Ok(Utilities.ConvertFrom<Session, DbSession>(session)) : ResultOr<Session>.Failed($"Could create session for user: '{user.Uid}'");
}
catch (Exception e)
{
_log.Error(e, e.Message);
return ResultOr<Session>.Failed(e.Message, ResultStatus.InternalFail);
}
}
internal async Task<ResultOr<AuthorizationSet>> ValidateSession(string sessionId)
{
if (sessionId.IsNullEmptyWhiteSpace()) return ResultOr<AuthorizationSet>.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<AuthorizationSet>.Failed($"Could not get session: '{sessionId}'");
var dbUser = await access.Users.Where(u => u.Uid == dbSession.Uid).FirstOrDefaultAsync();
if (dbUser == null) return ResultOr<AuthorizationSet>.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<AuthorizationSet>.Ok(set);
}
catch (Exception e)
{
_log.Error(e, e.Message);
return ResultOr<AuthorizationSet>.Failed(e.Message, ResultStatus.InternalFail);
}
}
public async Task<ResultOr<User>> InsertUserAsync(InsertUser user)
{
User? retUser;
try
{
var dbNew = Utilities.ConvertFrom<DbUser, InsertUser>(user);
if (dbNew == null) return ResultOr<User>.Failed("Failed to map user!", ResultStatus.InternalFail);
DbAccess access = new DbAccess(_configuration);
if (dbNew.Uid.IsNullEmptyWhiteSpace())
{
if (user.Password.IsNullEmptyWhiteSpace()) return ResultOr<User>.Failed("To create a new user, create a password!");
dbNew.Uid = Guid.NewGuid().ToString();
retUser = Utilities.ConvertFrom<User, DbUser>(dbNew);
access.Users.Add(dbNew);
}
else
{
var dbUser = await access.Users.Where(u => u.Uid == user.Uid).FirstOrDefaultAsync();
if (dbUser == null)
return ResultOr<User>.Failed($"Failed to get user by id: '{user.Uid}'");
dbUser.Update(user);
retUser = Utilities.ConvertFrom<User, DbUser>(dbUser);
access.Users.Update(dbUser);
}
bool saved = await access.SaveChangesAsync() > 0; bool saved = await access.SaveChangesAsync() > 0;
if (!saved) 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<User>.Failed("Could not save user to database!"); return ResultOr<User>.Failed("Could not save user to database!");
} }
} }
catch (Exception e) catch (Exception e)
{ {
_log.Error(e, "Failed to create user: {UserName}", user.UserName); _log.Error(e, "Failed to insert user: {UserName}", user.Uid);
return ResultOr<User>.Failed($"Failed to create user: {user.UserName}", ResultStatus.InternalFail); return ResultOr<User>.Failed($"Failed to insert user: {user.Uid}", ResultStatus.InternalFail);
}
return ResultOr<User>.Ok(newUser.ToDto());
}
public async Task<ResultOr<User>> UpdateUser(ModifyUser user)
{
if (user.UserName.IsNullEmptyWhiteSpace())
return ResultOr<User>.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<User>.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<User>.Failed($"Could not save user: '{dbUser.Uid}'");
}
return ResultOr<User>.Ok(dbUser.ToDto());
}
catch (Exception e)
{
_log.Error(e, e.Message);
return ResultOr<User>.Failed(e.Message, ResultStatus.InternalFail);
} }
return retUser == null ? ResultOr<User>.Failed("Could not return user!") : ResultOr<User>.Ok(retUser);
} }
public async Task<Result> RemoveUserAsync(string userId) public async Task<Result> RemoveUserAsync(string userId)
@ -101,11 +141,13 @@ namespace SharpRSS.API.Data
} }
} }
public async Task<ListResult<User>> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "") public async Task<ListResult<UserItem>> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "")
{ {
if (results is 0 or > 20) results = 20; if (results is 0 or > 20) results = 20;
try try
{ {
int totalCount;
IEnumerable<DbUser> items;
DbAccess access = new DbAccess(_configuration); DbAccess access = new DbAccess(_configuration);
if (!searchQuery.IsNullEmptyWhiteSpace()) if (!searchQuery.IsNullEmptyWhiteSpace())
{ {
@ -113,16 +155,20 @@ namespace SharpRSS.API.Data
EF.Functions.Like(u.Uid, $"%{searchQuery}%") || EF.Functions.Like(u.Uid, $"%{searchQuery}%") ||
EF.Functions.Like(u.Email, $"%{searchQuery}%") || EF.Functions.Like(u.Email, $"%{searchQuery}%") ||
EF.Functions.Like(u.DisplayName, $"%{searchQuery}%")); EF.Functions.Like(u.DisplayName, $"%{searchQuery}%"));
var searchResults = await queryResult.Skip(skip).Take(results).ToListAsync(); items = await queryResult.Skip(skip).Take(results).ToListAsync();
return new ListResult<User>(searchResults, queryResult.Count()); totalCount = queryResult.Count();
} }
var users = await access.Users.Skip(skip).Take(results).ToListAsync(); else
return new ListResult<User>(users, access.Users.Count()); {
items = await access.Users.Skip(skip).Take(results).ToListAsync();
totalCount = access.Users.Count();
}
return new ListResult<UserItem>(items.Select(du => Utilities.ConvertFrom<UserItem, DbUser>(du) ?? new UserItem() { Uid = "NULL" }).Where(ui => ui.Uid != "NULL"), totalCount);
} }
catch (Exception e) catch (Exception e)
{ {
_log.Error(e, "Error while loading users from database!"); _log.Error(e, e.Message);
return new ListResult<User>(null, 0, "Error while loading users from database!", ResultStatus.Failed); return new ListResult<UserItem>(null, 0, e.Message, ResultStatus.InternalFail);
} }
} }
@ -132,13 +178,114 @@ namespace SharpRSS.API.Data
try try
{ {
DbAccess access = new DbAccess(_configuration); 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, user == null ? $"Could not find user with id: {userId}!" : "", user == null ? ResultStatus.Failed : ResultStatus.Ok); return new ResultOr<User>(user, user == null ? $"Could not find user with id: {userId}!" : "", user == null ? ResultStatus.Failed : ResultStatus.Ok);
} }
catch (Exception e) catch (Exception e)
{ {
_log.Error(e, e.Message); _log.Error(e, e.Message);
return new ResultOr<User>(null, e.Message, ResultStatus.Failed); return new ResultOr<User>(null, e.Message, ResultStatus.InternalFail);
}
}
public async Task<ResultOr<Group>> InsertGroupAsync(InsertGroup group)
{
if (group.DisplayName.IsNullEmptyWhiteSpace()) return ResultOr<Group>.Failed("Display name is empty!");
try
{
var dbNew = Utilities.ConvertFrom<DbGroup, InsertGroup>(group);
if (dbNew == null) return ResultOr<Group>.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<Group>.Failed($"Could not find a group with id: '{group.Gid}'");
access.Groups.Update(dbNew);
}
bool saved = await access.SaveChangesAsync() > 0;
return saved ? ResultOr<Group>.Ok(Utilities.ConvertFrom<Group, DbGroup>(dbNew)) : ResultOr<Group>.Failed($"Failed to save group: '{@group.DisplayName}'");
}
catch (Exception e)
{
_log.Error(e, e.Message);
return ResultOr<Group>.Failed(e.Message, ResultStatus.InternalFail);
}
}
public async Task<Result> 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<ListResult<GroupItem>> GetGroupsAsync(int results = 20, int skip = 0, string searchQuery = "")
{
try
{
int totalCount;
IEnumerable<DbGroup> items;
DbAccess access = new DbAccess(_configuration);
if (!searchQuery.IsNullEmptyWhiteSpace())
{
IQueryable<DbGroup> 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<GroupItem>(items.Select(dg => Utilities.ConvertFrom<GroupItem, DbGroup>(dg) ?? new GroupItem() { Gid = "NULL" }).Where(gi => gi.Gid != "NULL"), totalCount);
}
catch (Exception e)
{
_log.Error(e, e.Message);
return new ListResult<GroupItem>(null, 0, e.Message, ResultStatus.InternalFail);
}
}
public async Task<ResultOr<Group>> GetGroupAsync(string groupId)
{
if (groupId.IsNullEmptyWhiteSpace()) return ResultOr<Group>.Failed($"Parameter '{nameof(groupId)}' is empty!");
try
{
DbAccess access = new DbAccess(_configuration);
var group = Utilities.ConvertFrom<Group, DbGroup>(await access.Groups.Where(g => g.Gid == groupId).FirstOrDefaultAsync());
return group == null ? ResultOr<Group>.Failed($"Failed to get group from id: {groupId}") : ResultOr<Group>.Ok(group);
}
catch (Exception e)
{
_log.Error(e, e.Message);
return ResultOr<Group>.Failed(e.Message, ResultStatus.InternalFail);
} }
} }
} }

View File

@ -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; }
}
}

View File

@ -1,9 +1,14 @@
using SharpRSS.API.Contracts.DTO; using System;
using Newtonsoft.Json;
using SharpRSS.API.Contracts.DTOs.Groups;
namespace SharpRSS.API.Models 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;
} }
} }

View File

@ -2,11 +2,12 @@ using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Serilog; using Serilog;
using SharpRSS.API.Contracts.DTO; using SharpRSS.API.Contracts.DTOs;
using SharpRSS.API.Contracts.DTOs.Sessions;
namespace SharpRSS.API.Models namespace SharpRSS.API.Models
{ {
internal class DbSession : Session internal class DbSession
{ {
public DbSession() { } public DbSession() { }
private DbSession(string uid, double expiresMinutes) private DbSession(string uid, double expiresMinutes)
@ -24,6 +25,11 @@ namespace SharpRSS.API.Models
Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}"; Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}";
} }
public string Uid { get; set; } 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) public static DbSession CreateSession(string uid, double expiresMinutes = 10080)
{ {

View File

@ -1,18 +1,18 @@
using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharpRSS.API.Contracts.DTO; using SharpRSS.API.Contracts.DTOs.Users;
using SharpRSS.API.Contracts.Payloads;
using SharpRSS.API.Cryptography; using SharpRSS.API.Cryptography;
using ToolQit.Extensions; using ToolQit.Extensions;
namespace SharpRSS.API.Models namespace SharpRSS.API.Models
{ {
// Database model // Database model
internal class DbUser : User internal class DbUser
{ {
public DbUser() { } public DbUser() { }
public DbUser(ModifyUser user) public DbUser(InsertUser user)
{ {
Uid = user.UserName; Uid = user.Uid;
DisplayName = user.DisplayName; DisplayName = user.DisplayName;
Email = user.Email; Email = user.Email;
Gid = user.GroupId; Gid = user.GroupId;
@ -20,6 +20,12 @@ namespace SharpRSS.API.Models
PswHash = Hasher.HashPassword(user.Password, out byte[] salt); PswHash = Hasher.HashPassword(user.Password, out byte[] salt);
Salt = 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[] PswHash { get; set; }
public byte[] Salt { get; set; } public byte[] Salt { get; set; }
@ -29,9 +35,9 @@ namespace SharpRSS.API.Models
return JsonConvert.DeserializeObject<User>(json) ?? new User(); return JsonConvert.DeserializeObject<User>(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()) if (!user.DisplayName.IsNullEmptyWhiteSpace())
DisplayName = user.DisplayName; DisplayName = user.DisplayName;
if (!user.Email.IsNullEmptyWhiteSpace()) if (!user.Email.IsNullEmptyWhiteSpace())
@ -44,7 +50,6 @@ namespace SharpRSS.API.Models
PswHash = Hasher.HashPassword(user.Password, out byte[] salt); PswHash = Hasher.HashPassword(user.Password, out byte[] salt);
Salt = salt; Salt = salt;
} }
return true;
} }
} }
} }