diff --git a/EShopOnAbp.sln b/EShopOnAbp.sln index e7a4264d..73d5919b 100644 --- a/EShopOnAbp.sln +++ b/EShopOnAbp.sln @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EShopOnAbp.BasketService", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EShopOnAbp.CmskitService.HttpApi.Host", "services\cmskit\src\EShopOnAbp.CmskitService.HttpApi.Host\EShopOnAbp.CmskitService.HttpApi.Host.csproj", "{D5B9D5A5-44AA-42F8-867C-95B54780C9DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EShopOnAbp.Keycloak.DbMigrator", "shared\EShopOnAbp.Keycloak.DbMigrator\EShopOnAbp.Keycloak.DbMigrator.csproj", "{774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +115,10 @@ Global {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Release|Any CPU.Build.0 = Release|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +140,7 @@ Global {7AE4C613-780E-4DA5-9B57-76F0D40D5146} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} {E373DD66-3247-4D95-B325-064BBBC337B1} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} {D5B9D5A5-44AA-42F8-867C-95B54780C9DC} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C} = {B8B59303-2178-459B-91A8-DF353044E090} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {26F82565-C6A4-439D-93A4-3251E3E7D5B0} diff --git a/etc/docker/docker-compose.infrastructure.override.yml b/etc/docker/docker-compose.infrastructure.override.yml index c3c7ddeb..7b9c83a2 100644 --- a/etc/docker/docker-compose.infrastructure.override.yml +++ b/etc/docker/docker-compose.infrastructure.override.yml @@ -38,8 +38,8 @@ services: DB_USER: "postgres" DB_PASSWORD: "myPassw0rd" KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KC_HEALTH_ENABLED: true + KEYCLOAK_ADMIN_PASSWORD: "1q2w3E*" + KC_HEALTH_ENABLED: "true" entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs new file mode 100644 index 00000000..ce020de4 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using EShopOnAbp.DbMigrator.Keycloak; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Volo.Abp; + +namespace EShopOnAbp.DbMigrator; + +public class DbMigratorHostedService : IHostedService +{ + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IConfiguration _configuration; + + public DbMigratorHostedService( + IHostApplicationLifetime hostApplicationLifetime, + IConfiguration configuration) + { + _hostApplicationLifetime = hostApplicationLifetime; + _configuration = configuration; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using (var application = AbpApplicationFactory.Create(options => + { + options.Services.ReplaceConfiguration(_configuration); + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + })) + { + application.Initialize(); + + await application + .ServiceProvider + .GetRequiredService() + .MigrateAsync(cancellationToken); + + application.Shutdown(); + + _hostApplicationLifetime.StopApplication(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj new file mode 100644 index 00000000..98834c63 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj @@ -0,0 +1,34 @@ + + + + Exe + net6.0 + EShopOnAbp.DbMigrator + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + Always + + + + diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs new file mode 100644 index 00000000..f739f1e6 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs @@ -0,0 +1,21 @@ +using EShopOnAbp.DbMigrator.Keycloak; +using EShopOnAbp.Shared.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace EShopOnAbp.DbMigrator; + +[DependsOn( + typeof(EShopOnAbpSharedHostingModule) +)] +public class EShopOnAbpDbMigratorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + context.Services.AddHttpClient(KeycloakService.HttpClientName); + + Configure(configuration); + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/KeyCloakDataSeeder.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/KeyCloakDataSeeder.cs new file mode 100644 index 00000000..abd7dcfa --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeyCloakDataSeeder.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using EShopOnAbp.DbMigrator.Keycloak; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator; + +public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency +{ + private readonly KeycloakService _keycloakService; + + public KeyCloakDataSeeder(KeycloakService keycloakService) + { + _keycloakService = keycloakService; + } + + public async Task SeedAsync(DataSeedContext context) + { + var result = await _keycloakService.GetAdminAccessTokenAsync("master"); + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakClientOptions.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakClientOptions.cs new file mode 100644 index 00000000..417638d7 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakClientOptions.cs @@ -0,0 +1,6 @@ +namespace EShopOnAbp.DbMigrator.Keycloak; + +public class KeycloakClientOptions +{ + +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakService.cs new file mode 100644 index 00000000..7a8c9a70 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/KeycloakService.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using EShopOnAbp.DbMigrator.Keycloak.Models; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator.Keycloak; + +public class KeycloakService : ITransientDependency +{ + public const string HttpClientName = "KeycloakServiceHttpClientName"; + + // TODO: Option + public const string BaseUrl = "http://localhost:8080/admin/realms"; + public const string AdminClientId = "admin-cli"; + public const string AdminUserName = "admin"; + public const string AdminPassword = "1q2w3E*"; + + private readonly ILogger _logger; + private readonly IHttpClientFactory _clientFactory; + + public KeycloakService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + // public async Task CreateClientAsync(string realm, Client client) + // { + // HttpResponseMessage response = await InternalCreateClientAsync(realm, client).ConfigureAwait(false); + // + // var locationPathAndQuery = response.Headers.Location.PathAndQuery; + // var clientId = response.IsSuccessStatusCode + // ? locationPathAndQuery.Substring(locationPathAndQuery.LastIndexOf("/", StringComparison.Ordinal) + 1) + // : null; + // return clientId; + // } + + private HttpClient CreateKeycloakApiHttpClient(string realm) + { + var httpClient = _clientFactory.CreateClient(HttpClientName); + httpClient.BaseAddress = new Uri(BaseUrl); + + return httpClient; + } + + private async Task CreateKeycloakApiHttpClientAsync(string realm, string token = null) + { + var httpClient = CreateKeycloakApiHttpClient(realm); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + if (!string.IsNullOrEmpty(token)) + { + var accessToken = await GetAdminAccessTokenAsync(realm); + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue($"Bearer", $"{accessToken}"); + } + + return httpClient; + } + + public async Task GetAdminAccessTokenAsync(string realm) + { + var httpClient = _clientFactory.CreateClient(HttpClientName); + httpClient.BaseAddress = new Uri(BaseUrl); + + var result = string.Empty; + + var formContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("client_id", AdminClientId), + new KeyValuePair("username", AdminUserName), + new KeyValuePair("password", AdminPassword), + new KeyValuePair("grant_type", "password") + }); + + var httpResponseMessage = + await httpClient.PostAsync($"/realms/{realm}/protocol/openid-connect/token", formContent); + + if (httpResponseMessage.IsSuccessStatusCode) + { + var response = await httpResponseMessage.Content.ReadFromJsonAsync(); + result = response?.AccessToken; + } + + return result; + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/Models/AccessTokenResult.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/Models/AccessTokenResult.cs new file mode 100644 index 00000000..83d2266b --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/Keycloak/Models/AccessTokenResult.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace EShopOnAbp.DbMigrator.Keycloak.Models; + +public class AccessTokenResult +{ + [JsonPropertyName("access_token")] public string AccessToken { get; set; } + [JsonPropertyName("expires_in")] public int Expiration { get; set; } + [JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } + + [JsonPropertyName("refresh_expires_in")] + public int RefreshExpiration { get; set; } + + [JsonPropertyName("token_type")] public string TokenType { get; set; } + [JsonPropertyName("scope")] public string Scope { get; set; } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs new file mode 100644 index 00000000..aa0cf304 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator; + +public class MigrationService: ITransientDependency +{ + private readonly ILogger _logger; + private readonly IDataSeeder _dataSeeder; + + public MigrationService(ILogger logger, IDataSeeder dataSeeder) + { + _logger = logger; + _dataSeeder = dataSeeder; + } + + public async Task MigrateAsync(CancellationToken cancellationToken) + { + // Check if keycloak api is available + + //Seed data + await _dataSeeder.SeedAsync(); + + _logger.LogInformation("Migration completed!"); + } + +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs new file mode 100644 index 00000000..bd60e4fa --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using EShopOnAbp.DbMigrator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; + +class Program +{ + async static Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() +#if DEBUG + .MinimumLevel.Debug() +#else + .MinimumLevel.Information() +#endif + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .Enrich.WithProperty("Application", $"DbMigrator") + .WriteTo.Async(c => c.File("Logs/logs.txt")) + .WriteTo.Async(c => c.Console()) + .CreateLogger(); + + await CreateHostBuilder(args).RunConsoleAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .AddAppSettingsSecretsJson() + .ConfigureLogging((context, logging) => logging.ClearProviders()) + .ConfigureServices((hostContext, services) => { services.AddHostedService(); }); +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json b/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json new file mode 100644 index 00000000..077404aa --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file