2023-09-10 21:32:25 +02:00
|
|
|
using System;
|
2023-10-09 01:58:53 +02:00
|
|
|
using System.Collections.Generic;
|
2023-09-10 21:32:25 +02:00
|
|
|
using System.Linq;
|
|
|
|
using System.Threading.Tasks;
|
2023-10-08 00:46:42 +02:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2023-09-10 21:32:25 +02:00
|
|
|
using Microsoft.Extensions.Configuration;
|
2023-10-09 01:58:53 +02:00
|
|
|
using SharpRSS.API.Contracts.DTOs.Groups;
|
|
|
|
using SharpRSS.API.Contracts.DTOs.Sessions;
|
|
|
|
using SharpRSS.API.Contracts.DTOs.Users;
|
2023-10-08 00:46:42 +02:00
|
|
|
using SharpRSS.API.Contracts.Models;
|
2023-10-09 01:58:53 +02:00
|
|
|
using SharpRSS.API.Cryptography;
|
2023-09-10 21:32:25 +02:00
|
|
|
using SharpRSS.API.Models;
|
|
|
|
using ToolQit;
|
2023-10-08 00:46:42 +02:00
|
|
|
using ToolQit.Extensions;
|
2023-09-10 21:32:25 +02:00
|
|
|
using ToolQit.Logging;
|
|
|
|
|
|
|
|
namespace SharpRSS.API.Data
|
|
|
|
{
|
|
|
|
public class AuthService
|
|
|
|
{
|
|
|
|
public AuthService(IConfiguration configuration)
|
|
|
|
{
|
|
|
|
_configuration = configuration;
|
|
|
|
_log = LogManager.CreateLogger(typeof(AuthService));
|
|
|
|
_log.Information("Setting up service...");
|
|
|
|
}
|
|
|
|
|
|
|
|
private readonly IConfiguration _configuration;
|
|
|
|
private readonly ILog _log;
|
|
|
|
|
2023-10-09 01:58:53 +02:00
|
|
|
public async Task<ResultOr<Session>> Authenticate(AuthenticateUser authUser)
|
2023-09-10 21:32:25 +02:00
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
if (authUser.UserName.IsNullEmptyWhiteSpace() || authUser.Password.IsNullEmptyWhiteSpace())
|
|
|
|
return ResultOr<Session>.Failed("Username/Password cannot be empty!");
|
2023-10-08 00:46:42 +02:00
|
|
|
try
|
|
|
|
{
|
|
|
|
DbAccess access = new DbAccess(_configuration);
|
2023-10-09 01:58:53 +02:00
|
|
|
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);
|
2023-10-08 00:46:42 +02:00
|
|
|
bool saved = await access.SaveChangesAsync() > 0;
|
2023-10-09 01:58:53 +02:00
|
|
|
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}'");
|
2023-10-09 21:05:38 +02:00
|
|
|
if (!dbSession.Expired && dbSession.Expires - DateTime.Now <= TimeSpan.FromDays(1))
|
|
|
|
{
|
|
|
|
DbSession extended = dbSession.Extend();
|
|
|
|
access.Sessions.Update(extended);
|
|
|
|
bool sessionExtended = await access.SaveChangesAsync() > 0;
|
|
|
|
if (sessionExtended)
|
|
|
|
{
|
|
|
|
dbSession = extended;
|
|
|
|
_log.Information("Extended session: '{SessionId}' to '{SessionExpirationDate}'", dbSession.Sid, dbSession.Expires);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
_log.Warning("Failed to extend session: '{SessionId}'", dbSession.Sid);
|
|
|
|
}
|
2023-10-09 01:58:53 +02:00
|
|
|
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();
|
2023-10-09 21:05:38 +02:00
|
|
|
if (dbGroup == null) return ResultOr<AuthorizationSet>.Failed($"Could not get group: '{dbUser.Gid}'");
|
2023-10-09 01:58:53 +02:00
|
|
|
var set = new AuthorizationSet()
|
2023-10-08 00:46:42 +02:00
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
Session = dbSession,
|
|
|
|
User = dbUser,
|
|
|
|
Group = dbGroup
|
|
|
|
};
|
|
|
|
return ResultOr<AuthorizationSet>.Ok(set);
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
|
|
|
catch (Exception e)
|
2023-09-10 21:32:25 +02:00
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
_log.Error(e, e.Message);
|
|
|
|
return ResultOr<AuthorizationSet>.Failed(e.Message, ResultStatus.InternalFail);
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 01:58:53 +02:00
|
|
|
public async Task<ResultOr<User>> InsertUserAsync(InsertUser user)
|
2023-10-08 00:46:42 +02:00
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
User? retUser;
|
2023-09-10 21:32:25 +02:00
|
|
|
try
|
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
var dbNew = Utilities.ConvertFrom<DbUser, InsertUser>(user);
|
|
|
|
if (dbNew == null) return ResultOr<User>.Failed("Failed to map user!", ResultStatus.InternalFail);
|
|
|
|
|
2023-10-08 00:46:42 +02:00
|
|
|
DbAccess access = new DbAccess(_configuration);
|
2023-10-09 01:58:53 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2023-10-08 00:46:42 +02:00
|
|
|
bool saved = await access.SaveChangesAsync() > 0;
|
|
|
|
if (!saved)
|
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
_log.Warning("Failed to save user: {UserName} to database", user.Uid);
|
|
|
|
return ResultOr<User>.Failed("Could not save user to database!");
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
2023-09-10 21:32:25 +02:00
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
_log.Error(e, "Failed to insert user: {UserName}", user.Uid);
|
|
|
|
return ResultOr<User>.Failed($"Failed to insert user: {user.Uid}", ResultStatus.InternalFail);
|
2023-09-10 21:32:25 +02:00
|
|
|
}
|
2023-10-09 01:58:53 +02:00
|
|
|
return retUser == null ? ResultOr<User>.Failed("Could not return user!") : ResultOr<User>.Ok(retUser);
|
2023-09-17 21:41:31 +02:00
|
|
|
}
|
2023-10-09 01:58:53 +02:00
|
|
|
|
2023-10-08 00:46:42 +02:00
|
|
|
public async Task<Result> RemoveUserAsync(string userId)
|
2023-09-17 21:41:31 +02:00
|
|
|
{
|
2023-10-08 00:46:42 +02:00
|
|
|
if (userId.IsNullEmptyWhiteSpace())
|
|
|
|
return new Result("User id is empty!", ResultStatus.Failed);
|
|
|
|
try
|
|
|
|
{
|
|
|
|
DbAccess access = new DbAccess(_configuration);
|
|
|
|
DbUser? dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
|
|
|
|
if (dbUser == null)
|
|
|
|
return new Result($"Could not find user: '{userId}'", ResultStatus.Failed);
|
|
|
|
access.Users.Remove(dbUser);
|
|
|
|
bool removed = await access.SaveChangesAsync() > 0;
|
|
|
|
return new Result(removed ? "" : $"Failed to remove user: '{dbUser.Uid}'!", removed ? ResultStatus.Ok : ResultStatus.Failed);
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
_log.Error(e, e.Message);
|
|
|
|
return new Result(e.Message, ResultStatus.InternalFail);
|
|
|
|
}
|
2023-09-17 21:41:31 +02:00
|
|
|
}
|
|
|
|
|
2023-10-09 01:58:53 +02:00
|
|
|
public async Task<ListResult<UserItem>> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "")
|
2023-09-17 21:41:31 +02:00
|
|
|
{
|
2023-10-08 00:46:42 +02:00
|
|
|
if (results is 0 or > 20) results = 20;
|
|
|
|
try
|
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
int totalCount;
|
|
|
|
IEnumerable<DbUser> items;
|
2023-10-08 00:46:42 +02:00
|
|
|
DbAccess access = new DbAccess(_configuration);
|
|
|
|
if (!searchQuery.IsNullEmptyWhiteSpace())
|
|
|
|
{
|
|
|
|
IQueryable<DbUser> queryResult = access.Users.Where(u =>
|
|
|
|
EF.Functions.Like(u.Uid, $"%{searchQuery}%") ||
|
|
|
|
EF.Functions.Like(u.Email, $"%{searchQuery}%") ||
|
|
|
|
EF.Functions.Like(u.DisplayName, $"%{searchQuery}%"));
|
2023-10-09 01:58:53 +02:00
|
|
|
items = await queryResult.Skip(skip).Take(results).ToListAsync();
|
|
|
|
totalCount = queryResult.Count();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
items = await access.Users.Skip(skip).Take(results).ToListAsync();
|
|
|
|
totalCount = access.Users.Count();
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
2023-10-09 01:58:53 +02:00
|
|
|
return new ListResult<UserItem>(items.Select(du => Utilities.ConvertFrom<UserItem, DbUser>(du) ?? new UserItem() { Uid = "NULL" }).Where(ui => ui.Uid != "NULL"), totalCount);
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
2023-10-09 01:58:53 +02:00
|
|
|
_log.Error(e, e.Message);
|
|
|
|
return new ListResult<UserItem>(null, 0, e.Message, ResultStatus.InternalFail);
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async Task<ResultOr<User>> GetUserAsync(string userId)
|
|
|
|
{
|
|
|
|
if (userId.IsNullEmptyWhiteSpace()) return new ResultOr<User>(null, "User ID is empty!", ResultStatus.Failed);
|
|
|
|
try
|
|
|
|
{
|
|
|
|
DbAccess access = new DbAccess(_configuration);
|
2023-10-09 01:58:53 +02:00
|
|
|
var dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
|
|
|
|
var user = dbUser?.ToDto();
|
2023-10-08 00:46:42 +02:00
|
|
|
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);
|
2023-10-09 01:58:53 +02:00
|
|
|
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);
|
2023-10-08 00:46:42 +02:00
|
|
|
}
|
2023-09-10 21:32:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|