initial commit
This commit is contained in:
commit
ad9248c65c
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.idea/
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
BIN
DigitalOps.Api/.DS_Store
vendored
Normal file
BIN
DigitalOps.Api/.DS_Store
vendored
Normal file
Binary file not shown.
20
DigitalOps.Api/AppModule.cs
Normal file
20
DigitalOps.Api/AppModule.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using DigitalOps.Authority;
|
||||
using DigitalOps.CQRS;
|
||||
using DigitalOps.Dal;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.Api;
|
||||
|
||||
public class AppModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddModule<DalModule>();
|
||||
|
||||
services.AddModule<CommandModule>();
|
||||
services.AddModule<QueryModule>();
|
||||
|
||||
services.AddModule<AuthorityModule>();
|
||||
return services;
|
||||
}
|
||||
}
|
38
DigitalOps.Api/DigitalOps.Api.csproj
Normal file
38
DigitalOps.Api/DigitalOps.Api.csproj
Normal file
@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery.AspNetCore" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="PoweredSoft.Data.EntityFrameworkCore" Version="3.0.0" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.4.0" />
|
||||
<PackageReference Include="OpenHarbor.CQRS" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.AspNetCore" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.FluentValidation" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="PoweredSoft.Module.Abstractions" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalOps.Authority\DigitalOps.Authority.csproj" />
|
||||
<ProjectReference Include="..\DigitalOps.CQRS\DigitalOps.CQRS.csproj" />
|
||||
<ProjectReference Include="..\DigitalOps.Dal\DigitalOps.Dal.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
127
DigitalOps.Api/Program.cs
Normal file
127
DigitalOps.Api/Program.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using DigitalOps.Api;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using FluentValidation.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using OpenHarbor.CQRS;
|
||||
using OpenHarbor.CQRS.AspNetCore.Mvc;
|
||||
using OpenHarbor.CQRS.DynamicQuery.AspNetCore;
|
||||
using PoweredSoft.Data;
|
||||
using PoweredSoft.Data.EntityFrameworkCore;
|
||||
using PoweredSoft.DynamicQuery;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
options.ForwardLimit = 2;
|
||||
});
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddCors();
|
||||
|
||||
builder.Services.AddPoweredSoftDataServices();
|
||||
builder.Services.AddPoweredSoftEntityFrameworkCoreDataServices();
|
||||
builder.Services.AddPoweredSoftDynamicQuery();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
builder.Services
|
||||
.AddFluentValidation();
|
||||
|
||||
builder.Services.AddModule<AppModule>();
|
||||
|
||||
var mvcBuilder = builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(jsonOptions =>
|
||||
{
|
||||
jsonOptions.JsonSerializerOptions.Converters.Insert(0, new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
mvcBuilder
|
||||
.AddOpenHarborCommands();
|
||||
|
||||
mvcBuilder
|
||||
.AddOpenHarborQueries()
|
||||
.AddOpenHarborDynamicQueries();
|
||||
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["JwtBearer:Authority"];
|
||||
// check how to set up AudienceValidator to whitelist sites using it
|
||||
options.TokenValidationParameters.ValidateAudience = false;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var connectionString = builder.Configuration.GetSection("Database").GetValue<string>("ConnectionString");
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
|
||||
dataSourceBuilder.MapEnum<OrganizationRole>("organization_role");
|
||||
|
||||
var dataSource = dataSourceBuilder.Build();
|
||||
|
||||
builder.Services.AddDbContextPool<MainDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(dataSource, o =>
|
||||
{
|
||||
// todo: ef 9.0+
|
||||
// o.MapEnum<RouteFileType>("route_file_type");
|
||||
});
|
||||
|
||||
if (builder.Configuration.GetValue<bool>("Debug"))
|
||||
{
|
||||
AppContext.SetSwitch("Npgsql.EnableConnectionStringLogging", true);
|
||||
options
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
}
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseCors(options =>
|
||||
{
|
||||
var origins = new List<string> {"https://hoppscotch.io"};
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
origins.Add("http://localhost:8100");
|
||||
}
|
||||
|
||||
options.WithOrigins(origins.ToArray());
|
||||
options.AllowCredentials();
|
||||
options.AllowAnyHeader();
|
||||
options.AllowAnyMethod();
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
41
DigitalOps.Api/Properties/launchSettings.json
Normal file
41
DigitalOps.Api/Properties/launchSettings.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:28117",
|
||||
"sslPort": 44365
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5155",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7199;http://localhost:5155",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
DigitalOps.Api/appsettings.Development.json
Normal file
14
DigitalOps.Api/appsettings.Development.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Database": {
|
||||
"ConnectionString": "Host=workstation;Port=5432;Database=digital-operations;Username=planb;Password=-mbd2018-"
|
||||
},
|
||||
"JwtBearer": {
|
||||
"Authority": "https://login-planb.openharbor.io/realms/plan-b"
|
||||
}
|
||||
}
|
9
DigitalOps.Api/appsettings.json
Normal file
9
DigitalOps.Api/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
10
DigitalOps.Authority/Attributes/GuestAttribute.cs
Normal file
10
DigitalOps.Authority/Attributes/GuestAttribute.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace DigitalOps.Authority.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class AllowGuestAttribute : System.Attribute
|
||||
{
|
||||
public AllowGuestAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
21
DigitalOps.Authority/AuthorityModule.cs
Normal file
21
DigitalOps.Authority/AuthorityModule.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions.Security;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.Authority;
|
||||
|
||||
public class AuthorityModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddScoped<IQueryAuthorizationService, CQAuthorizationService>();
|
||||
|
||||
services
|
||||
.AddScoped<ICommandAuthorizationService, CQAuthorizationService>();
|
||||
|
||||
services.AddScoped<UserIdentityService>();
|
||||
return services;
|
||||
}
|
||||
}
|
25
DigitalOps.Authority/DigitalOps.Authority.csproj
Normal file
25
DigitalOps.Authority/DigitalOps.Authority.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenHarbor.CQRS.Abstractions" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="PoweredSoft.Module.Abstractions" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalOps.Dal\DigitalOps.Dal.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions">
|
||||
<HintPath>..\..\..\..\..\..\..\usr\local\share\dotnet\shared\Microsoft.AspNetCore.App\8.0.11\Microsoft.AspNetCore.Http.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
43
DigitalOps.Authority/Services/CQAuthorizationService.cs
Normal file
43
DigitalOps.Authority/Services/CQAuthorizationService.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Reflection;
|
||||
using DigitalOps.Authority.Attributes;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.Abstractions.Security;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
|
||||
namespace DigitalOps.Authority.Services;
|
||||
|
||||
public class CQAuthorizationService(IQueryDiscovery queryDiscovery, UserIdentityService userIdentityService) : IQueryAuthorizationService, ICommandAuthorizationService
|
||||
{
|
||||
public async Task<AuthorizationResult> IsAllowedAsync(bool isQuery, Type queryOrCommandType, CancellationToken cancellationToken)
|
||||
{
|
||||
// determine subject type.
|
||||
Type subjectType = queryOrCommandType;
|
||||
if (isQuery)
|
||||
{
|
||||
var queryMeta = queryDiscovery.FindQuery(queryOrCommandType);
|
||||
if (queryMeta != null && queryMeta is DynamicQueryMeta dqMeta)
|
||||
{
|
||||
subjectType = dqMeta.DestinationType;
|
||||
}
|
||||
}
|
||||
|
||||
// allow guest calls.
|
||||
var allowGuestAttributes = subjectType.GetCustomAttribute<AllowGuestAttribute>(true);
|
||||
if (null != allowGuestAttributes)
|
||||
return AuthorizationResult.Allowed;
|
||||
|
||||
if (false == userIdentityService.IsAuthenticated())
|
||||
return AuthorizationResult.Unauthorized;
|
||||
|
||||
|
||||
var isAllowed = await userIdentityService.IsAuthorizedAsync(cancellationToken);
|
||||
return isAllowed ? AuthorizationResult.Allowed : AuthorizationResult.Forbidden;
|
||||
}
|
||||
|
||||
Task<AuthorizationResult> IQueryAuthorizationService.IsAllowedAsync(Type queryType,
|
||||
CancellationToken cancellationToken)
|
||||
=> IsAllowedAsync(true, queryType, cancellationToken);
|
||||
|
||||
Task<AuthorizationResult> ICommandAuthorizationService.IsAllowedAsync(Type commandType, CancellationToken cancellationToken)
|
||||
=> IsAllowedAsync(false, commandType, cancellationToken);
|
||||
}
|
148
DigitalOps.Authority/Services/UserIdentityService.cs
Normal file
148
DigitalOps.Authority/Services/UserIdentityService.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using System.Security.Claims;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DigitalOps.Authority.Services;
|
||||
|
||||
public class UserIdentityService
|
||||
{
|
||||
private User? _user;
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly MainDbContext _dbContext;
|
||||
|
||||
public string? SubjectId { get; }
|
||||
public string? Email { get; }
|
||||
public string? FirstName { get; }
|
||||
public string? LastName { get; }
|
||||
public bool? EmailVerified { get; }
|
||||
public string? AuthIssuer { get; }
|
||||
|
||||
public UserIdentityService(MainDbContext dbContext, IHttpContextAccessor httpContextAccessor,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_configuration = configuration;
|
||||
|
||||
// microsoft you twats! NameIdentifier = Sub
|
||||
SubjectId = ResolveClaim(ClaimTypes.NameIdentifier);
|
||||
Email = ResolveClaim(ClaimTypes.Email);
|
||||
FirstName = ResolveClaim(ClaimTypes.GivenName);
|
||||
LastName = ResolveClaim(ClaimTypes.Surname);
|
||||
var emailVerifiedString = ResolveClaim("email_verified");
|
||||
if (null != emailVerifiedString && bool.TryParse(emailVerifiedString, out var emailVerified))
|
||||
{
|
||||
EmailVerified = emailVerified;
|
||||
}
|
||||
|
||||
AuthIssuer = ResolveClaim("iss");
|
||||
}
|
||||
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
if (!isAuthenticated || null == EmailVerified)
|
||||
return false;
|
||||
|
||||
return EmailVerified ?? false;
|
||||
}
|
||||
|
||||
private string? ResolveClaim(params string[] name)
|
||||
{
|
||||
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
if (false == isAuthenticated)
|
||||
return null;
|
||||
|
||||
var claim = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(claim => name.Contains(claim.Type));
|
||||
return claim?.Value;
|
||||
}
|
||||
|
||||
public async Task<User?> GetUserOrDefaultAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (false == IsAuthenticated())
|
||||
return null;
|
||||
|
||||
if (null != _user)
|
||||
return _user;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
return null;
|
||||
|
||||
var userOidc = await _dbContext.UserOidcs
|
||||
.AsNoTracking()
|
||||
.Include(oidc => oidc.User)
|
||||
.FirstOrDefaultAsync(oidc => oidc.Issuer == AuthIssuer && oidc.Subject == SubjectId, cancellationToken);
|
||||
|
||||
if (userOidc != null)
|
||||
{
|
||||
_user = userOidc.User;
|
||||
return _user;
|
||||
}
|
||||
|
||||
// otherwise search for the user by email
|
||||
var email = Email?.ToLower() ?? "";
|
||||
|
||||
_user = await _dbContext.Users
|
||||
.Include(user => user.UserOidcs)
|
||||
.FirstOrDefaultAsync(user => user.Email.ToLower() == email, cancellationToken);
|
||||
|
||||
// create user if it doesn't exist
|
||||
if (null == _user)
|
||||
{
|
||||
_user = await CreateUserAsync(cancellationToken);
|
||||
return _user;
|
||||
}
|
||||
|
||||
await CreateUserOidcAsync(_user, true, cancellationToken);
|
||||
return _user;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorizedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await GetUserOrDefaultAsync(cancellationToken);
|
||||
return null != user;
|
||||
}
|
||||
|
||||
private async Task<UserOidc> CreateUserOidcAsync(User user, bool save, CancellationToken cancellationToken)
|
||||
{
|
||||
// if user exist but oidc doesn't, we need to link the account
|
||||
// todo: security concerns: a thirdparty oidc could be use to fake email verification to hijack an account, this has to be addressed before public release
|
||||
|
||||
var mainIssuer = _configuration["JwtBearer:Authority"];
|
||||
var userOidc = new UserOidc
|
||||
{
|
||||
User = user,
|
||||
Issuer = AuthIssuer,
|
||||
Subject = SubjectId,
|
||||
VerifiedAt = (mainIssuer == AuthIssuer) ? DateTime.UtcNow : null
|
||||
};
|
||||
|
||||
_dbContext.UserOidcs.Add(userOidc);
|
||||
|
||||
if (save)
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return userOidc;
|
||||
}
|
||||
|
||||
private async Task<User> CreateUserAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Email = Email,
|
||||
FirstName = FirstName,
|
||||
LastName = LastName,
|
||||
};
|
||||
|
||||
await CreateUserOidcAsync(user, false, cancellationToken);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using DigitalOps.Dal.Validators;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.Authority.Validators;
|
||||
|
||||
public class HasAccessToClientValidator : AbstractValidator<long>
|
||||
{
|
||||
private readonly OrganizationRole _role;
|
||||
private readonly MainDbContext _dbContext;
|
||||
private readonly UserIdentityService _userIdentityService;
|
||||
|
||||
public HasAccessToClientValidator(OrganizationRole role, MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
_role = role;
|
||||
_userIdentityService = userIdentityService;
|
||||
_dbContext = dbContext;
|
||||
|
||||
RuleFor(clientId => clientId)
|
||||
.Cascade(CascadeMode.Stop)
|
||||
.SetValidator(new DbEntityExistValidator<Client, long>(dbContext))
|
||||
.CustomAsync(async (clientId, validationContext, cancellationToken) =>
|
||||
{
|
||||
var organizationClient = await _dbContext.OrganizationClients
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(organizationClient => organizationClient.ClientId == clientId, cancellationToken);
|
||||
|
||||
if (organizationClient is null)
|
||||
return;
|
||||
|
||||
var validation = new HasAccessToOrganizationValidator(_role, _dbContext, _userIdentityService);
|
||||
var validationResult = validation.Validate(organizationClient.OrganizationId);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
foreach (var error in validationResult.Errors)
|
||||
validationContext.AddFailure(error);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.Authority.Validators;
|
||||
|
||||
public class HasAccessToOrganizationValidator : AbstractValidator<long>
|
||||
{
|
||||
private readonly OrganizationRole _role;
|
||||
private readonly MainDbContext _dbContext;
|
||||
private readonly UserIdentityService _userIdentityService;
|
||||
|
||||
public HasAccessToOrganizationValidator(OrganizationRole role, MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
_role = role;
|
||||
_userIdentityService = userIdentityService;
|
||||
_dbContext = dbContext;
|
||||
|
||||
RuleFor(organizationId => organizationId)
|
||||
.MustAsync(HasAccess)
|
||||
.WithMessage("You do not have the proper role to this organization for this action.");
|
||||
}
|
||||
|
||||
private async Task<bool> HasAccess(long organizationId, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
if (user is null)
|
||||
return false;
|
||||
|
||||
// todo: extract this into a reusable helper class
|
||||
var roles = new List<OrganizationRole>();
|
||||
if (_role == OrganizationRole.Owner)
|
||||
roles.AddRange([OrganizationRole.Owner]);
|
||||
|
||||
if (_role == OrganizationRole.Admin)
|
||||
roles.AddRange([OrganizationRole.Owner, OrganizationRole.Admin]);
|
||||
|
||||
if (_role == OrganizationRole.Member)
|
||||
roles.AddRange([OrganizationRole.Owner, OrganizationRole.Admin, OrganizationRole.Member]);
|
||||
|
||||
var result = await _dbContext.OrganizationUsers
|
||||
.AsNoTracking()
|
||||
.Where(organizationUser => organizationUser.OrganizationId == organizationId)
|
||||
.Where(organizationUser => roles.Contains(organizationUser.Role))
|
||||
.AnyAsync(cancellationToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
19
DigitalOps.CQRS/CommandModule.cs
Normal file
19
DigitalOps.CQRS/CommandModule.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using DigitalOps.CQRS.Commands.Client;
|
||||
using DigitalOps.CQRS.Commands.Organization;
|
||||
using DigitalOps.CQRS.Commands.Project;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS;
|
||||
|
||||
public class CommandModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddModule<RegisterOrganizationCommandsModule>();
|
||||
services.AddModule<RegisterProjectCommandsModule>();
|
||||
services.AddModule<RegisterClientCommandsModule>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
45
DigitalOps.CQRS/Commands/Client/AddClientCommand.cs
Normal file
45
DigitalOps.CQRS/Commands/Client/AddClientCommand.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Authority.Validators;
|
||||
using DigitalOps.CQRS.Services.Client;
|
||||
using DigitalOps.CQRS.Services.Client.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using DigitalOps.Dal.Validators;
|
||||
using FluentValidation;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Client;
|
||||
|
||||
public class AddClientCommand
|
||||
{
|
||||
public long OrganizationId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
||||
public class AddClientCommandHandler(ClientService clientService) : ICommandHandler<AddClientCommand>
|
||||
{
|
||||
public async Task HandleAsync(AddClientCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await clientService.AddClientAsync(new AddClientOptions
|
||||
{
|
||||
Name = command.Name,
|
||||
OrganizationId = command.OrganizationId
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddClientCommandValidator : AbstractValidator<AddClientCommand>
|
||||
{
|
||||
public AddClientCommandValidator(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
RuleFor(command => command.Name)
|
||||
.NotEmpty()
|
||||
.MinimumLength(3);
|
||||
|
||||
RuleFor(command => command.OrganizationId)
|
||||
.Cascade(CascadeMode.Stop)
|
||||
.NotEmpty()
|
||||
.SetValidator(new DbEntityExistValidator<Dal.DbEntity.Organization, long>(dbContext))
|
||||
.SetValidator(new HasAccessToOrganizationValidator(OrganizationRole.Member, dbContext, userIdentityService));
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using DigitalOps.CQRS.Services.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.FluentValidation;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Client;
|
||||
|
||||
public class RegisterClientCommandsModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddModule<RegisterClientServicesModule>();
|
||||
|
||||
services.AddCommand<AddClientCommand, AddClientCommandHandler,
|
||||
AddClientCommandValidator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using DigitalOps.CQRS.Services.Organization;
|
||||
using DigitalOps.CQRS.Services.Organization.Options;
|
||||
using FluentValidation;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Organization;
|
||||
|
||||
public class AddOrganizationCommand
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
||||
public class AddOrganizationCommandHandler(OrganizationService organizationService) : ICommandHandler<AddOrganizationCommand>
|
||||
{
|
||||
public async Task HandleAsync(AddOrganizationCommand command, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
await organizationService.RegisterOrganizationAsync(new RegisterOrganizationOptions
|
||||
{
|
||||
Name = command.Name
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddOrganizationCommandValidator : AbstractValidator<AddOrganizationCommand>
|
||||
{
|
||||
public AddOrganizationCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.MinimumLength(3);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using DigitalOps.CQRS.Services.Organization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.FluentValidation;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Organization;
|
||||
|
||||
public class RegisterOrganizationCommandsModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddModule<RegisterOrganizationServicesModule>();
|
||||
|
||||
services.AddCommand<AddOrganizationCommand, AddOrganizationCommandHandler,
|
||||
AddOrganizationCommandValidator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
42
DigitalOps.CQRS/Commands/Project/AddProjectCommand.cs
Normal file
42
DigitalOps.CQRS/Commands/Project/AddProjectCommand.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Authority.Validators;
|
||||
using DigitalOps.CQRS.Services.Project;
|
||||
using DigitalOps.CQRS.Services.Project.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using FluentValidation;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Project;
|
||||
|
||||
public class AddProjectCommand
|
||||
{
|
||||
public long ClientId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
||||
public class AddProjectCommandHandler(ProjectService projectService) : ICommandHandler<AddProjectCommand>
|
||||
{
|
||||
public async Task HandleAsync(AddProjectCommand command, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
await projectService.AddProjectAsync(new AddProjectOptions
|
||||
{
|
||||
Name = command.Name,
|
||||
ClientId = command.ClientId,
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddProjectCommandValidator : AbstractValidator<AddProjectCommand>
|
||||
{
|
||||
public AddProjectCommandValidator(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
RuleFor(command => command.Name)
|
||||
.NotEmpty()
|
||||
.MinimumLength(3);
|
||||
|
||||
RuleFor(command => command.ClientId)
|
||||
.NotEmpty()
|
||||
.SetValidator(new HasAccessToClientValidator(OrganizationRole.Member, dbContext, userIdentityService));
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using DigitalOps.CQRS.Services.Project;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.FluentValidation;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.Project;
|
||||
|
||||
public class RegisterProjectCommandsModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddModule<RegisterProjectServicesModule>();
|
||||
|
||||
services.AddCommand<AddProjectCommand, AddProjectCommandHandler,
|
||||
AddProjectCommandValidator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
45
DigitalOps.CQRS/Commands/TimeEntry/AddTimeEntryCommand.cs
Normal file
45
DigitalOps.CQRS/Commands/TimeEntry/AddTimeEntryCommand.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.CQRS.Services.TimeEntry;
|
||||
using DigitalOps.CQRS.Services.TimeEntry.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.Validators;
|
||||
using FluentValidation;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Commands.TimeEntry;
|
||||
|
||||
public class AddTimeEntryCommand
|
||||
{
|
||||
public long ProjectId { get; set; }
|
||||
public long? ForUser { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public DateTime StartAt { get; set; }
|
||||
public DateTime? EndAt { get; set; }
|
||||
public TimeOnly? Offset { get; set; }
|
||||
}
|
||||
|
||||
public class AddTimeEntryCommandHandler(TimeEntryService timeEntryService) : ICommandHandler<AddTimeEntryCommand>
|
||||
{
|
||||
public async Task HandleAsync(AddTimeEntryCommand command, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
await timeEntryService.AddTimeEntryAsync(new AddTimeEntryOptions
|
||||
{
|
||||
ProjectId = command.ProjectId,
|
||||
ForUser = command.ForUser,
|
||||
Description = command.Description,
|
||||
StartAt = command.StartAt,
|
||||
EndAt = command.EndAt,
|
||||
Offset = command.Offset,
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddTimeEntryCommandValidator : AbstractValidator<AddTimeEntryCommand>
|
||||
{
|
||||
public AddTimeEntryCommandValidator(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
RuleFor(command => command.ProjectId)
|
||||
.NotEmpty()
|
||||
.SetValidator(new DbEntityExistValidator<Dal.DbEntity.Project, long>(dbContext));
|
||||
}
|
||||
}
|
22
DigitalOps.CQRS/DigitalOps.CQRS.csproj
Normal file
22
DigitalOps.CQRS/DigitalOps.CQRS.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="10.4.0" />
|
||||
|
||||
<PackageReference Include="OpenHarbor.CQRS.Abstractions" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.FluentValidation" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="PoweredSoft.Module.Abstractions" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalOps.Authority\DigitalOps.Authority.csproj" />
|
||||
<ProjectReference Include="..\DigitalOps.Dal\DigitalOps.Dal.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
38
DigitalOps.CQRS/Queries/Client/ClientQuery.cs
Normal file
38
DigitalOps.CQRS/Queries/Client/ClientQuery.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PoweredSoft.DynamicLinq;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Client;
|
||||
|
||||
public class ClientItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public IEnumerable<long> OrganizationIds { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ClientQueryableProvider(MainDbContext dbContext, UserIdentityService userIdentityService): IQueryableProviderOverride<ClientItem>
|
||||
{
|
||||
public async Task<IQueryable<ClientItem>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
|
||||
var queryable = dbContext.Clients
|
||||
.AsNoTracking()
|
||||
.Where(client =>
|
||||
client.OrganizationClients.Any(organizationClient => organizationClient.Organization.OrganizationUsers.Any(organizationUser => organizationUser.UserId == user!.Id)))
|
||||
.Select(client => new ClientItem
|
||||
{
|
||||
Id = client.Id,
|
||||
OrganizationIds = client.OrganizationClients.Select(organizationClient => organizationClient.OrganizationId),
|
||||
Name = client.Name,
|
||||
CreatedAt = client.CreatedAt,
|
||||
UpdatedAt = client.UpdatedAt
|
||||
});
|
||||
|
||||
return queryable;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.DynamicQuery;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Client;
|
||||
|
||||
public class RegisterClientQueriesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDynamicQuery<ClientItem>()
|
||||
.AddQueryableProviderOverride<ClientItem, ClientQueryableProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
16
DigitalOps.CQRS/Queries/Health/HealthQuery.cs
Normal file
16
DigitalOps.CQRS/Queries/Health/HealthQuery.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using DigitalOps.Authority.Attributes;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Health;
|
||||
|
||||
[AllowGuest]
|
||||
public class HealthQuery
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class HealthQueryHandler : IQueryHandler<HealthQuery, bool>
|
||||
{
|
||||
public Task<bool> HandleAsync(HealthQuery query, CancellationToken cancellationToken = new CancellationToken())
|
||||
=> Task.FromResult(true);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Health;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddHealthQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddQuery<HealthQuery, bool, HealthQueryHandler>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
35
DigitalOps.CQRS/Queries/Organization/OrganizationQuery.cs
Normal file
35
DigitalOps.CQRS/Queries/Organization/OrganizationQuery.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Organization;
|
||||
|
||||
public class OrganizationItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationQueryableProvider(MainDbContext dbContext, UserIdentityService userIdentityService): IQueryableProviderOverride<OrganizationItem>
|
||||
{
|
||||
public async Task<IQueryable<OrganizationItem>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
|
||||
var queryable = dbContext.Organizations
|
||||
.AsNoTracking()
|
||||
.Where(organization =>
|
||||
organization.OrganizationUsers.Any(organizationUser => organizationUser.UserId == user!.Id))
|
||||
.Select(organization => new OrganizationItem
|
||||
{
|
||||
Id = organization.Id,
|
||||
Name = organization.Name,
|
||||
CreatedAt = organization.CreatedAt,
|
||||
UpdatedAt = organization.UpdatedAt
|
||||
});
|
||||
|
||||
return queryable;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.DynamicQuery;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Organization;
|
||||
|
||||
public class RegisterOrganizationQueriesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDynamicQuery<OrganizationItem>()
|
||||
.AddQueryableProviderOverride<OrganizationItem, OrganizationQueryableProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
72
DigitalOps.CQRS/Queries/Project/ProjectQuery.cs
Normal file
72
DigitalOps.CQRS/Queries/Project/ProjectQuery.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Project;
|
||||
|
||||
public class ProjectQueryParams
|
||||
{
|
||||
public long? OrganizationId { get; set; }
|
||||
public long? ClientId { get; set; }
|
||||
}
|
||||
|
||||
public class ProjectItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long ClientId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ProjectQueryableProvider(MainDbContext dbContext, UserIdentityService userIdentityService): IQueryableProviderOverride<ProjectItem>
|
||||
{
|
||||
public async Task<IQueryable<ProjectItem>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
long? organizationId = null;
|
||||
long? clientId = null;
|
||||
|
||||
if (query is IDynamicQueryParams<ProjectQueryParams> dynamicQuery)
|
||||
{
|
||||
var queryParams = dynamicQuery.GetParams();
|
||||
organizationId = queryParams?.OrganizationId;
|
||||
clientId = queryParams?.ClientId;
|
||||
}
|
||||
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
|
||||
var queryable = dbContext.Projects
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (organizationId is null)
|
||||
{
|
||||
// don't bother to call the database if organizationId is not set
|
||||
return Enumerable.Empty<ProjectItem>().AsQueryable();
|
||||
}
|
||||
|
||||
var organizationUser = await dbContext.OrganizationUsers
|
||||
.FirstOrDefaultAsync(organizationUser => organizationUser.UserId == user!.Id, cancellationToken);
|
||||
|
||||
if (clientId is not null)
|
||||
{
|
||||
queryable = queryable.Where(project => project.ClientId == clientId);
|
||||
}
|
||||
|
||||
queryable = queryable
|
||||
.Where(project => project.Client.OrganizationClients.Any(organizationClient =>
|
||||
organizationClient.OrganizationId == organizationUser!.OrganizationId));
|
||||
|
||||
var result = queryable.Select(project => new ProjectItem
|
||||
{
|
||||
Id = project.Id,
|
||||
ClientId = project.ClientId,
|
||||
Name = project.Name,
|
||||
CreatedAt = project.CreatedAt,
|
||||
UpdatedAt = project.UpdatedAt
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.DynamicQuery;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Queries.Project;
|
||||
|
||||
public class RegisterProjectQueriesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDynamicQueryWithParams<ProjectItem, ProjectQueryParams>()
|
||||
.AddQueryableProviderOverride<ProjectItem, ProjectQueryableProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
22
DigitalOps.CQRS/QueryModule.cs
Normal file
22
DigitalOps.CQRS/QueryModule.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using DigitalOps.CQRS.Queries.Client;
|
||||
using DigitalOps.CQRS.Queries.Health;
|
||||
using DigitalOps.CQRS.Queries.Organization;
|
||||
using DigitalOps.CQRS.Queries.Project;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS;
|
||||
|
||||
public class QueryModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHealthQueries();
|
||||
|
||||
services.AddModule<RegisterOrganizationQueriesModule>();
|
||||
services.AddModule<RegisterClientQueriesModule>();
|
||||
services.AddModule<RegisterProjectQueriesModule>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
36
DigitalOps.CQRS/Services/Client/ClientService.cs
Normal file
36
DigitalOps.CQRS/Services/Client/ClientService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.CQRS.Services.Client.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Client;
|
||||
|
||||
public class ClientService(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
public async Task AddClientAsync(AddClientOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
if (null == user)
|
||||
return;
|
||||
|
||||
var organization = await dbContext.Organizations.FirstOrDefaultAsync(organization => organization.Id == options.OrganizationId, cancellationToken);
|
||||
|
||||
if (null == organization)
|
||||
return;
|
||||
|
||||
var client = new Dal.DbEntity.Client
|
||||
{
|
||||
Name = options.Name
|
||||
};
|
||||
|
||||
var organizationClient = new OrganizationClient
|
||||
{
|
||||
Organization = organization,
|
||||
Client = client
|
||||
};
|
||||
|
||||
organization.OrganizationClients.Add(organizationClient);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace DigitalOps.CQRS.Services.Client.Options;
|
||||
|
||||
public class AddClientOptions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long OrganizationId { get; set; }
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Client;
|
||||
|
||||
public class RegisterClientServicesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ClientService>();
|
||||
return services;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace DigitalOps.CQRS.Services.Organization.Options;
|
||||
|
||||
public class RegisterOrganizationOptions
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
}
|
32
DigitalOps.CQRS/Services/Organization/OrganizationService.cs
Normal file
32
DigitalOps.CQRS/Services/Organization/OrganizationService.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.CQRS.Services.Organization.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Organization;
|
||||
|
||||
public class OrganizationService(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
public async Task RegisterOrganizationAsync(RegisterOrganizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var organization = new Dal.DbEntity.Organization
|
||||
{
|
||||
Name = options.Name
|
||||
};
|
||||
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (null == user)
|
||||
return;
|
||||
|
||||
var organizationUser = new OrganizationUser
|
||||
{
|
||||
Organization = organization,
|
||||
Role = OrganizationRole.Owner,
|
||||
UserId = user.Id,
|
||||
};
|
||||
|
||||
dbContext.OrganizationUsers.Add(organizationUser);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Organization;
|
||||
|
||||
public class RegisterOrganizationServicesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<OrganizationService>();
|
||||
return services;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace DigitalOps.CQRS.Services.Project.Options;
|
||||
|
||||
public class AddProjectOptions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long ClientId { get; set; }
|
||||
}
|
29
DigitalOps.CQRS/Services/Project/ProjectService.cs
Normal file
29
DigitalOps.CQRS/Services/Project/ProjectService.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.CQRS.Services.Project.Options;
|
||||
using DigitalOps.Dal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Project;
|
||||
|
||||
public class ProjectService(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
public async Task AddProjectAsync(AddProjectOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
if (null == user)
|
||||
return;
|
||||
|
||||
var client = await dbContext.Clients.FirstOrDefaultAsync(client => client.Id == options.ClientId, cancellationToken);
|
||||
if (null == client)
|
||||
return;
|
||||
|
||||
var project = new Dal.DbEntity.Project
|
||||
{
|
||||
Name = options.Name,
|
||||
Client = client
|
||||
};
|
||||
|
||||
await dbContext.Projects.AddAsync(project, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.Project;
|
||||
|
||||
public class RegisterProjectServicesModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ProjectService>();
|
||||
return services;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
namespace DigitalOps.CQRS.Services.TimeEntry.Options;
|
||||
|
||||
public class AddTimeEntryOptions
|
||||
{
|
||||
public long ProjectId { get; set; }
|
||||
public long? ForUser { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public DateTime StartAt { get; set; }
|
||||
public DateTime? EndAt { get; set; }
|
||||
public TimeOnly? Offset { get; set; }
|
||||
}
|
30
DigitalOps.CQRS/Services/TimeEntry/TimeEntryService.cs
Normal file
30
DigitalOps.CQRS/Services/TimeEntry/TimeEntryService.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using DigitalOps.Authority.Services;
|
||||
using DigitalOps.CQRS.Services.TimeEntry.Options;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
|
||||
namespace DigitalOps.CQRS.Services.TimeEntry;
|
||||
|
||||
public class TimeEntryService(MainDbContext dbContext, UserIdentityService userIdentityService)
|
||||
{
|
||||
public async Task AddTimeEntryAsync(AddTimeEntryOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userIdentityService.GetUserOrDefaultAsync(cancellationToken);
|
||||
if (user is null)
|
||||
return;
|
||||
|
||||
var timeEntry = new ProjectTimeEntry
|
||||
{
|
||||
ProjectId = options.ProjectId,
|
||||
EntryUserId = user.Id,
|
||||
UserId = options.ForUser ?? user.Id,
|
||||
Description = options.Description,
|
||||
Offset = options.Offset,
|
||||
StartedAt = options.StartAt,
|
||||
EndedAt = options.EndAt
|
||||
};
|
||||
|
||||
dbContext.ProjectTimeEntries.Add(timeEntry);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
6
DigitalOps.Dal.Abstractions/IHasId.cs
Normal file
6
DigitalOps.Dal.Abstractions/IHasId.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DigitalOps.Dal.Abstractions;
|
||||
|
||||
public interface IHasId<T>
|
||||
{
|
||||
public T Id { get; set; }
|
||||
}
|
15
DigitalOps.Dal/DalModule.cs
Normal file
15
DigitalOps.Dal/DalModule.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
using PoweredSoft.Data.Core;
|
||||
using PoweredSoft.Module.Abstractions;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public class DalModule : IModule
|
||||
{
|
||||
public IServiceCollection ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton<IAsyncQueryableHandlerService, InMemoryQueryableHandlerService>()
|
||||
.AddTransient(typeof(IQueryableProvider<>), typeof(DefaultQueryableProvider<>));
|
||||
}
|
||||
}
|
8
DigitalOps.Dal/DbEntity/Client.Extensions.cs
Normal file
8
DigitalOps.Dal/DbEntity/Client.Extensions.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using DigitalOps.Dal.Abstractions;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Client : IHasId<long>
|
||||
{
|
||||
|
||||
}
|
19
DigitalOps.Dal/DbEntity/Client.cs
Normal file
19
DigitalOps.Dal/DbEntity/Client.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Client
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual ICollection<OrganizationClient> OrganizationClients { get; set; } = new List<OrganizationClient>();
|
||||
|
||||
public virtual ICollection<Project> Projects { get; set; } = new List<Project>();
|
||||
}
|
21
DigitalOps.Dal/DbEntity/OidcProvider.cs
Normal file
21
DigitalOps.Dal/DbEntity/OidcProvider.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class OidcProvider
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long OrganizationId { get; set; }
|
||||
|
||||
public string Label { get; set; } = null!;
|
||||
|
||||
public string Issuer { get; set; } = null!;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual Organization Organization { get; set; } = null!;
|
||||
}
|
8
DigitalOps.Dal/DbEntity/Organization.Extensions.cs
Normal file
8
DigitalOps.Dal/DbEntity/Organization.Extensions.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using DigitalOps.Dal.Abstractions;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Organization : IHasId<long>
|
||||
{
|
||||
|
||||
}
|
21
DigitalOps.Dal/DbEntity/Organization.cs
Normal file
21
DigitalOps.Dal/DbEntity/Organization.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Organization
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual ICollection<OidcProvider> OidcProviders { get; set; } = new List<OidcProvider>();
|
||||
|
||||
public virtual ICollection<OrganizationClient> OrganizationClients { get; set; } = new List<OrganizationClient>();
|
||||
|
||||
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; } = new List<OrganizationUser>();
|
||||
}
|
21
DigitalOps.Dal/DbEntity/OrganizationClient.cs
Normal file
21
DigitalOps.Dal/DbEntity/OrganizationClient.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class OrganizationClient
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long OrganizationId { get; set; }
|
||||
|
||||
public long ClientId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual Client Client { get; set; } = null!;
|
||||
|
||||
public virtual Organization Organization { get; set; } = null!;
|
||||
}
|
15
DigitalOps.Dal/DbEntity/OrganizationRole.cs
Normal file
15
DigitalOps.Dal/DbEntity/OrganizationRole.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public enum OrganizationRole
|
||||
{
|
||||
[PgName("owner")]
|
||||
Owner,
|
||||
|
||||
[PgName("admin")]
|
||||
Admin,
|
||||
|
||||
[PgName("member")]
|
||||
Member
|
||||
}
|
6
DigitalOps.Dal/DbEntity/OrganizationUser.Extensions.cs
Normal file
6
DigitalOps.Dal/DbEntity/OrganizationUser.Extensions.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class OrganizationUser
|
||||
{
|
||||
public OrganizationRole Role { get; set; }
|
||||
}
|
21
DigitalOps.Dal/DbEntity/OrganizationUser.cs
Normal file
21
DigitalOps.Dal/DbEntity/OrganizationUser.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class OrganizationUser
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long OrganizationId { get; set; }
|
||||
|
||||
public long UserId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual Organization Organization { get; set; } = null!;
|
||||
|
||||
public virtual User User { get; set; } = null!;
|
||||
}
|
8
DigitalOps.Dal/DbEntity/Project.Extensions.cs
Normal file
8
DigitalOps.Dal/DbEntity/Project.Extensions.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using DigitalOps.Dal.Abstractions;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Project : IHasId<long>
|
||||
{
|
||||
|
||||
}
|
21
DigitalOps.Dal/DbEntity/Project.cs
Normal file
21
DigitalOps.Dal/DbEntity/Project.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class Project
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long ClientId { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual Client Client { get; set; } = null!;
|
||||
|
||||
public virtual ICollection<ProjectTimeEntry> ProjectTimeEntries { get; set; } = new List<ProjectTimeEntry>();
|
||||
}
|
45
DigitalOps.Dal/DbEntity/ProjectTimeEntry.cs
Normal file
45
DigitalOps.Dal/DbEntity/ProjectTimeEntry.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class ProjectTimeEntry
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long EntryUserId { get; set; }
|
||||
|
||||
public long UserId { get; set; }
|
||||
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
public string? AiDescription { get; set; }
|
||||
|
||||
public string? RejectedComment { get; set; }
|
||||
|
||||
public long? ApprovedBy { get; set; }
|
||||
|
||||
public DateTime StartedAt { get; set; }
|
||||
|
||||
public DateTime? EndedAt { get; set; }
|
||||
|
||||
public TimeOnly? Offset { get; set; }
|
||||
|
||||
public DateTime? ApprovedAt { get; set; }
|
||||
|
||||
public DateTime? RejectedAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual User? ApprovedByNavigation { get; set; }
|
||||
|
||||
public virtual User EntryUser { get; set; } = null!;
|
||||
|
||||
public virtual Project Project { get; set; } = null!;
|
||||
|
||||
public virtual User User { get; set; } = null!;
|
||||
}
|
31
DigitalOps.Dal/DbEntity/User.cs
Normal file
31
DigitalOps.Dal/DbEntity/User.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
public string FirstName { get; set; } = null!;
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public DateTime? RegisteredAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; } = new List<OrganizationUser>();
|
||||
|
||||
public virtual ICollection<ProjectTimeEntry> ProjectTimeEntryApprovedByNavigations { get; set; } = new List<ProjectTimeEntry>();
|
||||
|
||||
public virtual ICollection<ProjectTimeEntry> ProjectTimeEntryEntryUsers { get; set; } = new List<ProjectTimeEntry>();
|
||||
|
||||
public virtual ICollection<ProjectTimeEntry> ProjectTimeEntryUsers { get; set; } = new List<ProjectTimeEntry>();
|
||||
|
||||
public virtual ICollection<UserOidc> UserOidcs { get; set; } = new List<UserOidc>();
|
||||
}
|
23
DigitalOps.Dal/DbEntity/UserOidc.cs
Normal file
23
DigitalOps.Dal/DbEntity/UserOidc.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigitalOps.Dal.DbEntity;
|
||||
|
||||
public partial class UserOidc
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long UserId { get; set; }
|
||||
|
||||
public string Issuer { get; set; } = null!;
|
||||
|
||||
public string Subject { get; set; } = null!;
|
||||
|
||||
public DateTime? VerifiedAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual User User { get; set; } = null!;
|
||||
}
|
15
DigitalOps.Dal/DefaultQueryableProvider.cs
Normal file
15
DigitalOps.Dal/DefaultQueryableProvider.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public class DefaultQueryableProvider<TEntity>(MainDbContext context, IServiceProvider serviceProvider) : IQueryableProvider<TEntity>
|
||||
where TEntity : class
|
||||
{
|
||||
public Task<IQueryable<TEntity>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (serviceProvider.GetService(typeof(IQueryableProviderOverride<TEntity>)) is IQueryableProviderOverride<TEntity> queryableProviderOverride)
|
||||
return queryableProviderOverride.GetQueryableAsync(query, cancellationToken);
|
||||
|
||||
return Task.FromResult(context.Set<TEntity>().AsQueryable());
|
||||
}
|
||||
}
|
28
DigitalOps.Dal/DigitalOps.Dal.csproj
Normal file
28
DigitalOps.Dal/DigitalOps.Dal.csproj
Normal file
@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="FluentValidation" Version="10.4.0" />
|
||||
|
||||
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery.Abstractions" Version="8.0.0-preview.6" />
|
||||
<PackageReference Include="PoweredSoft.Data.Core" Version="3.0.0" />
|
||||
<PackageReference Include="PoweredSoft.Module.Abstractions" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalOps.Dal.Abstractions\DigitalOps.Dal.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
6
DigitalOps.Dal/IQueryableProviderOverride.cs
Normal file
6
DigitalOps.Dal/IQueryableProviderOverride.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public interface IQueryableProviderOverride<T>
|
||||
{
|
||||
Task<IQueryable<T>> GetQueryableAsync(object query, CancellationToken cancellationToken = default);
|
||||
}
|
48
DigitalOps.Dal/InMemoryQueryableHandlerService.cs
Normal file
48
DigitalOps.Dal/InMemoryQueryableHandlerService.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Linq.Expressions;
|
||||
using PoweredSoft.Data.Core;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public class InMemoryQueryableHandlerService : IAsyncQueryableHandlerService
|
||||
{
|
||||
public Task<bool> AnyAsync<T>(IQueryable<T> queryable, Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.Any(predicate));
|
||||
}
|
||||
|
||||
public Task<bool> AnyAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.Any());
|
||||
}
|
||||
|
||||
public bool CanHandle<T>(IQueryable<T> queryable)
|
||||
{
|
||||
var result = queryable is EnumerableQuery<T>;
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.Count());
|
||||
}
|
||||
|
||||
public Task<T> FirstOrDefaultAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.FirstOrDefault());
|
||||
}
|
||||
|
||||
public Task<T> FirstOrDefaultAsync<T>(IQueryable<T> queryable, Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.FirstOrDefault(predicate));
|
||||
}
|
||||
|
||||
public Task<long> LongCountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.LongCount());
|
||||
}
|
||||
|
||||
public Task<List<T>> ToListAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(queryable.ToList());
|
||||
}
|
||||
}
|
30
DigitalOps.Dal/MainDbContext.cs
Normal file
30
DigitalOps.Dal/MainDbContext.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public class MainDbContext(DbContextOptions options) : MainDbScaffoldedContext(options)
|
||||
{
|
||||
static MainDbContext() => NpgsqlConnection.GlobalTypeMapper
|
||||
.MapEnum<OrganizationRole>("organization_role");
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.HasPostgresEnum<OrganizationRole>("organization_role");
|
||||
|
||||
modelBuilder.Entity<OrganizationUser>(entity =>
|
||||
{
|
||||
entity.Property(organizationUser => organizationUser.Role)
|
||||
.HasColumnName("role")
|
||||
.HasColumnType("organization_role");
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
276
DigitalOps.Dal/MainDbScaffoldedContext.cs
Normal file
276
DigitalOps.Dal/MainDbScaffoldedContext.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public partial class MainDbScaffoldedContext : DbContext
|
||||
{
|
||||
public MainDbScaffoldedContext()
|
||||
{
|
||||
}
|
||||
|
||||
public MainDbScaffoldedContext(DbContextOptions options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual DbSet<Client> Clients { get; set; }
|
||||
|
||||
public virtual DbSet<OidcProvider> OidcProviders { get; set; }
|
||||
|
||||
public virtual DbSet<Organization> Organizations { get; set; }
|
||||
|
||||
public virtual DbSet<OrganizationClient> OrganizationClients { get; set; }
|
||||
|
||||
public virtual DbSet<OrganizationUser> OrganizationUsers { get; set; }
|
||||
|
||||
public virtual DbSet<Project> Projects { get; set; }
|
||||
|
||||
public virtual DbSet<ProjectTimeEntry> ProjectTimeEntries { get; set; }
|
||||
|
||||
public virtual DbSet<User> Users { get; set; }
|
||||
|
||||
public virtual DbSet<UserOidc> UserOidcs { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder.UseNpgsql("Name=Database:ConnectionString");
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasPostgresEnum("organization_role", new[] { "owner", "admin", "member" });
|
||||
|
||||
modelBuilder.Entity<Client>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("client_pkey");
|
||||
|
||||
entity.ToTable("client");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Name)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("name");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<OidcProvider>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("oidc_provider_pkey");
|
||||
|
||||
entity.ToTable("oidc_provider");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Issuer)
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnName("issuer");
|
||||
entity.Property(e => e.Label)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("label");
|
||||
entity.Property(e => e.OrganizationId).HasColumnName("organization_id");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasOne(d => d.Organization).WithMany(p => p.OidcProviders)
|
||||
.HasForeignKey(d => d.OrganizationId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("oidc_provider_organization_id_fkey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Organization>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("organization_pkey");
|
||||
|
||||
entity.ToTable("organization");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Name)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("name");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<OrganizationClient>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("organization_client_pkey");
|
||||
|
||||
entity.ToTable("organization_client");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.ClientId).HasColumnName("client_id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.OrganizationId).HasColumnName("organization_id");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasOne(d => d.Client).WithMany(p => p.OrganizationClients)
|
||||
.HasForeignKey(d => d.ClientId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("organization_client_client_id_fkey");
|
||||
|
||||
entity.HasOne(d => d.Organization).WithMany(p => p.OrganizationClients)
|
||||
.HasForeignKey(d => d.OrganizationId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("organization_client_organization_id_fkey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<OrganizationUser>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("organization_user_pkey");
|
||||
|
||||
entity.ToTable("organization_user");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.OrganizationId).HasColumnName("organization_id");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
|
||||
entity.HasOne(d => d.Organization).WithMany(p => p.OrganizationUsers)
|
||||
.HasForeignKey(d => d.OrganizationId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("organization_user_organization_id_fkey");
|
||||
|
||||
entity.HasOne(d => d.User).WithMany(p => p.OrganizationUsers)
|
||||
.HasForeignKey(d => d.UserId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("organization_user_user_id_fkey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Project>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("project_pkey");
|
||||
|
||||
entity.ToTable("project");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.ClientId).HasColumnName("client_id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Name)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("name");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
|
||||
entity.HasOne(d => d.Client).WithMany(p => p.Projects)
|
||||
.HasForeignKey(d => d.ClientId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("project_client_id_fkey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ProjectTimeEntry>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("project_time_entry_pkey");
|
||||
|
||||
entity.ToTable("project_time_entry");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.AiDescription)
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnName("ai_description");
|
||||
entity.Property(e => e.ApprovedAt).HasColumnName("approved_at");
|
||||
entity.Property(e => e.ApprovedBy).HasColumnName("approved_by");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Description)
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnName("description");
|
||||
entity.Property(e => e.EndedAt).HasColumnName("ended_at");
|
||||
entity.Property(e => e.EntryUserId).HasColumnName("entry_user_id");
|
||||
entity.Property(e => e.Offset).HasColumnName("offset");
|
||||
entity.Property(e => e.ProjectId).HasColumnName("project_id");
|
||||
entity.Property(e => e.RejectedAt).HasColumnName("rejected_at");
|
||||
entity.Property(e => e.RejectedComment)
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnName("rejected_comment");
|
||||
entity.Property(e => e.StartedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("started_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
|
||||
entity.HasOne(d => d.ApprovedByNavigation).WithMany(p => p.ProjectTimeEntryApprovedByNavigations)
|
||||
.HasForeignKey(d => d.ApprovedBy)
|
||||
.HasConstraintName("project_time_entry_approved_by_fkey");
|
||||
|
||||
entity.HasOne(d => d.EntryUser).WithMany(p => p.ProjectTimeEntryEntryUsers)
|
||||
.HasForeignKey(d => d.EntryUserId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("project_time_entry_entry_user_id_fkey");
|
||||
|
||||
entity.HasOne(d => d.Project).WithMany(p => p.ProjectTimeEntries)
|
||||
.HasForeignKey(d => d.ProjectId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("project_time_entry_project_id_fkey");
|
||||
|
||||
entity.HasOne(d => d.User).WithMany(p => p.ProjectTimeEntryUsers)
|
||||
.HasForeignKey(d => d.UserId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("project_time_entry_user_id_fkey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("user_pkey");
|
||||
|
||||
entity.ToTable("user");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Email)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("email");
|
||||
entity.Property(e => e.FirstName)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("first_name");
|
||||
entity.Property(e => e.LastName)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("last_name");
|
||||
entity.Property(e => e.RegisteredAt).HasColumnName("registered_at");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserOidc>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("user_oidc_pkey");
|
||||
|
||||
entity.ToTable("user_oidc");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)")
|
||||
.HasColumnName("created_at");
|
||||
entity.Property(e => e.Issuer)
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnName("issuer");
|
||||
entity.Property(e => e.Subject)
|
||||
.HasMaxLength(255)
|
||||
.HasColumnName("subject");
|
||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at");
|
||||
entity.Property(e => e.UserId).HasColumnName("user_id");
|
||||
entity.Property(e => e.VerifiedAt).HasColumnName("verified_at");
|
||||
|
||||
entity.HasOne(d => d.User).WithMany(p => p.UserOidcs)
|
||||
.HasForeignKey(d => d.UserId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("user_oidc_user_id_fkey");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
|
||||
}
|
12
DigitalOps.Dal/ServiceCollectionExtensions.cs
Normal file
12
DigitalOps.Dal/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DigitalOps.Dal;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddQueryableProviderOverride<T, TService>(this IServiceCollection services)
|
||||
where TService : class, IQueryableProviderOverride<T>
|
||||
{
|
||||
return services.AddTransient<IQueryableProviderOverride<T>, TService>();
|
||||
}
|
||||
}
|
25
DigitalOps.Dal/Validators/DbEntityExistValidator.cs
Normal file
25
DigitalOps.Dal/Validators/DbEntityExistValidator.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using DigitalOps.Dal.Abstractions;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalOps.Dal.Validators;
|
||||
|
||||
public class DbEntityExistValidator<TEntity, TValue> : AbstractValidator<TValue>
|
||||
where TEntity : class, IHasId<TValue>
|
||||
{
|
||||
private readonly MainDbContext _dbContext;
|
||||
|
||||
public DbEntityExistValidator(MainDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
|
||||
RuleFor(value => value)
|
||||
.NotNull()
|
||||
.MustAsync(IsEntityExist)
|
||||
.WithMessage("Invalid Entity");
|
||||
}
|
||||
|
||||
private Task<bool> IsEntityExist(TValue id,
|
||||
CancellationToken cancellationToken)
|
||||
=> _dbContext.Set<TEntity>().AnyAsync(entity => entity.Id!.Equals(id), cancellationToken);
|
||||
}
|
40
DigitalOps.sln
Normal file
40
DigitalOps.sln
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalOps.Api", "DigitalOps.Api\DigitalOps.Api.csproj", "{D850A48A-7D42-4909-BC42-573267D08969}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalOps.Dal", "DigitalOps.Dal\DigitalOps.Dal.csproj", "{A7333968-E66C-44DA-9CC3-51BD106B7562}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalOps.CQRS", "DigitalOps.CQRS\DigitalOps.CQRS.csproj", "{4C3ABA8F-C754-464C-A98D-32DC197ED787}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalOps.Authority", "DigitalOps.Authority\DigitalOps.Authority.csproj", "{19F8E730-6612-460B-ADB7-B09E9D1CB4E3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalOps.Dal.Abstractions", "DigitalOps.Dal.Abstractions\DigitalOps.Dal.Abstractions.csproj", "{A080C05D-2C84-4CFE-9CA5-658C255E9E3D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D850A48A-7D42-4909-BC42-573267D08969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D850A48A-7D42-4909-BC42-573267D08969}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D850A48A-7D42-4909-BC42-573267D08969}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D850A48A-7D42-4909-BC42-573267D08969}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A7333968-E66C-44DA-9CC3-51BD106B7562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7333968-E66C-44DA-9CC3-51BD106B7562}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7333968-E66C-44DA-9CC3-51BD106B7562}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7333968-E66C-44DA-9CC3-51BD106B7562}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4C3ABA8F-C754-464C-A98D-32DC197ED787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4C3ABA8F-C754-464C-A98D-32DC197ED787}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4C3ABA8F-C754-464C-A98D-32DC197ED787}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4C3ABA8F-C754-464C-A98D-32DC197ED787}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{19F8E730-6612-460B-ADB7-B09E9D1CB4E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{19F8E730-6612-460B-ADB7-B09E9D1CB4E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{19F8E730-6612-460B-ADB7-B09E9D1CB4E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{19F8E730-6612-460B-ADB7-B09E9D1CB4E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A080C05D-2C84-4CFE-9CA5-658C255E9E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A080C05D-2C84-4CFE-9CA5-658C255E9E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A080C05D-2C84-4CFE-9CA5-658C255E9E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A080C05D-2C84-4CFE-9CA5-658C255E9E3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
6
DigitalOps.sln.DotSettings.user
Normal file
6
DigitalOps.sln.DotSettings.user
Normal file
@ -0,0 +1,6 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncTaskMethodBuilderT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe358d67bcd5815a5083706ec88db8489a9872915d46156ba24277cd21bd662_003FAsyncTaskMethodBuilderT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClaimTypes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F55b2fe7c224b4edab7d70d62ecb210be15800_003F15_003Ff9f21e74_003FClaimTypes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe747192abb38e2df82cbdb37e721567726f559914a7b81f8b26ba537de632f4_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APropertyRule_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2129ab9af120518a1b3bb7cdb229807958261bc78f10563bba7f66ce332ba1_003FPropertyRule_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F58886d0933cc48d2ad52601392d2f56b1400_003F6d_003F95c7d0d2_003FServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
3
readme.md
Normal file
3
readme.md
Normal file
@ -0,0 +1,3 @@
|
||||
## Database Scaffolding to generate Entities
|
||||
|
||||
`dotnet ef dbcontext scaffold "Name=Database:ConnectionString" Npgsql.EntityFrameworkCore.PostgreSQL --context MainDbScaffoldedContext --context-dir ./ --output-dir DbEntity --startup-project DigitalOps.Api --project DigitalOps.Dal --force`
|
Loading…
Reference in New Issue
Block a user