Implemented users db & API side.

This commit is contained in:
Max 2023-10-08 00:46:42 +02:00
parent b114bf3a10
commit f95e985c7b
23 changed files with 390 additions and 198 deletions

View File

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

View File

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

View File

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

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

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

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

View File

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

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

View 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
}
}

View 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);
}
}

View File

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

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

View File

@ -12,14 +12,14 @@ namespace SharpRSS.API.Auth
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SessionAuthorizeAttribute : Attribute, IAuthorizationFilter
{
public SessionAuthorizeAttribute(string permission = "")
public SessionAuthorizeAttribute(string group = "")
{
_log = LogManager.CreateLogger(typeof(SessionAuthorizeAttribute));
_perm = permission;
_group = group;
}
private readonly ILog _log;
private readonly string _perm;
private readonly string _group;
public void OnAuthorization(AuthorizationFilterContext context)
{
@ -29,9 +29,10 @@ namespace SharpRSS.API.Auth
return;
}
return;
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;
}

View File

@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SharpRSS.API.Auth;
using SharpRSS.API.Contracts;
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.Models;
using SharpRSS.API.Models.Auth;
using ToolQit;
using ToolQit.Logging;
namespace SharpRSS.API.Controllers
{
@ -22,34 +19,64 @@ namespace SharpRSS.API.Controllers
public AuthController(AuthService authService)
{
_authService = authService;
_log = LogManager.CreateLogger(typeof(AuthController));
}
private readonly ILog _log;
private readonly AuthService _authService;
[HttpPost("[action]")]
[AllowAnonymous]
public async Task<ActionResult<string>> Authenticate(AuthenticateUser authenticateUser)
{ // Return test result
return Ok(new { Expires = DateTime.Now.Add(TimeSpan.FromDays(7)), SessionToken = Guid.NewGuid().ToString(), Released = DateTime.Now });
[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("user")]
[SessionAuthorize("auth:user:create")]
public async Task<ActionResult<UserDto>> CreateUser(AuthenticateUser authenticateUser)
[HttpPost("updateuser")]
[Produces("application/json")]
[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);
if (result.Success)
return Ok(Models.Auth.User.ToDto(result.Value ?? new User()));
return BadRequest(new ApiResult(result.Message, ApiResults.Error));
var updatedUserResult = await _authService.UpdateUser(payload);
return updatedUserResult.Success ? Ok(updatedUserResult) : updatedUserResult.Status == ResultStatus.Failed ? BadRequest(updatedUserResult) : StatusCode(StatusCodes.Status500InternalServerError, updatedUserResult);
}
[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")]
[SessionAuthorize("auth:user:get")]
public async Task<ActionResult<ApiListResult<IEnumerable<UserDto>>>> GetUsers(int take, int skip)
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ResultOr<User>>> GetUser(string userId)
{
var usersAuth = await _authService.GetUsers(take, skip);
List<UserDto> users = usersAuth.Value?.Select(Models.Auth.User.ToDto).ToList() ?? new List<UserDto>();
return Ok(new ApiListResult<IEnumerable<UserDto>>(users.Count, await _authService.UserCount(), users));
var userResult = await _authService.GetUserAsync(userId);
return userResult.Success ? Ok(userResult) : BadRequest(userResult);
}
[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);
}
}
}

View File

@ -1,13 +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.Models.User;
using SharpRSS.API.Cryptography;
using SharpRSS.API.Contracts.DTO;
using SharpRSS.API.Contracts.Models;
using SharpRSS.API.Contracts.Payloads;
using SharpRSS.API.Models;
using SharpRSS.API.Models.Auth;
using ToolQit;
using ToolQit.Extensions;
using ToolQit.Logging;
namespace SharpRSS.API.Data
@ -24,54 +25,121 @@ namespace SharpRSS.API.Data
private readonly IConfiguration _configuration;
private readonly ILog _log;
public async Task<Result<User>> CreateUser(AuthenticateUser authenticateUserRequest)
public async Task<ResultOr<User>> CreateUser(ModifyUser user)
{
bool result;
if (authenticateUserRequest.UserName.Any(char.IsWhiteSpace))
return new Result<User>(null, message: "Username should not contain space/whitespaces!");
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);
if (user.UserName.IsNullEmptyWhiteSpace() || user.Password.IsNullEmptyWhiteSpace())
return ResultOr<User>.Failed("Username or password cannot be empty!");
DbUser newUser;
try
{
int entries = await access.SaveChangesAsync();
result = entries > 0;
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);
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)
{
_log.Error(e, "Error creating user: {UserName}", user.UserName);
return new Result<User>(user, message: "Could not create user!");
_log.Error(e, "Failed to create user: {UserName}", user.UserName);
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)
take = 50;
await using DbAccess access = new DbAccess(_configuration);
IEnumerable<User> users = access.Users.Skip(skip).Take(take).ToList();
if (!users.Any())
return new Result<IEnumerable<User>>(users, false, "No users found!");
return new Result<IEnumerable<User>>(users, true, "Ok");
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);
}
}
public async Task<int> UserCount()
public async Task<Result> RemoveUserAsync(string userId)
{
await using DbAccess access = new DbAccess(_configuration);
return access.Users.Count();
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);
}
}
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);
}
}
}
}

View File

@ -1,32 +1,37 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using SharpRSS.API.Models.Auth;
using SharpRSS.API.Models;
using ToolQit;
using ToolQit.Logging;
namespace SharpRSS.API.Data
{
public sealed class DbAccess : DbContext
internal sealed class DbAccess : DbContext
{
public DbAccess(IConfiguration configuration)
{
_log = LogManager.CreateLogger(typeof(DbAccess));
_log.Debug("Initializing new context...");
_configuration = configuration;
_dbServer = _configuration["DataBase:Server"] ?? "Unknown";
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.LazyLoadingEnabled = false;
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 ILog _log;
private readonly string _dbServer;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connection = _configuration["DataBase:Connection"] ?? throw new ArgumentNullException(nameof(_configuration), "No connection string in appsettings!");
string server = _configuration["DataBase:Server"] ?? "Unknown";
switch (server)
switch (_dbServer)
{
case "MariaDB":
var dbSrvVersion = ServerVersion.AutoDetect(connection);
@ -34,14 +39,24 @@ namespace SharpRSS.API.Data
optionsBuilder.UseMySql(connection, dbSrvVersion);
break;
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!");
}
}
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);
}
}

View File

@ -1,6 +1,6 @@
namespace SharpRSS.API.Data
{
public class SharpRssService
public class RssService
{
}

View File

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

View File

@ -0,0 +1,9 @@
using SharpRSS.API.Contracts.DTO;
namespace SharpRSS.API.Models
{
internal class DbGroup : Group
{
}
}

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

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

View File

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

View File

@ -16,7 +16,6 @@ SetupSerilog();
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddSerilog();
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
@ -26,7 +25,7 @@ builder.Services.AddSwaggerGen(con =>
con.OperationFilter<SwaggerSessionHeader>();
});
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<SharpRssService>();
builder.Services.AddScoped<RssService>();
var app = builder.Build();