From 8eefa30d52722d68e6afeb75d899c84c1c5d0bfb Mon Sep 17 00:00:00 2001 From: DavidGudEnough Date: Fri, 10 Jan 2025 15:59:51 -0500 Subject: [PATCH] implement cqrs command, add keycloak service --- CH.Api/Program.cs | 4 +- .../Energy/CreateEnergyProviderCommand.cs | 25 +++ .../Command/Energy/CreateEnergyRateCommand.cs | 35 +++- .../CreateEnergyRateExceptionCommand.cs | 39 +++- .../Energy/DisableEnergyProviderCommand.cs | 32 ++++ .../Energy/DisableEnergyRateCommand.cs | 32 ++++ .../Energy/EnableEnergyProviderCommand.cs | 29 +++ .../Command/Energy/EnableEnergyRateCommand.cs | 19 ++ .../Energy/ServiceCollectionExtension.cs | 33 +++- .../Energy/UpdateEnergyProviderCommand.cs | 28 ++- .../Command/Energy/UpdateEnergyRateCommand.cs | 39 ++++ .../UpdateEnergyRateExceptionCommand.cs | 37 +++- CH.CQRS/CommandModule.cs | 3 +- CH.CQRS/Service/CryptoStats/CryptoService.cs | 3 +- CH.CQRS/Service/Energy/EnergyService.cs | 151 ++++++++++++++- .../CreateEnergyProviderCommandOptions.cs | 3 +- .../Options/CreateEnergyRateCommandOptions.cs | 8 +- ...CreateEnergyRateExceptionCommandOptions.cs | 7 +- .../DisableEnergyProviderCommandOptions.cs | 3 +- .../DisableEnergyRateCommandOptions.cs | 3 +- .../EnableEnergyProviderCommandOptions.cs | 6 + .../Options/EnableEnergyRateCommandOptions.cs | 6 + .../UpdateEnergyProviderCommandOptions.cs | 3 +- .../Options/UpdateEnergyRateCommandOptions.cs | 7 +- ...UpdateEnergyRateExceptionCommandOptions.cs | 6 +- CH.CQRS/SharedModule.cs | 2 + CH.Dal/CHDbContext.cs | 6 +- CH.Dal/CHDbScaffoldedContext.cs | 34 +++- CH.Dal/DbEntity/EnergyProvider.cs | 4 + CH.Dal/DbEntity/EnergyRate.cs | 6 + .../DbEntity/EnergyRateException.Extension.cs | 2 +- CH.Dal/DbEntity/EnergyRateException.cs | 2 +- CH.Dal/DbEntity/EnergyRateUpdate.cs | 25 +++ ... EnergyRateExceptionThresholdResetType.cs} | 4 +- CH.KeycloakApi/CH.KeycloakApi.csproj | 13 ++ CH.KeycloakApi/KeycloakService.cs | 180 ++++++++++++++++++ CH.KeycloakApi/KeycloakSettings.cs | 10 + CH.KeycloakApi/KeycloakUser.cs | 34 ++++ ConstellationHeating.sln | 6 + 39 files changed, 861 insertions(+), 28 deletions(-) create mode 100644 CH.CQRS/Command/Energy/DisableEnergyProviderCommand.cs create mode 100644 CH.CQRS/Command/Energy/DisableEnergyRateCommand.cs create mode 100644 CH.CQRS/Command/Energy/EnableEnergyProviderCommand.cs create mode 100644 CH.CQRS/Command/Energy/EnableEnergyRateCommand.cs create mode 100644 CH.CQRS/Command/Energy/UpdateEnergyRateCommand.cs create mode 100644 CH.CQRS/Service/Energy/Options/EnableEnergyProviderCommandOptions.cs create mode 100644 CH.CQRS/Service/Energy/Options/EnableEnergyRateCommandOptions.cs create mode 100644 CH.Dal/DbEntity/EnergyRateUpdate.cs rename CH.Enum/{EnergyRateExceptionTresholdResetType.cs => EnergyRateExceptionThresholdResetType.cs} (51%) create mode 100644 CH.KeycloakApi/CH.KeycloakApi.csproj create mode 100644 CH.KeycloakApi/KeycloakService.cs create mode 100644 CH.KeycloakApi/KeycloakSettings.cs create mode 100644 CH.KeycloakApi/KeycloakUser.cs diff --git a/CH.Api/Program.cs b/CH.Api/Program.cs index fb0e6c3..c49f50f 100644 --- a/CH.Api/Program.cs +++ b/CH.Api/Program.cs @@ -17,6 +17,7 @@ using PoweredSoft.Data.EntityFrameworkCore; using PoweredSoft.DynamicQuery; using PoweredSoft.Module.Abstractions; using System.Text.Json.Serialization; +using CH.Enum; var builder = WebApplication.CreateBuilder(args); @@ -110,7 +111,8 @@ mvcBuilder var connectionString = builder.Configuration.GetSection("Database").GetValue("ConnectionString"); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); - +dataSourceBuilder.MapEnum("currency"); +dataSourceBuilder.MapEnum("energy_rate_exception_threshold_reset_type"); var dataSource = dataSourceBuilder.Build(); builder.Services.AddDbContextPool(options => diff --git a/CH.CQRS/Command/Energy/CreateEnergyProviderCommand.cs b/CH.CQRS/Command/Energy/CreateEnergyProviderCommand.cs index e52e2e6..4244093 100644 --- a/CH.CQRS/Command/Energy/CreateEnergyProviderCommand.cs +++ b/CH.CQRS/Command/Energy/CreateEnergyProviderCommand.cs @@ -1,6 +1,31 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + namespace CH.CQRS.Command.Energy; public class CreateEnergyProviderCommand { + public required string Name { get; set; } + public bool Active { get; set; } +} +public class CreateEnergyProviderCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(CreateEnergyProviderCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.CreateEnergyProviderAsync(new CreateEnergyProviderCommandOptions + { + Name = command.Name, + Active = command.Active + },cancellationToken); + } +} + +public class CreateEnergyProviderCommandValidator : AbstractValidator +{ + public CreateEnergyProviderCommandValidator() + { + } } \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/CreateEnergyRateCommand.cs b/CH.CQRS/Command/Energy/CreateEnergyRateCommand.cs index ad73d62..4847144 100644 --- a/CH.CQRS/Command/Energy/CreateEnergyRateCommand.cs +++ b/CH.CQRS/Command/Energy/CreateEnergyRateCommand.cs @@ -1,6 +1,39 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + namespace CH.CQRS.Command.Energy; public class CreateEnergyRateCommand { - + public long ProviderId { get; set; } + public required string Name { get; set; } + public decimal Price { get; set; } + public required string? Currency { get; set; } + public bool Active { get; set; } +} +public class CreateEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(CreateEnergyRateCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.CreateEnergyRateAsync(new CreateEnergyRateCommandOptions + { + ProviderId = command.ProviderId, + Name = command.Name, + Price = command.Price, + Currency = command.Currency, + Active = command.Active + },cancellationToken); + } +} + +public class CreateEnergyRateCommandValidator : AbstractValidator +{ + public CreateEnergyRateCommandValidator() + { + RuleFor(command => command.Price).GreaterThanOrEqualTo(0); + RuleFor(command => command.ProviderId) + .NotEmpty(); + } } \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/CreateEnergyRateExceptionCommand.cs b/CH.CQRS/Command/Energy/CreateEnergyRateExceptionCommand.cs index f2958d0..f1fe389 100644 --- a/CH.CQRS/Command/Energy/CreateEnergyRateExceptionCommand.cs +++ b/CH.CQRS/Command/Energy/CreateEnergyRateExceptionCommand.cs @@ -1,6 +1,43 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + namespace CH.CQRS.Command.Energy; public class CreateEnergyRateExceptionCommand { - + public long RateId { get; set; } + public required string Name { get; set; } + public decimal EnergyThreshold { get; set; } + public string? ResetType { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } +} + +public class CreateEnergyRateExceptionCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(CreateEnergyRateExceptionCommand command, + CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.CreateEnergyRateExceptionAsync(new CreateEnergyRateExceptionCommandOptions + { + RateId = command.RateId, + Name = command.Name, + EnergyThreshold = command.EnergyThreshold, + ResetType = command.ResetType, + StartedAt = command.StartedAt, + EndedAt = command.EndedAt + }, cancellationToken); + } +} +public class CreateEnergyRateExceptionCommandValidator : AbstractValidator +{ + public CreateEnergyRateExceptionCommandValidator() + { + RuleFor(command => command.EnergyThreshold) + .GreaterThan(0); + RuleFor(command => command.RateId) + .NotEmpty(); + } } \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/DisableEnergyProviderCommand.cs b/CH.CQRS/Command/Energy/DisableEnergyProviderCommand.cs new file mode 100644 index 0000000..684a98d --- /dev/null +++ b/CH.CQRS/Command/Energy/DisableEnergyProviderCommand.cs @@ -0,0 +1,32 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + +namespace CH.CQRS.Command.Energy; + +public class DisableEnergyProviderCommand +{ + public long ProviderId { get; set; } + public DateTime? DisabledAt { get; set; } +} +public class DisableEnergyProviderCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(DisableEnergyProviderCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.DisableEnergyProviderAsync(new DisableEnergyProviderCommandOptions + { + ProviderId = command.ProviderId, + DisabledAt = command.DisabledAt, + }, cancellationToken); + } +} + +public class DisableEnergyProviderCommandValidator : AbstractValidator +{ + public DisableEnergyProviderCommandValidator() + { + RuleFor(command => command.ProviderId) + .NotEmpty(); + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/DisableEnergyRateCommand.cs b/CH.CQRS/Command/Energy/DisableEnergyRateCommand.cs new file mode 100644 index 0000000..d231b0c --- /dev/null +++ b/CH.CQRS/Command/Energy/DisableEnergyRateCommand.cs @@ -0,0 +1,32 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + +namespace CH.CQRS.Command.Energy; + +public class DisableEnergyRateCommand +{ + public long RateId { get; set; } + public DateTime? DisabledAt { get; set; } +} +public class DisableEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(DisableEnergyRateCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.DisableEnergyRateAsync(new DisableEnergyRateCommandOptions + { + RateId = command.RateId, + DisabledAt = command.DisabledAt + }, cancellationToken); + } +} + +public class DisableEnergyRateCommandValidator : AbstractValidator +{ + public DisableEnergyRateCommandValidator() + { + RuleFor(command => command.RateId) + .NotEmpty(); + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/EnableEnergyProviderCommand.cs b/CH.CQRS/Command/Energy/EnableEnergyProviderCommand.cs new file mode 100644 index 0000000..77fd40b --- /dev/null +++ b/CH.CQRS/Command/Energy/EnableEnergyProviderCommand.cs @@ -0,0 +1,29 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + +namespace CH.CQRS.Command.Energy; + +public class EnableEnergyProviderCommand +{ + public long ProviderId { get; set; } +} +public class EnableEnergyProviderCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(EnableEnergyProviderCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.EnableEnergyProviderAsync(new EnableEnergyProviderCommandOptions() + { + ProviderId = command.ProviderId + }, cancellationToken); + } +} + +public class EnableEnergyProviderCommandValidator : AbstractValidator +{ + public EnableEnergyProviderCommandValidator() + { + + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/EnableEnergyRateCommand.cs b/CH.CQRS/Command/Energy/EnableEnergyRateCommand.cs new file mode 100644 index 0000000..ed1bc00 --- /dev/null +++ b/CH.CQRS/Command/Energy/EnableEnergyRateCommand.cs @@ -0,0 +1,19 @@ +using CH.CQRS.Service.Energy; +using OpenHarbor.CQRS.Abstractions; + +namespace CH.CQRS.Command.Energy; + +public class EnableEnergyRateCommand +{ + public long RateId { get; set; } +} +public class EnableEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(EnableEnergyRateCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.EnableEnergyRateAsync(new EnableEnergyRateCommandOptions + { + RateId = command.RateId + }, cancellationToken); + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/ServiceCollectionExtension.cs b/CH.CQRS/Command/Energy/ServiceCollectionExtension.cs index 410215a..7944ebf 100644 --- a/CH.CQRS/Command/Energy/ServiceCollectionExtension.cs +++ b/CH.CQRS/Command/Energy/ServiceCollectionExtension.cs @@ -1,6 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenHarbor.CQRS.Abstractions; +using OpenHarbor.CQRS.FluentValidation; namespace CH.CQRS.Command.Energy; -public class ServiceCollectionExtension +public static class ServiceCollectionExtension { + public static IServiceCollection AddEnergyCommand(this IServiceCollection services) + { + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + services + .AddCommand(); + return services; + } } \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/UpdateEnergyProviderCommand.cs b/CH.CQRS/Command/Energy/UpdateEnergyProviderCommand.cs index 96cff39..6b93fd7 100644 --- a/CH.CQRS/Command/Energy/UpdateEnergyProviderCommand.cs +++ b/CH.CQRS/Command/Energy/UpdateEnergyProviderCommand.cs @@ -1,6 +1,32 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + namespace CH.CQRS.Command.Energy; public class UpdateEnergyProviderCommand { - + public long ProviderId { get; set; } + public required string Name { get; set; } +} + +public class UpdateEnergyProviderCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(UpdateEnergyProviderCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.UpdateEnergyProviderAsync(new UpdateEnergyProviderCommandOptions + { + ProviderId = command.ProviderId, + Name = command.Name + },cancellationToken); + } +} +public class UpdateEnergyProviderCommandValidator : AbstractValidator +{ + public UpdateEnergyProviderCommandValidator() + { + RuleFor(command => command.ProviderId) + .NotEmpty(); + } } \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/UpdateEnergyRateCommand.cs b/CH.CQRS/Command/Energy/UpdateEnergyRateCommand.cs new file mode 100644 index 0000000..18cb192 --- /dev/null +++ b/CH.CQRS/Command/Energy/UpdateEnergyRateCommand.cs @@ -0,0 +1,39 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + +namespace CH.CQRS.Command.Energy; + +public class UpdateEnergyRateCommand +{ + public long RateId { get; set; } + public bool SendAlert { get; set; } + public required DateTime StartedAt { get; set; } + public DateTime? AppliedAt { get; set; } + public decimal? Rate { get; set; } + public DateTime? UpdatedAt { get; set; } +} +public class UpdateEnergyRateCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(UpdateEnergyRateCommand command, CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.UpdateEnergyRateAsync(new UpdateEnergyRateCommandOptions + { + RateId = command.RateId, + StartedAt = command.StartedAt, + AppliedAt = command.AppliedAt, + Rate = command.Rate, + SendAlert = command.SendAlert, + UpdatedAt = command.UpdatedAt + }, cancellationToken); + } +} + +public class UpdateEnergyRateCommandValidator : AbstractValidator +{ + public UpdateEnergyRateCommandValidator() + { + + } +} \ No newline at end of file diff --git a/CH.CQRS/Command/Energy/UpdateEnergyRateExceptionCommand.cs b/CH.CQRS/Command/Energy/UpdateEnergyRateExceptionCommand.cs index bc924c9..5282949 100644 --- a/CH.CQRS/Command/Energy/UpdateEnergyRateExceptionCommand.cs +++ b/CH.CQRS/Command/Energy/UpdateEnergyRateExceptionCommand.cs @@ -1,6 +1,41 @@ +using CH.CQRS.Service.Energy; +using CH.CQRS.Service.Energy.Options; +using FluentValidation; +using OpenHarbor.CQRS.Abstractions; + namespace CH.CQRS.Command.Energy; public class UpdateEnergyRateExceptionCommand { - + public long EnergyRateExceptionId { get; set; } + public decimal EnergyThreshold { get; set; } + public string? ResetType { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } +} +public class UpdateEnergyRateExceptionCommandHandler(EnergyService energyService) : ICommandHandler +{ + public Task HandleAsync(UpdateEnergyRateExceptionCommand command, + CancellationToken cancellationToken = new CancellationToken()) + { + return energyService.UpdateEnergyRateExceptionAsync(new UpdateEnergyRateExceptionCommandOptions + { + EnergyRateExceptionId = command.EnergyRateExceptionId, + EnergyThreshold = command.EnergyThreshold, + ResetType = command.ResetType, + StartedAt = command.StartedAt, + EndedAt = command.EndedAt + },cancellationToken); + } +} + +public class UpdateEnergyRateExceptionCommandValidator : AbstractValidator +{ + public UpdateEnergyRateExceptionCommandValidator() + { + RuleFor(command => command.EnergyRateExceptionId) + .NotEmpty(); + RuleFor(command => command.EnergyThreshold) + .GreaterThan(0); + } } \ No newline at end of file diff --git a/CH.CQRS/CommandModule.cs b/CH.CQRS/CommandModule.cs index 412fca9..18f9f44 100644 --- a/CH.CQRS/CommandModule.cs +++ b/CH.CQRS/CommandModule.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using CH.CQRS.Command.Energy; namespace CH.CQRS; public class CommandModule : IModule @@ -15,7 +16,7 @@ public class CommandModule : IModule { services.AddUserCommand(); services.AddCryptoCommand(); - + services.AddEnergyCommand(); return services; } } diff --git a/CH.CQRS/Service/CryptoStats/CryptoService.cs b/CH.CQRS/Service/CryptoStats/CryptoService.cs index fc1a557..3900cf1 100644 --- a/CH.CQRS/Service/CryptoStats/CryptoService.cs +++ b/CH.CQRS/Service/CryptoStats/CryptoService.cs @@ -1,5 +1,4 @@ - -using CH.CryptoStats.CoinGecko; +using CH.CryptoStats.CoinGecko; using CH.CryptoStats.CoinMarketCap; namespace CH.CQRS.Service.CryptoStats; diff --git a/CH.CQRS/Service/Energy/EnergyService.cs b/CH.CQRS/Service/Energy/EnergyService.cs index c8cc19a..9dd6d6a 100644 --- a/CH.CQRS/Service/Energy/EnergyService.cs +++ b/CH.CQRS/Service/Energy/EnergyService.cs @@ -1,6 +1,153 @@ +using CH.CQRS.Command.Energy; +using CH.CQRS.Service.Energy.Options; +using CH.Dal; +using CH.Dal.DbEntity; +using CH.Enum; +using Microsoft.EntityFrameworkCore; + namespace CH.CQRS.Service.Energy; -public class EnergyService +public class EnergyService(CHDbContext dbContext) { - + public async Task CreateEnergyProviderAsync(CreateEnergyProviderCommandOptions options, CancellationToken cancellationToken) + { + var energyProvider = new EnergyProvider + { + Name = options.Name, + CreatedAt = DateTime.UtcNow, + Active = options.Active + }; + await dbContext.AddAsync(energyProvider, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + } + + public async Task CreateEnergyRateAsync(CreateEnergyRateCommandOptions options, CancellationToken cancellationToken) + { + var provider = await dbContext.EnergyProviders.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken); + var energyRate = new EnergyRate + { + Name = options.Name, + Currency = (Currency)System.Enum.Parse(typeof(Currency), options.Currency?.ToUpper()), + Price = options.Price, + CreatedAt = DateTime.UtcNow, + Provider = provider, + Active = options.Active + }; + await dbContext.AddAsync(energyRate, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + } + + public async Task CreateEnergyRateExceptionAsync(CreateEnergyRateExceptionCommandOptions options, + CancellationToken cancellationToken) + { + var energyRate = await dbContext.EnergyRates.FirstOrDefaultAsync(energyRate => energyRate.Id == options.RateId,cancellationToken); + var energyRateException = new EnergyRateException + { + Name = options.Name, + CreatedAt = DateTime.UtcNow, + Rate = energyRate, + StartedAt = options.StartedAt, + EndedAt = options.EndedAt, + EnergyThreshold = options.EnergyThreshold, + ResetType = (EnergyRateExceptionThresholdResetType)System.Enum + .Parse(typeof(EnergyRateExceptionThresholdResetType), options.ResetType.ToLower() ?? ""), + }; + await dbContext.AddAsync(energyRateException, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateEnergyProviderAsync(UpdateEnergyProviderCommandOptions options, + CancellationToken cancellationToken) + { + var provider = await dbContext.EnergyProviders.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken); + if (null != provider) + { + provider.Name = options.Name; + provider.UpdatedAt = DateTime.UtcNow; + await dbContext.SaveChangesAsync(cancellationToken); + } + + } + + public async Task UpdateEnergyRateAsync(UpdateEnergyRateCommandOptions options, CancellationToken cancellationToken) + { + var energyRateUpdate = new EnergyRateUpdate + { + RateId = options.RateId, + Rate = options.Rate, + StartedAt = options.StartedAt, + CreatedAt = DateTime.UtcNow, + UpdatedAt = options.UpdatedAt, + AppliedAt = options.AppliedAt, + SendAlert = options.SendAlert, + }; + await dbContext.AddAsync(energyRateUpdate, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateEnergyRateExceptionAsync(UpdateEnergyRateExceptionCommandOptions options, + CancellationToken cancellationToken) + { + var energyRateException = await dbContext.EnergyRateExceptions + .FirstOrDefaultAsync(rateException => rateException.Id == options.EnergyRateExceptionId, cancellationToken); + if (null != energyRateException) + { + energyRateException.EnergyThreshold = options.EnergyThreshold; + energyRateException.StartedAt = options.StartedAt; + energyRateException.EndedAt = options.EndedAt; + energyRateException.UpdatedAt = DateTime.UtcNow; + if (String.IsNullOrWhiteSpace(options.ResetType)) + { + energyRateException.ResetType = (EnergyRateExceptionThresholdResetType)System.Enum.Parse(typeof(EnergyRateExceptionThresholdResetType),options.ResetType) ; + } + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public async Task DisableEnergyProviderAsync(DisableEnergyProviderCommandOptions options, + CancellationToken cancellationToken) + { + var energyProvider = await dbContext.EnergyProviders.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken); + if (null != energyProvider) + { + energyProvider.Active = false; + energyProvider.DisabledAt = options.DisabledAt; + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public async Task DisableEnergyRateAsync(DisableEnergyRateCommandOptions options, CancellationToken cancellationToken) + { + var energyRate = await dbContext.EnergyRates.FirstOrDefaultAsync(rate => rate.Id == options.RateId, cancellationToken); + if (null != energyRate) + { + energyRate.Active = false; + energyRate.DisabledAt = options.DisabledAt; + await dbContext.SaveChangesAsync(cancellationToken); + } + } + public async Task EnableEnergyProviderAsync(EnableEnergyProviderCommandOptions options, + CancellationToken cancellationToken) + { + var energyProvider = await dbContext.EnergyProviders.FirstOrDefaultAsync(provider => provider.Id == options.ProviderId, cancellationToken); + if (null != energyProvider) + { + energyProvider.Active = true; + energyProvider.DisabledAt = null; + await dbContext.SaveChangesAsync(cancellationToken); + } + } + + public async Task EnableEnergyRateAsync(EnableEnergyRateCommandOptions options, CancellationToken cancellationToken) + { + var energyRate = await dbContext.EnergyRates.FirstOrDefaultAsync(rate => rate.Id == options.RateId, cancellationToken); + if (null != energyRate) + { + energyRate.Active = true; + energyRate.DisabledAt = null; + await dbContext.SaveChangesAsync(cancellationToken); + } + } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/CreateEnergyProviderCommandOptions.cs b/CH.CQRS/Service/Energy/Options/CreateEnergyProviderCommandOptions.cs index dbb1353..52778da 100644 --- a/CH.CQRS/Service/Energy/Options/CreateEnergyProviderCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/CreateEnergyProviderCommandOptions.cs @@ -2,5 +2,6 @@ namespace CH.CQRS.Service.Energy.Options; public class CreateEnergyProviderCommandOptions { - + public required string Name { get; set; } + public bool Active { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/CreateEnergyRateCommandOptions.cs b/CH.CQRS/Service/Energy/Options/CreateEnergyRateCommandOptions.cs index b3316be..32c8d00 100644 --- a/CH.CQRS/Service/Energy/Options/CreateEnergyRateCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/CreateEnergyRateCommandOptions.cs @@ -1,6 +1,12 @@ +using Amazon.Runtime.Internal.Transform; + namespace CH.CQRS.Service.Energy.Options; public class CreateEnergyRateCommandOptions { - + public long ProviderId { get; set; } + public required string Name { get; set; } + public decimal? Price { get; set; } + public required string? Currency { get; set; } + public bool Active { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/CreateEnergyRateExceptionCommandOptions.cs b/CH.CQRS/Service/Energy/Options/CreateEnergyRateExceptionCommandOptions.cs index 8fb87c4..aae8a68 100644 --- a/CH.CQRS/Service/Energy/Options/CreateEnergyRateExceptionCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/CreateEnergyRateExceptionCommandOptions.cs @@ -2,5 +2,10 @@ namespace CH.CQRS.Service.Energy.Options; public class CreateEnergyRateExceptionCommandOptions { - + public long RateId { get; set; } + public required string Name { get; set; } + public decimal EnergyThreshold { get; set; } + public string? ResetType { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/DisableEnergyProviderCommandOptions.cs b/CH.CQRS/Service/Energy/Options/DisableEnergyProviderCommandOptions.cs index f0fc843..5c1b5a5 100644 --- a/CH.CQRS/Service/Energy/Options/DisableEnergyProviderCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/DisableEnergyProviderCommandOptions.cs @@ -2,5 +2,6 @@ namespace CH.CQRS.Service.Energy.Options; public class DisableEnergyProviderCommandOptions { - + public long ProviderId { get; set; } + public DateTime? DisabledAt { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/DisableEnergyRateCommandOptions.cs b/CH.CQRS/Service/Energy/Options/DisableEnergyRateCommandOptions.cs index ddffc2c..116a275 100644 --- a/CH.CQRS/Service/Energy/Options/DisableEnergyRateCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/DisableEnergyRateCommandOptions.cs @@ -2,5 +2,6 @@ namespace CH.CQRS.Service.Energy.Options; public class DisableEnergyRateCommandOptions { - + public long RateId { get; set; } + public DateTime? DisabledAt { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/EnableEnergyProviderCommandOptions.cs b/CH.CQRS/Service/Energy/Options/EnableEnergyProviderCommandOptions.cs new file mode 100644 index 0000000..a70bf49 --- /dev/null +++ b/CH.CQRS/Service/Energy/Options/EnableEnergyProviderCommandOptions.cs @@ -0,0 +1,6 @@ +namespace CH.CQRS.Service.Energy.Options; + +public class EnableEnergyProviderCommandOptions +{ + public long ProviderId { get; set; } +} \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/EnableEnergyRateCommandOptions.cs b/CH.CQRS/Service/Energy/Options/EnableEnergyRateCommandOptions.cs new file mode 100644 index 0000000..db880e7 --- /dev/null +++ b/CH.CQRS/Service/Energy/Options/EnableEnergyRateCommandOptions.cs @@ -0,0 +1,6 @@ +namespace CH.CQRS.Command.Energy; + +public class EnableEnergyRateCommandOptions +{ + public long RateId { get; set; } +} \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/UpdateEnergyProviderCommandOptions.cs b/CH.CQRS/Service/Energy/Options/UpdateEnergyProviderCommandOptions.cs index 1787979..b176d5d 100644 --- a/CH.CQRS/Service/Energy/Options/UpdateEnergyProviderCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/UpdateEnergyProviderCommandOptions.cs @@ -2,5 +2,6 @@ namespace CH.CQRS.Service.Energy.Options; public class UpdateEnergyProviderCommandOptions { - + public long ProviderId { get; set; } + public required string Name { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/UpdateEnergyRateCommandOptions.cs b/CH.CQRS/Service/Energy/Options/UpdateEnergyRateCommandOptions.cs index 36bb578..0e27825 100644 --- a/CH.CQRS/Service/Energy/Options/UpdateEnergyRateCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/UpdateEnergyRateCommandOptions.cs @@ -2,5 +2,10 @@ namespace CH.CQRS.Service.Energy.Options; public class UpdateEnergyRateCommandOptions { - + public long RateId { get; set; } + public bool SendAlert { get; set; } + public required DateTime StartedAt { get; set; } + public DateTime? AppliedAt { get; set; } + public decimal? Rate { get; set; } + public DateTime? UpdatedAt { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/Service/Energy/Options/UpdateEnergyRateExceptionCommandOptions.cs b/CH.CQRS/Service/Energy/Options/UpdateEnergyRateExceptionCommandOptions.cs index 9ed008e..4b31bf6 100644 --- a/CH.CQRS/Service/Energy/Options/UpdateEnergyRateExceptionCommandOptions.cs +++ b/CH.CQRS/Service/Energy/Options/UpdateEnergyRateExceptionCommandOptions.cs @@ -2,5 +2,9 @@ namespace CH.CQRS.Service.Energy.Options; public class UpdateEnergyRateExceptionCommandOptions { - + public long EnergyRateExceptionId { get; set; } + public decimal EnergyThreshold { get; set; } + public string? ResetType { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } } \ No newline at end of file diff --git a/CH.CQRS/SharedModule.cs b/CH.CQRS/SharedModule.cs index 2dbbf45..deeed0a 100644 --- a/CH.CQRS/SharedModule.cs +++ b/CH.CQRS/SharedModule.cs @@ -1,4 +1,5 @@ using CH.CQRS.Service.CryptoStats; +using CH.CQRS.Service.Energy; using CH.CQRS.Service.User; using CH.CryptoStats.Abstractions; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,7 @@ public class SharedModule : IModule { services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/CH.Dal/CHDbContext.cs b/CH.Dal/CHDbContext.cs index c77e635..691a673 100644 --- a/CH.Dal/CHDbContext.cs +++ b/CH.Dal/CHDbContext.cs @@ -9,12 +9,12 @@ public class CHDbContext(DbContextOptions options) : CHDbScaffoldedContext(optio { static CHDbContext() => NpgsqlConnection.GlobalTypeMapper .MapEnum("currency") - .MapEnum("energy_rate_exception_treshold_reset_type"); + .MapEnum("energy_rate_exception_threshold_reset_type"); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasPostgresEnum("currency"); - modelBuilder.HasPostgresEnum("energy_rate_exception_treshold_reset_type"); + modelBuilder.HasPostgresEnum("energy_rate_exception_threshold_reset_type"); modelBuilder.Entity(entity => { @@ -26,7 +26,7 @@ public class CHDbContext(DbContextOptions options) : CHDbScaffoldedContext(optio { entity.Property(e => e.ResetType) .HasColumnName("reset_type") - .HasColumnType("energy_rate_exception_treshold_reset_type"); + .HasColumnType("energy_rate_exception_threshold_reset_type"); }); } diff --git a/CH.Dal/CHDbScaffoldedContext.cs b/CH.Dal/CHDbScaffoldedContext.cs index 50756ff..deec810 100644 --- a/CH.Dal/CHDbScaffoldedContext.cs +++ b/CH.Dal/CHDbScaffoldedContext.cs @@ -22,6 +22,8 @@ public partial class CHDbScaffoldedContext : DbContext public virtual DbSet EnergyRateExceptions { get; set; } + public virtual DbSet EnergyRateUpdates { get; set; } + public virtual DbSet Locations { get; set; } public virtual DbSet Machines { get; set; } @@ -35,7 +37,7 @@ public partial class CHDbScaffoldedContext : DbContext { modelBuilder .HasPostgresEnum("currency", new[] { "USD", "CAD" }) - .HasPostgresEnum("energy_rate_exception_treshold_reset_type", new[] { "daily" }); + .HasPostgresEnum("energy_rate_exception_threshold_reset_type", new[] { "daily" }); modelBuilder.Entity(entity => { @@ -44,9 +46,11 @@ public partial class CHDbScaffoldedContext : DbContext entity.ToTable("energy_provider"); entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Active).HasColumnName("active"); entity.Property(e => e.CreatedAt) .HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)") .HasColumnName("created_at"); + entity.Property(e => e.DisabledAt).HasColumnName("disabled_at"); entity.Property(e => e.Name) .HasMaxLength(255) .HasColumnName("name"); @@ -60,9 +64,11 @@ public partial class CHDbScaffoldedContext : DbContext entity.ToTable("energy_rate"); entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Active).HasColumnName("active"); entity.Property(e => e.CreatedAt) .HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)") .HasColumnName("created_at"); + entity.Property(e => e.DisabledAt).HasColumnName("disabled_at"); entity.Property(e => e.Name) .HasMaxLength(255) .HasColumnName("name"); @@ -86,7 +92,7 @@ public partial class CHDbScaffoldedContext : DbContext .HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)") .HasColumnName("created_at"); entity.Property(e => e.EndedAt).HasColumnName("ended_at"); - entity.Property(e => e.EnergyTreshold).HasColumnName("energy_treshold"); + entity.Property(e => e.EnergyThreshold).HasColumnName("energy_threshold"); entity.Property(e => e.Name) .HasMaxLength(255) .HasColumnName("name"); @@ -99,6 +105,30 @@ public partial class CHDbScaffoldedContext : DbContext .HasConstraintName("energy_rate_exception_rate_id_fkey"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("energy_rate_update_pkey"); + + entity.ToTable("energy_rate_update"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.AppliedAt).HasColumnName("applied_at"); + entity.Property(e => e.CreatedAt) + .HasDefaultValueSql("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text)") + .HasColumnName("created_at"); + entity.Property(e => e.Rate).HasColumnName("rate"); + entity.Property(e => e.RateId).HasColumnName("rate_id"); + entity.Property(e => e.SendAlert) + .HasDefaultValue(false) + .HasColumnName("send_alert"); + entity.Property(e => e.StartedAt).HasColumnName("started_at"); + entity.Property(e => e.UpdatedAt).HasColumnName("updated_at"); + + entity.HasOne(d => d.RateNavigation).WithMany(p => p.EnergyRateUpdates) + .HasForeignKey(d => d.RateId) + .HasConstraintName("energy_rate_update_rate_id_fkey"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("location_pkey"); diff --git a/CH.Dal/DbEntity/EnergyProvider.cs b/CH.Dal/DbEntity/EnergyProvider.cs index 9e558ac..be8ca36 100644 --- a/CH.Dal/DbEntity/EnergyProvider.cs +++ b/CH.Dal/DbEntity/EnergyProvider.cs @@ -9,6 +9,10 @@ public partial class EnergyProvider public string Name { get; set; } = null!; + public bool Active { get; set; } + + public DateTime? DisabledAt { get; set; } + public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } diff --git a/CH.Dal/DbEntity/EnergyRate.cs b/CH.Dal/DbEntity/EnergyRate.cs index 0a8c54e..463493a 100644 --- a/CH.Dal/DbEntity/EnergyRate.cs +++ b/CH.Dal/DbEntity/EnergyRate.cs @@ -13,11 +13,17 @@ public partial class EnergyRate public decimal? Price { get; set; } + public bool Active { get; set; } + + public DateTime? DisabledAt { get; set; } + public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } public virtual ICollection EnergyRateExceptions { get; set; } = new List(); + public virtual ICollection EnergyRateUpdates { get; set; } = new List(); + public virtual EnergyProvider? Provider { get; set; } } diff --git a/CH.Dal/DbEntity/EnergyRateException.Extension.cs b/CH.Dal/DbEntity/EnergyRateException.Extension.cs index 6c9d7ae..6d57040 100644 --- a/CH.Dal/DbEntity/EnergyRateException.Extension.cs +++ b/CH.Dal/DbEntity/EnergyRateException.Extension.cs @@ -5,5 +5,5 @@ namespace CH.Dal.DbEntity; public partial class EnergyRateException { [Column(TypeName = "energy_rate_exception_treshold_reset_type")] - public EnergyRateExceptionTresholdResetType ResetType { get; set; } + public EnergyRateExceptionThresholdResetType ResetType { get; set; } } \ No newline at end of file diff --git a/CH.Dal/DbEntity/EnergyRateException.cs b/CH.Dal/DbEntity/EnergyRateException.cs index fe8291a..84619f9 100644 --- a/CH.Dal/DbEntity/EnergyRateException.cs +++ b/CH.Dal/DbEntity/EnergyRateException.cs @@ -11,7 +11,7 @@ public partial class EnergyRateException public string? Name { get; set; } - public decimal? EnergyTreshold { get; set; } + public decimal? EnergyThreshold { get; set; } public DateTime? StartedAt { get; set; } diff --git a/CH.Dal/DbEntity/EnergyRateUpdate.cs b/CH.Dal/DbEntity/EnergyRateUpdate.cs new file mode 100644 index 0000000..f273470 --- /dev/null +++ b/CH.Dal/DbEntity/EnergyRateUpdate.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace CH.Dal.DbEntity; + +public partial class EnergyRateUpdate +{ + public long Id { get; set; } + + public long? RateId { get; set; } + + public bool SendAlert { get; set; } + + public DateTime StartedAt { get; set; } + + public decimal? Rate { get; set; } + + public DateTime? AppliedAt { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public virtual EnergyRate? RateNavigation { get; set; } +} diff --git a/CH.Enum/EnergyRateExceptionTresholdResetType.cs b/CH.Enum/EnergyRateExceptionThresholdResetType.cs similarity index 51% rename from CH.Enum/EnergyRateExceptionTresholdResetType.cs rename to CH.Enum/EnergyRateExceptionThresholdResetType.cs index 915dc1e..ede9540 100644 --- a/CH.Enum/EnergyRateExceptionTresholdResetType.cs +++ b/CH.Enum/EnergyRateExceptionThresholdResetType.cs @@ -2,8 +2,8 @@ using NpgsqlTypes; namespace CH.Enum; -public enum EnergyRateExceptionTresholdResetType +public enum EnergyRateExceptionThresholdResetType { [PgName("daily")] - Daily, + daily, } \ No newline at end of file diff --git a/CH.KeycloakApi/CH.KeycloakApi.csproj b/CH.KeycloakApi/CH.KeycloakApi.csproj new file mode 100644 index 0000000..3d9dde1 --- /dev/null +++ b/CH.KeycloakApi/CH.KeycloakApi.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/CH.KeycloakApi/KeycloakService.cs b/CH.KeycloakApi/KeycloakService.cs new file mode 100644 index 0000000..86fa16b --- /dev/null +++ b/CH.KeycloakApi/KeycloakService.cs @@ -0,0 +1,180 @@ + using System.Text; + using IdentityModel.Client; + using Microsoft.Extensions.Configuration; + using Newtonsoft.Json; + + namespace CH.KeycloakApi; + + public class KeycloakService + { + private readonly KeycloakSettings settings; + + public KeycloakService(IConfiguration configuration) + { + this.settings = new KeycloakSettings(); + configuration.Bind("Keycloak", settings); + } + + public KeycloakSettings Settings => settings; + + public async Task GetTokenAsync() + { + var tokenEndpoint = $"{this.settings.Endpoint}/realms/master/protocol/openid-connect/token"; + var client = new HttpClient(); + var response = await client.RequestTokenAsync(new TokenRequest + { + Address = tokenEndpoint, + GrantType = "client_credentials", + ClientId = this.settings.ClientId, + ClientSecret = this.settings.ClientSecret, + }); + + return response.AccessToken; + } + + public async Task GetUserByEmailAsync(string realm, string email) + { + var httpClient = new HttpClient(); + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users?email={email}"; + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var ret = JsonConvert.DeserializeObject>(json); + return ret.FirstOrDefault(); + } + + public async Task GetUserByIdAsync(string realm, string id) + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users/{id}"; + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var ret = JsonConvert.DeserializeObject(json); + return ret; + } + + public async Task SendChangePasswordEmailAsync(string realm, string id) + { + //PUT /{realm}/users/{id}/execute-actions-email + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users/{id}/execute-actions-email"; + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var putJson = JsonConvert.SerializeObject(new string[] { + "UPDATE_PASSWORD" + }); + var response = await httpClient.PutAsync(url, new StringContent(putJson, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + } + + public async Task> GetUsersAsync(string realm, string search = null, int max = 100) + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users?max={max}"; + if (!string.IsNullOrWhiteSpace(search)) + url += $"&search={search}"; + + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var ret = JsonConvert.DeserializeObject>(json); + return ret; + } + + public async Task ChangePasswordAsync(string realm, string id, string newPassword, bool temporary) + { + ///auth/admin/realms/{realm}/users/{id}/reset-password + /////{ "type": "password", "temporary": false, "value": "my-new-password" } + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users/{id}/reset-password"; + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var putJson = JsonConvert.SerializeObject(new { + type = "password", + temporary, + value = newPassword + }); + var response = await httpClient.PutAsync(url, new StringContent(putJson, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + } + + + + public async Task UpdateUserByIdAsync(string realm, string id, string email, string firstName, string lastName, bool enabled) + { + var user = await GetUserByIdAsync(realm, id); + if (user == null) + throw new Exception($"no user {email} from on realm {realm}"); + + user.Email = email; + user.FirstName = firstName; + user.LastName = lastName; + user.Username = email; + user.Enabled = enabled; + + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users/{id}"; + + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var putJson = JsonConvert.SerializeObject(user); + var response = await httpClient.PutAsync(url, new StringContent(putJson, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + } + + public async Task EmailExistsAsync(string realm, string email) + { + return await this.GetUserByEmailAsync(realm, email) != null; + } + + public async Task CreateOrUpdateAsync(string realm, string email, string firstName, string lastName, bool enabled) + { + var existingUser = await GetUserByEmailAsync(realm, email); + if (existingUser != null) + { + await UpdateUserByIdAsync(realm, existingUser.Id, email, firstName, lastName, enabled); + return await GetUserByEmailAsync(realm, email); + } + + return await CreateUserAsync(realm, email, firstName, lastName, enabled); + } + + public async Task CreateUserAsync(string realm, string email, string firstName, string lastName, bool enabled) + { + long epochTicks = new DateTime(1970, 1, 1).Ticks; + long unixTime = ((DateTime.UtcNow.Ticks - epochTicks) / TimeSpan.TicksPerSecond); + + var user = new KeycloakUser() + { + CreatedTimestamp = unixTime, + Username = email, + FirstName = firstName, + LastName = lastName, + Enabled = enabled, + Totp = false, + EmailVerified = false, + RequiredActions = new List(), + Attributes = null, + Email = email, + NotBefore = 0, + Access = new Dictionary + { + { "manageGroupMembership", true }, + { "view", true }, + { "mapRoles", true }, + { "impersonate", true }, + { "manage", true } + } + }; + + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", await GetTokenAsync()); + var url = $"{this.settings.Endpoint}/admin/realms/{realm}/users"; + var postJson = JsonConvert.SerializeObject(user); + var response = await httpClient.PostAsync(url, new StringContent(postJson, Encoding.UTF8, "application/json")); + response.EnsureSuccessStatusCode(); + var ret = await GetUserByEmailAsync(realm, email); + return ret; + } + } \ No newline at end of file diff --git a/CH.KeycloakApi/KeycloakSettings.cs b/CH.KeycloakApi/KeycloakSettings.cs new file mode 100644 index 0000000..4049e36 --- /dev/null +++ b/CH.KeycloakApi/KeycloakSettings.cs @@ -0,0 +1,10 @@ +namespace CH.KeycloakApi; + +public class KeycloakSettings +{ + public string Endpoint { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string EmployeeRealm { get; set; } + public string UserRealm { get; set; } +} \ No newline at end of file diff --git a/CH.KeycloakApi/KeycloakUser.cs b/CH.KeycloakApi/KeycloakUser.cs new file mode 100644 index 0000000..b53de69 --- /dev/null +++ b/CH.KeycloakApi/KeycloakUser.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CH.KeycloakApi; + +public class KeycloakUser +{ + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("createdTimestamp")] + public long CreatedTimestamp { get; set; } + [JsonProperty("username")] + public string Username { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("totp")] + public bool Totp { get; set; } + [JsonProperty("emailVerified")] + public bool EmailVerified { get; set; } + [JsonProperty("firstName")] + public string FirstName { get; set; } + [JsonProperty("lastName")] + public string LastName { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("notBefore")] + public int NotBefore { get; set; } + [JsonProperty("requiredActions")] + public List RequiredActions { get; set; } + [JsonProperty("attributes")] + public JObject Attributes { get; set; } + [JsonProperty("access")] + public Dictionary Access { get; set; } +} \ No newline at end of file diff --git a/ConstellationHeating.sln b/ConstellationHeating.sln index fa03266..54dd1ae 100644 --- a/ConstellationHeating.sln +++ b/ConstellationHeating.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.Energy.HydroQuebec", "CH EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.Enum", "CH.Enum\CH.Enum.csproj", "{45E17ADC-A1C9-4EE0-BA6E-A4B52F0621BD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CH.KeycloakApi", "CH.KeycloakApi\CH.KeycloakApi.csproj", "{28648B65-7D84-460A-993E-0F98D94CECFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {45E17ADC-A1C9-4EE0-BA6E-A4B52F0621BD}.Debug|Any CPU.Build.0 = Debug|Any CPU {45E17ADC-A1C9-4EE0-BA6E-A4B52F0621BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {45E17ADC-A1C9-4EE0-BA6E-A4B52F0621BD}.Release|Any CPU.Build.0 = Release|Any CPU + {28648B65-7D84-460A-993E-0F98D94CECFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28648B65-7D84-460A-993E-0F98D94CECFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28648B65-7D84-460A-993E-0F98D94CECFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28648B65-7D84-460A-993E-0F98D94CECFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE