mirror of
https://github.com/hmaxnl/SharpRSS.git
synced 2024-11-10 16:04:19 +01:00
Implemented users db & API side.
This commit is contained in:
parent
b114bf3a10
commit
f95e985c7b
|
@ -1,15 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace SharpRSS.API.Contracts
|
|
||||||
{
|
|
||||||
public class ApiError
|
|
||||||
{
|
|
||||||
public ApiError()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public bool Logged { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
namespace SharpRSS.API.Contracts
|
|
||||||
{
|
|
||||||
public class ApiResult
|
|
||||||
{
|
|
||||||
public ApiResult(string message, ApiResults result)
|
|
||||||
{
|
|
||||||
Message = message;
|
|
||||||
Result = result;
|
|
||||||
}
|
|
||||||
public string Message { get; }
|
|
||||||
public ApiResults Result { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ApiResults
|
|
||||||
{
|
|
||||||
Ok,
|
|
||||||
Warning,
|
|
||||||
Error,
|
|
||||||
Invalid
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
namespace SharpRSS.API.Contracts.DTO
|
|
||||||
{
|
|
||||||
public class ApiListResult<TResultValue>
|
|
||||||
{
|
|
||||||
public ApiListResult(int hits, int total, TResultValue? data)
|
|
||||||
{
|
|
||||||
Hits = hits;
|
|
||||||
Total = total;
|
|
||||||
Data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Hits { get; }
|
|
||||||
public int Total { get; }
|
|
||||||
public TResultValue? Data { get; }
|
|
||||||
}
|
|
||||||
}
|
|
12
SharpRSS.API.Contracts/DTO/Group.cs
Normal file
12
SharpRSS.API.Contracts/DTO/Group.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Contracts.DTO
|
||||||
|
{
|
||||||
|
public class Group
|
||||||
|
{
|
||||||
|
public string Gid { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public bool Administrator { get; set; }
|
||||||
|
public DateTime DateCreated { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
11
SharpRSS.API.Contracts/DTO/Session.cs
Normal file
11
SharpRSS.API.Contracts/DTO/Session.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Contracts.DTO
|
||||||
|
{
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public string Sid { get; set; } = string.Empty;
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
SharpRSS.API.Contracts/DTO/User.cs
Normal file
14
SharpRSS.API.Contracts/DTO/User.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Contracts.DTO
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string Uid { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Gid { get; set; } = string.Empty;
|
||||||
|
public bool Active { get; set; }
|
||||||
|
public DateTime DateCreated { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace SharpRSS.API.Contracts.DTO
|
|
||||||
{
|
|
||||||
public class UserDto
|
|
||||||
{
|
|
||||||
public UserDto(string id, string userName, string mail, string role, DateTime dateCreated)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
UserName = userName;
|
|
||||||
Mail = mail;
|
|
||||||
Role = role;
|
|
||||||
DateCreated = dateCreated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
public string UserName { get; }
|
|
||||||
public string Mail { get; }
|
|
||||||
public string Role { get; }
|
|
||||||
public DateTime DateCreated { get; }
|
|
||||||
}
|
|
||||||
}
|
|
16
SharpRSS.API.Contracts/Models/ListResult.cs
Normal file
16
SharpRSS.API.Contracts/Models/ListResult.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Contracts.Models
|
||||||
|
{
|
||||||
|
public class ListResult<TItem> : ResultOr<IEnumerable<TItem>>
|
||||||
|
{
|
||||||
|
public ListResult(IEnumerable<TItem>? value, long total, string message = "", ResultStatus status = ResultStatus.Ok) : base(value, message, status)
|
||||||
|
{
|
||||||
|
TotalFound = total;
|
||||||
|
}
|
||||||
|
public long TotalFound { get; }
|
||||||
|
public int Hits => Value?.Count() ?? 0;
|
||||||
|
}
|
||||||
|
}
|
23
SharpRSS.API.Contracts/Models/Result.cs
Normal file
23
SharpRSS.API.Contracts/Models/Result.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace SharpRSS.API.Contracts.Models
|
||||||
|
{
|
||||||
|
public class Result
|
||||||
|
{
|
||||||
|
public Result(string message = "", ResultStatus status = ResultStatus.Ok)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Success => Status == ResultStatus.Ok;
|
||||||
|
public string Message { get; }
|
||||||
|
public ResultStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ResultStatus
|
||||||
|
{
|
||||||
|
Ok,
|
||||||
|
Failed,
|
||||||
|
InternalFail,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
}
|
17
SharpRSS.API.Contracts/Models/ResultOr.cs
Normal file
17
SharpRSS.API.Contracts/Models/ResultOr.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Contracts.Models
|
||||||
|
{
|
||||||
|
public class ResultOr<TValue> : Result
|
||||||
|
{
|
||||||
|
public ResultOr(TValue? value, string message, ResultStatus status) : base(message, status)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TValue? Value { get; }
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
namespace SharpRSS.API.Contracts.Models.User
|
namespace SharpRSS.API.Contracts.Payloads
|
||||||
{
|
{
|
||||||
public class AuthenticateUser
|
public abstract class AuthenticateUser
|
||||||
{
|
{
|
||||||
public string UserName { get; set; } = string.Empty;
|
public string UserName { get; set; } = string.Empty;
|
||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
12
SharpRSS.API.Contracts/Payloads/ModifyUser.cs
Normal file
12
SharpRSS.API.Contracts/Payloads/ModifyUser.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace SharpRSS.API.Contracts.Payloads
|
||||||
|
{
|
||||||
|
public class ModifyUser
|
||||||
|
{
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string GroupId { get; set; } = string.Empty;
|
||||||
|
public bool Active { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,14 +12,14 @@ 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 permission = "")
|
public SessionAuthorizeAttribute(string group = "")
|
||||||
{
|
{
|
||||||
_log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute));
|
_log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute));
|
||||||
_perm = permission;
|
_group = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ILog _log;
|
private readonly ILog _log;
|
||||||
private readonly string _perm;
|
private readonly string _group;
|
||||||
|
|
||||||
public void OnAuthorization(AuthorizationFilterContext context)
|
public void OnAuthorization(AuthorizationFilterContext context)
|
||||||
{
|
{
|
||||||
|
@ -29,9 +29,10 @@ namespace SharpRSS.API.Auth
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out StringValues val))
|
if (context.HttpContext.Request.Headers.TryGetValue("SRSS-Session", out StringValues val))
|
||||||
{
|
{
|
||||||
//TODO: if no permission check for valid session, if permission check if session has access!
|
//TODO: if no permission check for valid session, permission check if session has access!
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SharpRSS.API.Auth;
|
using SharpRSS.API.Auth;
|
||||||
using SharpRSS.API.Contracts;
|
|
||||||
using SharpRSS.API.Contracts.DTO;
|
using SharpRSS.API.Contracts.DTO;
|
||||||
using SharpRSS.API.Contracts.Models.User;
|
using SharpRSS.API.Contracts.Models;
|
||||||
|
using SharpRSS.API.Contracts.Payloads;
|
||||||
using SharpRSS.API.Data;
|
using SharpRSS.API.Data;
|
||||||
using SharpRSS.API.Models;
|
using ToolQit;
|
||||||
using SharpRSS.API.Models.Auth;
|
using ToolQit.Logging;
|
||||||
|
|
||||||
namespace SharpRSS.API.Controllers
|
namespace SharpRSS.API.Controllers
|
||||||
{
|
{
|
||||||
|
@ -22,34 +19,64 @@ namespace SharpRSS.API.Controllers
|
||||||
public AuthController(AuthService authService)
|
public AuthController(AuthService authService)
|
||||||
{
|
{
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
|
_log = LogManager.CreateLogger(typeof(AuthController));
|
||||||
}
|
}
|
||||||
|
private readonly ILog _log;
|
||||||
|
|
||||||
private readonly AuthService _authService;
|
private readonly AuthService _authService;
|
||||||
|
|
||||||
[HttpPost("[action]")]
|
[HttpPost("createuser")]
|
||||||
[AllowAnonymous]
|
[Produces("application/json")]
|
||||||
public async Task<ActionResult<string>> Authenticate(AuthenticateUser authenticateUser)
|
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||||
{ // Return test result
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
return Ok(new { Expires = DateTime.Now.Add(TimeSpan.FromDays(7)), SessionToken = Guid.NewGuid().ToString(), Released = DateTime.Now });
|
[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("user")]
|
[HttpPost("updateuser")]
|
||||||
[SessionAuthorize("auth:user:create")]
|
[Produces("application/json")]
|
||||||
public async Task<ActionResult<UserDto>> CreateUser(AuthenticateUser authenticateUser)
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<ActionResult<ResultOr<User>>> UpdateUser(ModifyUser payload)
|
||||||
{
|
{
|
||||||
Result<User> result = await _authService.CreateUser(authenticateUser);
|
var updatedUserResult = await _authService.UpdateUser(payload);
|
||||||
if (result.Success)
|
return updatedUserResult.Success ? Ok(updatedUserResult) : updatedUserResult.Status == ResultStatus.Failed ? BadRequest(updatedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, updatedUserResult);
|
||||||
return Ok(Models.Auth.User.ToDto(result.Value ?? new User()));
|
}
|
||||||
return BadRequest(new ApiResult(result.Message, ApiResults.Error));
|
|
||||||
|
[HttpDelete("deleteuser")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("user")]
|
[HttpGet("user")]
|
||||||
[SessionAuthorize("auth:user:get")]
|
[Produces("application/json")]
|
||||||
public async Task<ActionResult<ApiListResult<IEnumerable<UserDto>>>> GetUsers(int take, int skip)
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ResultOr<User>>> GetUser(string userId)
|
||||||
{
|
{
|
||||||
var usersAuth = await _authService.GetUsers(take, skip);
|
var userResult = await _authService.GetUserAsync(userId);
|
||||||
List<UserDto> users = usersAuth.Value?.Select(Models.Auth.User.ToDto).ToList() ?? new List<UserDto>();
|
return userResult.Success ? Ok(userResult) : BadRequest(userResult);
|
||||||
return Ok(new ApiListResult<IEnumerable<UserDto>>(users.Count, await _authService.UserCount(), users));
|
}
|
||||||
|
|
||||||
|
[HttpGet("users")]
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
var usersResult = await _authService.GetUsersAsync(results, skip, search);
|
||||||
|
return usersResult.Success ? Ok(usersResult) : usersResult.Status == ResultStatus.Failed ? BadRequest(usersResult) : StatusCode(StatusCodes.Status500InternalServerError, usersResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +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.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using SharpRSS.API.Contracts.Models.User;
|
using SharpRSS.API.Contracts.DTO;
|
||||||
using SharpRSS.API.Cryptography;
|
using SharpRSS.API.Contracts.Models;
|
||||||
|
using SharpRSS.API.Contracts.Payloads;
|
||||||
using SharpRSS.API.Models;
|
using SharpRSS.API.Models;
|
||||||
using SharpRSS.API.Models.Auth;
|
|
||||||
using ToolQit;
|
using ToolQit;
|
||||||
|
using ToolQit.Extensions;
|
||||||
using ToolQit.Logging;
|
using ToolQit.Logging;
|
||||||
|
|
||||||
namespace SharpRSS.API.Data
|
namespace SharpRSS.API.Data
|
||||||
|
@ -24,54 +25,121 @@ namespace SharpRSS.API.Data
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILog _log;
|
private readonly ILog _log;
|
||||||
|
|
||||||
public async Task<Result<User>> CreateUser(AuthenticateUser authenticateUserRequest)
|
public async Task<ResultOr<User>> CreateUser(ModifyUser user)
|
||||||
{
|
{
|
||||||
bool result;
|
if (user.UserName.IsNullEmptyWhiteSpace() || user.Password.IsNullEmptyWhiteSpace())
|
||||||
if (authenticateUserRequest.UserName.Any(char.IsWhiteSpace))
|
return ResultOr<User>.Failed("Username or password cannot be empty!");
|
||||||
return new Result<User>(null, message: "Username should not contain space/whitespaces!");
|
DbUser newUser;
|
||||||
|
|
||||||
await using DbAccess access = new DbAccess(_configuration);
|
|
||||||
var user = access.Users.FirstOrDefault(u => u.UserName == authenticateUserRequest.UserName);
|
|
||||||
if (user != null)
|
|
||||||
return new Result<User>(user, message:"User name already exists!");
|
|
||||||
|
|
||||||
byte[] hashedPwdBytes = Hasher.HashPassword(authenticateUserRequest.Password, out byte[] salt);
|
|
||||||
user = new User()
|
|
||||||
{
|
|
||||||
UserName = authenticateUserRequest.UserName,
|
|
||||||
Mail = "",
|
|
||||||
Password = hashedPwdBytes,
|
|
||||||
Salt = salt
|
|
||||||
};
|
|
||||||
access.Users.Add(user);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int entries = await access.SaveChangesAsync();
|
DbAccess access = new DbAccess(_configuration);
|
||||||
result = entries > 0;
|
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);
|
||||||
|
bool saved = await access.SaveChangesAsync() > 0;
|
||||||
|
if (!saved)
|
||||||
|
{
|
||||||
|
_log.Warning("Failed to save user: {UserName} to database", user.UserName);
|
||||||
|
return ResultOr<User>.Failed("Could not save user to database!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_log.Error(e, "Error creating user: {UserName}", user.UserName);
|
_log.Error(e, "Failed to create user: {UserName}", user.UserName);
|
||||||
return new Result<User>(user, message: "Could not create user!");
|
return ResultOr<User>.Failed($"Failed to create user: {user.UserName}", ResultStatus.InternalFail);
|
||||||
}
|
}
|
||||||
return new Result<User>(user, result, "Ok");
|
return ResultOr<User>.Ok(newUser.ToDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<IEnumerable<User>>> GetUsers(int take = 50, int skip = 0)
|
public async Task<ResultOr<User>> UpdateUser(ModifyUser user)
|
||||||
{
|
{
|
||||||
if (take is 0 or > 50)
|
if (user.UserName.IsNullEmptyWhiteSpace())
|
||||||
take = 50;
|
return ResultOr<User>.Failed("Username cannot be empty!");
|
||||||
await using DbAccess access = new DbAccess(_configuration);
|
try
|
||||||
IEnumerable<User> users = access.Users.Skip(skip).Take(take).ToList();
|
{
|
||||||
if (!users.Any())
|
DbAccess access = new DbAccess(_configuration);
|
||||||
return new Result<IEnumerable<User>>(users, false, "No users found!");
|
DbUser? dbUser = await access.Users.Where(u => u.Uid == user.UserName).FirstOrDefaultAsync();
|
||||||
return new Result<IEnumerable<User>>(users, true, "Ok");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> UserCount()
|
public async Task<Result> RemoveUserAsync(string userId)
|
||||||
{
|
{
|
||||||
await using DbAccess access = new DbAccess(_configuration);
|
if (userId.IsNullEmptyWhiteSpace())
|
||||||
return access.Users.Count();
|
return new Result("User id is empty!", ResultStatus.Failed);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DbAccess access = new DbAccess(_configuration);
|
||||||
|
DbUser? dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
|
||||||
|
if (dbUser == null)
|
||||||
|
return new Result($"Could not find user: '{userId}'", ResultStatus.Failed);
|
||||||
|
access.Users.Remove(dbUser);
|
||||||
|
bool removed = await access.SaveChangesAsync() > 0;
|
||||||
|
return new Result(removed ? "" : $"Failed to remove user: '{dbUser.Uid}'!", removed ? ResultStatus.Ok : ResultStatus.Failed);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_log.Error(e, e.Message);
|
||||||
|
return new Result(e.Message, ResultStatus.InternalFail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ListResult<User>> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "")
|
||||||
|
{
|
||||||
|
if (results is 0 or > 20) results = 20;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DbAccess access = new DbAccess(_configuration);
|
||||||
|
if (!searchQuery.IsNullEmptyWhiteSpace())
|
||||||
|
{
|
||||||
|
IQueryable<DbUser> queryResult = access.Users.Where(u =>
|
||||||
|
EF.Functions.Like(u.Uid, $"%{searchQuery}%") ||
|
||||||
|
EF.Functions.Like(u.Email, $"%{searchQuery}%") ||
|
||||||
|
EF.Functions.Like(u.DisplayName, $"%{searchQuery}%"));
|
||||||
|
var searchResults = await queryResult.Skip(skip).Take(results).ToListAsync();
|
||||||
|
return new ListResult<User>(searchResults, queryResult.Count());
|
||||||
|
}
|
||||||
|
var users = await access.Users.Skip(skip).Take(results).ToListAsync();
|
||||||
|
return new ListResult<User>(users, access.Users.Count());
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
User? user = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,32 +1,37 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using SharpRSS.API.Models.Auth;
|
using SharpRSS.API.Models;
|
||||||
using ToolQit;
|
using ToolQit;
|
||||||
using ToolQit.Logging;
|
using ToolQit.Logging;
|
||||||
|
|
||||||
namespace SharpRSS.API.Data
|
namespace SharpRSS.API.Data
|
||||||
{
|
{
|
||||||
public sealed class DbAccess : DbContext
|
internal sealed class DbAccess : DbContext
|
||||||
{
|
{
|
||||||
public DbAccess(IConfiguration configuration)
|
public DbAccess(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_log = LogManager.CreateLogger(typeof(DbAccess));
|
_log = LogManager.CreateLogger(typeof(DbAccess));
|
||||||
|
_log.Debug("Initializing new context...");
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_dbServer = _configuration["DataBase:Server"] ?? "Unknown";
|
||||||
|
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
ChangeTracker.LazyLoadingEnabled = false;
|
||||||
Database.EnsureCreated();
|
Database.EnsureCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<DbUser> Users { get; set; }
|
||||||
|
public DbSet<DbGroup> Groups { get; set; }
|
||||||
|
public DbSet<DbSession> Sessions { get; set; }
|
||||||
|
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILog _log;
|
private readonly ILog _log;
|
||||||
|
private readonly string _dbServer;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
string connection = _configuration["DataBase:Connection"] ?? throw new ArgumentNullException(nameof(_configuration), "No connection string in appsettings!");
|
string connection = _configuration["DataBase:Connection"] ?? throw new ArgumentNullException(nameof(_configuration), "No connection string in appsettings!");
|
||||||
string server = _configuration["DataBase:Server"] ?? "Unknown";
|
switch (_dbServer)
|
||||||
switch (server)
|
|
||||||
{
|
{
|
||||||
case "MariaDB":
|
case "MariaDB":
|
||||||
var dbSrvVersion = ServerVersion.AutoDetect(connection);
|
var dbSrvVersion = ServerVersion.AutoDetect(connection);
|
||||||
|
@ -34,14 +39,24 @@ namespace SharpRSS.API.Data
|
||||||
optionsBuilder.UseMySql(connection, dbSrvVersion);
|
optionsBuilder.UseMySql(connection, dbSrvVersion);
|
||||||
break;
|
break;
|
||||||
default: // TODO: Add more database support.
|
default: // TODO: Add more database support.
|
||||||
_log.Warning("No valid db server: {Server}/nSupported db servers: 'MariaDB'", server);
|
_log.Warning("No valid db server: {Server}\nSupported db servers: 'MariaDB'", _dbServer);
|
||||||
throw new Exception("Database server not specified!");
|
throw new Exception("Database server not specified!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<User>().ToTable("srss_user");
|
switch (_dbServer)
|
||||||
|
{
|
||||||
|
case "MariaDB":
|
||||||
|
modelBuilder.UseCollation("utf8mb4_unicode_520_ci");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
modelBuilder.Entity<DbUser>().ToTable("srss_users").HasKey(dbu => dbu.Uid);
|
||||||
|
modelBuilder.Entity<DbGroup>().ToTable("srss_groups").HasKey(dbg => dbg.Gid);
|
||||||
|
modelBuilder.Entity<DbSession>().ToTable("srss_sessions").HasKey(dbs => dbs.Sid);
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace SharpRSS.API.Data
|
namespace SharpRSS.API.Data
|
||||||
{
|
{
|
||||||
public class SharpRssService
|
public class RssService
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using SharpRSS.API.Contracts.DTO;
|
|
||||||
|
|
||||||
namespace SharpRSS.API.Models.Auth
|
|
||||||
{
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
public static UserDto ToDto(User user) => new UserDto(user.Id, user.UserName, user.Mail, user.Role, user.DateCreated);
|
|
||||||
|
|
||||||
[Key]
|
|
||||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
||||||
[Required]
|
|
||||||
public string UserName { get; set; }
|
|
||||||
[Required]
|
|
||||||
[EmailAddress]
|
|
||||||
public string Mail { get; set; }
|
|
||||||
[Required]
|
|
||||||
public byte[] Password { get; set; }
|
|
||||||
[Required]
|
|
||||||
public byte[] Salt { get; set; }
|
|
||||||
[Required]
|
|
||||||
public string Role { get; set; } = "User";
|
|
||||||
[Required]
|
|
||||||
public DateTime DateCreated { get; set; } = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
9
SharpRSS.API/Models/DbGroup.cs
Normal file
9
SharpRSS.API/Models/DbGroup.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using SharpRSS.API.Contracts.DTO;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Models
|
||||||
|
{
|
||||||
|
internal class DbGroup : Group
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
34
SharpRSS.API/Models/DbSession.cs
Normal file
34
SharpRSS.API/Models/DbSession.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Serilog;
|
||||||
|
using SharpRSS.API.Contracts.DTO;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Models
|
||||||
|
{
|
||||||
|
internal class DbSession : Session
|
||||||
|
{
|
||||||
|
public DbSession() { }
|
||||||
|
private DbSession(string uid, double expiresMinutes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid) || string.IsNullOrWhiteSpace(uid))
|
||||||
|
{
|
||||||
|
Log.Error("User id is null or empty cannot create session!");
|
||||||
|
throw new Exception("User id cannot be null!");
|
||||||
|
}
|
||||||
|
Uid = uid;
|
||||||
|
Created = DateTime.Now;
|
||||||
|
Expires = Created.AddMinutes(expiresMinutes);
|
||||||
|
using SHA1 sha1 = SHA1.Create();
|
||||||
|
string uidHash = Convert.ToHexString(sha1.ComputeHash(Encoding.UTF8.GetBytes(Uid)));
|
||||||
|
Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}";
|
||||||
|
}
|
||||||
|
public string Uid { get; set; }
|
||||||
|
|
||||||
|
public static DbSession CreateSession(string uid, double expiresMinutes = 10080)
|
||||||
|
{
|
||||||
|
DbSession newSession = new DbSession(uid, expiresMinutes);
|
||||||
|
return newSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
SharpRSS.API/Models/DbUser.cs
Normal file
50
SharpRSS.API/Models/DbUser.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SharpRSS.API.Contracts.DTO;
|
||||||
|
using SharpRSS.API.Contracts.Payloads;
|
||||||
|
using SharpRSS.API.Cryptography;
|
||||||
|
using ToolQit.Extensions;
|
||||||
|
|
||||||
|
namespace SharpRSS.API.Models
|
||||||
|
{
|
||||||
|
// Database model
|
||||||
|
internal class DbUser : User
|
||||||
|
{
|
||||||
|
public DbUser() { }
|
||||||
|
public DbUser(ModifyUser user)
|
||||||
|
{
|
||||||
|
Uid = user.UserName;
|
||||||
|
DisplayName = user.DisplayName;
|
||||||
|
Email = user.Email;
|
||||||
|
Gid = user.GroupId;
|
||||||
|
Active = user.Active;
|
||||||
|
PswHash = Hasher.HashPassword(user.Password, out byte[] salt);
|
||||||
|
Salt = salt;
|
||||||
|
}
|
||||||
|
public byte[] PswHash { get; set; }
|
||||||
|
public byte[] Salt { get; set; }
|
||||||
|
|
||||||
|
public User ToDto()
|
||||||
|
{
|
||||||
|
string json = JsonConvert.SerializeObject(this);
|
||||||
|
return JsonConvert.DeserializeObject<User>(json) ?? new User();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Update(ModifyUser user)
|
||||||
|
{
|
||||||
|
if (user == null) return false;
|
||||||
|
if (!user.DisplayName.IsNullEmptyWhiteSpace())
|
||||||
|
DisplayName = user.DisplayName;
|
||||||
|
if (!user.Email.IsNullEmptyWhiteSpace())
|
||||||
|
Email = user.Email;
|
||||||
|
if (!user.GroupId.IsNullEmptyWhiteSpace())
|
||||||
|
Gid = user.GroupId;
|
||||||
|
Active = user.Active;
|
||||||
|
if (!user.Password.IsNullEmptyWhiteSpace())
|
||||||
|
{
|
||||||
|
PswHash = Hasher.HashPassword(user.Password, out byte[] salt);
|
||||||
|
Salt = salt;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
namespace SharpRSS.API.Models
|
|
||||||
{
|
|
||||||
public class Result<TValue>
|
|
||||||
{
|
|
||||||
public Result(TValue? value, bool success = false, string message = "")
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
Success = success;
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
public TValue? Value { get; }
|
|
||||||
public bool Success { get; }
|
|
||||||
public string Message { get; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,6 @@ SetupSerilog();
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Logging.AddSerilog();
|
builder.Logging.AddSerilog();
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
@ -26,7 +25,7 @@ builder.Services.AddSwaggerGen(con =>
|
||||||
con.OperationFilter<SwaggerSessionHeader>();
|
con.OperationFilter<SwaggerSessionHeader>();
|
||||||
});
|
});
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<SharpRssService>();
|
builder.Services.AddScoped<RssService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user