This commit is contained in:
2023-02-19 00:43:43 +01:00
parent 9719a0c0fd
commit 1e66851113
146 changed files with 738 additions and 382 deletions

View File

@ -0,0 +1,129 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
using PrivaPub.Extensions;
using PrivaPub.Resources;
using PrivaPub.Services;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/admin")]
public class AdminController : ControllerBase
{
readonly IRootUsersService RootUsersService;
readonly ILogger<RootUserController> Logger;
readonly IStringLocalizer Localizer;
public AdminController(ILogger<RootUserController> logger,
IStringLocalizer<GenericRes> localizer,
IRootUsersService rootUsersService)
{
Logger = logger;
Localizer = localizer;
RootUsersService = rootUsersService;
}
[HttpDelete, Route("/clientapi/admin/remove/users"), Authorize(Policy = Policies.IsAdmin)]
public async Task<IActionResult> RemoveUsers([Required] UsersIds usersIds)
{
var result = new WebResult();
try
{
usersIds.UserIdList.Remove(User.GetUserId());
result = await RootUsersService.RemoveUserAsync(usersIds);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(RemoveUsers)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Authorize(Policy = Policies.IsAdmin), Route("/clientapi/admin/ban/users")]
public async Task<IActionResult> BanUsers([Required] UsersIds usersIds)
{
if (!ModelState.IsValid)
return BadRequest(usersIds);
var result = new WebResult();
try
{
//var isUserResult = await UsersService.UserIsAdminAsync(User.GetUserId());
//if (isUserResult.IsValid && !(bool)isUserResult.Data)
//if (isUserResult is { IsValid: true } and not { Data: bool })
// return Unauthorized();
usersIds.UserIdList.Remove(User.GetUserId());
result = await RootUsersService.BanUserAsync(usersIds);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(BanUsers)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Authorize(Policy = Policies.IsAdmin), Route("/clientapi/admin/unban/users")]
public async Task<IActionResult> UnbanUsers([Required] UsersIds usersIds)
{
if (!ModelState.IsValid)
return BadRequest(usersIds);
var result = new WebResult();
try
{
//var isUserResult = await UsersService.UserIsAdminAsync(User.GetUserId());
//if (isUserResult.IsValid && !(bool)isUserResult.Data)
// return Unauthorized();
usersIds.UserIdList.Remove(User.GetUserId());
result = await RootUsersService.UnbanUserAsync(usersIds);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(UnbanUsers)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
//[HttpGet, Authorize(Policy = Policies.IsAdmin)]
//public async Task<IActionResult> GetUsers()
//{
// var result = new WebResult();
// try
// {
// var isUserResult = await UsersService.UserIsAdminAsync(User.GetUserId());
// if (isUserResult.IsValid && !(bool)isUserResult.Data)
// return Unauthorized();
// result = await UsersService.GetUsersAsync();
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// return Ok(result.Data);
// }
// catch (Exception ex)
// {
// Logger.LogError(ex, $"{nameof(User)}.{nameof(GetUsers)}()");
// return BadRequest(result.Invalidate(ex.Message));
// }
//}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.Data;
using PrivaPub.Services.ClientToServer.Public;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/data")]
public class DataController : ControllerBase
{
readonly IDataService DataService;
readonly ILogger<DataController> Logger;
public DataController(IDataService dataService,
ILogger<DataController> logger)
{
DataService = dataService;
Logger = logger;
}
[HttpGet, Route("/clientapi/data/ping"), AllowAnonymous]
public async ValueTask<IActionResult> Ping() => NoContent();
[HttpGet, Route("/clientapi/data/current-version"), AllowAnonymous]
public IActionResult CurrentVersion() =>
Ok(DataService.GetCurrentVersion());
[HttpGet, Route("/clientapi/data/languages"), AllowAnonymous]
public async Task<IActionResult> Languages(CancellationToken cancellationToken)
{
var result = new WebResult();
try
{
var languages = await DataService.GetLanguages(cancellationToken);
var viewLanguages = new List<ViewLanguage>();
viewLanguages.AddRange(languages.Select(l => new ViewLanguage
{
Name = l.NativeName,
International2Code = l.International2Code
}).ToArray());
return Ok(viewLanguages);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(DataController)}.{nameof(Languages)}()");
return BadRequest(result.Invalidate(ex.Message, exception: ex));
}
}
}
}

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/moderator")]
public class ModeratorController : ControllerBase
{
}
}

View File

@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.Extensions;
using PrivaPub.Resources;
using PrivaPub.Services.ClientToServer.Private;
using PrivaPub.ClientModels.User.Avatar;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/avatar/private"),
Authorize(Policy = Policies.IsUser)]
public class PrivateAvatarController : ControllerBase
{
readonly ILogger<PrivateAvatarController> _logger;
readonly IPrivateAvatarUsersService _privateAvatarUsersService;
readonly IStringLocalizer _localizer;
public PrivateAvatarController(IPrivateAvatarUsersService privateAvatarUsersService,
IStringLocalizer<GenericRes> localizer,
ILogger<PrivateAvatarController> logger)
{
_privateAvatarUsersService = privateAvatarUsersService;
_localizer = localizer;
_logger = logger;
}
[HttpPost, Route("/clientapi/avatar/private/insert")]
public async Task<IActionResult> InsertAvatar(InsertAvatarForm model)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(_localizer["Invalid model."]));
try
{
model.RootId = User.GetUserId();
result = await _privateAvatarUsersService.InsertAvatar(model);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result.Data);
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PrivateAvatarController)}.{nameof(InsertAvatar)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/avatar/private/update")]
public async Task<IActionResult> UpdateAvatar(UpdateAvatarForm model)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(_localizer["Invalid model."]));
try
{
model.RootId = User.GetUserId();
result = await _privateAvatarUsersService.UpdateAvatar(model);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result.Data);
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PrivateAvatarController)}.{nameof(UpdateAvatar)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
}
}

View File

@ -0,0 +1,404 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.Resources;
using PrivaPub.ClientModels.User;
using PrivaPub.Extensions;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.Services;
using PrivaPub.StaticServices;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.Controllers.ClientToServer
{
[ApiController,
Route("clientapi/user")]
public class RootUserController : ControllerBase
{
readonly IRootUsersService UsersService;
readonly AuthTokenManager AuthTokenManager;
readonly ILogger<RootUserController> Logger;
readonly IStringLocalizer Localizer;
public RootUserController(IRootUsersService usersService,
AuthTokenManager authTokenManager,
IStringLocalizer<GenericRes> localizer,
ILogger<RootUserController> logger)
{
UsersService = usersService;
AuthTokenManager = authTokenManager;
Localizer = localizer;
Logger = logger;
}
#region User endpoints
[HttpPost, Route("/clientapi/user/signup"), Authorize(Policy = Policies.IsUser), AllowAnonymous]
public async Task<IActionResult> SignUp(LoginForm signUpForm)
{
if (User.Identity?.IsAuthenticated ?? false) return Redirect("/");
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.SignUpAsync(signUpForm);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
(var user, var userSettings) = ((RootUser, ViewAvatarServer))result.Data;
var jwtUser = AuthTokenManager.GenerateToken(user, userSettings);
Logger.LogInformation(
$"{nameof(SignUp)}();IP:[{HttpContext.Connection?.RemoteIpAddress}];\nUser-Agent:[{Request.Headers["User-Agent"]}];\nUserId:[{user.ID}]");
return Ok(jwtUser);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(SignUp)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/login"), Authorize(Policy = Policies.IsUser), AllowAnonymous]
public async Task<IActionResult> Login(LoginForm loginForm)
{
if (User.Identity?.IsAuthenticated ?? false) return Redirect("/discussions");
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.LoginAsync(loginForm);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
var (user, userSettings) =
((RootUser, ViewAvatarServer))result.Data;
var jwtUser = AuthTokenManager.GenerateToken(user, userSettings);
Logger.LogInformation(
$"{nameof(Login)}();IP:[{HttpContext.Connection?.RemoteIpAddress}];\nUser-Agent:[{Request.Headers["User-Agent"]}];\nUserId:[{user.ID}]");
return Ok(jwtUser);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(Login)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
//[HttpPost, Authorize(Policy = Policies.IsUser), AllowAnonymous]
//public async Task<IActionResult> InvitationSignUp(InvitationLoginForm signUpForm)
//{
// if (User.Identity?.IsAuthenticated ?? false) return Redirect("/discussions");
// var result = new WebResult();
// if (!ModelState.IsValid)
// return BadRequest(result.Invalidate(Localizer["Invalid model."]));
// try
// {
// result = await DiscussionsService.GetInvitationConfiguration(signUpForm.InvitationCode, includePassword: true);
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// var configuration = result.Data as InvitationConfiguration;
// if (configuration.IsPasswordRequired && signUpForm.InvitationPassword != configuration.Password)
// {
// result.Invalidate(Localizer["Invalid password."], StatusCodes.Status406NotAcceptable);
// return StatusCode(result.StatusCode, result);
// }
// result = await UsersService.SignUpAsync(new LoginForm
// {
// Username = signUpForm.Username,
// Password = signUpForm.Password,
// LightThemeIndexColour = signUpForm.ThemeIndexColour,
// ThemeIsDarkMode = signUpForm.ThemeIsDarkMode,
// InvitationPassword = signUpForm.InvitationPassword
// }, signUpForm.InvitationCode, configuration.IsPasswordRequired);
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// (var user, var userSettings) = ((User, ViewUserSettings))result.Data;
// var jwtUser = AuthTokenManager.GenerateToken(user, userSettings);
// Logger.LogInformation(
// $"{nameof(InvitationSignUp)}();IP:[{HttpContext.Connection?.RemoteIpAddress}];\nUser-Agent:[{Request.Headers["User-Agent"]}];\nUserId:[{user.ID}]");
// return Ok(jwtUser);
// }
// catch (Exception ex)
// {
// Logger.LogError(ex, $"{nameof(User)}.{nameof(InvitationSignUp)}()");
// return BadRequest(result.Invalidate(ex.Message));
// }
//}
//[HttpPost, Authorize(Policy = Policies.IsUser), AllowAnonymous]
//public async Task<IActionResult> InvitationLogin(InvitationLoginForm loginForm)
//{
// if (User.Identity?.IsAuthenticated ?? false) return Redirect("/discussions" + loginForm.InvitationCode);
// var result = new WebResult();
// if (!ModelState.IsValid)
// return BadRequest(result.Invalidate(Localizer["Invalid model."]));
// try
// {
// result = await DiscussionsService.GetInvitationConfiguration(loginForm.InvitationCode, includePassword: true);
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// var configuration = result.Data as InvitationConfiguration;
// if (configuration.IsPasswordRequired && loginForm.InvitationPassword != configuration.Password)
// {
// result.Invalidate(Localizer["Invalid password."], StatusCodes.Status406NotAcceptable);
// return StatusCode(result.StatusCode, result);
// }
// result = await UsersService.LoginAsync(new LoginForm
// {
// Username = loginForm.Username,
// Password = loginForm.Password,
// ThemeIsDarkMode = loginForm.ThemeIsDarkMode,
// LightThemeIndexColour = loginForm.ThemeIndexColour,
// InvitationPassword = loginForm.InvitationPassword
// }, loginForm.InvitationCode);
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// var (user, userSettings/*, userCurrentTier*/) = ((User, ViewUserSettings/*, UserCurrentTier*/))result.Data;
// var jwtUser = AuthTokenManager.GenerateToken(user, userSettings, 0
// /*userCurrentTier == null ? default : (userCurrentTier.EndPeriod - DateTime.UtcNow).Days*/);
// Logger.LogInformation(
// $"{nameof(InvitationLogin)}();IP:[{HttpContext.Connection?.RemoteIpAddress}];\nUser-Agent:[{Request.Headers["User-Agent"]}];\nUserId:[{user.ID}]");
// return Ok(jwtUser);
// }
// catch (Exception ex)
// {
// Logger.LogError(ex, $"{nameof(User)}.{nameof(InvitationLogin)}()");
// return BadRequest(result.Invalidate(ex.Message));
// }
//}
[HttpGet, Route("/clientapi/user/logout"), Authorize(Policy = Policies.IsUser)]
public IActionResult Logout()
{
var result = new WebResult();
try
{
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(Logout)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/update"), Authorize(Policy = Policies.IsUser)]
public async Task<IActionResult> UpdateUser(UserForm userEmailForm)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.UpdateUserAsync(userEmailForm, User.GetUserId());
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdateUser)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/update/settings"), Authorize(Policy = Policies.IsUser)]
public async Task<IActionResult> UpdateUserSettings(ViewAvatarServer userSettings)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.UpdateUserSettingsAsync(userSettings, User.GetUserId());
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdateUserSettings)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/update/password"), Authorize(Policy = Policies.IsUser)]
public async Task<IActionResult> UpdatePassword(UserPasswordForm userPasswordForm)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.UpdateUserPasswordAsync(userPasswordForm, User.GetUserId());
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok();
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdatePassword)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
//[HttpGet, Authorize(Policy = Policies.IsUser)]
//public async Task<IActionResult> GetUser()
//{
// var result = new WebResult();
// try
// {
// result = await UsersService.GetUserAsync(User.GetUserId());
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// return Ok(result.Data);
// }
// catch (Exception ex)
// {
// Logger.LogError(ex, $"{nameof(User)}.{nameof(GetUser)}()");
// return BadRequest(result.Invalidate(ex.Message));
// }
//}
[HttpGet, Route("/clientapi/user/settings"), Authorize(Policy = Policies.IsUser)]
public async Task<IActionResult> GetUserSettings()
{
var result = new WebResult();
try
{
result = await UsersService.GetUserSettingsAsync(User.GetUserId());
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result.Data);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(GetUserSettings)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/recover/password"), AllowAnonymous]
public async Task<IActionResult> RecoverPassword(PasswordRecoveryForm passwordRecoveryForm)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
var host = $"{Request.Scheme}://{Request.Host.Host}";
result = await UsersService.SetupAndSendRecoveryEmail(passwordRecoveryForm, host);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(RecoverPassword)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/recover/valid"), AllowAnonymous]
public async Task<IActionResult> IsValidRecoveryCode(
[FromBody,
Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "RecoveryCode", ResourceType = typeof(FieldsNameResource))]
string recoveryCode)
{
var result = new WebResult();
try
{
result = await UsersService.IsValidRecoveryCode(recoveryCode);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result.Data);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(IsValidRecoveryCode)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
[HttpPost, Route("/clientapi/user/recover/update/password"), AllowAnonymous]
public async Task<IActionResult> ChangePassword(NewPasswordForm newPasswordForm)
{
var result = new WebResult();
if (!ModelState.IsValid)
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
try
{
result = await UsersService.ChangePassword(newPasswordForm);
if (!result.IsValid)
return StatusCode(result.StatusCode, result);
return Ok(result.Data);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(User)}.{nameof(ChangePassword)}()");
return BadRequest(result.Invalidate(ex.Message));
}
}
//[HttpDelete, Route("delete"), Authorize(Policy = Policies.IsUser)]
//public async Task<IActionResult> RemoveSelf()
//{
// var result = new WebResult();
// try
// {
// //result = await UsersService.RemoveUserAsync(User.GetUserId());
// if (!result.IsValid)
// return StatusCode(result.StatusCode, result);
// await HttpContext.SignOutAsync();
// return Ok();
// }
// catch (Exception ex)
// {
// Logger.LogError(ex, $"{nameof(User)}.{nameof(RemoveSelf)}()");
// return BadRequest(result.Invalidate(ex.Message));
// }
//}
#endregion User endpoints
#region Auth refresh
[HttpGet, Route("/clientapi/user/sniff/again"), Authorize(Policy = Policies.IsUser)]
public async Task<IActionResult> SniffAgain()
{
return Ok();
}
#endregion
}
}

View File

@ -0,0 +1,111 @@
using Markdig;
using Microsoft.AspNetCore.Mvc;
using PrivaPub.ClientModels.Resources;
using PrivaPub.Extensions;
using PrivaPub.Models.ActivityPub;
using PrivaPub.Models.Group;
using PrivaPub.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
namespace PrivaPub.Controllers.ServerToServer
{
[ApiController,
Route("peasants"), Produces("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"; charset=utf-8")]
public class PeasantsController : ControllerBase
{
readonly IGroupUsersService _groupUsersService;
public PeasantsController(IGroupUsersService groupUsersService)
{
_groupUsersService = groupUsersService;
}
[HttpGet, Route("{actor}")]
public async Task<IActionResult> GetActor(
[Required(ErrorMessageResourceName = "Required",
ErrorMessageResourceType = typeof(FieldsNameResource))] string actor,
CancellationToken cancellation)
{
var getResult = await _groupUsersService.GetGroup(actor, cancellation);
if (!getResult.IsValid)
return StatusCode(getResult.StatusCode, getResult);
var actorResponse = new ActivityPubActor();
actorResponse.Context[1] = string.Format(actorResponse.Context[1].ToString(), HttpContext.Request.PathBase);
actorResponse.Id = HttpContext.GetHostWithPath();
actorResponse.ProfileURL = HttpContext.GetHostWithPath();
actorResponse.Inbox = $"{HttpContext.GetHostWithPath()}/mouth";
actorResponse.Outbox = $"{HttpContext.GetHostWithPath()}/anus";
actorResponse.Endpoints = new()
{
SharedInboxURL = $"{HttpContext.GetHostWithPath()}/human-centipede",
OAuthAuthorizationEndpoint = $"{HttpContext.GetHost()}/sniff/again",
};
actorResponse.PreferredUsername = actor;
actorResponse.Name = actor;
if (getResult.Data is DmGroup dmGroup)
{
actorResponse.Summary = Markdown.ToHtml(dmGroup.Description);
actorResponse.Published = dmGroup.CreationDate;
}
else if (getResult.Data is Group dbGroup)
{
actorResponse.Summary = Markdown.ToHtml(dbGroup.Description);
actorResponse.Published = dbGroup.CreationDate;
}
return Ok(actorResponse);
}
[HttpGet, Route("{actor}/anus")]
public async Task<IActionResult> Anus(
[FromQuery] bool? page,
[Required(ErrorMessageResourceName = "Required",
ErrorMessageResourceType = typeof(FieldsNameResource))] string actor)
{
if (!page.HasValue)
return Ok(new ActivityPubOrderedCollection
{
Id = HttpContext.Request.Path,
FirstItem = $"{HttpContext.GetHostWithPath()}?page=true",
});
return Ok();
}
[HttpPost, Route("{actor}/mouth")]
public async Task<IActionResult> Month(
[Required(ErrorMessageResourceName = "Required",
ErrorMessageResourceType = typeof(FieldsNameResource))] string actor,
[FromBody] JsonDocument json)
{
return Ok();
}
[HttpPost, Route("{actor}/human-centipede")]
public async Task<IActionResult> HumanCentipede(
[Required(ErrorMessageResourceName = "Required",
ErrorMessageResourceType = typeof(FieldsNameResource))] string actor,
[FromBody] JsonDocument json)
{
return Ok();
}
[HttpPost, Route("/human-centipede")]
public async Task<IActionResult> PublicHumanCentipede(
[FromBody] JsonDocument json)
{
return Ok();
}
}
}

View File

@ -0,0 +1,69 @@
using Markdig;
using Microsoft.AspNetCore.Mvc;
using PrivaPub.ClientModels.Resources;
using PrivaPub.Extensions;
using PrivaPub.Models.ActivityPub;
using PrivaPub.Models.Group;
using PrivaPub.Services;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.Controllers.ServerToServer
{
[ApiController,
Route("users"), Produces("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"; charset=utf-8")]
public class UsersController : ControllerBase
{
readonly IRootUsersService _rootUsersService;
public UsersController(IRootUsersService rootUsersService)
{
_rootUsersService = rootUsersService;
}
[HttpGet, Route("{actor}")]
public async Task<IActionResult> GetActor(
[Required(ErrorMessageResourceName = "Required",
ErrorMessageResourceType = typeof(FieldsNameResource))] string actor,
CancellationToken cancellation)
{
var getResult = await _groupUsersService.GetGroup(actor, cancellation);
if (!getResult.IsValid)
return StatusCode(getResult.StatusCode, getResult);
var actorResponse = new ActivityPubActor();
actorResponse.Context[1] = string.Format(actorResponse.Context[1].ToString(), HttpContext.Request.PathBase);
actorResponse.Id = $"{HttpContext.GetHost()}/peasants/{actor}";
actorResponse.ProfileURL = $"{HttpContext.GetHost()}/peasants/{actor}";
actorResponse.Inbox = $"{HttpContext.GetHost()}/peasants/{actor}/mouth";
actorResponse.Outbox = $"{HttpContext.GetHost()}/peasants/{actor}/anus";
actorResponse.Endpoints = new()
{
SharedInboxURL = $"{HttpContext.GetHost()}/peasants/{actor}/human-centipede",
OAuthAuthorizationEndpoint = $"{HttpContext.GetHost()}/sniff/again",
};
actorResponse.PreferredUsername = actor;
actorResponse.Name = actor;
if (getResult.Data is DmGroup dmGroup)
{
actorResponse.Summary = Markdown.ToHtml(dmGroup.Description);
actorResponse.Published = dmGroup.CreationDate;
}
else if (getResult.Data is Group dbGroup)
{
actorResponse.Summary = Markdown.ToHtml(dbGroup.Description);
actorResponse.Published = dbGroup.CreationDate;
}
return Ok(actorResponse);
}
#region Admin
#endregion
}
}

213
PrivaPub/Data/InitDb.cs Normal file
View File

@ -0,0 +1,213 @@
using MongoDB.Entities;
using PrivaPub.Models.Data;
using PrivaPub.StaticServices;
using System.Text;
using System.Text.Json;
namespace PrivaPub.Data
{
public static class InitDb
{
public static async Task Init(this DbEntities dbClient, IPasswordHasher passwordHasher)
{
await SyncLanguages(dbClient);
await UpdateNewLanguages(dbClient);
//await SyncUserSettings(dbClient);
//await ClearUsers(dbClient);
//var botsInsertionAwaiter = InsertBots(dbClient, passwordHasher);
//var updateAnswersAwaiter = UpdateAnswersLikes(dbClient);
//Task.WaitAll(botsInsertionAwaiter);
//await SyncShareDiscussions(dbClient);
}
//static async Task UpdateAnswersLikes(DbCollections dbClient)
//{
// if (await dbClient.AnswersLikes.Find(al => al.AnswerId != null).AnyAsync()) return;
// foreach (var answerLikes in await dbClient.AnswersLikes.Find(al => true).ToListAsync())
// {
// answerLikes.AnswerId = answerLikes.Id;
// answerLikes.Id = ObjectId.GenerateNewId();
// await dbClient.AnswersLikes.InsertOneAsync(answerLikes);
// _ = await dbClient.AnswersLikes.DeleteOneAsync(al => al.Id == answerLikes.AnswerId);
// }
//}
//static async Task ClearUsers(DbColl dbClient)
//{
// await dbClient.Users.DeleteManyAsync(u => true);
// await dbClient.UsersSettings.DeleteManyAsync(u => true);
// await dbClient.Answers.DeleteManyAsync(u => true);
// await dbClient.AnswersLikes.DeleteManyAsync(u => true);
// await dbClient.Comments.DeleteManyAsync(u => true);
// await dbClient.EmailRecoveries.DeleteManyAsync(u => true);
// await dbClient.Threads.DeleteManyAsync(u => true);
// await dbClient.EDiscussions.DeleteManyAsync(u => true);
// await dbClient.Logs.DeleteManyAsync(u => true);
// await dbClient.ShareDiscussions.De(u => true);
// await dbClient.DiscussionFiles.DeleteManyAsync(u => true);
// await dbClient.UsersGroups.DeleteManyAsync(u => true);
//}
// static async Task SyncShareDiscussions(DbColl dbClient)
// {
// var anyThread = await dbClient.Discussions.Match(t => true).ExecuteAnyAsync();
// var anyShareDiscussion = await dbClient.ShareDiscussions.Match(st => true).ExecuteAnyAsync();
//
// if (anyThread == anyShareDiscussion) return;
//
// var discussions = await dbClient.Discussions.ManyAsync(t => true);
//
// foreach (var discussion in discussions)
// {
// var newShareDiscussion = new ShareDiscussion
// {
// DiscussionId = discussion.ID,
// InvitationCode = $"{Guid.NewGuid():N}{Guid.NewGuid():N}"
// };
// await newShareDiscussion.SaveAsync();
// }
// }
static async Task SyncLanguages(DbEntities dbClient)
{
if (await dbClient.Languages.ExecuteAnyAsync()) return;
var languagesJson = await File.ReadAllTextAsync(Path.Combine(Directory.GetCurrentDirectory(), "Data", "languagesNative.json"), Encoding.UTF8);
var languagesRows = JsonSerializer.Deserialize<IEnumerable<LanguagesRow>>(languagesJson);
var languages = new List<Language>();
foreach (var languageRow in languagesRows ?? new LanguagesRow[]
{
})
languages.Add(new Language
{
EnglishName = languageRow.EnglishName,
NativeName = languageRow.NativeName,
International2Code = languageRow.International2Code
});
await DB.SaveAsync(languages);
}
static async Task UpdateNewLanguages(DbEntities dbClient)
{
var bulgarianLanguage = await dbClient.Languages.Match(l => l.International2Code == "bg").ExecuteFirstAsync();
if (bulgarianLanguage.NativeName == "Български") return;
var languagesToUpdate = new Dictionary<string, string>()
{
{
"bg", "Български"
},
{
"cs", "Česky"
},
{
"da", "Dansk"
},
{
"nl", "Nederlands"
},
{
"et", "Eesti"
},
{
"fi", "Suomalainen"
},
{
"el", "Ελληνική"
},
{
"hu", "Magyar"
},
{
"lv", "Latviešu"
},
{
"lt", "Lietuvių kalba"
},
{
"pl", "Polski"
},
{
"pt", "Português"
},
{
"ro", "Românesc"
},
{
"sk", "Slovenská"
},
{
"sl", "Slovenski"
},
{
"sv", "Svenska"
}
};
foreach (var languageToUpdate in languagesToUpdate)
{
var bgLang = await dbClient.Languages.Match(l => l.International2Code == languageToUpdate.Key).ExecuteFirstAsync();
bgLang.NativeName = languageToUpdate.Value;
await bgLang.SaveAsync();
}
}
// static async Task SyncUserSettings(DbColl dbClient)
// {
// if (!await dbClient.Users.ExecuteAnyAsync()) return;
//
// var users = await dbClient.Users.ManyAsync(u => true);
// var usersId = users.Select(u => u.ID).ToList();
// var usersSettings = await dbClient.UsersSettings
// .ManyAsync(us => true);
//
// var usersWithoutSettings = users.Where(u => !usersSettings.Any(us => us.UserId == u.ID)).Select(u => u.ID).ToList();
// if (usersWithoutSettings.Count == 0) return;
//
// var defaultLanguage = await dbClient.Languages
// .Match(l => l.International2Code == "en")
// .ExecuteFirstAsync();
// foreach (var userId in usersWithoutSettings)
// await DB.SaveAsync(new UserSettings
// {
// UserId = userId,
// LanguageId = defaultLanguage.International2Code
// });
// }
//static async Task InsertBots(DbEntities dbClient, IPasswordHasher passwordHasher)
//{
// if (await dbClient.Users.Match(u => u.UserName == "bot0").ExecuteAnyAsync()) return;
// var newBots = new List<User>();
// var newBotsUserSettings = new List<UserSettings>();
// for (int i = 0; i < 100; i++)
// newBots.Add(new User
// {
// HashedPassword = passwordHasher.Hash("Asdfmov13!!!"),
// UserName = $"bot{i}"
// });
// await newBots.SaveAsync();
// var defaultLanguage = await dbClient.Languages
// .Match(l => l.International2Code == "en")
// .ExecuteFirstAsync();
// foreach (var newBot in newBots)
// newBotsUserSettings.Add(new()
// {
// UserId = newBot.ID,
// LanguageId = defaultLanguage.International2Code,
// });
// await newBotsUserSettings.SaveAsync();
//}
}
}

View File

@ -0,0 +1,9 @@
namespace PrivaPub.Data
{
public class LanguagesRow
{
public string EnglishName { get; set; }
public string NativeName { get; set; }
public string International2Code { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Tokens;
using PrivaPub.Services;
using System.Text;
namespace PrivaPub.Extensions
{
public static class AddAuthExtension
{
public static AuthenticationBuilder AddPrivaPubAuth(this AuthenticationBuilder builder, IConfiguration configuration)
{
builder.AddJwtBearer(options => {
#if DEBUG
options.RequireHttpsMetadata = false;
#endif
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["AppConfiguration:Jwt:Issuer"],
ValidAudience = configuration["AppConfiguration:Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["AppConfiguration:Jwt:Key"]))
};
options.Events = new JwtEvents();
});
return builder;
}
}
}

View File

@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Authorization;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using PrivaPub.ClientModels;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Math;
using PrivaPub.Models.User;
using System.Security.Claims;
namespace PrivaPub.Extensions
{
public static class Extensions
{
public static string GetLogsConnectionString(this IConfiguration configuration) =>
configuration.GetSection("Serilog")
?.GetSection("WriteTo")
?.GetChildren()
?.First()
?.GetSection("Args")
?.GetSection("databaseUrl")
?.Value;
public static AuthorizationPolicy IsAdminPolicy() =>
new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
.RequireClaim(Policies.IsAdmin, true.ToString().ToLower())
.RequireClaim(Policies.IsUser, true.ToString().ToLower())
.RequireClaim(Policies.IsModerator, true.ToString().ToLower())
.Build();
public static AuthorizationPolicy IsUserPolicy() =>
new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
.RequireClaim(Policies.IsUser, true.ToString().ToLower())
.Build();
public static AuthorizationPolicy IsModeratorPolicy() =>
new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
.RequireClaim(Policies.IsUser, true.ToString().ToLower())
.RequireClaim(Policies.IsModerator, true.ToString().ToLower())
.Build();
public static string GetHostWithPath(this HttpContext httpContext) =>
$"https://{httpContext.Request.Host}{httpContext.Request.Path}";
public static string GetHost(this HttpContext httpContext) =>
$"https://{httpContext.Request.Host}";
public static X509Certificate2 GetX509Certificate2(string certName)
{
var keypairgen = new RsaKeyPairGenerator();
keypairgen.Init(new KeyGenerationParameters(new SecureRandom(new CryptoApiRandomGenerator()), 512));
var keypair = keypairgen.GenerateKeyPair();
var gen = new X509V3CertificateGenerator();
var CN = new X509Name("CN=" + certName);
var SN = BigInteger.ProbablePrime(120, new Random());
gen.SetSerialNumber(SN);
gen.SetSubjectDN(CN);
gen.SetIssuerDN(CN);
gen.SetNotAfter(DateTime.MaxValue);
gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
gen.SetSignatureAlgorithm("MD5WithRSA");
gen.SetPublicKey(keypair.Public);
var newCert = gen.Generate(keypair.Private);
return new X509Certificate2(DotNetUtilities.ToX509Certificate((Org.BouncyCastle.X509.X509Certificate)newCert));
}
public static UserPolicyType GetHighestPolicy(this ClaimsPrincipal claimsPrincipal)
{
if (bool.Parse(claimsPrincipal.FindFirstValue(Policies.IsAdmin)))
return UserPolicyType.IsAdmin;
if (bool.Parse(claimsPrincipal.FindFirstValue(Policies.IsModerator)))
return UserPolicyType.IsModerator;
if (bool.Parse(claimsPrincipal.FindFirstValue(Policies.IsUser)))
return UserPolicyType.IsUser;
return UserPolicyType.IsUser;
}
}
}

View File

@ -0,0 +1,23 @@

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace PrivaPub.Extensions
{
public class OperationCancelledExceptionFilter : ExceptionFilterAttribute
{
readonly Serilog.ILogger Logger;
public OperationCancelledExceptionFilter(Serilog.ILogger logger) =>
Logger = logger.ForContext<OperationCancelledExceptionFilter>();
public override void OnException(ExceptionContext context)
{
if (context.Exception is not OperationCanceledException) return;
Logger.Information($"Request for {context.HttpContext.Request.Path} was cancelled.");
context.ExceptionHandled = true;
context.Result = new StatusCodeResult(499);
}
}
}

View File

@ -0,0 +1,36 @@
using PrivaPub.ClientModels;
using System.Security.Claims;
namespace PrivaPub.Extensions
{
public static class StringExtensions
{
public static string GetUserId(this ClaimsPrincipal claimsPrincipal)
{
return claimsPrincipal?.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.UserData)?.Value;
}
public static string GetUserName(this ClaimsPrincipal claimsPrincipal)
{
var userId = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
return string.IsNullOrEmpty(userId) ? default : userId;
}
public static List<string> GetUserPolicies(this ClaimsPrincipal claimsPrincipal)
{
var policies = new List<string>();
if (claimsPrincipal.Claims.Any(c => c.Type == Policies.IsAdmin && bool.Parse(c.Value)))
policies.Add(Policies.IsAdmin);
if (claimsPrincipal.Claims.Any(c => c.Type == Policies.IsUser && bool.Parse(c.Value)))
policies.Add(Policies.IsUser);
if (claimsPrincipal.Claims.Any(c => c.Type == Policies.IsModerator && bool.Parse(c.Value)))
policies.Add(Policies.IsModerator);
return policies;
}
}
}

View File

@ -0,0 +1,216 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.ResponseCompression;
using PrivaPub.ClientModels;
using PrivaPub.Extensions;
using PrivaPub.Models;
using PrivaPub.Services;
using PrivaPub.StaticServices;
using System.Text.Json.Serialization;
using NSign.Providers;
using NSign;
using NSign.Signatures;
using NSign.Client;
using System.Text;
using Microsoft.OpenApi.Models;
using PrivaPub.Services.ClientToServer.Public;
namespace PrivaPub.Middleware
{
public static class PrivaPubConfigurations
{
public static IServiceCollection PrivaPubAppSettingsConfiguration(this IServiceCollection service, IConfiguration configuration)
{
return service
.Configure<MongoSettings>(configuration.GetSection(nameof(MongoSettings)))
.Configure<AppConfiguration>(configuration.GetSection(nameof(AppConfiguration)));
}
public static IServiceCollection PrivaPubWorkersConfiguration(this IServiceCollection service)
{
return service;
//.AddHostedService<DiscussionsWorker>()
//.AddHostedService<GroupsCleanerWorker>()
//.AddHostedService<PoliciesCleanerWorker>();
}
public static IServiceCollection PrivaPubHTTPSignature(this IServiceCollection service, IConfiguration configuration)
{
//HTTP CLIENT
service.Configure<AddDigestOptions>(options => options.WithHash(AddDigestOptions.Hash.Sha256))
.ConfigureMessageSigningOptions(options =>
{
options.SignatureName = "PrivaPub";
options
.WithMandatoryComponent(SignatureComponent.Path)
.WithMandatoryComponent(SignatureComponent.RequestTarget)
.SetParameters = signatureParams => signatureParams.WithKeyId("keyId");
})
.Services.Configure<SignatureVerificationOptions>(options =>
{
})
.AddHttpClient<ActivityPubClient>(nameof(ActivityPubClient))
.ConfigureHttpClient(httpClient =>
{
httpClient.DefaultRequestHeaders.Accept.Add(new("application/ld+json"));
})
.AddDigestAndSigningHandlers()
//.AddSignatureVerifiationHandler()
.Services
.AddSingleton<ISigner>(new HmacSha256SignatureProvider(Encoding.UTF8.GetBytes(configuration["AppConfiguration:Jwt:Key"])));
//MESSAGE RESPONSE
return service;
//.Configure<RequestSignatureVerificationOptions>(options =>
//{
// options.SignaturesToVerify.Add("sample");
// options.RequiredSignatureComponents.Add(SignatureComponent.Path);
// options.RequiredSignatureComponents.Add(SignatureComponent.Method);
// options.RequiredSignatureComponents.Add(SignatureComponent.Digest);
//})
//.AddSignatureVerification(serviceProvider =>
//{
// var memoryCache = serviceProvider.GetRequiredService<IMemoryCache>();
// //var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
// //httpContextAccessor.HttpContext.Request.
// var cert = memoryCache.GetOrCreate("PrivaPub", (cacheEntry) => Extensions.Extensions.GetX509Certificate2("PrivaPubCert"));
// return new RsaPkcs15Sha256SignatureProvider(cert, "anon");
//})
//.ConfigureMessageSigningOptions(options =>
//{
// options.WithMandatoryComponent(SignatureComponent.Path)
// .WithMandatoryComponent(SignatureComponent.Method)
// .WithMandatoryComponent(SignatureComponent.Digest)
// .WithOptionalComponent(new HttpHeaderDictionaryStructuredComponent(NSign.Constants.Headers.Signature, "sample", bindRequest: true));
// options.SignatureName = "resp";
// options.SetParameters = (sigParams) =>
// {
// sigParams.WithCreatedNow();
// };
//})
//.ValidateOnStart()
//.Services
//.AddHttpClient("ActivityPub", (serviceProvider, client) =>
//{
// client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("NSignSample", "0.1-beta"));
//}).Services;
//.AddSingleton<ISigner>(new RsaPssSha512SignatureProvider(
// new X509Certificate2(@"path\to\certificate.pfx", "PasswordForPfx"),
// "my-cert"));
}
public static IServiceCollection PrivaPubAuthServicesConfiguration(this IServiceCollection service, IConfiguration configuration)
{
return service
.AddAuthorization(options =>
{
options.AddPolicy(Policies.IsUser, Extensions.Extensions.IsUserPolicy());
options.AddPolicy(Policies.IsAdmin, Extensions.Extensions.IsAdminPolicy());
options.AddPolicy(Policies.IsModerator, Extensions.Extensions.IsModeratorPolicy());
})
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddPrivaPubAuth(configuration)
.Services
.AddSingleton<AuthTokenManager>()
.AddSingleton<IPasswordHasher, PasswordHasher>();
}
public static IServiceCollection PrivaPubInternalizationConfiguration(this IServiceCollection service, IConfiguration configuration)
{
return service
.AddLocalization()
.AddSingleton<RequestLocalizationOptionsService>();
}
public static IServiceCollection PrivaPubOptimizationConfiguration(this IServiceCollection service)
{
return service.AddResponseCompression(opts =>
{
opts.Providers.Add<BrotliCompressionProvider>();
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" });
});
}
public static IServiceCollection PrivaPubDataBaseConfiguration(this IServiceCollection service)
{
return service.AddSingleton<DbEntities>();
}
public static IServiceCollection PrivaPubServicesConfiguration(this IServiceCollection service)
{
return service
.AddTransient<IDataService, DataService>()
.AddTransient<IRootUsersService, RootUsersService>()
.AddTransient<IPublicAvatarUsersService, PublicAvatarUsersService>()
.AddSingleton<AppConfigurationService>()
.AddHttpContextAccessor()
.AddMemoryCache()
.AddSingleton<IPasswordHasher, PasswordHasher>();
}
public static IServiceCollection PrivaPubMiddlewareConfiguration(this IServiceCollection service)
{
return service
.AddEndpointsApiExplorer()
.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new()
{
In = ParameterLocation.Header,
Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
});
c.AddSecurityRequirement(new()
{
{
new()
{
Reference = new()
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[]{}
}
});
})
.AddControllers(options => { options.Filters.Add<OperationCancelledExceptionFilter>(); })
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.IgnoreReadOnlyFields = false;
options.JsonSerializerOptions.IgnoreReadOnlyProperties = false;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
}).Services;
}
public static IServiceCollection PrivaPubCORSConfiguration(this IServiceCollection service)
{
return service.AddCors(options =>
{
options.DefaultPolicyName = "DefaultCORS";
options.AddDefaultPolicy(configure =>
{
configure.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin()
.AllowAnyMethod()
.DisallowCredentials();
});
});
}
}
}

View File

@ -0,0 +1,44 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubActivity))]
public class ActivityPubActivity : ActivityPubObject
{
[JsonPropertyName("actor")]
public ActivityPubActor Actor { get; set; }
[JsonPropertyName("object")]
public ActivityPubObject Object { get; set; }
[JsonPropertyName("target")]
public ActivityPubObject Target { get; set; }
[JsonPropertyName("result")]
public ActivityPubResult Result { get; set; }
[JsonPropertyName("origin")]
public ActivityPubOrigin Origin { get; set; }
[JsonPropertyName("instrument")]
public ActivityPubInstrument Instrument { get; set; }
[JsonIgnore]
public bool NeedsObjectForInbox => Type is
ObjectType.Create or
ObjectType.Update or
ObjectType.Delete or
ObjectType.Follow or
ObjectType.Add or
ObjectType.Remove or
ObjectType.Like or
ObjectType.Block or
ObjectType.Undo;
[JsonIgnore]
public bool NeedsTargetForInbox => Type is
ObjectType.Add or
ObjectType.Remove;
}
}

View File

@ -0,0 +1,79 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubActor))]
public class ActivityPubActor : ActivityPubObject
{
[JsonPropertyName("type")]
public new ObjectType? Type => ObjectType.Person;
[JsonPropertyName("inbox")]
public string Inbox { get; set; }
[JsonPropertyName("outbox")]
public string Outbox { get; set; }
[JsonPropertyName("url")]
public string ProfileURL { get; set; }
[JsonPropertyName("preferredUsername")]
public string PreferredUsername { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("icon")]
public ActivityPubIcon Icon { get; set; }
[JsonPropertyName("image")]
public ActivityPubIcon Thumbnail { get; set; }
[JsonPropertyName("manuallyApprovesFollowers")]
public bool ManuallyApprovesFollowers { get; set; } = false;
[JsonPropertyName("discoverable")]
public bool Discoverable { get; set; } = true;
[JsonPropertyName("publicKey")]
public ActivityPubPublicKey PublicKey { get; set; } = new();
[JsonPropertyName("endpoints")]
public ActivityPubActorEndpoints Endpoints { get; set; } = new();
[JsonPropertyName("attachment")]
public IEnumerable<ActivityPubAttachment> Attachment { get; set; } = Enumerable.Empty<ActivityPubAttachment>();
[JsonPropertyName("tag")]
public IEnumerable<string> Tags => Enumerable.Empty<string>();
}
public class ActivityPubAttachment
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("value")]
public string Value { get; set; }
}
public class ActivityPubActorEndpoints
{
[JsonPropertyName("sharedInbox")]
public string SharedInboxURL { get; set; }
[JsonPropertyName("oauthAuthorizationEndpoint")]
public string OAuthAuthorizationEndpoint { get; set; }
[JsonExtensionData]
public Dictionary<string, object> OtherData { get; set; }
}
public class ActivityPubPublicKey
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("owner")]
public string Owner { get; set; }
[JsonPropertyName("publicKeyPem")]
public string PublicKeyPem { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubCollection))]
public class ActivityPubCollection : ActivityPubObject
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.Collection;
[JsonPropertyName("current")]
public string RecentlyUpdatedItem { get; set; }
[JsonPropertyName("first")]
public string FirstItem { get; set; }
[JsonPropertyName("last")]
public string LastItem { get; set; }
[JsonPropertyName("items")]
public List<ActivityPubLink> Items { get; set; }
[JsonPropertyName("totalItems")]
public int TotalItems => Items.Count;
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubCollectionPage))]
public class ActivityPubCollectionPage : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.CollectionPage;
[JsonPropertyName("partOf")]
public string BaseCollectionLink { get; set; }
[JsonPropertyName("next")]
public string NextCollectionLink { get; set; }
[JsonPropertyName("prev")]
public string PreviousCollectionLink { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubLink))]
public class ActivityPubLink : ActivityPubObject
{
[JsonPropertyName("id")]
public new ObjectType Type { get; set; } = ObjectType.Mention;
[JsonPropertyName("href")]
public string Href { get; set; }
[JsonPropertyName("preview")]
public string Preview { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("rel")]
public string Relation { get; set; }
[JsonPropertyName("hreflang")]
public string HrefLang { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
}
}

View File

@ -0,0 +1,126 @@
using PrivaPub.Models.ActivityPub.Extra;
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubObject))]
public partial class ActivityPubObject
{
[JsonPropertyName("@context")]
public List<object> Context => new()
{
"https://www.w3.org/ns/activitystreams",
"https://{0}/schemas/litepub-0.1.jsonld",
new ActivityPubContextLanguage()
};
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public ObjectType? Type { get; set; }
[JsonPropertyName("mediaType")]
public string MediaType { get; set; }
[JsonPropertyName("published")]
public DateTime? Published { get; set; }
[JsonPropertyName("updated")]
public DateTime? Updated { get; set; }
[JsonExtensionData]
public Dictionary<string, object> OtherData { get; set; }
[JsonPropertyName("source")]
public ActivityPubSource Source { get; set; }
//and part of link
[JsonPropertyName("to")]
public string[] To { get; set; }
[JsonPropertyName("cc")]
public string[] Cc { get; set; }
[JsonPropertyName("bcc")]
public string[] Bcc { get; set; }
[JsonPropertyName("bto")]
public string[] Bto { get; set; }
[JsonPropertyName("audience")]
public ActivityPubAudience Audience { get; set; }
[JsonIgnore]
public MacroType MacroType => Type switch
{
ObjectType.Article or
ObjectType.Audio or
ObjectType.Document or
ObjectType.Event or
ObjectType.Image or
ObjectType.Note or
ObjectType.Page or
ObjectType.Place or
ObjectType.Profile or
ObjectType.Relationship or
ObjectType.Tombstone or
ObjectType.Video => MacroType.Object,
ObjectType.Mention => MacroType.Link,
ObjectType.Application or
ObjectType.Group or
ObjectType.Organization or
ObjectType.Person or
ObjectType.Service => MacroType.Actor,
ObjectType.Accept or
ObjectType.Add or
ObjectType.Announce or
ObjectType.Arrive or
ObjectType.Block or
ObjectType.Create or
ObjectType.Delete or
ObjectType.Dislike or
ObjectType.Flag or
ObjectType.Follow or
ObjectType.Ignore or
ObjectType.Invite or
ObjectType.Join or
ObjectType.Leave or
ObjectType.Like or
ObjectType.Listen or
ObjectType.Move or
ObjectType.Offer or
ObjectType.Question or
ObjectType.Reject or
ObjectType.Read or
ObjectType.Remove or
ObjectType.TentativeReject or
ObjectType.TentativeAccept or
ObjectType.Travel or
ObjectType.Undo or
ObjectType.Update or
ObjectType.View => MacroType.Activity,
ObjectType.Collection => MacroType.Collection,
ObjectType.CollectionPage => MacroType.CollectionPage,
ObjectType.OrderedCollection => MacroType.OrderedCollection,
ObjectType.OrderedCollectionPage => MacroType.OrderedCollectionPage,
_ => MacroType.Unknown
};
}
public class ActivityPubContextLanguage
{
[JsonPropertyName("@language")]
public string Language { get; set; } = "und";
}
public class ActivityPubSource
{
[JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("mediaType")]
public string MediaType { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubOrderedCollection))]
public class ActivityPubOrderedCollection : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.OrderedCollection;
[JsonPropertyName("orderedItems")]
public List<ActivityPubLink> OrderedItems { get; set; }
[JsonPropertyName("totalItems")]
public new int TotalItems => OrderedItems?.Count ?? default;
}
}

View File

@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubOrderedCollectionPage))]
public class ActivityPubOrderedCollectionPage : ActivityPubCollection
{
[JsonPropertyName("type")]
public new ObjectType Type => ObjectType.OrderedCollectionPage;
[JsonPropertyName("partOf")]
public string BaseCollectionLink { get; set; }
[JsonPropertyName("next")]
public string NextCollectionLink { get; set; }
[JsonPropertyName("prev")]
public string PreviousCollectionLink { get; set; }
[JsonPropertyName("startIndex")]
public uint? StartIndex { get; set; }
[JsonPropertyName("orderedItems")]
public List<ActivityPubLink> OrderedItems { get; set; }
[JsonPropertyName("totalItems")]
public new int TotalItems => OrderedItems?.Count ?? default;
}
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub
{
[JsonSerializable(typeof(ActivityPubTombstone))]
public class ActivityPubTombstone : ActivityPubObject
{
[JsonPropertyName("formerType")]
public ObjectType FormerType { get; set; } = ObjectType.Unknown;
[JsonPropertyName("deleted")]
public DateTime Deleted { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubAudience
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubIcon
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("url")]
public string URL { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubInstrument
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubOrigin
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace PrivaPub.Models.ActivityPub.Extra
{
[JsonSerializable(typeof(ActivityPubPublicKey))]
public class ActivityPubResult
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@ -0,0 +1,17 @@
namespace PrivaPub.Models.ActivityPub
{
public enum MacroType
{
Object,
Link,
Actor,
Activity,
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
Unknown
}
}

View File

@ -0,0 +1,63 @@
namespace PrivaPub.Models.ActivityPub
{
public enum ObjectType
{
//Object types
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
//Link types
Mention,
//Actor
Application,
Group,
Organization,
Person,
Service,
//Activity
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
//Collections
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
//NULL or default
Unknown
}
}

View File

@ -0,0 +1,21 @@
using MongoDB.Entities;
using PrivaPub.Models.Email;
using PrivaPub.StaticServices;
namespace PrivaPub.Models
{
public class AppConfiguration : Entity
{
public bool RequiresFirstTimeSetup { get; set; } = true;
public string Version { get; set; }
public int MaxAllowedUploadFiles { get; set; }
public int MaxAllowedFileSize { get; set; }
public string ValidDiscussionFilesTypes { get; set; }
public List<string> SupportedLanguages { get; set; } = new();
public string BackendBaseAddress { get; set; }
public EmailConfiguration EmailConfiguration { get; set; }
public HashingOptions HashingOptions { get; set; }
public DateTime LastUpdateDate { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,11 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Data
{
public class Language : Entity
{
public string EnglishName { get; set; }
public string NativeName { get; set; }
public string International2Code { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace PrivaPub.Models.Email
{
public class EmailConfiguration
{
public string SmtpServer { get; set; }
public int SmtpPort { get; set; }
public bool UseSSL { get; set; }
public string SmtpUsername { get; set; }
public string SmtpPassword { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace PrivaPub.Models
{
public class MongoSettings
{
public string Database { get; set; }
public string LogsDatabase { get; set; }
public string ConnectionString { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Post
{
public class DmPost : Entity
{
public string GroupUserId { get; set; }
public string AnsweringToPostId { get; set; }
public string GroupId { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public List<PostMedia> Media { get; set; } = new();
public List<float> Location { get; set; } = new();
public bool HasContentWarning { get; set; } = false;
public bool IsFederatedCopy { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? UpdateDate { get; set; } = DateTime.UtcNow;
public string UpdateReason { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using MongoDB.Entities;
namespace PrivaPub.Models.Post
{
public class Post : Entity
{
public string GroupUserId { get; set; }
public string AnsweringToPostId { get; set; }
public string GroupId { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public List<PostMedia> Media { get; set; } = new();
public List<float> Location { get; set; } = new();
public float RangeKm { get; set; } = 5.0f;
public bool HasContentWarning { get; set; } = false;
public bool IsFederatedCopy { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? UpdateDate { get; set; } = DateTime.UtcNow;
public string UpdateReason { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace PrivaPub.Models.Post
{
public class PostBoost
{
public string PostId { get; set; }
public string GroupUserId { get; set; }
public bool IsFederatedCopy { get; set; }
public DateTime CreationDate { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace PrivaPub.Models.Post
{
public class PostMedia
{
public Guid Id { get; set; } = Guid.NewGuid();
public string ContentType { get; set; }
public string FileName { get; set; }
public string Extension { get; set; }
public string Path { get; set; }
public string URL { get; set; }
}
}

View File

@ -0,0 +1,116 @@
using MongoDB.Entities;
namespace PrivaPub.Models.User
{
public class Avatar : Entity
{
public string Url { get; set; }//url
public string Name { get; set; }//name
public string UserName { get; set; }//preferredUsername
public string Biography { get; set; }//summary
public Dictionary<string, string> SharedPersonalContacts { get; set; } = new();
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public AvatarAccountState AccountState { get; set; } = AvatarAccountState.Normal;
public AvatarSettings Settings { get; set; } = new();
public Dictionary<string, string> Fields { get; set; } = new();
public string Domain { get; set; }
public string PersonalNote { get; set; }
public string ModerationNote { get; set; }
public string InboxURL { get; set; }
public string OutboxURL { get; set; }
public string MovedToURL { get; set; }
public string PictureURL { get; set; }//icon
public string ThumbnailURL { get; set; }//image
public AvatarType AvatarType { get; set; } = AvatarType.Person;
public AvatarServer AvatarServer { get; set; } = AvatarServer.PrivaPub;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? SilencedAt { get; set; }
public DateTime? SuspendedAt { get; set; }
public DateTime? BannedAt { get; set; }
public DateTime? DeletionAt { get; set; }
}
public class ForeignAvatar : Entity
{
public string Url { get; set; }//url
public string Name { get; set; }//name
public string UserName { get; set; }//preferredUsername
public string Biography { get; set; }//summary
public Dictionary<string, string> SharedPersonalContacts { get; set; } = new();
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public AvatarAccountState AccountState { get; set; } = AvatarAccountState.Normal;
public AvatarSettings Settings { get; set; } = new();
public Dictionary<string, string> Fields { get; set; } = new();
public string Domain { get; set; }
public string PersonalNote { get; set; }
public string ModerationNote { get; set; }
public bool IsDiscoverable { get; set; } = true;
public string InboxURL { get; set; }
public string OutboxURL { get; set; }
public string MovedToURL { get; set; }
public string PictureURL { get; set; }//icon
public string ThumbnailURL { get; set; }//image
public AvatarType AvatarType { get; set; } = AvatarType.Person;
public AvatarServer AvatarServer { get; set; } = AvatarServer.Unknown;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? SilencedAt { get; set; }
public DateTime? SuspendedAt { get; set; }
public DateTime? BannedAt { get; set; }
public DateTime? DeletionAt { get; set; }
}
public class AvatarSettings
{
public string LanguageCode { get; set; } = "en-GB";
public bool IsDefault { get; set; } = true;
public short IconsThemeIndexColour { get; set; } = 25;
public short LightThemeIndexColour { get; set; } = 25;
public short DarkThemeIndexColour { get; set; } = 215;
public bool PreferSystemTheming { get; set; } = true;
public bool ThemeIsDarkMode { get; set; } = false;
public bool ThemeIsDarkGray { get; set; } = false;
public bool ThemeIsLightGray { get; set; } = true;
}
public enum AvatarServer
{
Unknown,
Pleroma,
Mastodon,
Akkoma,
Misskey,
PrivaPub
}
public enum AvatarType
{
Application,
Group,
Organization,
Person,
Service
}
public enum AvatarAccountState
{
Normal,
Silenced,
Suspended,
Banned,
Deleted
}
}

View File

@ -0,0 +1,12 @@
using MongoDB.Entities;
namespace PrivaPub.Models.User
{
public class EmailRecovery : Entity
{
public string RootUserId { get; set; }
public string RecoveryCode { get; set; }
public string RequestIP { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,10 @@
using MongoDB.Entities;
namespace PrivaPub.Models.User
{
public class RootToAvatar : Entity
{
public string RootId { get; set; }
public string AvatarId { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using MongoDB.Entities;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.Models.User
{
public class RootUser : Entity
{
[StringLength(32)]
public string UserName { get; set; }
[EmailAddress]
public string Email { get; set; }
public bool IsEmailValidated { get; set; } = false;
public bool IsBanned { get; set; } = false;
public string HashedPassword { get; set; }
public List<string> Policies { get; set; } = new() { ClientModels.Policies.IsUser };
public List<ContactItem> Contacts { get; set; } = new();
public RootUserSettings Settings { get; set; } = new();
public string ResetPasswordToken { get; set; }
public DateTime? ResetPasswordTokenSentAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
}
public class ContactItem
{
public string Type { get; set; }
public string Contact { get; set; }
public string Note { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
public class RootUserSettings
{
public string LanguageCode { get; set; } = "en-GB";
public short IconsThemeIndexColour { get; set; } = 25;
public short LightThemeIndexColour { get; set; } = 25;
public short DarkThemeIndexColour { get; set; } = 215;
public bool PreferSystemTheming { get; set; } = true;
public bool ThemeIsDarkMode { get; set; } = false;
public bool ThemeIsDarkGray { get; set; } = false;
public bool ThemeIsLightGray { get; set; } = true;
}
}

View File

@ -0,0 +1,14 @@
using MongoDB.Entities;
namespace PrivaPub.Models.User
{
public class RootUserNote : Entity
{
public string RootUserId { get; set; }
public string AvatarUserId { get; set; }
public string Note { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,9 @@
namespace PrivaPub.Models.User
{
public enum UserPolicyType
{
IsUser,
IsModerator,
IsAdmin
}
}

View File

@ -0,0 +1,24 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;
namespace PrivaPub.Models.Data
{
[BsonIgnoreExtraElements]
public class PrivaPub : Entity
{
public DateTime Timestamp { get; set; }
public string Level { get; set; }
public string RenderedMessage { get; set; }
[BsonIgnoreIfDefault, BsonIgnoreIfNull]
public DbLogProperties Properties { get; set; }
[BsonIgnoreIfDefault, BsonIgnoreIfNull]
public string Exception { get; set; }
}
[BsonIgnoreExtraElements]
public class DbLogProperties
{
public string SourceContext { get; set; }
}
}

56
PrivaPub/PrivaPub.csproj Normal file
View File

@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<!--<Nullable>enable</Nullable>-->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="3.5.0" />
<PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="MongoDB.Entities" Version="21.0.2" />
<PackageReference Include="NSign.Abstractions" Version="0.16.0" />
<PackageReference Include="NSign.AspNetCore" Version="0.16.0" />
<PackageReference Include="NSign.Client" Version="0.16.0" />
<PackageReference Include="NSign.SignatureProviders" Version="0.16.0" />
<PackageReference Include="PasswordGenerator" Version="2.1.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.MongoDB" Version="5.3.1" />
<PackageReference Include="SshKeyGenerator" Version="1.1.51" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Helpers\" />
<Folder Include="Services\ServerToServer\" />
<Folder Include="Workers\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SocialPub.ClientModels\PrivaPub.ClientModels.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\GenericRes.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>GenericRes.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\GenericRes.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>GenericRes.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

142
PrivaPub/Program.cs Normal file
View File

@ -0,0 +1,142 @@
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using MongoDB.Entities;
using Serilog;
using PrivaPub.Data;
using PrivaPub.Extensions;
using PrivaPub.Middleware;
using PrivaPub.Models;
using PrivaPub.Models.Data;
using PrivaPub.Services;
using PrivaPub.StaticServices;
try
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
if (builder.Environment.IsProduction())
{
serverOptions.ListenLocalhost(6970
//, options =>
//{
// options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
//}
);
serverOptions.UseSystemd();
serverOptions.AddServerHeader = false;
}
});
builder.Host.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
});
try
{
builder.Services.PrivaPubAppSettingsConfiguration(builder.Configuration)
.PrivaPubWorkersConfiguration()
.PrivaPubAuthServicesConfiguration(builder.Configuration)
.PrivaPubInternalizationConfiguration(builder.Configuration)
.PrivaPubOptimizationConfiguration()
.PrivaPubDataBaseConfiguration()
.PrivaPubServicesConfiguration()
.PrivaPubHTTPSignature(builder.Configuration)
.PrivaPubCORSConfiguration()
.PrivaPubMiddlewareConfiguration();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "ConfigureServices");
throw;
}
try
{
var mongoSettings = builder.Configuration.GetSection(nameof(MongoSettings)).Get<MongoSettings>();
await DB.InitAsync(mongoSettings.Database, MongoClientSettings.FromConnectionString(mongoSettings.ConnectionString));
var logsConnectionString = builder.Configuration.GetLogsConnectionString();
await DB.InitAsync(mongoSettings.LogsDatabase, MongoClientSettings.FromConnectionString(logsConnectionString));
DB.DatabaseFor<PrivaPub>(mongoSettings.LogsDatabase);
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, $"{nameof(Program)}.{nameof(Program)}() DB Instantiation");
throw;
}
var app = default(WebApplication);
try
{
app = builder.Build();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "Build");
throw;
}
try
{
var localizationService = app.Services.GetService<RequestLocalizationOptionsService>();
if (app.Environment.IsProduction())
{
app.UseResponseCompression();
app.UseForwardedHeaders(new()
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
}
else if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("DefaultCORS");
app.UseStaticFiles();
app.UseRequestLocalization(await localizationService.Get());
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//app.UseWhen(context => context.Request.Path.StartsWithSegments("/peasants") ||
// context.Request.Path.StartsWithSegments("/users"),
// app => app.UseSignatureVerification().UseDigestVerification());
app.MapControllers();
//app.MapFallbackToFile("index.html");
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "Use");
throw;
}
Log.ForContext<Program>().Information($"Starting collAnon at {nameof(Program)}()");
try
{
var dbClient = app.Services.GetService(typeof(DbEntities)) as DbEntities;
var passwordHasher = app.Services.GetService(typeof(IPasswordHasher)) as IPasswordHasher;
await dbClient.Init(passwordHasher);
}
catch (Exception ex)
{
Log.ForContext<Program>().Warning(ex, $"{nameof(Program)}.{nameof(Program)}() DB Init");
}
await app.RunAsync();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, $"{nameof(Program)}.{nameof(Program)}()");
}

View File

@ -0,0 +1,41 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55406",
"sslPort": 44347
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5293",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7195;http://localhost:5293",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,81 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PrivaPub.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class GenericRes {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal GenericRes() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PrivaPub.Resources.GenericRes", typeof(GenericRes).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Forbidden: {0}.
/// </summary>
public static string Forbidden___0_ {
get {
return ResourceManager.GetString("Forbidden: {0}", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unauthorized: {0}.
/// </summary>
public static string Unauthorized___0_ {
get {
return ResourceManager.GetString("Unauthorized: {0}", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Forbidden: {0}" xml:space="preserve">
<value>Forbidden: {0}</value>
</data>
<data name="Unauthorized: {0}" xml:space="preserve">
<value>Unauthorized: {0}</value>
</data>
</root>

View File

@ -0,0 +1,12 @@
namespace PrivaPub.Services
{
public class ActivityPubClient : HttpClient
{
//readonly HttpClient client;
//public ActivityPubClient(HttpClient client)
//{
// this.client = client;
//}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;
using MongoDB.Entities;
using PrivaPub.Models;
using PrivaPub.StaticServices;
namespace PrivaPub.Services
{
public class AppConfigurationService
{
public AppConfiguration AppConfiguration;
readonly DbEntities _dbEntities;
AppConfiguration _appConfigurationFromFile;
public AppConfigurationService(DbEntities dbEntities, IOptionsMonitor<AppConfiguration> appConfiguration)
{
_dbEntities = dbEntities;
_appConfigurationFromFile = appConfiguration.CurrentValue;
}
public async Task Init()
{
var dbAppConfiguration = await _dbEntities.AppConfiguration.ExecuteFirstAsync();
if (dbAppConfiguration != default)
{
if (AppConfiguration == default || AppConfiguration.LastUpdateDate != dbAppConfiguration.LastUpdateDate)
AppConfiguration = dbAppConfiguration;
return;
}
dbAppConfiguration = _appConfigurationFromFile;
await dbAppConfiguration.SaveAsync();
AppConfiguration = dbAppConfiguration;
}
}
}

View File

@ -0,0 +1,115 @@
using Microsoft.Extensions.Localization;
using MongoDB.Entities;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User.Avatar;
using PrivaPub.Models;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.StaticServices;
using System.Security.Cryptography;
namespace PrivaPub.Services.ClientToServer.Private
{
public interface IPrivateAvatarUsersService
{
Task<WebResult> UpdateAvatar(UpdateAvatarForm form);
Task<WebResult> InsertAvatar(InsertAvatarForm form);
}
public class PrivateAvatarUsersService : IPrivateAvatarUsersService
{
readonly AppConfigurationService _appConfiguration;
readonly DbEntities _dbEntities;
readonly IStringLocalizer<GenericRes> _localizer;
readonly ILogger<PrivateAvatarUsersService> _logger;
public PrivateAvatarUsersService(AppConfigurationService appConfiguration,
DbEntities dbEntities,
IStringLocalizer<GenericRes> localizer,
ILogger<PrivateAvatarUsersService> logger)
{
_appConfiguration = appConfiguration;
_dbEntities = dbEntities;
_localizer = localizer;
_logger = logger;
}
public async Task<WebResult> InsertAvatar(InsertAvatarForm form)
{
var result = new WebResult();
try
{
if (await _dbEntities.Avatars.Match(a => a.UserName == form.UserName).ExecuteAnyAsync())
return result.Invalidate(_localizer["The username '{0}' is already take.", form.UserName]);
if (!await IsValidRootUser(form.RootId))
return result.Invalidate(_localizer["You can't do this action because of your account status."]);
var newAvatar = new Avatar
{
Name = form.Name,
UserName = form.UserName,
Biography = form.Biography,
Fields = form.Fields,
PersonalNote = form.PersonalNote,
};
if (!form.Settings.IsDefault)
newAvatar.Settings = new()
{
IsDefault = false,
DarkThemeIndexColour = form.Settings.DarkThemeIndexColour,
IconsThemeIndexColour = form.Settings.IconsThemeIndexColour,
LanguageCode = form.Settings.LanguageCode,
LightThemeIndexColour = form.Settings.LightThemeIndexColour,
PreferSystemTheming = form.Settings.PreferSystemTheming,
ThemeIsDarkGray = form.Settings.ThemeIsDarkGray,
ThemeIsDarkMode = form.Settings.ThemeIsDarkMode,
ThemeIsLightGray = form.Settings.ThemeIsLightGray
};
if (_appConfiguration.AppConfiguration == default)
await _appConfiguration.Init();
newAvatar.Url = _appConfiguration.AppConfiguration.BackendBaseAddress + $"/peasants/{form.UserName}";
newAvatar.Domain = _appConfiguration.AppConfiguration.BackendBaseAddress;
newAvatar.InboxURL = _appConfiguration.AppConfiguration.BackendBaseAddress + $"/peasants/{form.UserName}/mouth";
newAvatar.OutboxURL = _appConfiguration.AppConfiguration.BackendBaseAddress + $"/peasants/{form.UserName}/anus";
var rsa = RSA.Create();
newAvatar.PrivateKey = rsa.ExportRSAPrivateKeyPem();
newAvatar.PublicKey = rsa.ExportRSAPublicKeyPem();
await newAvatar.SaveAsync();
result.Data = newAvatar;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PrivateAvatarUsersService)}.{nameof(InsertAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
public async Task<WebResult> UpdateAvatar(UpdateAvatarForm form)
{
var result = new WebResult();
try
{
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PrivateAvatarUsersService)}.{nameof(UpdateAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
async ValueTask<bool> IsValidRootUser(string rootUserId) =>
await _dbEntities.RootUsers.MatchID(rootUserId)
.Match(ru => !ru.DeletedAt.HasValue)
.ExecuteAnyAsync();
}
}

View File

@ -0,0 +1,37 @@
using PrivaPub.Models.Data;
using PrivaPub.StaticServices;
namespace PrivaPub.Services.ClientToServer.Public
{
public class DataService : IDataService
{
readonly DbEntities DbEntities;
readonly AppConfigurationService AppConfigurationService;
public DataService(DbEntities dbCollections, AppConfigurationService appConfigurationService)
{
DbEntities = dbCollections;
AppConfigurationService = appConfigurationService;
}
public string GetCurrentVersion() =>
AppConfigurationService.AppConfiguration.Version;
public async Task<IEnumerable<Language>> GetLanguages(CancellationToken cancellationToken)
{
return (await DbEntities.Languages
.Match(l => AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(l.International2Code))
.ExecuteAsync(cancellationToken)).OrderBy(l => l.NativeName).ToArray();
}
public async Task<bool> IsValidLanguageId(string languageId)
{
var anyLanguageWithId = await DbEntities.Languages.Match(l => l.ID == languageId).ExecuteAnyAsync();
if (!anyLanguageWithId)
return false;
var language2Code = (await DbEntities.Languages
.Match(l => l.ID == languageId).ExecuteFirstAsync())?.International2Code;
return AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(language2Code);
}
}
}

View File

@ -0,0 +1,13 @@
using PrivaPub.Models.Data;
namespace PrivaPub.Services.ClientToServer.Public
{
public interface IDataService
{
string GetCurrentVersion();
Task<IEnumerable<Language>> GetLanguages(CancellationToken cancellationToken);
Task<bool> IsValidLanguageId(string languageId);
}
}

View File

@ -0,0 +1,174 @@
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.StaticServices;
namespace PrivaPub.Services.ClientToServer.Public
{
public interface IPublicAvatarUsersService
{
Task<WebResult> GetLocalAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token);
Task<IEnumerable<Avatar>> GetLocalAvatars(UserPolicyType userPolicyType, CancellationToken token);
Task<WebResult> GetForeignAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token);
Task<IEnumerable<ForeignAvatar>> GetForeignAvatars(UserPolicyType userPolicyType, CancellationToken token);
Task<IEnumerable<Avatar>> GetRootAvatars(string rootUserId, CancellationToken token);
}
public class PublicAvatarUsersService : IPublicAvatarUsersService
{
readonly DbEntities _dbEntities;
readonly IStringLocalizer<GenericRes> _localizer;
readonly ILogger<PublicAvatarUsersService> _logger;
public PublicAvatarUsersService(DbEntities dbEntities,
IStringLocalizer<GenericRes> localizer,
ILogger<PublicAvatarUsersService> logger)
{
_dbEntities = dbEntities;
_localizer = localizer;
_logger = logger;
}
public async Task<WebResult> GetLocalAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token)
{
var result = new WebResult();
try
{
var query = _dbEntities.Avatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsAdmin:
query.Match(a => a.UserName == actor);
break;
}
var avatar = await query.ExecuteFirstAsync(token);
if (avatar == default)
return result.Invalidate(_localizer["User '{0}' not found.", actor], StatusCodes.Status404NotFound);
result.Data = avatar;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetLocalAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
public async Task<IEnumerable<Avatar>> GetLocalAvatars(UserPolicyType userPolicyType, CancellationToken token)
{
try
{
var query = _dbEntities.Avatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsAdmin:
break;
}
var avatars = await query.ExecuteAsync(token);
return avatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetLocalAvatars)}");
return Enumerable.Empty<Avatar>();
}
}
public async Task<WebResult> GetForeignAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token)
{
var result = new WebResult();
try
{
var query = _dbEntities.ForeignAvatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsAdmin:
query.Match(a => a.UserName == actor);
break;
}
var avatar = await query.ExecuteFirstAsync(token);
if (avatar == default)
return result.Invalidate(_localizer["User '{0}' not found.", actor], StatusCodes.Status404NotFound);
result.Data = avatar;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetForeignAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
public async Task<IEnumerable<ForeignAvatar>> GetForeignAvatars(UserPolicyType userPolicyType, CancellationToken token)
{
try
{
var query = _dbEntities.ForeignAvatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsAdmin:
break;
}
var foreignAvatars = await query.ExecuteAsync(token);
return foreignAvatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetForeignAvatars)}");
return Enumerable.Empty<ForeignAvatar>();
}
}
public async Task<IEnumerable<Avatar>> GetRootAvatars(string rootUserId, CancellationToken token)
{
try
{
var rootUser = _dbEntities.RootUsers.MatchID(rootUserId).ExecuteFirstAsync(token);
if (rootUser == default)
return Enumerable.Empty<Avatar>();
var rootToAvatars = await _dbEntities.RootToAvatars.Match(ra => ra.RootId == rootUserId).ExecuteAsync(token);
if (rootToAvatars.Count == 0)
return Enumerable.Empty<Avatar>();
var avatarIds = rootToAvatars.Select(ra => ra.AvatarId).ToArray();
var avatars = await _dbEntities.Avatars
.Match(a => avatarIds.Contains(a.ID))
.ProjectExcluding(a => a.Settings)
.ExecuteAsync(token);
return avatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetRootAvatars)}");
return Enumerable.Empty<Avatar>();
}
}
}
}

View File

@ -0,0 +1,34 @@
#pragma warning disable 8625
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
namespace PrivaPub.Services
{
public interface IRootUsersService
{
Task<WebResult> LoginAsync(LoginForm loginForm, string invitationCode = default, bool isPasswordRequired = false);
Task<WebResult> SignUpAsync(LoginForm signUpForm, string invitationCode = default, bool isPasswordRequired = false);
Task<WebResult> BanUserAsync(UsersIds usersIds);
Task<WebResult> UnbanUserAsync(UsersIds usersIds);
Task<WebResult> RemoveUserAsync(UsersIds usersIds);
Task<WebResult> UpdateUserAsync(UserForm userEmailForm, string userId);
Task<WebResult> UpdateUserPasswordAsync(UserPasswordForm userPasswordForm, string userId);
Task<WebResult> GetUserSettingsAsync(string userId, LoginForm loginForm = default);
Task<WebResult> UpdateUserSettingsAsync(ViewAvatarServer userSettings, string userId);
Task<WebResult> SetupAndSendRecoveryEmail(PasswordRecoveryForm passwordRecoveryForm, string host);
Task<WebResult> IsValidRecoveryCode(string recoveryCode);
Task<WebResult> ChangePassword(NewPasswordForm newPasswordForm);
}
}

View File

@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.Resources;
using System.Text;
using System.Text.Json;
namespace PrivaPub.Services
{
public class JwtEvents : JwtBearerEvents
{
ILogger<JwtEvents> _logger { get; set; }
const string contentType = "application/json";
public override async Task AuthenticationFailed(AuthenticationFailedContext context)
{
try
{
var localizer = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizer<GenericRes>>();
var webResult = new WebResult().Invalidate(localizer["Unauthorized: {0}", context.Exception], StatusCodes.Status401Unauthorized);
context.Response.ContentType = contentType;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webResult)));
}
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at AuthenticationFailed()");
}
}
public override async Task Forbidden(ForbiddenContext context)
{
try
{
var localizer = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizer<GenericRes>>();
var webResult = new WebResult().Invalidate(localizer["Forbidden: {0}", context.Result.None ? "N/A" : context.Result.Failure?.Message ?? "N/A"], StatusCodes.Status403Forbidden);
context.Response.ContentType = contentType;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webResult)));
}
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at AuthenticationFailed()");
}
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Localization;
using PrivaPub.StaticServices;
using System.Globalization;
namespace PrivaPub.Services
{
public class RequestLocalizationOptionsService
{
RequestLocalizationOptions RequestLocalizationOptions { get; set; }
readonly AppConfigurationService AppConfigurationService;
public RequestLocalizationOptionsService(AppConfigurationService appConfigurationService)
{
AppConfigurationService = appConfigurationService;
}
public async Task<RequestLocalizationOptions> Get()
{
if (RequestLocalizationOptions != default) return RequestLocalizationOptions;
if (AppConfigurationService.AppConfiguration == default)
await AppConfigurationService.Init();
RequestLocalizationOptions = new RequestLocalizationOptions();
var cultures = AppConfigurationService
.AppConfiguration
.SupportedLanguages
.Select(sl => new CultureInfo(sl))
.ToArray();
RequestLocalizationOptions.DefaultRequestCulture = new(cultures.First(), cultures.First());
RequestLocalizationOptions.SupportedCultures = cultures;
RequestLocalizationOptions.SupportedUICultures = cultures;
RequestLocalizationOptions.RequestCultureProviders = new IRequestCultureProvider[] {
new AcceptLanguageHeaderRequestCultureProvider { Options = RequestLocalizationOptions }
};
RequestLocalizationOptions.ApplyCurrentCultureToResponseHeaders = true;
return RequestLocalizationOptions;
}
}
}

View File

@ -0,0 +1,577 @@
using MailKit.Net.Smtp;
using Microsoft.Extensions.Localization;
using MimeKit;
using MongoDB.Driver;
using MongoDB.Entities;
using PasswordGenerator;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
using PrivaPub.Models;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.StaticServices;
using System.Globalization;
#pragma warning disable 8603
#pragma warning disable 8625
namespace PrivaPub.Services
{
public class RootUsersService : IRootUsersService
{
readonly DbEntities DbEntities;
readonly IPasswordHasher PasswordHasher;
readonly IStringLocalizer Localizer;
readonly ILogger<RootUsersService> Logger;
readonly AppConfigurationService AppConfigurationService;
readonly AuthTokenManager AuthTokenManager;
public RootUsersService(
IStringLocalizer<GenericRes> localizer,
ILogger<RootUsersService> logger,
IPasswordHasher passwordHasher,
DbEntities dbEntities,
AppConfigurationService appConfigurationService,
AuthTokenManager authTokenManager)
{
DbEntities = dbEntities;
AuthTokenManager = authTokenManager;
PasswordHasher = passwordHasher;
Localizer = localizer;
Logger = logger;
AppConfigurationService = appConfigurationService;
}
public async Task<WebResult> SignUpAsync(LoginForm signUpForm, string invitationCode = default,
bool isPasswordRequired = false)
{
var result = new WebResult();
try
{
signUpForm.UserName = signUpForm.UserName.ToLower();
if (await DbEntities.RootUsers.Match(u => u.UserName == signUpForm.UserName).ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' already taken.", signUpForm.UserName]);
var signUpPasswordHashed = PasswordHasher.Hash(signUpForm.Password);
var newUser = new RootUser
{
UserName = signUpForm.UserName,
HashedPassword = signUpPasswordHashed
};
if (signUpForm.UserName == "admin")
{
newUser.Policies.Clear();
newUser.Policies.Add(Policies.IsAdmin);
newUser.Policies.Add(Policies.IsUser);
newUser.Policies.Add(Policies.IsModerator);
}
await newUser.SaveAsync();
var cultureLanguage = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
var language = await DbEntities.Languages.Match(l => l.International2Code == cultureLanguage).ExecuteFirstAsync();
var newRootUserSettings = new RootUserSettings
{
RootUserId = newUser.ID,
LanguageCode = language?.International2Code ?? "en",
LightThemeIndexColour = signUpForm.LightThemeIndexColour,
DarkThemeIndexColour = signUpForm.DarkThemeIndexColour,
IconsThemeIndexColour = signUpForm.IconsThemeIndexColour,
ThemeIsDarkGray = signUpForm.ThemeIsDarkGray,
ThemeIsLightGray = signUpForm.ThemeIsLightGray,
PreferSystemTheming = signUpForm.PreferSystemTheming,
ThemeIsDarkMode = signUpForm.ThemeIsDarkMode
};
await newRootUserSettings.SaveAsync();
//if (!string.IsNullOrEmpty(invitationCode))
//{
// if (isPasswordRequired)
// result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// Password = signUpForm.InvitationPassword
// }, newUser.ID);
// else
// result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// }, newUser.ID);
// if (!result.IsValid)
// return result;
//}
result.Data = (newUser, new ViewAvatarServer
{
LanguageCode = newRootUserSettings.LanguageCode,
LightThemeIndexColour = newRootUserSettings.LightThemeIndexColour,
ThemeIsDarkMode = newRootUserSettings.ThemeIsDarkMode,
IconsThemeIndexColour = newRootUserSettings.IconsThemeIndexColour,
ThemeIsDarkGray = newRootUserSettings.ThemeIsDarkGray,
ThemeIsLightGray = newRootUserSettings.ThemeIsLightGray,
DarkThemeIndexColour = newRootUserSettings.DarkThemeIndexColour,
PreferSystemTheming = newRootUserSettings.PreferSystemTheming
});
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(SignUpAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> LoginAsync(LoginForm loginForm, string invitationCode = default,
bool isPasswordRequired = false)
{
var result = new WebResult();
try
{
loginForm.UserName = loginForm.UserName.ToLower();
if (!await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName && u.DeletionDate == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", loginForm.UserName]);
var user = await DbEntities.RootUsers.Match(u => u.UserName == loginForm.UserName).ExecuteFirstAsync();
if (user.IsBanned)
return result.Invalidate(Localizer["User '{0}' banned.", user.UserName]);
var (verified, needsUpgrade) = PasswordHasher.Check(user.HashedPassword, loginForm.Password);
if (!verified)
return result.Invalidate(Localizer["Wrong password."]);
if (needsUpgrade)
result.ErrorMessage = Localizer["Needs upgrade!"];
var userSettingsResult = await GetUserSettingsAsync(user.ID, loginForm);
var userSettings = (ViewAvatarServer)userSettingsResult.Data;
//if (!string.IsNullOrEmpty(invitationCode))
//{
// if (isPasswordRequired)
// result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// Password = loginForm.InvitationPassword
// }, user.ID);
// else
// result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// }, user.ID);
// if (!result.IsValid)
// return result;
//}
result.Data = (user, userSettings);
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(LoginAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> UpdateUserAsync(UserForm userForm, string userId)
{
var result = new WebResult();
try
{
var currentUser = await DbEntities.RootUsers.Match(u => u.ID == userId).ExecuteFirstAsync();
if (!string.IsNullOrEmpty(userForm.Email) && currentUser.Email != userForm.Email)
{
var emailAlreadyUsed = await DbEntities.RootUsers.Match(u => u.Email == userForm.Email).ExecuteAnyAsync();
if (emailAlreadyUsed)
return result.Invalidate(Localizer["Email '{0}' already taken.", userForm.Email]);
}
await DB.Update<RootUser>()
.Match(u => u.ID == userId)
.Modify(u => u.Email, userForm.Email)
.ExecuteAsync();
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> UpdateUserSettingsAsync(ViewAvatarServer userSettings, string userId)
{
var result = new WebResult();
try
{
var isSupportedLanguage = AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(userSettings.LanguageCode);
if (!isSupportedLanguage)
return result.Invalidate(Localizer["Language code '{0}' unsupported.", userSettings.LanguageCode]);
var languageCodeExists = await DbEntities.Languages
.Match(l => l.International2Code == userSettings.LanguageCode).ExecuteAnyAsync();
if (!languageCodeExists)
return result.Invalidate(Localizer["Language code '{0}' doesn't exist.", userSettings.LanguageCode]);
var language = await DbEntities.Languages.Match(l => l.International2Code == userSettings.LanguageCode)
.ExecuteFirstAsync();
_ = await DB.Update<RootUserSettings>()
.Match(u => u.RootUserId == userId)
.Modify(us => us.LanguageCode, language.International2Code)
.Modify(us => us.LightThemeIndexColour, userSettings.LightThemeIndexColour)
.Modify(us => us.DarkThemeIndexColour, userSettings.DarkThemeIndexColour)
.Modify(us => us.PreferSystemTheming, userSettings.PreferSystemTheming)
.Modify(us => us.ThemeIsDarkMode, userSettings.ThemeIsDarkMode)
.Modify(us => us.ThemeIsDarkGray, userSettings.ThemeIsDarkGray)
.Modify(us => us.ThemeIsLightGray, userSettings.ThemeIsLightGray)
.Modify(us => us.IconsThemeIndexColour, userSettings.IconsThemeIndexColour)
.ExecuteAsync();
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserSettingsAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> UpdateUserPasswordAsync(UserPasswordForm userPasswordForm, string userId)
{
var result = new WebResult();
try
{
var user = await DbEntities.RootUsers.Match(u => u.ID == userId && u.DeletionDate == null).ExecuteFirstAsync();
if (user == null)
return result.Invalidate(Localizer["Username '{0}' not found.", userId]);
var (verified, needsUpgrade) = PasswordHasher.Check(user.HashedPassword, userPasswordForm.OldPassword);
if (!verified)
return result.Invalidate(Localizer["Wrong password."]);
var newPasswordHashed = PasswordHasher.Hash(userPasswordForm.NewPassword);
user.HashedPassword = newPasswordHashed;
await user.SaveAsync();
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UpdateUserPasswordAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> RemoveUserAsync(UsersIds usersIds)
{
var result = new WebResult();
try
{
var users = await DbEntities.RootUsers.Match(u => usersIds.UserIdList.Contains(u.ID) && u.DeletionDate == default).ExecuteAsync();
if (users == null || users.Count == 0)
return result.Invalidate(Localizer["User already deleted."]);
foreach (var user in users)
{
user.Email = Localizer["Deleted user"];
user.HashedPassword = null;
user.Policies.Clear();
user.IsBanned = false;
user.IsEmailValidated = false;
//user.TempSecret = null;
user.UserName = Localizer["Deleted user"];
user.DeletionDate = DateTime.UtcNow;
await user.SaveAsync();
}
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(RemoveUserAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> BanUserAsync(UsersIds usersIds)
{
var result = new WebResult();
try
{
await DB.Update<RootUser>()
.Match(u => usersIds.UserIdList.Contains(u.ID))
.Modify(u => u.IsBanned, true)
.ExecuteAsync();
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(BanUserAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> UnbanUserAsync(UsersIds usersIds)
{
var result = new WebResult();
try
{
await DB.Update<RootUser>()
.Match(u => usersIds.UserIdList.Contains(u.ID))
.Modify(u => u.IsBanned, false)
.ExecuteAsync();
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(UnbanUserAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> GetUserSettingsAsync(string userId, LoginForm loginForm = default)
{
var result = new WebResult();
try
{
var userSettings = await DbEntities.RootUsersSettings.Match(u => u.RootUserId == userId).ExecuteFirstAsync();
if (loginForm != default && (loginForm.ThemeIsDarkMode != userSettings.ThemeIsDarkMode))
{
userSettings.ThemeIsDarkMode = loginForm.ThemeIsDarkMode;
await userSettings.SaveAsync();
}
result.Data = new ViewAvatarServer
{
LanguageCode = userSettings.LanguageCode,
LightThemeIndexColour = userSettings.LightThemeIndexColour,
DarkThemeIndexColour = userSettings.DarkThemeIndexColour,
PreferSystemTheming = userSettings.PreferSystemTheming,
ThemeIsDarkMode = userSettings.ThemeIsDarkMode,
IconsThemeIndexColour = userSettings.IconsThemeIndexColour,
ThemeIsDarkGray = userSettings.ThemeIsDarkGray,
ThemeIsLightGray = userSettings.ThemeIsLightGray
};
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(GetUserSettingsAsync)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> SetupAndSendRecoveryEmail(PasswordRecoveryForm passwordRecoveryForm, string host)
{
var result = new WebResult();
try
{
var usernameExists = false;
if (passwordRecoveryForm.IsEmailDisabled)
{
if (!await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName && u.DeletionDate == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName],
StatusCodes.Status404NotFound);
usernameExists = true;
}
else
{
if (!await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email && u.DeletionDate == null)
.ExecuteAnyAsync())
return result.Invalidate(Localizer["Username '{0}' not found.", passwordRecoveryForm.UserName],
StatusCodes.Status404NotFound);
}
var user = default(RootUser);
if (usernameExists)
user = await DbEntities.RootUsers.Match(u => u.UserName == passwordRecoveryForm.UserName).ExecuteFirstAsync();
else
user = await DbEntities.RootUsers.Match(u => u.Email == passwordRecoveryForm.Email).ExecuteFirstAsync();
if (string.IsNullOrEmpty(user.Email))
return result.Invalidate(Localizer["This User doesn't have an email, no way to recover."],
StatusCodes.Status423Locked);
var emailRecovery = await DbEntities.EmailRecoveries
.Match(er => er.RootUserId == user.ID).ExecuteFirstAsync();
if (emailRecovery is null)
{
emailRecovery = new()
{
RootUserId = user.ID
};
await emailRecovery.SaveAsync();
}
var recoveryCodeGenerator = new Password(true, true, true, false, 127);
emailRecovery.RecoveryCode = recoveryCodeGenerator.Next();
await emailRecovery.SaveAsync();
using (var smtpClient = new SmtpClient())
{
try
{
await smtpClient.ConnectAsync(AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer, AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpPort,
AppConfigurationService.AppConfiguration.EmailConfiguration.UseSSL);
if (!smtpClient.IsConnected)
{
Logger.LogError($"Failed to connect to the SMTP server({AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer}).");
return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.ServiceNotAvailable);
}
}
catch (Exception ex)
{
Logger.LogError(ex, $"Error at connection to the SMTP server({AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpServer}).");
return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.ServiceNotAvailable, exception: ex);
}
try
{
await smtpClient.AuthenticateAsync(AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername, AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpPassword);
if (!smtpClient.IsAuthenticated)
{
Logger.LogError($"Failed SMTP authentication of {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}.");
return result.Invalidate(Localizer["Failed to send email."],
(int)SmtpStatusCode.TemporaryAuthenticationFailure);
}
}
catch (Exception ex)
{
Logger.LogError(ex, $"Failed SMTP authentication of {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}.");
return result.Invalidate(Localizer["Failed to send email."],
(int)SmtpStatusCode.TemporaryAuthenticationFailure, exception: ex);
}
try
{
var toParsed = await smtpClient.VerifyAsync(user.Email);
if (toParsed == null)
return result.Invalidate($"Invalid email of {user.Email}.", (int)SmtpStatusCode.MailboxUnavailable);
}
catch (OperationCanceledException ex)
{
Logger.LogWarning(
$"SMTP operation canceled exception at email verification of {user.Email}. Exception=[{ex.Message}]");
}
catch (SmtpCommandException ex)
{
Logger.LogWarning(
$"SMTP command exception at email verification of {user.Email}. Exception=[{ex.Message}]");
}
catch (SmtpProtocolException ex)
{
Logger.LogWarning(
$"SMTP protocol exception at email verification of {user.Email}. Exception=[{ex.Message}]");
}
catch (Exception ex)
{
Logger.LogWarning($"General exception at email verification of {user.Email}. Exception=[{ex.Message}]");
}
var message = new MimeMessage();
message.From.Add(new MailboxAddress(Localizer["Eugene - collAnon support"], AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername));
message.To.Add(MailboxAddress.Parse(user.Email));
message.Subject = Localizer["PrivaPub - Password recovery link"];
message.Body = new TextPart("plain")
{
Text = string.Format(Localizer[@"Hey {0},
Eugene from collAnon, following is the password recovery link:
{1}
-- Eugene"], user.UserName, $"{host}/password-recovery?rc={emailRecovery.RecoveryCode}")
};
try
{
await smtpClient.SendAsync(message);
}
catch (Exception ex)
{
Logger.LogError(ex, $"Error at email sending to {user.Email} from {AppConfigurationService.AppConfiguration.EmailConfiguration.SmtpUsername}.");
return result.Invalidate(Localizer["Failed to send email."], (int)SmtpStatusCode.TransactionFailed, exception: ex);
}
await smtpClient.DisconnectAsync(quit: true);
}
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(SetupAndSendRecoveryEmail)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> IsValidRecoveryCode(string recoveryCode)
{
var result = new WebResult();
try
{
var isValidRecoveryCode = await DbEntities.EmailRecoveries
.Match(er => er.RecoveryCode == recoveryCode).ExecuteAnyAsync();
result.Data = isValidRecoveryCode;
return result;
}
catch (Exception ex)
{
return result.Invalidate(ex.Message, exception: ex);
}
}
public async Task<WebResult> ChangePassword(NewPasswordForm newPasswordForm)
{
var result = new WebResult();
try
{
var isValidRecoveryCode = await DbEntities.EmailRecoveries
.Match(er => er.RecoveryCode == newPasswordForm.RecoveryCode).ExecuteAnyAsync();
if (!isValidRecoveryCode)
return result.Invalidate(Localizer["Invalid recovery code."], StatusCodes.Status404NotFound);
var emailRecovery = await DbEntities.EmailRecoveries
.Match(er => er.RecoveryCode == newPasswordForm.RecoveryCode)
.Project(er => er.Include(nameof(EmailRecovery.RootUserId)))
.ExecuteFirstAsync();
if (emailRecovery is null || emailRecovery.RootUserId is null)
return result.Invalidate(Localizer["User not found."]);
var newHashedPassword = PasswordHasher.Hash(newPasswordForm.NewPassword);
_ = await DB.Update<RootUser>()
.Match(u => u.ID == emailRecovery.RootUserId && u.DeletionDate == null)
.Modify(u => u.HashedPassword, newHashedPassword)
.ExecuteAsync();
_ = await DB.DeleteAsync<EmailRecovery>(er => er.RecoveryCode == newPasswordForm.RecoveryCode);
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(RootUsersService)}.{nameof(ChangePassword)}()");
return result.Invalidate(ex.Message, exception: ex);
}
}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.IdentityModel.Tokens;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
using PrivaPub.Models.User;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace PrivaPub.StaticServices
{
public class AuthTokenManager
{
readonly IConfiguration Configuration;
public AuthTokenManager(IConfiguration configuration)
{
Configuration = configuration;
}
public JwtUser GenerateToken(RootUser user, ViewAvatarServer userSettings)
{
var expiration = DateTime.UtcNow.AddHours(int.Parse(Configuration["AppConfiguration:Jwt:HoursTimeout"]));
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AppConfiguration:Jwt:Key"]));
var jwtUser = new JwtUser
{
UserId = user.ID,
Email = user.Email,
Username = user.UserName,
Expiration = expiration.Ticks,
Policies = user.Policies,
UserSettings = userSettings
};
var claims = new List<Claim>
{
new(ClaimTypes.UserData, user.ID),
new(ClaimTypes.Name, user.UserName)
};
claims.Add(new(Policies.IsUser, $"{user.Policies.Contains(Policies.IsUser)}".ToLower()));
claims.Add(new(Policies.IsModerator, $"{user.Policies.Contains(Policies.IsModerator)}".ToLower()));
claims.Add(new(Policies.IsAdmin, $"{user.Policies.Contains(Policies.IsAdmin)}".ToLower()));
var token = new JwtSecurityToken(issuer: Configuration["AppConfiguration:Jwt:Issuer"], audience: Configuration["AppConfiguration:Jwt:Audience"],
claims: claims,
expires: expiration,
signingCredentials: new(securityKey, SecurityAlgorithms.HmacSha512)
);
var tokenHandler = new JwtSecurityTokenHandler();
jwtUser.Token = tokenHandler.WriteToken(token);
return jwtUser;
}
}
}

View File

@ -0,0 +1,27 @@
using MongoDB.Entities;
using PrivaPub.Models;
using PrivaPub.Models.Data;
using PrivaPub.Models.Post;
using PrivaPub.Models.User;
namespace PrivaPub.StaticServices
{
public class DbEntities
{
public Find<RootUser> RootUsers { get { return DB.Find<RootUser>(); } }
public Find<RootUserNote> RootUserNotes { get { return DB.Find<RootUserNote>(); } }
public Find<AppConfiguration> AppConfiguration { get { return DB.Find<AppConfiguration>(); } }
public Find<Language> Languages { get { return DB.Find<Language>(); } }
public Find<EmailRecovery> EmailRecoveries { get { return DB.Find<EmailRecovery>(); } }
public Find<Post> Posts { get { return DB.Find<Post>(); } }
public Find<DmPost> DmPosts { get { return DB.Find<DmPost>(); } }
public Find<Avatar> Avatars { get { return DB.Find<Avatar>(); } }
public Find<ForeignAvatar> ForeignAvatars { get { return DB.Find<ForeignAvatar>(); } }
public Find<RootToAvatar> RootToAvatars { get { return DB.Find<RootToAvatar>(); } }
}
}

View File

@ -0,0 +1,9 @@
namespace PrivaPub.StaticServices
{
public interface IPasswordHasher
{
string Hash(string password);
(bool verified, bool needsUpgrade) Check(string hash, string password);
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
namespace PrivaPub.StaticServices
{
public sealed class PasswordHasher : IPasswordHasher
{
private const int SaltSize = 16;
private const int KeySize = 32;
private HashingOptions Options { get; }
public PasswordHasher(IOptionsMonitor<HashingOptions> options)
{
Options = options.CurrentValue;
}
public (bool verified, bool needsUpgrade) Check(string hash, string password)
{
var parts = hash.Split('~', 3);
if (parts.Length != 3)
throw new FormatException("Unexpected hash format.");
var iterations = Convert.ToInt32(parts[2]);
var salt = Convert.FromBase64String(parts[1]);
var key = Convert.FromBase64String(parts[0]);
var needsUpgrade = iterations != Options.Iterations;
using (var algorithm = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA512))
{
var keyToCheck = algorithm.GetBytes(KeySize);
var verified = keyToCheck.SequenceEqual(key);
return (verified, needsUpgrade);
}
}
public string Hash(string password)
{
using (var algorithm = new Rfc2898DeriveBytes(password, SaltSize, Options.Iterations, HashAlgorithmName.SHA512))
{
var key = Convert.ToBase64String(algorithm.GetBytes(KeySize));
var salt = Convert.ToBase64String(algorithm.Salt);
return $"{key}~{salt}~{Options.Iterations}";
}
}
}
public sealed class HashingOptions
{
public int Iterations { get; set; } = 9789;
}
}

View File

@ -0,0 +1,97 @@
{
"MongoSettings": {
"Database": "PrivaPub",
"LogsDatabase": "logs",
"ConnectionString": "mongodb://localhost:27017"
},
"AppConfiguration": {
"RequiresFirstTimeSetup": null,
"Version": "0.0.0",
"MaxAllowedUploadFiles": 3,
"MaxAllowedFileSize": 2097152,
"ValidDiscussionFilesTypes": ".odt,.docx,.pdf,.xlsx,.ods,.odp,.pptx,.png,.jpg,.jpeg",
"SupportedLanguages": [
"en",
"it",
"de",
"fr",
"es",
"ja",
"ru",
"zh",
"bg",
"cs",
"da",
"nl",
"et",
"fi",
"el",
"hu",
"lv",
"lt",
"pl",
"pt",
"ro",
"sk",
"sl",
"sv"
],
"BackendBaseAddress": "https://localhost:7195",
"EmailConfiguration": {
"SmtpServer": "mail.privateemail.com",
"SmtpPort": 465,
"UseSSL": true,
"SmtpUsername": "support@collanon.app",
"SmtpPassword": "c4kXUJFQeKC2dVQbZqxZ"
},
"HashingOptions": {
"Iterations": 10101
},
"Jwt": {
"Key": "ITNN8mPfS2ivOqr1eRWK0Rac3sRAchQdG8BUy0pK4vQ3",
"Issuer": "https://localhost",
"Audience": "https://localhost",
"HoursTimeout": 24
}
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "https://0.0.0.0:7195",
"Protocols": "Http2"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft.AspNetCore": "Warning"
}
},
"WriteTo": [
{
"Name": "MongoDB",
"Args": {
"databaseUrl": "mongodb://localhost:27017/logs",
"collectionName": "collanonquick",
"cappedMaxSizeMb": "1024",
"cappedMaxDocuments": "10000"
}
},
{
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
}
}
]
}
}

View File

@ -0,0 +1,65 @@
{
"MongoSettings": {
"Database": "PrivaPub",
"LogsDatabase": "logs",
"ConnectionString": "mongodb://localhost:27017"
},
"AppConfiguration": {
"Version": "0.0.0",
"MaxAllowedUploadFiles": 3,
"MaxAllowedFileSize": 2097152,
"ValidDiscussionFilesTypes": ".odt,.docx,.pdf,.xlsx,.ods,.odp,.pptx,.png,.jpg,.jpeg",
"SupportedLanguages": [
"en", "it", "de", "fr", "es", "ja", "ru", "zh", "bg", "cs", "da", "nl", "et", "fi", "el", "hu", "lv", "lt", "pl", "pt", "ro", "sk", "sl", "sv"
],
"EmailConfiguration": {
"SmtpServer": "mail.privateemail.com",
"SmtpPort": 465,
"UseSSL": true,
"SmtpUsername": "support@collanon.app",
"SmtpPassword": "c4kXUJFQeKC2dVQbZqxZ"
},
"HashingOptions": {
"Iterations": 10101
},
"Jwt": {
"Key": "ITNN8mPfS2ivOqr1eRWK0Rac3sRAchQdG8BUy0pK4vQ3",
"Issuer": "https://localhost",
"Audience": "https://localhost",
"HoursTimeout": 365
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning"
}
},
"WriteTo": [
{
"Name": "MongoDB",
"Args": {
"databaseUrl": "mongodb://localhost:27017/logs",
"collectionName": "PrivaPub",
"cappedMaxSizeMb": "1024",
"cappedMaxDocuments": "10000"
}
},
{
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
}
}
]
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}