Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cfodev 469 create architecture test project #16

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cats.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{62ABFE2F-D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.UnitTests", "test\Application.UnitTests\Application.UnitTests.csproj", "{2FC2E6B9-5186-4A39-A2CC-0975ED2AEC1E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchitectureTests", "test\ArchitectureTests\ArchitectureTests.csproj", "{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -79,6 +81,10 @@ Global
{2FC2E6B9-5186-4A39-A2CC-0975ED2AEC1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FC2E6B9-5186-4A39-A2CC-0975ED2AEC1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FC2E6B9-5186-4A39-A2CC-0975ED2AEC1E}.Release|Any CPU.Build.0 = Release|Any CPU
{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -93,6 +99,7 @@ Global
{7721D89A-D919-41A8-8B5B-8D5BF862B3F7} = {28581377-5421-47F6-A678-9510117A7791}
{50485EFF-8E12-4F8A-A087-A9882D2C31C7} = {28581377-5421-47F6-A678-9510117A7791}
{2FC2E6B9-5186-4A39-A2CC-0975ED2AEC1E} = {62ABFE2F-D4F3-4E44-AA07-20D679B04CF3}
{386B9FE9-E2C6-443A-A8EE-D1DC79E867C6} = {62ABFE2F-D4F3-4E44-AA07-20D679B04CF3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {916B96E7-FFB9-4D43-A9BD-88CB137B2783}
Expand Down
277 changes: 277 additions & 0 deletions db/seed/001_development_seed.sql

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions db/seed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Database Seed Scripts

This directory contains SQL scripts to seed the database with initial data.

## How to Use

1. Ensure your database is created and migrated using EF Core.
2. Run the seed scripts in the alphabetical order
6 changes: 6 additions & 0 deletions src/Application/Common/Security/AllowAnonymousAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Cfo.Cats.Application.Common.Security;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class AllowAnonymousAttribute : Attribute
{
}
3 changes: 2 additions & 1 deletion src/Application/Common/Security/RequestAuthorizeAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Cfo.Cats.Application.Common.Security;


public class RequestAuthorizeAttribute : Attribute
{
/// <summary>
Expand All @@ -18,4 +19,4 @@ public RequestAuthorizeAttribute() { }
/// Gets or sets the policy name that determines access to the resource.
/// </summary>
public string Policy { get; set; } = string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ public async Task<Result<int>> Handle(AddEditKeyValueCommand request, Cancellati
var keyValue = await _context.KeyValues.FindAsync(new object[] { request.Id }, cancellationToken);
_ = keyValue ?? throw new NotFoundException($"KeyValue Pair {request.Id} Not Found.");
keyValue = _mapper.Map(request, keyValue);
keyValue.AddDomainEvent(new UpdatedEvent<KeyValue>(keyValue));
keyValue.AddDomainEvent(new KeyValueUpdatedDomainEvent(keyValue));
await _context.SaveChangesAsync(cancellationToken);
return await Result<int>.SuccessAsync(keyValue.Id);
}
else
{
var keyValue = _mapper.Map<KeyValue>(request);
keyValue.AddDomainEvent(new UpdatedEvent<KeyValue>(keyValue));
keyValue.AddDomainEvent(new KeyValueUpdatedDomainEvent(keyValue));
_context.KeyValues.Add(keyValue);
await _context.SaveChangesAsync(cancellationToken);
return await Result<int>.SuccessAsync(keyValue.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public async Task<Result<int>> Handle(DeleteKeyValueCommand request, Cancellatio
var items = await context.KeyValues.Where(x => request.Id.Contains(x.Id)).ToListAsync(cancellationToken);
foreach (var item in items)
{
var changeEvent = new UpdatedEvent<KeyValue>(item);
var changeEvent = new KeyValueUpdatedDomainEvent(item);
item.AddDomainEvent(changeEvent);
context.KeyValues.Remove(item);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ public async Task<Result> Handle(ImportKeyValuesCommand request, CancellationTok
{
var exist = await _context.KeyValues.AnyAsync(x => x.Name == item.Name && x.Value == item.Value,
cancellationToken);
if (exist) continue;
if (exist)
{
continue;
}

item.AddDomainEvent(new CreatedEvent<KeyValue>(item));
item.AddDomainEvent(new KeyValueCreatedDomainEvent(item));
await _context.KeyValues.AddAsync(item, cancellationToken);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ namespace Cfo.Cats.Application.Features.KeyValues.EventHandlers;
public class KeyValueChangedEventHandler(
IPicklistService picklistService,
ILogger<KeyValueChangedEventHandler> logger
) : INotificationHandler<UpdatedEvent<KeyValue>>
) : INotificationHandler<UpdatedDomainEvent<KeyValue>>
{

public Task Handle(UpdatedEvent<KeyValue> notification, CancellationToken cancellationToken)
public Task Handle(UpdatedDomainEvent<KeyValue> notification, CancellationToken cancellationToken)
{
logger.LogInformation("KeyValue Changed {DomainEvent},{@Entity}", nameof(notification), notification.Entity);
picklistService.Refresh();
Expand Down
103 changes: 57 additions & 46 deletions src/Application/Pipeline/AuthorizationBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,68 +29,79 @@ CancellationToken cancellationToken
.GetType()
.GetCustomAttributes<RequestAuthorizeAttribute>()
.ToArray();
if (authorizeAttributes.Any())

if (authorizeAttributes.Any() == false)
{
// Must be authenticated user
var userId = currentUserService.UserId;
if (userId is null or 0)
// if we have no authorization attribute, then we must explicitly allow all or error
var anyUserAttributes = request.GetType()
.GetCustomAttributes<AllowAnonymousAttribute>()
.SingleOrDefault();

if (anyUserAttributes == null)
{
throw new UnauthorizedAccessException();
throw new UnauthorizedAccessException("Invalid authorization configuration.");
}
}

// DefaultRole-based authorization
var authorizeAttributesWithRoles = authorizeAttributes
.Where(a => !string.IsNullOrWhiteSpace(a.Roles))
.ToArray();
// Must be authenticated user
var userId = currentUserService.UserId;
if (userId is null or 0)
{
throw new UnauthorizedAccessException();
}

if (authorizeAttributesWithRoles.Any())
{
var authorized = false;
// DefaultRole-based authorization
var authorizeAttributesWithRoles = authorizeAttributes
.Where(a => !string.IsNullOrWhiteSpace(a.Roles))
.ToArray();

if (authorizeAttributesWithRoles.Any())
{
var authorized = false;

foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))
foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))
{
foreach (var role in roles)
{
foreach (var role in roles)
var isInRole = await identityService.IsInRoleAsync(
userId.Value,
role.Trim(),
cancellationToken
);
if (isInRole)
{
var isInRole = await identityService.IsInRoleAsync(
userId.Value,
role.Trim(),
cancellationToken
);
if (isInRole)
{
authorized = true;
break;
}
authorized = true;
break;
}
}
}

// Must be a member of at least one role in roles
if (!authorized)
{
throw new ForbiddenException("You are not authorized to access this resource.");
}
// Must be a member of at least one role in roles
if (!authorized)
{
throw new ForbiddenException("You are not authorized to access this resource.");
}
}

// Policy-based authorization
var authorizeAttributesWithPolicies = authorizeAttributes
.Where(a => !string.IsNullOrWhiteSpace(a.Policy))
.ToArray();
if (authorizeAttributesWithPolicies.Any())
// Policy-based authorization
var authorizeAttributesWithPolicies = authorizeAttributes
.Where(a => !string.IsNullOrWhiteSpace(a.Policy))
.ToArray();
if (authorizeAttributesWithPolicies.Any())
{
foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))
{
foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))
var authorized = await identityService.AuthorizeAsync(
userId.Value,
policy,
cancellationToken
);

if (!authorized)
{
var authorized = await identityService.AuthorizeAsync(
userId.Value,
policy,
cancellationToken
throw new ForbiddenException(
"You are not authorized to access this resource."
);

if (!authorized)
{
throw new ForbiddenException(
"You are not authorized to access this resource."
);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Domain/Common/Contracts/IEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public interface IEntity

public interface IEntity<TId> : IEntity
{
TId Id { get; set; }
TId Id { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Cfo.Cats.Domain.Common.Events;

public class CreatedEvent<T>(T entity) : DomainEvent
public abstract class CreatedDomainEvent<T>(T entity) : DomainEvent
where T : IEntity
{
public T Entity { get; } = entity;
Expand Down
9 changes: 9 additions & 0 deletions src/Domain/Common/Events/DeletedDomainEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Cfo.Cats.Domain.Common.Contracts;

namespace Cfo.Cats.Domain.Common.Events;

public abstract class DeletedDomainEvent<T>(T entity) : DomainEvent
where T : IEntity
{
public T Entity { get; } = entity;
}
14 changes: 0 additions & 14 deletions src/Domain/Common/Events/DeletedEvent.cs

This file was deleted.

9 changes: 9 additions & 0 deletions src/Domain/Common/Events/UpdatedDomainEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Cfo.Cats.Domain.Common.Contracts;

namespace Cfo.Cats.Domain.Common.Events;

public abstract class UpdatedDomainEvent<T>(T entity) : DomainEvent
where T : IEntity
{
public T Entity { get; } = entity;
}
14 changes: 0 additions & 14 deletions src/Domain/Common/Events/UpdatedEvent.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Domain/Entities/Administration/Contract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private Contract(string id, int lotNumber, string description, string? tenantId,
Description = description;
Lifetime = new Lifetime(startDate, endDate);

AddDomainEvent( new ContractCreatedEvent(this) );
AddDomainEvent( new ContractCreatedDomainEvent(this) );
}
public int LotNumber { get; private set; }

Expand Down
6 changes: 3 additions & 3 deletions src/Domain/Entities/Administration/Location.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
private readonly List<Location> _childLocations = new();

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected Location()
private Location()
{
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
Expand All @@ -33,13 +33,13 @@
_contractId = contractId;
_lifetime = new Lifetime(lifetimeStart, lifetimeEnd);

this.AddDomainEvent(new LocationCreatedEvent(this));
this.AddDomainEvent(new LocationCreatedDomainEvent(this));
}

public static Location Create(string name, int genderProvisionId, int locationTypeId, string? contractId,
DateTime lifetimeStart, DateTime lifetimeEnd)
{
return new(name, genderProvisionId, locationTypeId, contractId, lifetimeStart, lifetimeEnd);

Check warning on line 42 in src/Domain/Entities/Administration/Location.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'contractId' in 'Location.Location(string name, int genderProvisionId, int locationTypeId, string contractId, DateTime lifetimeStart, DateTime lifetimeEnd)'.
}

public string Name => _name;
Expand All @@ -48,7 +48,7 @@

public LocationType LocationType => LocationType.FromValue(_locationTypeId);

public virtual Contract Contract { get; private set; }
public virtual Contract? Contract { get; private set; }

public Lifetime Lifetime => _lifetime;

Expand Down
2 changes: 1 addition & 1 deletion src/Domain/Entities/Administration/Tenant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private Tenant(string id, string name, string description)
Name = name;
Description = description;

AddDomainEvent(new TenantCreatedEvent(this));
AddDomainEvent(new TenantCreatedDomainEvent(this));
}

public static Tenant Create(string id, string name, string description)
Expand Down
Loading
Loading