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

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;
namespace SharpRSS.API.Contracts.DTO
namespace SharpRSS.API.Contracts.DTOs.Sessions
{
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 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 Password { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;

View File

@ -1,6 +1,6 @@
using System;
namespace SharpRSS.API.Contracts.DTO
namespace SharpRSS.API.Contracts.DTOs.Users
{
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 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);
}
}

View File

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

View File

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

View File

@ -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<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")]
[HttpPost("authenticate")]
[AllowAnonymous]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[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);
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);
}
[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")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -55,10 +66,13 @@ namespace SharpRSS.API.Controllers
public async Task<ActionResult<Result>> 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<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);
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.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,59 +28,96 @@ namespace SharpRSS.API.Data
private readonly IConfiguration _configuration;
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())
return ResultOr<User>.Failed("Username or password cannot be empty!");
DbUser newUser;
if (authUser.UserName.IsNullEmptyWhiteSpace() || authUser.Password.IsNullEmptyWhiteSpace())
return ResultOr<Session>.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<User>.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<Session>.Failed("Unknown user");
if (!user.Active) return ResultOr<Session>.Failed("User is not active, contact your administrator to enable this account!");
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;
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!");
}
}
catch (Exception e)
{
_log.Error(e, "Failed to create user: {UserName}", user.UserName);
return ResultOr<User>.Failed($"Failed to create user: {user.UserName}", 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);
_log.Error(e, "Failed to insert user: {UserName}", user.Uid);
return ResultOr<User>.Failed($"Failed to insert user: {user.Uid}", ResultStatus.InternalFail);
}
return retUser == null ? ResultOr<User>.Failed("Could not return user!") : ResultOr<User>.Ok(retUser);
}
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;
try
{
int totalCount;
IEnumerable<DbUser> 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<User>(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<User>(users, access.Users.Count());
else
{
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)
{
_log.Error(e, "Error while loading users from database!");
return new ListResult<User>(null, 0, "Error while loading users from database!", ResultStatus.Failed);
_log.Error(e, e.Message);
return new ListResult<UserItem>(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, 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<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
{
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.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)
{

View File

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