(refactor): dataprovider iqueriable and selector

This commit is contained in:
Maksym Sadovnychyy 2022-12-09 00:33:50 +01:00
parent 7033f00664
commit 9e59ca7f30
84 changed files with 756 additions and 367 deletions

View File

@ -9,7 +9,6 @@ namespace Core.Abstractions.Models {
public abstract class RequestModelBase<T> : ModelBase, IValidatableObject {
public abstract T ToDomainObject();
public abstract IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
}
}

View File

@ -6,14 +6,16 @@ using MongoDB.Driver.GridFS;
using DomainResults.Common;
using Extensions;
using DataProviders.Buckets;
namespace DataProviders.Abstractions {
namespace DataProviders.Abstractions
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class BucketDataProviderBase : DataProviderBase<BucketDataProviderBase> {
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class BucketDataProviderBase : DataProviderBase<BucketDataProviderBase> {
private readonly GridFSBucket _bucket;

View File

@ -8,6 +8,8 @@ using MongoDB.Driver;
using DomainResults.Common;
using DomainObjects.Abstractions;
using System;
using System.Collections.Generic;
namespace DataProviders.Abstractions {
@ -35,9 +37,13 @@ namespace DataProviders.Abstractions {
ISessionService sessionService,
string databaseName,
string collectionName
) : base (logger, client, databaseName) {
) : base(logger, client, databaseName) {
_idGenerator = idGenerator;
_sessionService = sessionService;
if (!_database.ListCollectionNames().ToList().Contains(collectionName))
_database.CreateCollection(collectionName);
_collection = _database.GetCollection<T>(collectionName);
}
@ -54,7 +60,7 @@ namespace DataProviders.Abstractions {
public Task<(Guid?, IDomainResult)> InsertAsync(T obj, Guid sessionId) =>
InsertAsync(obj, sessionId);
private async Task<(Guid?, IDomainResult)> InsertAsync(T obj, Guid? sessionId) {
protected virtual async Task<(Guid?, IDomainResult)> InsertAsync(T obj, Guid? sessionId) {
try {
if (sessionId != null)
await _collection.InsertOneAsync(_sessionService.GetSession(sessionId.Value), obj);
@ -83,7 +89,7 @@ namespace DataProviders.Abstractions {
private protected Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, Guid sessionId) =>
InsertManyAsync(objList, sessionId);
private async Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, Guid? sessionId) {
protected virtual async Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, Guid? sessionId) {
try {
if (sessionId != null)
await _collection.InsertManyAsync(_sessionService.GetSession(sessionId.Value), objList);
@ -99,26 +105,91 @@ namespace DataProviders.Abstractions {
}
#endregion
#region Get
private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate) =>
GetWithPredicate(predicate, 0, 0);
#region Count
private protected (int?, IDomainResult) CountWithPredicate(Expression<Func<T, bool>> predicate) =>
CountWithPredicate(new List<Expression<Func<T, bool>>> { predicate });
private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate, int skip, int limit) {
private protected (int?, IDomainResult) CountWithPredicate(List<Expression<Func<T, bool>>> predicates) {
try {
var result = _collection
.Find(predicate).Skip(skip).Limit(limit).ToList();
var query = GetWithPredicate(predicates);
return result != null && result.Count > 0
? IDomainResult.Success(result)
: IDomainResult.NotFound<List<T>?>();
var result = query.Count();
return IDomainResult.Success(result);
}
catch (Exception ex) {
_logger.LogError(ex, "Data provider error");
return IDomainResult.Failed<List<T>?>();
return IDomainResult.Failed<int?>();
}
}
#endregion
#region Get
/// <summary>
///
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="predicate"></param>
/// <param name="selector"></param>
/// <returns></returns>
private protected (List<TResult>?, IDomainResult) GetWithPredicate<TResult>(Expression<Func<T, bool>> predicate, Expression<Func<T, TResult>> selector) =>
GetWithPredicate(new List<Expression<Func<T, bool>>> { predicate }, selector, null, null);
private protected (List<TResult>?, IDomainResult) GetWithPredicate<TResult>(Expression<Func<T, bool>> predicates, Expression<Func<T, TResult>> selector, int? skip, int? limit) =>
GetWithPredicate(new List<Expression<Func<T, bool>>> { predicates }, selector, skip, limit);
/// <summary>
///
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="predicates"></param>
/// <param name="selector"></param>
/// <param name="skip"></param>
/// <param name="limit"></param>
/// <returns></returns>
private protected (List<TResult>?, IDomainResult) GetWithPredicate<TResult>(List<Expression<Func<T, bool>>> predicates, Expression<Func<T, TResult>> selector, int? skip, int? limit) {
try {
var query = GetWithPredicate(predicates).Select(selector);
if (skip != null)
query = query.Skip(skip.Value);
if (limit != null)
query = query.Take(limit.Value);
var result = query.ToList();
return result != null && result.Count > 0
? IDomainResult.Success(result)
: IDomainResult.NotFound<List<TResult>?>();
}
catch (Exception ex) {
_logger.LogError(ex, "Data provider error");
return IDomainResult.Failed<List<TResult>?>();
}
}
/// <summary>
///
/// </summary>
/// <param name="predicates"></param>
/// <returns></returns>
private protected IQueryable<T> GetWithPredicate(List<Expression<Func<T, bool>>> predicates) {
var query = GetQuery();
foreach (var predicate in predicates)
query = query.Where(predicate);
return query;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected virtual IQueryable<T> GetQuery() => _collection.AsQueryable();
#endregion
#region Update
private protected (Guid?, IDomainResult) UpdateWithPredicate(T obj, Expression<Func<T, bool>> predicate) =>
UpdateWithPredicateAsync(obj, predicate).Result;
@ -132,7 +203,7 @@ namespace DataProviders.Abstractions {
private protected Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, Guid sessionId) =>
UpdateWithPredicateAsync(obj, predicate, sessionId);
private async Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, Guid? sessionId) {
protected virtual async Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, Guid? sessionId) {
try {
@ -163,7 +234,7 @@ namespace DataProviders.Abstractions {
private protected Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid sessionId) =>
DeleteWithPredicateAsync(predicate, sessionId);
private async Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
protected virtual async Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
try {
if (sessionId != null)
await _collection.DeleteOneAsync<T>(_sessionService.GetSession(sessionId.Value), predicate);
@ -192,7 +263,7 @@ namespace DataProviders.Abstractions {
private protected Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid sessionId) =>
DeleteManyWithPredicateAsync(predicate, sessionId);
private async Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
protected virtual async Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
try {
if (sessionId != null)

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders {
public class BucketFile {
public Guid Id { get; private set; }
public Guid SiteId { get; private set; }
public Guid UserId { get; private set; }
public DateTime? Published { get; private set; }
public string Name { get; private set; }
public byte[] Bytes { get; private set; }
public string ContentType { get; private set; }
public BucketFile(Guid id, Guid siteId, Guid userId, DateTime? published, string name, byte[] bytes, string contentType) {
Id = id;
SiteId = siteId;
UserId = userId;
Published = published;
Name = name;
Bytes = bytes;
ContentType = contentType;
}
public BucketFile(Guid id, Guid siteId, Guid userId, string name, byte[] bytes, string contentType)
: this(id, siteId, userId, null, name, bytes, contentType) { }
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders.Buckets
{
public class BucketFile
{
public Guid Id { get; private set; }
public Guid SiteId { get; private set; }
public Guid UserId { get; private set; }
public DateTime? Published { get; private set; }
public string Name { get; private set; }
public byte[] Bytes { get; private set; }
public string ContentType { get; private set; }
public BucketFile(Guid id, Guid siteId, Guid userId, DateTime? published, string name, byte[] bytes, string contentType)
{
Id = id;
SiteId = siteId;
UserId = userId;
Published = published;
Name = name;
Bytes = bytes;
ContentType = contentType;
}
public BucketFile(Guid id, Guid siteId, Guid userId, string name, byte[] bytes, string contentType)
: this(id, siteId, userId, null, name, bytes, contentType) { }
}
}

View File

@ -7,12 +7,13 @@ using MongoDB.Driver;
using DataProviders.Abstractions;
using MongoDB.Driver.GridFS;
namespace DataProviders.Buckets {
namespace DataProviders.Buckets
{
/// <summary>
///
/// </summary>
public interface IImageBucketDataProvider {
/// <summary>
///
/// </summary>
public interface IImageBucketDataProvider {
/// <summary>
///

View File

@ -36,7 +36,7 @@ namespace DataProviders.Collections {
}
public (BlogDocument?, IDomainResult) Get(Guid siteId, Guid blogId) {
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Id == blogId);
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Id == blogId, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
@ -54,13 +54,13 @@ namespace DataProviders.Collections {
}
public (List<BlogDocument>?, IDomainResult) GetBySlugs(Guid siteId, List<string> slugs) =>
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)));
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)), x => x);
public (List<BlogDocument>?, IDomainResult) GetMany(Guid siteId, List<Guid> blogIds) =>
GetWithPredicate(x => x.SiteId == siteId && blogIds.Contains(x.Id));
GetWithPredicate(x => x.SiteId == siteId && blogIds.Contains(x.Id), x => x);
public (List<BlogDocument>?, IDomainResult) GetAll(Guid siteId, int skip, int take) =>
GetWithPredicate(x => x.SiteId == siteId);
GetWithPredicate(x => x.SiteId == siteId, x => x);
public (Guid?, IDomainResult) Update(BlogDocument blogItem) =>
UpdateWithPredicate(blogItem, x => x.Id == blogItem.Id);

View File

@ -37,7 +37,7 @@ namespace DataProviders.Collections {
}
public (Guid?, IDomainResult) CreateDefault(Guid siteId) {
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => y.Slug == "default"));
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => y.Slug == "default"), x => x);
if (result.IsSuccess && list != null)
return (list.First().Id, result);
@ -55,7 +55,7 @@ namespace DataProviders.Collections {
}
public (CategoryDocument?, IDomainResult) Get(Guid siteId, Guid categoryId) {
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Id == categoryId);
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Id == categoryId, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
@ -64,7 +64,7 @@ namespace DataProviders.Collections {
}
public (List<CategoryDocument>?, IDomainResult) GetMany(Guid siteId, List<Guid> categoryIds) =>
GetWithPredicate(x => x.SiteId == siteId && categoryIds.Contains(x.Id));
GetWithPredicate(x => x.SiteId == siteId && categoryIds.Contains(x.Id), x => x);
public (CategoryDocument?, IDomainResult) GetBySlug(Guid siteId, string slug) {
var (list, result) = GetBySlugs(siteId, new List<string> { slug });
@ -76,10 +76,10 @@ namespace DataProviders.Collections {
}
public (List<CategoryDocument>?, IDomainResult) GetBySlugs(Guid siteId, List<string> slugs) =>
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)));
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)), x => x);
public (List<CategoryDocument>?, IDomainResult) GetAll(Guid siteId) =>
GetWithPredicate(x => x.SiteId == siteId);
GetWithPredicate(x => x.SiteId == siteId, x => x);
public (Guid?, IDomainResult) Update(CategoryDocument obj) =>
UpdateWithPredicate(obj, x => x.Id == obj.Id);

View File

@ -0,0 +1,30 @@
using DomainObjects.Documents.Users;
using DomainResults.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders.Collections {
public interface ICompositeDataProvider {
}
public class CompositeDataProvider : ICompositeDataProvider {
private readonly ISiteDataProvider _siteDataProvider;
private readonly IUserDataProvider _userDataProvider;
public CompositeDataProvider(
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) {
_siteDataProvider = siteDataProvider;
_userDataProvider = userDataProvider;
}
}
}

View File

@ -25,6 +25,6 @@ namespace DataProviders.Collections {
ISessionService sessionService) : base(logger, client, idGenerator, sessionService, _databaseName, _collectionName) {
}
public (List<ContentDocument>?, IDomainResult) Get(Guid siteId) => GetWithPredicate(x => x.SiteId == siteId);
public (List<ContentDocument>?, IDomainResult) Get(Guid siteId) => GetWithPredicate(x => x.SiteId == siteId, x => x);
}
}

View File

@ -33,10 +33,10 @@ namespace DataProviders.Collections {
}
public (List<ShopCartDocument>?, IDomainResult) GetAll(Guid siteId, Guid userId) =>
GetWithPredicate(x => x.SiteId == siteId && x.UserId == userId, 0, 0);
GetWithPredicate(x => x.SiteId == siteId && x.UserId == userId, x => x, 0, 0);
public (ShopCartDocument?, IDomainResult) Get(Guid siteId, Guid userId, string sku) {
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.UserId == userId && x.Sku == sku, 0, 0);
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.UserId == userId && x.Sku == sku, x => x, 0, 0);
if (!result.IsSuccess || list == null)
return (null, result);

View File

@ -33,7 +33,7 @@ namespace DataProviders.Collections {
}
public (ShopDocument?, IDomainResult) Get(Guid siteId, string sku) {
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Sku == sku);
var (list, result) = GetWithPredicate(x => x.SiteId == siteId && x.Sku == sku, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
@ -51,10 +51,10 @@ namespace DataProviders.Collections {
}
public (List<ShopDocument>?, IDomainResult) GetBySlugs(Guid siteId, List<string> slugs) =>
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)));
GetWithPredicate(x => x.SiteId == siteId && x.L10n.Any(y => slugs.Contains(y.Slug)), x => x);
public (List<ShopDocument>?, IDomainResult) GetAll(Guid siteId, int skip, int take) =>
GetWithPredicate(x => x.SiteId == siteId, skip, take);
GetWithPredicate(x => x.SiteId == siteId, x => x, skip, take);
public (Guid?, IDomainResult) Update(ShopDocument shopCart) =>
UpdateWithPredicate(shopCart, x => x.Id == shopCart.Id);

View File

@ -0,0 +1,43 @@
using Microsoft.Extensions.Logging;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using DomainObjects.Documents.Sites;
using DomainResults.Common;
using DataProviders.Abstractions;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace DataProviders.Collections;
public interface ISiteDataProvider {
(Guid?, IDomainResult) Insert(Site site);
(Site?, IDomainResult) GetByHostName(string hostName);
}
public class SiteDataProvider : CollectionDataProviderBase<Site>, ISiteDataProvider {
private const string _databaseName = "reactredux";
private const string _collectionName = "sites";
public SiteDataProvider(
ILogger<SiteDataProvider> logger,
IMongoClient client,
IIdGenerator idGenerator,
ISessionService sessionService) : base(logger, client, idGenerator, sessionService, _databaseName, _collectionName) { }
public (Site?, IDomainResult) GetByHostName(string hostName) {
var (list, result) = GetWithPredicate(x => x.Hosts.Contains(hostName), x => x);
if (!result.IsSuccess || list == null)
return (null, result);
if (list.Count > 0)
return IDomainResult.Failed<Site?>();
return (list.First(), result);
}
}

View File

@ -4,77 +4,64 @@ using DataProviders.Abstractions;
using DomainResults.Common;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using DomainObjects.Documents.User;
using System.Runtime.CompilerServices;
using DomainObjects.Documents.Users;
namespace DataProviders.Collections
{
namespace DataProviders.Collections;
public interface IUserDataProvider {
(UserDocument?, IDomainResult) Get(Guid userId);
public interface IUserDataProvider {
(Guid?, IDomainResult) Insert(User user);
(User?, IDomainResult) Get(Guid userId);
(UserDocument?, IDomainResult) GetByUserIdAndHost(Guid userId, string host);
(User?, IDomainResult) GetByUsername(string nickName);
(UserDocument?, IDomainResult) GetByUsername(string nickName);
(User?, IDomainResult) GetByRecoveryToken(string recoveryToken);
(UserDocument?, IDomainResult) GetByRecoveryToken(string recoveryToken);
(Guid?, IDomainResult) Update(UserDocument user);
}
public class UserDataProvider : CollectionDataProviderBase<UserDocument>, IUserDataProvider {
private const string _databaseName = "reactredux";
private const string _collectionName = "users";
public UserDataProvider(
ILogger<UserDataProvider> logger,
IMongoClient client,
IIdGenerator idGenerator,
ISessionService sessionService) : base(logger, client, idGenerator, sessionService, _databaseName, _collectionName) {
}
public (UserDocument?, IDomainResult) Get(Guid userId) {
var (list, result) = GetWithPredicate(x => x.Id == userId);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (UserDocument?, IDomainResult) GetByUserIdAndHost(Guid userId, string host) {
var (list, result) = GetWithPredicate(x => x.Id == userId && x.Sites.Any(x => x.Hosts.Contains(host)));
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (UserDocument?, IDomainResult) GetByUsername(string username) {
var (list, result) = GetWithPredicate(x => x.Username == username);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (UserDocument?, IDomainResult) GetByRecoveryToken(string recoveryToken) {
var (list, result) = GetWithPredicate(x => x.Authentication.Password != null && x.Authentication.Password.RecoveryTokens.Any(y => !(DateTime.UtcNow > y.Expires) && y.Value == recoveryToken));
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (Guid?, IDomainResult) Update(UserDocument user) =>
UpdateWithPredicate(user, x => x.Id == user.Id);
}
(Guid?, IDomainResult) Update(User user);
}
public class UserDataProvider : CollectionDataProviderBase<User>, IUserDataProvider {
private const string _databaseName = "reactredux";
private const string _collectionName = "users";
public UserDataProvider(
ILogger<UserDataProvider> logger,
IMongoClient client,
IIdGenerator idGenerator,
ISessionService sessionService) : base(logger, client, idGenerator, sessionService, _databaseName, _collectionName) {
}
public (User?, IDomainResult) Get(Guid userId) {
var (list, result) = GetWithPredicate(x => x.Id == userId, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (User?, IDomainResult) GetByUsername(string username) {
var (list, result) = GetWithPredicate(x => x.Username == username, x => x);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (User?, IDomainResult) GetByRecoveryToken(string recoveryToken) {
var (list, result) = GetWithPredicate(x => x.Authentication.Password != null && x.Authentication.Password.RecoveryTokens.Any(y => !(DateTime.UtcNow > y.Expires) && y.Value == recoveryToken), x => x);
if (!result.IsSuccess || list == null)
return (null, result);
return (list.First(), result);
}
public (Guid?, IDomainResult) Update(User user) =>
UpdateWithPredicate(user, x => x.Id == user.Id);
}

View File

@ -28,9 +28,14 @@ namespace DataProviders.Extensions
services.AddSingleton<IShopCartDataProvider, ShopCartDataProvider>();
services.AddSingleton<ICategoryDataProvider, CategoryDataProvider>();
services.AddSingleton<IBlogCatalogDataProvider, BlogCatalogDataProvider>();
services.AddSingleton<ISiteDataProvider, SiteDataProvider>();
services.AddSingleton<IUserDataProvider, UserDataProvider>();
#endregion
#region Composite Collections
services.AddSingleton<ICompositeDataProvider, CompositeDataProvider>();
#endregion
#region Buckets
services.AddSingleton<IImageBucketDataProvider, ImageBucketDataProvider>();
#endregion

View File

@ -10,7 +10,8 @@ using DomainObjects.L10n;
using DomainObjects.Enumerations;
using DomainObjects.Pages;
using DomainObjects.Documents;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using DomainObjects.Documents.Sites;
namespace DataProviders
{
@ -335,8 +336,8 @@ namespace DataProviders
#endregion
#region User
if (!BsonClassMap.IsClassMapRegistered(typeof(Site))) {
BsonClassMap.RegisterClassMap<Site>(cm => {
if (!BsonClassMap.IsClassMapRegistered(typeof(SiteRole))) {
BsonClassMap.RegisterClassMap<SiteRole>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Role)
@ -377,8 +378,8 @@ namespace DataProviders
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(UserDocument))) {
BsonClassMap.RegisterClassMap<UserDocument>(cm => {
if (!BsonClassMap.IsClassMapRegistered(typeof(User))) {
BsonClassMap.RegisterClassMap<User>(cm => {
cm.AutoMap();
});
}

View File

@ -0,0 +1,9 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.Users;
public abstract class Token<T> : DomainObjectBase<T> {
public string Value { get; set; }
public DateTime Created { get; set; }
public DateTime Expires { get; set; }
}

View File

@ -9,6 +9,12 @@ public class Contact : DomainObjectBase<Contact> {
public bool Confirmed { get; set; }
public bool? Primary { get; set; }
public Contact(ContactTypes type, string value) {
Type = type;
Value = value;
Confirmed = false;
}
public override int GetHashCode() {
throw new NotImplementedException();
}

View File

@ -0,0 +1,18 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.Sites {
public class Site : DomainObjectDocumentBase<Site> {
public string Name { get; set; }
public List<string> Hosts { get; set; }
public Site(string name, List<string> hosts) {
Name = name;
Hosts = hosts;
}
public override int GetHashCode() {
throw new NotImplementedException();
}
}
}

View File

@ -1,13 +0,0 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.User;
public class AuthenticationToken : DomainObjectBase<AuthenticationToken> {
public string Value { get; set; }
public DateTime Created { get; set; }
public DateTime Expires { get; set; }
public override int GetHashCode() {
throw new NotImplementedException();
}
}

View File

@ -1,20 +0,0 @@
using CryptoProvider;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainObjects.Documents.User;
public class PasswordRecoveryToken {
public string Value { get; set; }
public DateTime Created { get; set; }
public DateTime Expires { get; set; }
public PasswordRecoveryToken() {
Value = RandomService.SecureRandomString(20);
Created = DateTime.UtcNow;
Expires = Created.AddMinutes(5);
}
}

View File

@ -1,16 +0,0 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.User
{
public class Site : DomainObjectBase<Site> {
public Guid SiteId { get; set; }
public List<string> Hosts { get; set; }
public Roles Role { get; set; }
public override int GetHashCode() {
throw new NotImplementedException();
}
}
}

View File

@ -1,4 +1,4 @@
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class Address
{

View File

@ -1,13 +1,13 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class Authentication : DomainObjectBase<Authentication> {
/// <summary>
/// Password object
/// </summary>
public Password? Password { get; set; }
public Password Password { get; set; }
/// <summary>
/// Password expiration optons

View File

@ -0,0 +1,10 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.Users;
public class AuthenticationToken : Token<AuthenticationToken> {
public override int GetHashCode() {
throw new NotImplementedException();
}
}

View File

@ -2,7 +2,7 @@
using CryptoProvider;
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class Password : DomainObjectBase<Password> {

View File

@ -1,6 +1,6 @@
using DomainObjects.Abstractions;
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class PasswordExpiration : DomainObjectBase<PasswordExpiration> {
public bool Enabled { get; set; }

View File

@ -0,0 +1,15 @@
using CryptoProvider;
namespace DomainObjects.Documents.Users;
public class PasswordRecoveryToken : Token<PasswordRecoveryToken> {
public PasswordRecoveryToken() {
Value = RandomService.SecureRandomString(20);
Created = DateTime.UtcNow;
Expires = Created.AddMinutes(5);
}
public override int GetHashCode() {
throw new NotImplementedException();
}
}

View File

@ -1,6 +1,6 @@
using Core.Abstractions;
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class Roles : Enumeration
{

View File

@ -0,0 +1,25 @@
using DomainObjects.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainObjects.Documents.Users {
public class SiteRole : DomainObjectBase<SiteRole> {
public Guid SiteId { get; set; }
public Roles Role { get; set; }
public SiteRole(Guid siteId, Roles role) {
SiteId = siteId;
Role = role;
}
public override int GetHashCode() {
throw new NotImplementedException();
}
}
}

View File

@ -1,11 +1,13 @@
using CryptoProvider;
using DomainObjects.Abstractions;
using DomainObjects.Documents.Sites;
using DomainObjects.Enumerations;
namespace DomainObjects.Documents.User;
namespace DomainObjects.Documents.Users;
public class UserDocument : DomainObjectDocumentBase<UserDocument> {
public class User : DomainObjectDocumentBase<User> {
public List<Site> Sites { get; set; }
public List<SiteRole> SiteRoles { get; set; }
public DateTime Created { get; set; }
@ -13,18 +15,43 @@ public class UserDocument : DomainObjectDocumentBase<UserDocument> {
public Authentication Authentication { get; set; }
public string Name { get; set; }
public string? Name { get; set; }
public string LastName { get; set; }
public string? LastName { get; set; }
public List<Contact> Contacts { get; set; }
public Address BillingAddress { get; set; }
public Address? BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
public Address? ShippingAddress { get; set; }
public List<AuthenticationToken> Tokens { get; set; }
public User(List<SiteRole> siteRoles, string userName, string email, string password) {
SiteRoles = siteRoles;
Created = DateTime.UtcNow;
Username = userName;
Authentication = new Authentication {
Password = new Password(password),
Expiration = new PasswordExpiration {
Enabled = true,
Days = 180
},
Expired = new List<Password>()
};
Contacts = new List<Contact> {
new Contact(ContactTypes.Email, email) {
Primary = true
}
};
Tokens = new List<AuthenticationToken>();
}
/// <summary>
/// By providing encryption parameters and token, system checks if this token is whitelisted
/// </summary>

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models.Account.Requests;
using WeatherForecast.Policies;
using WeatherForecast.Services;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Identity;
namespace WeatherForecast.Controllers;
@ -36,8 +36,6 @@ public class AccountController : ControllerBase {
_userDataProvider = userDataProvider;
}
#region Authless methods
/// <summary>
/// By providing username and password user obtains jwt token
/// </summary>
@ -50,10 +48,13 @@ public class AccountController : ControllerBase {
}
[HttpPost("[action]")]
public IActionResult Create() {
public IActionResult Register([FromBody] RegisterRequestModel requestData) {
return BadRequest();
}
#region Password
/// <summary>
/// Passing the Username in the request body is a more secure alternative to passing it as a GET param
/// </summary>
@ -78,7 +79,6 @@ public class AccountController : ControllerBase {
var result = _accountService.PasswordReset(requestData);
return result.ToActionResult();
}
#endregion
/// <summary>
/// For authenticated users that want to change their password the PUT request can be performed immediately without the email
@ -94,7 +94,7 @@ public class AccountController : ControllerBase {
if (!getUserResult.IsSuccess || user == null)
return BadRequest();
if ((await _authorizationService.AuthorizeAsync(User, new List<UserDocument> { user }, new PasswordChangeRequirement {
if ((await _authorizationService.AuthorizeAsync(User, new List<User> { user }, new PasswordChangeRequirement {
OldPassword = requestData.OldPassword
})).Succeeded) {
var result = _accountService.PasswordChange(user, requestData.OldPassword, requestData.NewPassword);
@ -103,4 +103,5 @@ public class AccountController : ControllerBase {
return Unauthorized();
}
#endregion
}

View File

@ -65,14 +65,11 @@ public class BlogItemController : ControllerBase {
[HttpPost("{siteId}")]
public async Task<IActionResult> Post([FromRoute] Guid siteId, [FromBody] BlogItemRequestModel requestData) {
var blogItem = requestData.ToDomainObject();
blogItem.SiteId = siteId;
var userId = User?.Identity?.Name?.ToNullableGuid();
if (userId == null)
return IDomainResult.Failed().ToActionResult();
blogItem.Author = userId.Value;
var blogItem = requestData.ToDomainObject(userId.Value, siteId);
if ((await _authorizationService.AuthorizeAsync(User, new List<BlogDocument> { blogItem }, new BlogAuthorizationRequirement {
Action = CrudActions.Create

View File

@ -1,5 +1,4 @@
using Core.Enumerations;
using DataProviders;
using DataProviders.Buckets;
using DomainResults.Common;
using DomainResults.Mvc;

View File

@ -0,0 +1,27 @@
using DomainResults.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models.Initialization.Requests;
using WeatherForecast.Services;
namespace WeatherForecast.Controllers;
[Route("api/[controller]")]
[ApiController]
public class InitializationController : ControllerBase {
private readonly IInitializationService _initializationService;
public InitializationController(
IInitializationService initializationService
) {
_initializationService = initializationService;
}
[HttpPost]
public IActionResult InitializeSystem([FromBody] InitializeSystemRequestModel requestData) {
var result = _initializationService.InitializeSystem(requestData);
return result.ToActionResult();
}
}

View File

@ -64,16 +64,12 @@ public class ShopItemController : ControllerBase {
/// <returns></returns>
[HttpPost("{siteId}/{sku}")]
public async Task<IActionResult> Post([FromRoute] Guid siteId, [FromRoute] string sku, [FromBody] ShopItemRequestModel requestData) {
var shopItem = requestData.ToDomainObject();
shopItem.SiteId = siteId;
shopItem.Sku = sku;
var userId = User?.Identity?.Name?.ToNullableGuid();
if (userId == null)
return IDomainResult.Failed().ToActionResult();
shopItem.Author = userId.Value;
var shopItem = requestData.ToDomainObject(sku, userId.Value, siteId);
if ((await _authorizationService.AuthorizeAsync(User, new List<ShopDocument> { shopItem }, new ShopAuthorizationRequirement {
Action = CrudActions.Create

View File

@ -42,7 +42,7 @@ namespace WeatherForecast.Models.Abstractions.PostItem.Requests {
///
/// </summary>
/// <returns></returns>
public override MediaAttachmentL10n ToDomainObject() {
public MediaAttachmentL10n ToDomainObject() {
if (HasValidationErrors()) throw new ValidationException();
return new MediaAttachmentL10n() {

View File

@ -33,7 +33,7 @@ namespace WeatherForecast.Models.Abstractions.PostItem.Requests {
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override MediaAttachment ToDomainObject() {
public MediaAttachment ToDomainObject() {
return new MediaAttachment {
Src = Src,

View File

@ -63,7 +63,7 @@ namespace WeatherForecast.Models.Abstractions.PostItem.Requests {
///
/// </summary>
/// <returns></returns>
public override PostItemL10n ToDomainObject() {
public PostItemL10n ToDomainObject() {
if (HasValidationErrors()) throw new ValidationException();
return new PostItemL10n() {

View File

@ -3,12 +3,25 @@ using Core.Enumerations;
using System.ComponentModel.DataAnnotations;
namespace WeatherForecast.Models.Account.Requests {
/// <summary>
///
/// </summary>
public class PasswordResetRequestModel : RequestModelBase {
/// <summary>
///
/// </summary>
public string Token { get; set; }
/// <summary>
///
/// </summary>
public string Password { get; set; }
/// <summary>
///
/// </summary>
public string RePassword { get; set; }
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {

View File

@ -0,0 +1,4 @@
namespace WeatherForecast.Models.Account.Requests {
public class RegisterRequestModel {
}
}

View File

@ -25,21 +25,22 @@ namespace WeatherForecast.Models.Blog.Requests {
///
/// </summary>
/// <returns></returns>
public override BlogDocument ToDomainObject() {
return new BlogDocument() {
L10n = L10n.Select(x => x.ToDomainObject()).ToList(),
public BlogDocument ToDomainObject(Guid userId, Guid siteId) => new () {
SiteId = siteId,
// Images
// Author
L10n = L10n.Select(x => x.ToDomainObject()).ToList(),
Tags = Tags,
Categories = Categories,
FamilyFriendly = FamilyFriendly,
// Images
Author = userId,
Tags = Tags,
Categories = Categories,
FamilyFriendly = FamilyFriendly,
ReadTime = ReadTime,
Likes = Likes
};
ReadTime = ReadTime,
Likes = Likes
};
}
/// <summary>
///

View File

@ -20,12 +20,12 @@ namespace WeatherForecast.Models.Category.Requests {
///
/// </summary>
/// <returns></returns>
public override CategoryDocument ToDomainObject() {
public CategoryDocument ToDomainObject(Guid siteId) => new CategoryDocument() {
SiteId = siteId,
L10n = L10n.Select(x => x.ToDomainObject()).ToList()
};
return new CategoryDocument() {
L10n = L10n.Select(x => x.ToDomainObject()).ToList()
};
}
/// <summary>
///

View File

@ -32,7 +32,7 @@ namespace WeatherForecast.Models.Category.Requests {
///
/// </summary>
/// <returns></returns>
public override CategoryL10n ToDomainObject() {
public CategoryL10n ToDomainObject() {
if (HasValidationErrors()) throw new ValidationException();
return new CategoryL10n() {

View File

@ -0,0 +1,50 @@
using Core.Abstractions.Models;
using Core.Enumerations;
using DomainObjects.Documents;
using System.ComponentModel.DataAnnotations;
using System.Reflection.Metadata;
namespace WeatherForecast.Models.Initialization.Requests {
public class InitializeSystemRequestModel : RequestModelBase<InitializeSystemRequestModel> {
public string SiteName { get; set; }
public string Host { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string RePassword { get; set; }
public InitializeSystemRequestModel ToDomainObject() {
throw new NotImplementedException();
}
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(SiteName))
yield return new ValidationResult($"{nameof(SiteName)} ${Errors.NullOrEmpty}");
if (string.IsNullOrWhiteSpace(Host))
yield return new ValidationResult($"{nameof(Host)} ${Errors.NullOrEmpty}");
if (string.IsNullOrWhiteSpace(Username))
yield return new ValidationResult($"{nameof(Username)} ${Errors.NullOrEmpty}");
if (string.IsNullOrWhiteSpace(Email))
yield return new ValidationResult($"{nameof(Email)} ${Errors.NullOrEmpty}");
if (string.IsNullOrWhiteSpace(Password))
yield return new ValidationResult($"{nameof(Password)} ${Errors.NullOrEmpty}");
if (string.IsNullOrWhiteSpace(RePassword))
yield return new ValidationResult($"{nameof(RePassword)} ${Errors.NullOrEmpty}");
if (string.Compare(Password, RePassword) != 0)
yield return new ValidationResult($"{nameof(Password)} and {nameof(RePassword)} ${Errors.NotMatched}");
}
}
}

View File

@ -21,9 +21,13 @@ namespace WeatherForecast.Models.Shop.Requests {
///
/// </summary>
/// <returns></returns>
public override ShopCartDocument ToDomainObject() {
public ShopCartDocument ToDomainObject(string sku, Guid userId, Guid siteId) {
return new ShopCartDocument() {
Sku = sku,
UserId = userId,
SiteId = siteId,
Quantity = Quantity.Value,
Created = DateTime.UtcNow
};

View File

@ -41,23 +41,24 @@ namespace WeatherForecast.Models.Requests {
///
/// </summary>
/// <returns></returns>
public override ShopDocument ToDomainObject() {
public ShopDocument ToDomainObject(string sku, Guid userId, Guid siteId) => new ShopDocument() {
Sku = sku,
SiteId = siteId,
return new ShopDocument() {
L10n = L10n.Select(x => x.ToDomainObject()).ToList(),
MediaAttachments = MediaAttachments?.Select(x => x.ToDomainObject()).ToList(),
// Author
Created = DateTime.UtcNow,
Tags = Tags,
FamilyFriendly = FamilyFriendly,
L10n = L10n.Select(x => x.ToDomainObject()).ToList(),
MediaAttachments = MediaAttachments?.Select(x => x.ToDomainObject()).ToList(),
Author = userId,
Created = DateTime.UtcNow,
Tags = Tags,
FamilyFriendly = FamilyFriendly,
BrandName = BrandName,
Rating = Rating,
Price = Price.Value,
NewPrice = NewPrice,
Quantity = Quantity.Value
};
BrandName = BrandName,
Rating = Rating,
Price = Price.Value,
NewPrice = NewPrice,
Quantity = Quantity.Value
};
}
/// <summary>
///

View File

@ -3,11 +3,12 @@ using Microsoft.AspNetCore.Authorization;
using DataProviders.Collections;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using CryptoProvider;
using Extensions;
using DomainObjects.Documents.Sites;
namespace WeatherForecast.Policies.Abstractions;
@ -22,12 +23,13 @@ public static class AuthorisationHandlerHelper {
/// <param name="context"></param>
/// <param name="aesKey"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
/// <returns></returns>
public static UserDocument? GetUser(AuthorizationHandlerContext context, IAesKey? aesKey, IHttpContextAccessor contextAccessor, IUserDataProvider userDataProvider) {
/// <returns>User document</returns>
public static (Site?, User?) GetUser(AuthorizationHandlerContext context, IAesKey? aesKey, IHttpContextAccessor contextAccessor, ISiteDataProvider siteDataProvider, IUserDataProvider userDataProvider) {
if (context == null || aesKey?.IV == null || aesKey?.Key == null)
return null;
return (null, null);
var userId = context.User?.Identity?.Name?.ToNullableGuid();
@ -36,16 +38,25 @@ public static class AuthorisationHandlerHelper {
var bearerToken = request?.Headers.Authorization.FirstOrDefault();
if (userId == null || host == null || bearerToken == null)
return null;
return (null, null);
var (user, getUserResult) = userDataProvider.GetByUserIdAndHost(userId.Value, host);
#region Validate if user belongs to site
var (site, getSiteResult) = siteDataProvider.GetByHostName(host);
if (!getSiteResult.IsSuccess || site == null)
return (null, null);
var (user, getUserResult) = userDataProvider.Get(userId.Value);
if (!getUserResult.IsSuccess || user == null)
return null;
return (null, null);
if (user.SiteRoles.Any(x => x.SiteId == site.Id))
return (null, null);
#endregion
if (!user.ValidateToken(aesKey.IV, aesKey.Key, bearerToken))
return null;
return (null, null);
return user;
return (site, user);
}
/// <summary>
@ -54,11 +65,8 @@ public static class AuthorisationHandlerHelper {
/// <param name="contextAccessor"></param>
/// <param name="user"></param>
/// <returns></returns>
public static Roles? GetRole(IHttpContextAccessor contextAccessor, UserDocument user) {
var request = contextAccessor.HttpContext?.Request;
var host = request?.Headers["X-Forwarded-Host"].FirstOrDefault();
var bearerToken = request?.Headers.Authorization.FirstOrDefault();
return user.Sites.Single(x => x.Hosts.Any(x => x == host)).Role;
public static Roles? GetRole(Site site, User user) {
return user.SiteRoles.SingleOrDefault(x => x.SiteId == site.Id)?.Role;
}
}
@ -71,6 +79,7 @@ public abstract class AuthorizationHandlerBase<TRequirement> : AuthorizationHand
private readonly IAesKey? _aesKey;
private readonly IHttpContextAccessor _contextAccessor;
private readonly ISiteDataProvider _siteDataProvider;
private readonly IUserDataProvider _userDataProvider;
/// <summary>
@ -78,14 +87,17 @@ public abstract class AuthorizationHandlerBase<TRequirement> : AuthorizationHand
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public AuthorizationHandlerBase(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) {
_aesKey = configuration.Value.JwtTokenEncryption;
_contextAccessor = contextAccessor;
_siteDataProvider = siteDataProvider;
_userDataProvider = userDataProvider;
}
@ -94,11 +106,11 @@ public abstract class AuthorizationHandlerBase<TRequirement> : AuthorizationHand
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected UserDocument? GetUser(AuthorizationHandlerContext context) =>
AuthorisationHandlerHelper.GetUser(context, _aesKey, _contextAccessor, _userDataProvider);
protected (Site?, User?) GetUser(AuthorizationHandlerContext context) =>
AuthorisationHandlerHelper.GetUser(context, _aesKey, _contextAccessor, _siteDataProvider, _userDataProvider);
protected Roles? GetRole(UserDocument user) =>
AuthorisationHandlerHelper.GetRole(_contextAccessor, user);
protected Roles? GetRole(Site site, User user) =>
AuthorisationHandlerHelper.GetRole(site, user);
}
/// <summary>
@ -110,6 +122,7 @@ public abstract class AuthorizationHandlerBase<TRequirement, TResource> : Author
private readonly IAesKey? _aesKey;
private readonly IHttpContextAccessor _contextAccessor;
private readonly ISiteDataProvider _siteDataProvider;
private readonly IUserDataProvider _userDataProvider;
/// <summary>
@ -117,14 +130,17 @@ public abstract class AuthorizationHandlerBase<TRequirement, TResource> : Author
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public AuthorizationHandlerBase(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) {
_aesKey = configuration.Value.JwtTokenEncryption;
_contextAccessor = contextAccessor;
_siteDataProvider = siteDataProvider;
_userDataProvider = userDataProvider;
}
@ -133,9 +149,9 @@ public abstract class AuthorizationHandlerBase<TRequirement, TResource> : Author
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
protected UserDocument? GetUser(AuthorizationHandlerContext context) =>
AuthorisationHandlerHelper.GetUser(context, _aesKey, _contextAccessor, _userDataProvider);
protected (Site?, User?) GetUser(AuthorizationHandlerContext context) =>
AuthorisationHandlerHelper.GetUser(context, _aesKey, _contextAccessor, _siteDataProvider, _userDataProvider);
protected Roles? GetRole(UserDocument user) =>
AuthorisationHandlerHelper.GetRole(_contextAccessor, user);
protected Roles? GetRole(Site site, User user) =>
AuthorisationHandlerHelper.GetRole(site, user);
}

View File

@ -7,7 +7,7 @@ using DataProviders.Collections;
using WeatherForecast.Policies.Abstractions;
using Microsoft.Extensions.Options;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
namespace WeatherForecast.Policies;
@ -21,12 +21,14 @@ public class BlogAuthorizationHandler : AuthorizationHandlerBase<BlogAuthorizati
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public BlogAuthorizationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(configuration, contextAccessor, userDataProvider) { }
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) { }
/// <summary>
///
@ -37,11 +39,11 @@ public class BlogAuthorizationHandler : AuthorizationHandlerBase<BlogAuthorizati
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BlogAuthorizationRequirement requirement, List<BlogDocument> resource) {
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
var userRole = GetRole(site, user);
// Can only Admin, Editor, Author, Contributor (cannot set publish date)
if (requirement.Action == CrudActions.Create

View File

@ -1,6 +1,6 @@
using Core.Enumerations;
using DataProviders.Collections;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using WeatherForecast.Policies.Abstractions;
@ -16,13 +16,16 @@ public class CategoryAuthorizationHandler : AuthorizationHandlerBase<CategoryAut
/// <summary>
///
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public CategoryAuthorizationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(configuration, contextAccessor, userDataProvider) { }
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) { }
/// <summary>
///
@ -32,11 +35,11 @@ public class CategoryAuthorizationHandler : AuthorizationHandlerBase<CategoryAut
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CategoryAuthorizationRequirement requirement) {
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
var userRole = GetRole(site, user);
// Can Admin, Editor, Shop manager
if ((requirement.Action == CrudActions.Create || requirement.Action == CrudActions.Update)

View File

@ -1,7 +1,7 @@
using Core.Enumerations;
using DataProviders;
using DataProviders.Buckets;
using DataProviders.Collections;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using FileSecurityService;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
@ -26,9 +26,10 @@ public class FileAuthorisationHandler : AuthorizationHandlerBase<FileAuthorisati
public FileAuthorisationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider,
IFileSecurityService fileSecurityService
) : base(configuration, contextAccessor, userDataProvider) {
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) {
_fileSecurityService = fileSecurityService;
}
@ -42,11 +43,11 @@ public class FileAuthorisationHandler : AuthorizationHandlerBase<FileAuthorisati
/// <exception cref="NotImplementedException"></exception>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FileAuthorisationRequirement requirement, List<BucketFile> resource) {
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
var userRole = GetRole(site, user);
if (resource.Any(x => {
var (fileCategory, signatureResult) = _fileSecurityService.CheckFileSignature(x.Name, x.Bytes, x.ContentType);

View File

@ -1,6 +1,6 @@
using CryptoProvider;
using DataProviders.Collections;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using WeatherForecast.Policies.Abstractions;
@ -11,18 +11,21 @@ namespace WeatherForecast.Policies;
/// <summary>
///
/// </summary>
public class PasswordChangeAuthorizationHandler : AuthorizationHandlerBase<PasswordChangeRequirement, List<UserDocument>> {
public class PasswordChangeAuthorizationHandler : AuthorizationHandlerBase<PasswordChangeRequirement, List<User>> {
/// <summary>
///
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public PasswordChangeAuthorizationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(configuration, contextAccessor, userDataProvider) { }
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) { }
/// <summary>
///
@ -31,27 +34,16 @@ public class PasswordChangeAuthorizationHandler : AuthorizationHandlerBase<Passw
/// <param name="requirement"></param>
/// <param name="resource"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PasswordChangeRequirement requirement, List<UserDocument> resource) {
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PasswordChangeRequirement requirement, List<User> resource) {
// User from token
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
if (userRole != Roles.Admin && resource.Any(x => x.Id != user.Id))
return Task.CompletedTask;
if (resource.Count() > 0 && resource.Any(x => x.Id == user.Id))
return Task.CompletedTask;
var userRole = GetRole(site, user);
if (resource.All(x => x.Id == user.Id)) {
if (user.Authentication.Password == null)
return Task.CompletedTask;
if (!HashService.ValidateHash(requirement.OldPassword, user.Authentication.Password.Salt, user.Authentication.Password.Hash))
if (user.Authentication.Password != null && user.Authentication.Password.Validate(requirement.OldPassword))
return Task.CompletedTask;
}

View File

@ -1,7 +1,7 @@
using Core.Enumerations;
using DataProviders.Collections;
using DomainObjects.Documents;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using WeatherForecast.Policies.Abstractions;
@ -18,12 +18,14 @@ namespace WeatherForecast.Policies {
/// </summary>
/// <param name="configuration"></param>
/// <param name="contextAccessor"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public ShopAuthorizationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(configuration, contextAccessor, userDataProvider) { }
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) { }
/// <summary>
///
@ -33,11 +35,12 @@ namespace WeatherForecast.Policies {
/// <param name="resource"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ShopAuthorizationRequirement requirement, List<ShopDocument> resource) {
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
var userRole = GetRole(site, user);
// Can Admin, Shop manager
if (requirement.Action == CrudActions.Create

View File

@ -1,6 +1,6 @@
using DataProviders.Collections;
using DomainObjects.Documents;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using WeatherForecast.Policies.Abstractions;
@ -21,8 +21,9 @@ namespace WeatherForecast.Policies {
public ShopCartAuthorizationHandler(
IOptions<Configuration> configuration,
IHttpContextAccessor contextAccessor,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(configuration, contextAccessor, userDataProvider) { }
) : base(configuration, contextAccessor, siteDataProvider, userDataProvider) { }
/// <summary>
///
@ -32,11 +33,12 @@ namespace WeatherForecast.Policies {
/// <param name="resource"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ShopCartAuthorizationRequirement requirement, List<ShopCartDocument> resource) {
var user = GetUser(context);
if (user == null)
var (site, user) = GetUser(context);
if (site == null || user == null)
return Task.CompletedTask;
var userRole = GetRole(user);
var userRole = GetRole(site, user);
// Leave only admin to manage others carts
if (userRole != Roles.Admin && resource.Any(x => x.UserId != user.Id))

View File

@ -1,23 +1,23 @@
using Core.Abstractions;
using DomainObjects;
using CryptoProvider;
using DataProviders.Collections;
using DomainResults.Common;
using Microsoft.Extensions.Options;
using WeatherForecast.Models.Account.Requests;
using DomainObjects.Documents.User;
using DomainObjects.Documents.Users;
using EmailProvider;
using DomainObjects.Enumerations;
using Org.BouncyCastle.Crypto;
using Microsoft.AspNetCore.Identity;
using Extensions;
using DomainObjects.Documents.Sites;
namespace WeatherForecast.Services
{
namespace WeatherForecast.Services {
/// <summary>
///
/// </summary>
public interface IAccountService {
/// <summary>
///
/// </summary>
public interface IAccountService {
/// <summary>
///
/// </summary>
@ -48,7 +48,7 @@ namespace WeatherForecast.Services
/// <param name="oldPassword"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
(Guid?, IDomainResult) PasswordChange(UserDocument user, string oldPassword, string newPassword);
(Guid?, IDomainResult) PasswordChange(User user, string oldPassword, string newPassword);
}
/// <summary>
@ -108,6 +108,17 @@ namespace WeatherForecast.Services
return IDomainResult.Success(token);
}
/// <summary>
/// Passing the Username in the request body is a more secure alternative to passing it as a GET param
/// </summary>
@ -137,10 +148,12 @@ namespace WeatherForecast.Services
emailBuilder.AddHtmlBody(template);
// TODO: save inside mongo file bucket
emailBuilder.DkimSign("maks-it.com", "default", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "dkim", "maks-it.com"));
using var smtpService = new SMTPService();
// TODO: change email password and manage configs inside database
smtpService.Connect("smtp.ionos.it", 465, true);
smtpService.Authenticate("commercial@maks-it.com", "E23{R#<X&#Lyz");
@ -159,7 +172,7 @@ namespace WeatherForecast.Services
public IDomainResult PasswordReset(PasswordResetRequestModel requestData) {
var (user, getUserResult) = _userDataProvider.GetByRecoveryToken(requestData.Token);
if(!getUserResult.IsSuccess || user == null)
if (!getUserResult.IsSuccess || user == null)
return IDomainResult.Failed();
user.Authentication.PasswordReset(requestData.Token, requestData.Password);
@ -175,7 +188,7 @@ namespace WeatherForecast.Services
/// <param name="user"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
public (Guid?, IDomainResult) PasswordChange(UserDocument user, string oldPassword, string newPassword) {
public (Guid?, IDomainResult) PasswordChange(User user, string oldPassword, string newPassword) {
user.Authentication.PasswordChange(oldPassword, newPassword);

View File

@ -137,12 +137,9 @@ namespace WeatherForecast.Services {
public (Guid?, IDomainResult) Update(BlogDocument blogItem, BlogItemRequestModel requestData) {
// construct domain object from model
var newItem = requestData.ToDomainObject();
var newItem = requestData.ToDomainObject(blogItem.Author, blogItem.SiteId);
newItem.Id = blogItem.Id;
newItem.SiteId = blogItem.SiteId;
newItem.Created = blogItem.Created;
newItem.Author = blogItem.Author;
var (categories, addCategoriesResult) = AddCategoryIfNullOrEmpty(blogItem.SiteId, requestData.Categories);
if (!addCategoriesResult.IsSuccess || categories == null)

View File

@ -85,8 +85,7 @@ namespace WeatherForecast.Services {
/// <returns></returns>
public (Guid?, IDomainResult) Post(Guid siteId, CategoryItemRequestModel requestModel) {
try {
var item = requestModel.ToDomainObject();
item.SiteId = siteId;
var item = requestModel.ToDomainObject(siteId);
var (_, getResult) = _categoryDataProvider.GetBySlugs(item.SiteId, item.L10n.Select(x => x.Slug).ToList());
if (getResult.IsSuccess)
@ -161,9 +160,8 @@ namespace WeatherForecast.Services {
return (null, result);
// construct domain object from model
var newItem = requestData.ToDomainObject();
var newItem = requestData.ToDomainObject(item.SiteId);
newItem.Id = item.Id;
newItem.SiteId = item.SiteId;
if (!item.Equals(newItem)) {
var (id, updateResult) = _categoryDataProvider.Update(newItem);

View File

@ -0,0 +1,75 @@
using Core.Abstractions;
using CryptoProvider;
using DataProviders.Collections;
using DomainObjects.Documents.Sites;
using DomainObjects.Documents.Users;
using DomainObjects.Enumerations;
using DomainObjects;
using DomainResults.Common;
using Extensions;
using Microsoft.Extensions.Options;
using WeatherForecast.Models.Initialization.Requests;
namespace WeatherForecast.Services;
public interface IInitializationService {
IDomainResult InitializeSystem(InitializeSystemRequestModel requestData);
}
/// <summary>
///
/// </summary>
public class InitializationService : ServiceBase<InitializationService>, IInitializationService {
private readonly IAesKey? _aesKey;
private readonly IJwtConfig? _jwtConfig;
private readonly ISiteDataProvider _siteDataProvider;
private readonly IUserDataProvider _userDataProvider;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="options"></param>
/// <param name="siteDataProvider"></param>
/// <param name="userDataProvider"></param>
public InitializationService(
ILogger<InitializationService> logger,
IOptions<Configuration> options,
ISiteDataProvider siteDataProvider,
IUserDataProvider userDataProvider
) : base(logger) {
_aesKey = options.Value.JwtTokenEncryption;
_jwtConfig = options.Value.JwtConfig;
_siteDataProvider = siteDataProvider;
_userDataProvider = userDataProvider;
}
/// <summary>
///
/// </summary>
/// <param name="requestData"></param>
/// <returns></returns>
public IDomainResult InitializeSystem(InitializeSystemRequestModel requestData) {
var userId = "fdc5aa50-ee68-4bae-a8e6-b8ae2c258f60".ToGuid();
var siteId = "404c8232-9048-4519-bfba-6e78dc7005ca".ToGuid();
var (_, siteInsetResult) = _siteDataProvider.Insert(new Site(requestData.SiteName, new List<string> { requestData.Host }) {
Id = siteId
});
var user = new User(new List<SiteRole> { new SiteRole(siteId, Roles.Admin) }, requestData.Username, requestData.Email, requestData.Password) {
Id = userId
};
user.Contacts.ForEach(x => x.Confirmed = true);
var (_, userInsertResult) = _userDataProvider.Insert(user);
return IDomainResult.Success();
}
}

View File

@ -84,11 +84,7 @@ public class ShopCartItemService : ServiceBase<ShopCartItemService>, IShopCartIt
if (getResult.IsSuccess)
return IDomainResult.Failed<Guid?>();
var item = requestModel.ToDomainObject();
item.SiteId = siteId;
item.UserId = userId;
item.Sku = sku;
var item = requestModel.ToDomainObject(sku, userId, siteId);
var (id, insertResult) = _shopCartDataProvider.Insert(item);
@ -131,11 +127,8 @@ public class ShopCartItemService : ServiceBase<ShopCartItemService>, IShopCartIt
public (Guid?, IDomainResult) Update(ShopCartDocument cartItem, ShopCartItemRequestModel requestData) {
// construct domain object from model
var newItem = requestData.ToDomainObject();
var newItem = requestData.ToDomainObject(cartItem.Sku, cartItem.UserId, cartItem.SiteId);
newItem.Id = cartItem.Id;
newItem.SiteId = cartItem.SiteId;
newItem.UserId = cartItem.UserId;
newItem.Sku = cartItem.Sku;
newItem.Created = cartItem.Created;
if (!cartItem.Equals(newItem)) {

View File

@ -153,12 +153,9 @@ namespace WeatherForecast.Services {
return (null, getResult);
// construct domain object from model
var newItem = requestData.ToDomainObject();
var newItem = requestData.ToDomainObject(sku, item.Author, siteId);
newItem.Id = item.Id;
newItem.SiteId = siteId;
newItem.Sku = sku;
newItem.Created = item.Created;
newItem.Author = item.Author;
var (categories, addCategoriesResult) = AddCategoryIfNullOrEmpty(siteId, requestData.Categories);
if (!addCategoriesResult.IsSuccess || categories == null)

View File

@ -94,6 +94,7 @@ namespace WeatherForecast {
services.AddScoped<ICategoryItemsService, CategoryItemsService>();
services.AddScoped<IContentService, ContentService>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<IInitializationService, InitializationService>();
services.AddScoped<IShopCartItemService, ShopCartItemService>();
services.AddScoped<IShopCartItemsService, ShopCartItemsService>();
services.AddScoped<IShopItemService, ShopItemService>();

View File

@ -3,4 +3,4 @@ WiredTiger 10.0.2: (December 21, 2021)
WiredTiger version
major=10,minor=0,patch=2
file:WiredTiger.wt
access_pattern_hint=none,allocation_size=4KB,app_metadata=,assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,collator=,columns=,dictionary=0,encryption=(keyid=,name=),format=btree,huffman_key=,huffman_value=,id=0,ignore_in_memory_cache_size=false,internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=4KB,key_format=S,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=32KB,leaf_value_max=0,log=(enabled=true),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=false,prefix_compression_min=4,readonly=false,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,cache_directory=,local_retention=300,name=,object_target_size=0),value_format=S,verbose=[],version=(major=1,minor=1),write_timestamp_usage=none,checkpoint=(WiredTigerCheckpoint.78531=(addr="018781e4a527c68d8881e43cd827ac8981e4935a7dbb808080e3022fc0e2dfc0",order=78531,time=1670018018,size=69632,newest_start_durable_ts=0,oldest_start_ts=0,newest_txn=3,newest_stop_durable_ts=0,newest_stop_ts=-1,newest_stop_txn=-11,prepare=0,write_gen=235943,run_write_gen=235628)),checkpoint_backup_info=,checkpoint_lsn=(29,118272)
access_pattern_hint=none,allocation_size=4KB,app_metadata=,assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,collator=,columns=,dictionary=0,encryption=(keyid=,name=),format=btree,huffman_key=,huffman_value=,id=0,ignore_in_memory_cache_size=false,internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=4KB,key_format=S,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=32KB,leaf_value_max=0,log=(enabled=true),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=false,prefix_compression_min=4,readonly=false,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,cache_directory=,local_retention=300,name=,object_target_size=0),value_format=S,verbose=[],version=(major=1,minor=1),write_timestamp_usage=none,checkpoint=(WiredTigerCheckpoint.87032=(addr="018781e4072edf558b81e49885698c8c81e48750482f808080e301bfc0e2dfc0",order=87032,time=1671750241,size=69632,newest_start_durable_ts=0,oldest_start_ts=0,newest_txn=442,newest_stop_durable_ts=0,newest_stop_ts=-1,newest_stop_txn=-11,prepare=0,write_gen=261469,run_write_gen=260980)),checkpoint_backup_info=,checkpoint_lsn=(33,135936)