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/apps/angular/src/environments/environment.ts b/apps/angular/src/environments/environment.ts index a5f81a2f..293d9acc 100644 --- a/apps/angular/src/environments/environment.ts +++ b/apps/angular/src/environments/environment.ts @@ -9,11 +9,11 @@ export const environment = { name: 'EShopOnAbp', }, oAuthConfig: { - issuer: 'https://localhost:44330', + issuer: 'http://localhost:8080/realms/master', redirectUri: baseUrl, clientId: 'Web', responseType: 'code', - scope: 'offline_access openid profile email phone AccountService IdentityService AdministrationService CatalogService OrderingService', + scope: 'offline_access openid profile email phone roles AdministrationService IdentityService BasketService CatalogService OrderingService PaymentService CmskitService', //requireHttps: true, }, apis: { diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbp.PublicWeb.csproj b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbp.PublicWeb.csproj index 37fe3838..0e9f9fdc 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbp.PublicWeb.csproj +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbp.PublicWeb.csproj @@ -8,7 +8,6 @@ - diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs index 42efd925..f36c9b6e 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs @@ -22,7 +22,14 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Polly; using StackExchange.Redis; using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http.Headers; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using Volo.Abp; using Volo.Abp.Account; using Volo.Abp.AspNetCore.Authentication.OpenIdConnect; @@ -37,11 +44,13 @@ using Volo.Abp.AspNetCore.SignalR; using Volo.Abp.AutoMapper; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.RabbitMq; using Volo.Abp.Http.Client; using Volo.Abp.Http.Client.IdentityModel.Web; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; using Volo.Abp.UI.Navigation; using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.VirtualFileSystem; @@ -68,10 +77,7 @@ namespace EShopOnAbp.PublicWeb; typeof(PaymentServiceHttpApiClientModule), typeof(AbpAutoMapperModule), typeof(CmskitServiceHttpApiClientModule), - typeof(CmsKitDomainModule), typeof(CmsKitPublicWebModule) - - )] public class EShopOnAbpPublicWebModule : AbpModule { @@ -142,26 +148,57 @@ public class EShopOnAbpPublicWebModule : AbpModule .AddAbpOpenIdConnect("oidc", options => { options.Authority = configuration["AuthServer:Authority"]; + options.ClientId = configuration["AuthServer:ClientId"]; + options.MetadataAddress = configuration["AuthServer:MetaAddress"]; options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - - options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; - - options.Scope.Add("role"); + options.Scope.Add("openid"); + options.Scope.Add("profile"); options.Scope.Add("email"); options.Scope.Add("phone"); - options.Scope.Add("AccountService"); + options.Scope.Add("roles"); + options.Scope.Add("offline_access"); + options.Scope.Add("AdministrationService"); options.Scope.Add("BasketService"); options.Scope.Add("CatalogService"); options.Scope.Add("PaymentService"); options.Scope.Add("OrderingService"); options.Scope.Add("CmskitService"); + + options.SaveTokens = true; + + //SameSite is needed for Chrome/Firefox, as they will give http error 500 back, if not set to unspecified. + // options.NonceCookie.SameSite = SameSiteMode.Unspecified; + // options.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + // + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = ClaimTypes.Role, + ValidateIssuer = true + }; + + options.Events.OnAuthorizationCodeReceived = async (authContext) => + { + var userLoggedInEto = CreateUserLoggedInEto(authContext.Principal, authContext.HttpContext); + if (userLoggedInEto != null) + { + var eventBus = + authContext.HttpContext.RequestServices.GetRequiredService(); + await eventBus.PublishAsync(userLoggedInEto); + } + }; + + if (AbpClaimTypes.UserName != "preferred_username") + { + options.ClaimActions.MapJsonKey(AbpClaimTypes.UserName, "preferred_username"); + options.ClaimActions.DeleteClaim("preferred_username"); + options.ClaimActions.RemoveDuplicate(AbpClaimTypes.UserName); + } }); + if (Convert.ToBoolean(configuration["AuthServer:IsOnProd"])) { context.Services.Configure("oidc", options => @@ -173,18 +210,21 @@ public class EShopOnAbpPublicWebModule : AbpModule options.Events.OnRedirectToIdentityProvider = async ctx => { // Intercept the redirection so the browser navigates to the right URL in your host - ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') + "connect/authorize"; + ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') + + "connect/authorize"; if (previousOnRedirectToIdentityProvider != null) { await previousOnRedirectToIdentityProvider(ctx); } }; - var previousOnRedirectToIdentityProviderForSignOut = options.Events.OnRedirectToIdentityProviderForSignOut; + var previousOnRedirectToIdentityProviderForSignOut = + options.Events.OnRedirectToIdentityProviderForSignOut; options.Events.OnRedirectToIdentityProviderForSignOut = async ctx => { // Intercept the redirection for signout so the browser navigates to the right URL in your host - ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') + "connect/endsession"; + ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') + + "connect/endsession"; if (previousOnRedirectToIdentityProviderForSignOut != null) { @@ -226,32 +266,6 @@ public class EShopOnAbpPublicWebModule : AbpModule }); } - private void ConfigureBasketHttpClient(ServiceConfigurationContext context) - { - context.Services.AddStaticHttpClientProxies( - typeof(BasketServiceContractsModule).Assembly, remoteServiceConfigurationName: BasketServiceConstants.RemoteServiceName - ); - - Configure(options => - { - options.FileSets.AddEmbedded(); - }); - } - - private void ConfigurePayment(IConfiguration configuration) - { - Configure(options => - { - options.PaymentSuccessfulCallbackUrl = - configuration["App:SelfUrl"].EnsureEndsWith('/') + "PaymentCompleted"; - }); - - Configure(options => - { - options.ConfigureIcon(PaymentMethodNames.PayPal, "fa-cc-paypal paypal"); - }); - } - public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); @@ -294,4 +308,66 @@ public class EShopOnAbpPublicWebModule : AbpModule // endpoints.MapMetrics(); }); } + + private void ConfigureBasketHttpClient(ServiceConfigurationContext context) + { + context.Services.AddStaticHttpClientProxies( + typeof(BasketServiceContractsModule).Assembly, + remoteServiceConfigurationName: BasketServiceConstants.RemoteServiceName + ); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + } + + private void ConfigurePayment(IConfiguration configuration) + { + Configure(options => + { + options.PaymentSuccessfulCallbackUrl = + configuration["App:SelfUrl"].EnsureEndsWith('/') + "PaymentCompleted"; + }); + + Configure(options => + { + options.ConfigureIcon(PaymentMethodNames.PayPal, "fa-cc-paypal paypal"); + }); + } + + private UserLoggedInEto CreateUserLoggedInEto(ClaimsPrincipal principal, HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); + + if (principal == null) + { + logger.LogWarning($"AuthorizationCode does not contain principal to create/update user!"); + return null; + } + + var claims = principal.Claims.ToList(); + + var userNameClaim = claims.FirstOrDefault(x => x.Type == "preferred_username"); + var emailClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.Email); + var isEmailVerified = claims.FirstOrDefault(x => x.Type == "email_verified")?.Value == "true"; + var phoneNumberClaim = claims.FirstOrDefault(x => x.Type == "phone"); + var userIdString = claims.First(t => t.Type == ClaimTypes.NameIdentifier).Value; + + if (!Guid.TryParse(userIdString, out Guid userId)) + { + logger.LogWarning( + $"Handling UserLoggedInEvent... User creation failed! {userIdString} can not be parsed!"); + return null; + } + + return new UserLoggedInEto + { + Id = userId, + Email = emailClaim?.Value, + UserName = userNameClaim?.Value, + Phone = phoneNumberClaim?.Value, + IsEmailVerified = isEmailVerified + }; + } } \ No newline at end of file diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/Pages/ProductDetail.cshtml.cs b/apps/public-web/src/EShopOnAbp.PublicWeb/Pages/ProductDetail.cshtml.cs index 32bcbdd0..f8dbf4f6 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/Pages/ProductDetail.cshtml.cs +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/Pages/ProductDetail.cshtml.cs @@ -29,8 +29,17 @@ namespace EShopOnAbp.PublicWeb.Pages public async Task OnGet(Guid id) { - IsPurschased = (await _orderAppService.GetMyOrdersAsync(new GetMyOrdersInput())).Any(p => p.Items.Any(p => p.ProductId == id)); Product = await _productAppService.GetAsync(id); + + try + { + IsPurschased = (await _orderAppService.GetMyOrdersAsync(new GetMyOrdersInput())).Any(p => p.Items.Any(p => p.ProductId == id)); + } + catch (Exception e) + { + IsPurschased = false; + Console.WriteLine(e); + } } } } \ No newline at end of file diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/UserLoggedInEto.cs b/apps/public-web/src/EShopOnAbp.PublicWeb/UserLoggedInEto.cs new file mode 100644 index 00000000..f7ffa497 --- /dev/null +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/UserLoggedInEto.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus; + +namespace EShopOnAbp.PublicWeb; + +[EventName("EShopOnAbp.Identity.UserLoggedIn")] +[Serializable] +public class UserLoggedInEto : EtoBase +{ + public Guid Id { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string UserName { get; set; } + public bool IsEmailVerified { get; set; } +} \ No newline at end of file diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json b/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json index 8d0d9e5b..dd89b1f3 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json @@ -36,12 +36,11 @@ "Url": "http://localhost:9200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "ClientId": "PublicWeb", - "ClientSecret": "1q2w3e*", "IsOnProd": "false", - "MetaAddress": "https://localhost:44330" + "MetaAddress": "http://localhost:8080/realms/master/.well-known/openid-configuration" }, "ReverseProxy": { "Routes": { diff --git a/etc/docker/docker-compose.infrastructure.override.yml b/etc/docker/docker-compose.infrastructure.override.yml index 3ae3deb5..7b9c83a2 100644 --- a/etc/docker/docker-compose.infrastructure.override.yml +++ b/etc/docker/docker-compose.infrastructure.override.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.8' services: rabbitmq: @@ -18,6 +18,7 @@ services: ports: - "5432:5432" environment: + POSTGRES_DB: keycloak POSTGRES_PASSWORD: "myPassw0rd" pgadmin: @@ -26,3 +27,19 @@ services: environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + + keycloak: + ports: + - "8080:8080" + environment: + DB_VENDOR: postgres + DB_ADDR: "postgres-db" + DB_DATABASE: "keycloak" + DB_USER: "postgres" + DB_PASSWORD: "myPassw0rd" + KEYCLOAK_ADMIN: admin + 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/etc/docker/docker-compose.infrastructure.yml b/etc/docker/docker-compose.infrastructure.yml index 547b3aae..3f72762f 100644 --- a/etc/docker/docker-compose.infrastructure.yml +++ b/etc/docker/docker-compose.infrastructure.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.8' services: rabbitmq: @@ -46,12 +46,21 @@ services: - eshoponabp-network pgadmin: - container_name: pgadmin_container + container_name: pgadmin image: dpage/pgadmin4:6.2 volumes: - pgadmin_data:/var/lib/pgadmin networks: - - eshoponabp-network + - eshoponabp-network + + keycloak: + container_name: keycloak + image: quay.io/keycloak/keycloak:19.0.2 + depends_on: + - postgres-db + restart: unless-stopped + networks: + - eshoponabp-network volumes: postgres_data: diff --git a/etc/k8s/eshoponabp/values.azure.yaml b/etc/k8s/eshoponabp/values.azure.yaml index 5df9c34e..7bd5b34b 100644 --- a/etc/k8s/eshoponabp/values.azure.yaml +++ b/etc/k8s/eshoponabp/values.azure.yaml @@ -18,8 +18,8 @@ authserver: host: auth.eshoponabp.com tlsSecret: eshop-wildcard-tls image: - repository: "volocr.azurecr.io/eshoponabp/app-authserver" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/app-authserver" + tag: 1.0.0 # web sub-chart override web: @@ -35,8 +35,8 @@ web: ingress: host: admin.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/app-web" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/app-web" + tag: 1.0.0 # public-web sub-chart override public-web: @@ -56,8 +56,8 @@ public-web: ingress: host: eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/app-publicweb" - tag: 1.0.6 + repository: "ghcr.io/volosoft/eshoponabp/app-publicweb" + tag: 1.0.0 # identity-service sub-chart override identity: @@ -92,8 +92,8 @@ identity: ingress: host: identity.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/service-identity" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/service-identity" + tag: 1.0.0 # administration sub-chart override administration: @@ -119,8 +119,8 @@ administration: ingress: host: administration.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/service-administration" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/service-administration" + tag: 1.0.0 # gateway-web sub-chart override gateway-web: @@ -140,8 +140,8 @@ gateway-web: ingress: host: gateway.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/gateway-web" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/gateway-web" + tag: 1.0.0 reRoutes: accountService: url: http://eshop-az-authserver @@ -172,8 +172,8 @@ gateway-web-public: ingress: host: gateway-public.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/gateway-web-public" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/gateway-web-public" + tag: 1.0.0 reRoutes: accountService: url: http://eshop-az-authserver @@ -214,8 +214,8 @@ basket: ingress: host: basket.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/service-basket" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/service-basket" + tag: 1.0.0 # catalog-service sub-chart override catalog: @@ -242,8 +242,8 @@ catalog: ingress: host: catalog.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/service-catalog" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/service-catalog" + tag: 1.0.0 # ordering-service sub-chart override ordering: @@ -294,7 +294,7 @@ cmskit: image: repository: "volocr.azurecr.io/eshoponabp/service-cmskit" tag: 1.0.5 - + # payment-service sub-chart override payment: config: @@ -315,8 +315,8 @@ payment: ingress: host: payment.eshoponabp.com image: - repository: "volocr.azurecr.io/eshoponabp/service-payment" - tag: 1.0.5 + repository: "ghcr.io/volosoft/eshoponabp/service-payment" + tag: 1.0.0 # Default values for eshoponabp. # This is a YAML-formatted file. diff --git a/gateways/web-public/src/EShopOnAbp.WebPublicGateway/appsettings.json b/gateways/web-public/src/EShopOnAbp.WebPublicGateway/appsettings.json index 119795be..c36665c8 100644 --- a/gateways/web-public/src/EShopOnAbp.WebPublicGateway/appsettings.json +++ b/gateways/web-public/src/EShopOnAbp.WebPublicGateway/appsettings.json @@ -4,7 +4,7 @@ "CorsOrigins": "https://localhost:44335" }, "AuthServer": { - "Authority": "https://localhost:44330", + "Authority": "http://localhost:8080/realms/master", "RequireHttpsMetadata": "true", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" diff --git a/gateways/web/src/EShopOnAbp.WebGateway/appsettings.json b/gateways/web/src/EShopOnAbp.WebGateway/appsettings.json index dea2bd3b..f9543eef 100644 --- a/gateways/web/src/EShopOnAbp.WebGateway/appsettings.json +++ b/gateways/web/src/EShopOnAbp.WebGateway/appsettings.json @@ -4,7 +4,7 @@ "CorsOrigins": "http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", + "Authority": "http://localhost:8080/realms/master", "RequireHttpsMetadata": "true", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" diff --git a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs index 9b349ff8..7ecb1575 100644 --- a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs +++ b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs @@ -91,7 +91,7 @@ public class AdministrationServiceHttpApiHostModule : AbpModule var configuration = context.ServiceProvider.GetRequiredService(); options.SwaggerEndpoint("/swagger/v1/swagger.json", "Administration Service API"); options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); - options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]); + // options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]); }); app.UseAbpSerilogEnrichers(); app.UseAuditing(); diff --git a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json index 8610b450..a9f4e973 100644 --- a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json +++ b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json @@ -1,13 +1,12 @@ { "App": { "SelfUrl": "https://localhost:44353", - "CorsOrigins": "https://localhost:44372,https://localhost:44373" + "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", - "SwaggerClientId": "WebGateway_Swagger", - "SwaggerClientSecret": "1q2w3e*" + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", + "SwaggerClientId": "SwaggerClient" }, "RemoteServices": { "AbpIdentity": { @@ -20,7 +19,7 @@ "GrantType": "client_credentials", "ClientId": "EShopOnAbp_AdministrationService", "ClientSecret": "1q2w3e*", - "Authority": "https://localhost:44330", + "Authority": "http://localhost:8080/realms/master", "Scope": "IdentityService" } }, diff --git a/services/basket/src/EShopOnAbp.BasketService/appsettings.json b/services/basket/src/EShopOnAbp.BasketService/appsettings.json index 93adbecc..7ef6ff5f 100644 --- a/services/basket/src/EShopOnAbp.BasketService/appsettings.json +++ b/services/basket/src/EShopOnAbp.BasketService/appsettings.json @@ -1,11 +1,11 @@ { "App": { "SelfUrl": "https://localhost:44355", - "CorsOrigins": "https://localhost:44372,https://localhost:44373,https://localhost:44335" + "CorsOrigins": "https://localhost:44372,https://localhost:44373,,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json b/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json index 645ce941..7a8b88c1 100644 --- a/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json +++ b/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json @@ -1,11 +1,11 @@ { "App": { "SelfUrl": "https://localhost:44354", - "CorsOrigins": "https://localhost:44372,https://localhost:44373,https://localhost:44335,http://localhost:4200" + "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json b/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json index 05701248..6fefb7e9 100644 --- a/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json +++ b/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json @@ -1,11 +1,11 @@ { "App": { "SelfUrl": "https://localhost:44358", - "CorsOrigins": "https://localhost:44372,https://localhost:44373" + "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, @@ -51,7 +51,7 @@ "GrantType": "client_credentials", "ClientId": "EShopOnAbp_CmskitService", "ClientSecret": "1q2w3e*", - "Authority": "https://localhost:44330", + "Authority": "http://localhost:8080/realms/master", "Scope": "IdentityService" } } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application.Contracts/ETOs/UserLoggedInEto.cs b/services/identity/src/EShopOnAbp.IdentityService.Application.Contracts/ETOs/UserLoggedInEto.cs new file mode 100644 index 00000000..d163d5f4 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application.Contracts/ETOs/UserLoggedInEto.cs @@ -0,0 +1,17 @@ +using System; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus; + +namespace EShopOnAbp.IdentityService.ETOs +{ + [EventName("EShopOnAbp.Identity.UserLoggedIn")] + [Serializable] + public class UserLoggedInEto : EtoBase + { + public Guid Id { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string UserName { get; set; } + public bool IsEmailVerified { get; set; } + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/UserLoggedInEventHandler.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/UserLoggedInEventHandler.cs new file mode 100644 index 00000000..058db2d8 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/UserLoggedInEventHandler.cs @@ -0,0 +1,98 @@ +using System.Threading.Tasks; +using EShopOnAbp.IdentityService.ETOs; +using Microsoft.Extensions.Logging; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; +using Volo.Abp.Uow; + +namespace EShopOnAbp.IdentityService; + +public class UserLoggedInEventHandler : IDistributedEventHandler, ITransientDependency +{ + private readonly IdentityUserManager _userManager; + private readonly ILogger _logger; + + public UserLoggedInEventHandler(IdentityUserManager userManager, ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + + [UnitOfWork] + public async virtual Task HandleEventAsync(UserLoggedInEto eventData) + { + if (eventData == null) + { + _logger.LogWarning($"Handling UserLoggedInEvent failed! No user information found!"); + return; + } + + var user = await _userManager.FindByIdAsync(eventData.Id.ToString()); + + if (user == null) + { + await CreateCurrentUserAsync(eventData); + } + else + { + await UpdateCurrentUserAsync(user, eventData); + } + } + + protected virtual async Task CreateCurrentUserAsync(UserLoggedInEto userInfo) + { + var user = new IdentityUser( + userInfo.Id, + userInfo.UserName, + userInfo.Email); + + user.SetEmailConfirmed(userInfo.IsEmailVerified); + + if (!string.IsNullOrEmpty(userInfo.Phone)) + { + user.SetPhoneNumber(userInfo.Phone, false); + } + + // This should run once to sync the admin userIds that seeded by IdentityModule and the Keycloak admin + if (userInfo.UserName == "admin") + { + var adminUser = await _userManager.FindByNameAsync("admin"); + await _userManager.DeleteAsync(adminUser); + } + + var result = await _userManager.CreateAsync(user); + + if (!result.Succeeded) + { + throw new AbpException(string.Join('\n', result.Errors)); + } + + _logger.LogInformation($"Handling UserLoggedInEvent... Created new user with Id:{userInfo.Id}"); + } + + protected virtual async Task UpdateCurrentUserAsync(IdentityUser user, UserLoggedInEto userInfo) + { + if (user.Email != userInfo.Email) + { + _logger.LogInformation($"Handling UserLoggedInEvent... Updating the user email with:{userInfo.Email}"); + await _userManager.SetEmailAsync(user, userInfo.Email); + } + + if (user.PhoneNumber != userInfo.Phone) + { + _logger.LogInformation( + $"Handling UserLoggedInEvent... Updating the user phone with:{userInfo.Phone}"); + await _userManager.SetPhoneNumberAsync(user, userInfo.Phone); + } + + if (user.UserName != userInfo.UserName) + { + _logger.LogInformation( + $"Handling UserLoggedInEvent... Updating the user name with:{userInfo.UserName}"); + await _userManager.SetUserNameAsync(user, userInfo.UserName); + } + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/IdentityServiceHttpApiHostModule.cs b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/IdentityServiceHttpApiHostModule.cs index 98ed74e0..c979c499 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/IdentityServiceHttpApiHostModule.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/IdentityServiceHttpApiHostModule.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Volo.Abp; using Volo.Abp.Modularity; @@ -40,10 +41,9 @@ public class IdentityServiceHttpApiHostModule : AbpModule { { "IdentityService", "Identity Service API" } }, - apiTitle: "IdentityService Gateway API" + apiTitle: "IdentityService API" ); - context.Services.AddCors(options => { options.AddDefaultPolicy(builder => @@ -62,6 +62,12 @@ public class IdentityServiceHttpApiHostModule : AbpModule .AllowCredentials(); }); }); + + // Keycloak handles the user creation that a user name can be multiple words + Configure(options => + { + options.User.AllowedUserNameCharacters = null; + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json index 9678ab64..9cdfa28f 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json +++ b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json @@ -1,11 +1,11 @@ { "App": { "SelfUrl": "https://localhost:44351", - "CorsOrigins": "https://localhost:44372,https://localhost:44373" + "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/ordering/src/EShopOnAbp.OrderingService.Application/Orders/OrderAppService.cs b/services/ordering/src/EShopOnAbp.OrderingService.Application/Orders/OrderAppService.cs index 93bcc00c..0ac0dfd8 100644 --- a/services/ordering/src/EShopOnAbp.OrderingService.Application/Orders/OrderAppService.cs +++ b/services/ordering/src/EShopOnAbp.OrderingService.Application/Orders/OrderAppService.cs @@ -40,6 +40,11 @@ public class OrderAppService : ApplicationService, IOrderAppService [AllowAnonymous] public async Task> GetMyOrdersAsync(GetMyOrdersInput input) { + if (CurrentUser.Id == null) + { + return new List(); + } + ISpecification specification = SpecificationFactory.Create(input.Filter); var orders = await _orderRepository.GetOrdersByUserId(CurrentUser.GetId(), specification, true); return CreateOrderDtoMapping(orders); diff --git a/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json b/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json index 5d6b54e1..865a6130 100644 --- a/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json +++ b/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json b/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json index 83d05c81..93582be1 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json +++ b/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs new file mode 100644 index 00000000..d28e0ecf --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +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..47d14f09 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj @@ -0,0 +1,33 @@ + + + + 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..fcdb8df2 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs @@ -0,0 +1,25 @@ +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(); + + Configure(options => + { + options.Url = configuration["Keycloak:url"]; + options.AdminUserName = configuration["Keycloak:adminUsername"]; + options.AdminPassword = configuration["Keycloak:adminPassword"]; + options.RealmName = configuration["Keycloak:realmName"]; + } + ); + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs new file mode 100644 index 00000000..6c24a9b8 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs @@ -0,0 +1,9 @@ +namespace EShopOnAbp.DbMigrator; + +public class KeycloakClientOptions +{ + public string Url { get; set; } + public string AdminUserName { get; set; } + public string AdminPassword { get; set; } + public string RealmName { get; set; } +} \ 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..2956e3d4 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Clients; +using Keycloak.Net.Models.ClientScopes; +using Keycloak.Net.Models.ProtocolMappers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator; + +public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public KeyCloakDataSeeder(IOptions keycloakClientOptions, ILogger logger, + IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + _keycloakOptions = keycloakClientOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public async Task SeedAsync(DataSeedContext context) + { + await UpdateAdminUserAsync(); + await CreateRoleMapperAsync(); + await CreateClientScopesAsync(); + await CreateClientsAsync(); + } + + private async Task CreateRoleMapperAsync() + { + var roleScope = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName)) + .FirstOrDefault(q => q.Name == "roles"); + if (roleScope == null) + return; + + if (!roleScope.ProtocolMappers.Any(q => q.Name == "roles")) + { + await _keycloakClient.CreateProtocolMapperAsync(_keycloakOptions.RealmName, roleScope.Id, + new ProtocolMapper() + { + Name = "roles", + Protocol = "openid-connect", + _ProtocolMapper = "oidc-usermodel-realm-role-mapper", + Config = new Dictionary() + { + { "access.token.claim", "true" }, + { "id.token.claim", "true" }, + { "claim.name", "roles" }, + { "multivalued", "true" }, + { "userinfo.token.claim", "true" }, + } + }); + } + } + + private async Task CreateClientScopesAsync() + { + await CreateScopeAsync("AdministrationService"); + await CreateScopeAsync("IdentityService"); + await CreateScopeAsync("BasketService"); + await CreateScopeAsync("CatalogService"); + await CreateScopeAsync("OrderingService"); + await CreateScopeAsync("PaymentService"); + await CreateScopeAsync("CmskitService"); + } + + private async Task CreateScopeAsync(string scopeName) + { + var scope = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName)) + .FirstOrDefault(q => q.Name == scopeName); + + if (scope == null) + { + scope = new ClientScope + { + Name = scopeName, + Description = scopeName + " scope", + Protocol = "openid-connect", + Attributes = new Attributes + { + ConsentScreenText = scopeName, + DisplayOnConsentScreen = "true", + IncludeInTokenScope = "true" + }, + ProtocolMappers = new List() + { + new ProtocolMapper() + { + Name = scopeName, + Protocol = "openid-connect", + _ProtocolMapper = "oidc-audience-mapper", + Config = + new + Dictionary() //TODO: Update when //https://github.com/AnderssonPeter/Keycloak.Net/pull/5 is merged + { + { "id.token.claim", "false" }, + { "access.token.claim", "true" }, + { "included.custom.audience", scopeName } + } + } + } + }; + + await _keycloakClient.CreateClientScopeAsync(_keycloakOptions.RealmName, scope); + } + } + + private async Task CreateClientsAsync() + { + await CreatePublicWebClientAsync(); + await CreateSwaggerClientAsync(); // TODO: Test when Volo.Abp.Swashbuckle v6.0.1 is released (https://github.com/abpframework/abp/pull/14409) + await CreateWebClientAsync(); + } + + private async Task CreateWebClientAsync() + { + var webClient = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "Web")) + .FirstOrDefault(); + + if (webClient == null) + { + var webRootUrl = _configuration[$"Clients:Web:RootUrl"]; + webClient = new Client + { + ClientId = "Web", + Name = "Angular Back-Office Web Application", + Protocol = "openid-connect", + Enabled = true, + BaseUrl = webRootUrl, + RedirectUris = new List + { + $"{webRootUrl.TrimEnd('/')}" + }, + FrontChannelLogout = true, + PublicClient = true + }; + webClient.Attributes = new Dictionary + { + { "post.logout.redirect.uris", $"{webRootUrl.TrimEnd('/')}" } + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, webClient); + + await AddOptionalClientScopesAsync( + "Web", + new List + { + "AdministrationService", "IdentityService", "BasketService", "CatalogService", + "OrderingService", "PaymentService", "CmskitService" + } + ); + } + } + + private async Task CreateSwaggerClientAsync() + { + var swaggerClient = + (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "SwaggerClient")) + .FirstOrDefault(); + + if (swaggerClient == null) + { + var webGatewaySwaggerRootUrl = _configuration[$"Clients:WebGateway:RootUrl"].TrimEnd('/'); + var publicWebGatewayRootUrl = _configuration[$"Clients:PublicWebGateway:RootUrl"].TrimEnd('/'); + var accountServiceRootUrl = _configuration[$"Clients:AccountService:RootUrl"].TrimEnd('/'); + var identityServiceRootUrl = _configuration[$"Clients:IdentityService:RootUrl"].TrimEnd('/'); + var administrationServiceRootUrl = _configuration[$"Clients:AdministrationService:RootUrl"].TrimEnd('/'); + var catalogServiceRootUrl = _configuration[$"Clients:CatalogService:RootUrl"].TrimEnd('/'); + var basketServiceRootUrl = _configuration[$"Clients:BasketService:RootUrl"].TrimEnd('/'); + var orderingServiceRootUrl = _configuration[$"Clients:OrderingService:RootUrl"].TrimEnd('/'); + var paymentServiceRootUrl = _configuration[$"Clients:PaymentService:RootUrl"].TrimEnd('/'); + var cmskitServiceRootUrl = _configuration[$"Clients:CmskitService:RootUrl"].TrimEnd('/'); + + swaggerClient = new Client + { + ClientId = "SwaggerClient", + Name = "Swagger Client Application", + Protocol = "openid-connect", + Enabled = true, + RedirectUris = new List + { + $"{webGatewaySwaggerRootUrl}/swagger/oauth2-redirect.html", // WebGateway redirect uri + $"{publicWebGatewayRootUrl}/swagger/oauth2-redirect.html", // PublicWebGateway redirect uri + $"{accountServiceRootUrl}/swagger/oauth2-redirect.html", // AccountService redirect uri + $"{identityServiceRootUrl}/swagger/oauth2-redirect.html", // IdentityService redirect uri + $"{administrationServiceRootUrl}/swagger/oauth2-redirect.html", // AdministrationService redirect uri + $"{catalogServiceRootUrl}/swagger/oauth2-redirect.html", // CatalogService redirect uri + $"{basketServiceRootUrl}/swagger/oauth2-redirect.html", // BasketService redirect uri + $"{orderingServiceRootUrl}/swagger/oauth2-redirect.html", // OrderingService redirect uri + $"{paymentServiceRootUrl}/swagger/oauth2-redirect.html", // PaymentService redirect uri + $"{cmskitServiceRootUrl}/swagger/oauth2-redirect.html" // CmskitService redirect uri + }, + FrontChannelLogout = true, + PublicClient = true + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, swaggerClient); + } + } + + private async Task CreatePublicWebClientAsync() + { + var publicWebClient = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "PublicWeb")) + .FirstOrDefault(); + + if (publicWebClient == null) + { + var publicWebRootUrl = _configuration[$"Clients:PublicWeb:RootUrl"]; + publicWebClient = new Client + { + ClientId = "PublicWeb", + Name = "Public Web Application", + Protocol = "openid-connect", + Enabled = true, + BaseUrl = publicWebRootUrl, + RedirectUris = new List + { + $"{publicWebRootUrl.TrimEnd('/')}/signin-oidc" + }, + FrontChannelLogout = true, + PublicClient = true, + ImplicitFlowEnabled = true // for hybrid flow + }; + publicWebClient.Attributes = new Dictionary + { + { "post.logout.redirect.uris", $"{publicWebRootUrl.TrimEnd('/')}/signout-callback-oidc" } + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, publicWebClient); + + await AddOptionalClientScopesAsync( + "PublicWeb", + new List + { + "AdministrationService", "IdentityService", "BasketService", "CatalogService", + "OrderingService", "PaymentService", "CmskitService" + } + ); + } + } + + private async Task AddOptionalClientScopesAsync(string clientName, List scopes) + { + var client = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: clientName)) + .FirstOrDefault(); + if (client == null) + { + _logger.LogError($"Couldn't find {clientName}! Could not seed optional scopes!"); + return; + } + + var clientOptionalScopes = + (await _keycloakClient.GetOptionalClientScopesAsync(_keycloakOptions.RealmName, client.Id)).ToList(); + + var clientScopes = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName)).ToList(); + + foreach (var scope in scopes) + { + if (!clientOptionalScopes.Any(q => q.Name == scope)) + { + var serviceScope = clientScopes.First(q => q.Name == scope); + _logger.LogInformation($"Seeding {scope} scope to {clientName}."); + await _keycloakClient.UpdateOptionalClientScopeAsync(_keycloakOptions.RealmName, client.Id, + serviceScope.Id); + } + } + } + + private async Task UpdateAdminUserAsync() + { + var users = await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: "admin"); + var adminUser = users.FirstOrDefault(); + if (adminUser == null) + { + throw new Exception( + "Keycloak admin user is not provided, check if KEYCLOAK_ADMIN environment variable is passed properly."); + } + + if (string.IsNullOrEmpty(adminUser.Email)) + { + adminUser.Email = "admin@abp.io"; + adminUser.FirstName = "admin"; + adminUser.EmailVerified = true; + + _logger.LogInformation("Updating admin user with email and first name..."); + await _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, adminUser.Id, adminUser); + } + } +} \ 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..13bcec2d --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json @@ -0,0 +1,46 @@ +{ + "Keycloak": { + "url": "http://localhost:8080", + "adminUsername": "admin", + "adminPassword": "1q2w3E*", + "realmName": "master" + }, + "Clients": { + "Web": { + "RootUrl": "http://localhost:4200" + }, + "PublicWeb": { + "RootUrl": "https://localhost:44335" + }, + "WebGateway": { + "RootUrl": "https://localhost:44372" + }, + "PublicWebGateway": { + "RootUrl": "https://localhost:44373" + }, + "AccountService": { + "RootUrl": "https://localhost:44330" + }, + "IdentityService": { + "RootUrl": "https://localhost:44351" + }, + "AdministrationService": { + "RootUrl": "https://localhost:44353" + }, + "CatalogService": { + "RootUrl": "https://localhost:44354" + }, + "BasketService": { + "RootUrl": "https://localhost:44355" + }, + "OrderingService": { + "RootUrl": "https://localhost:44356" + }, + "PaymentService": { + "RootUrl": "https://localhost:44357" + }, + "CmskitService": { + "RootUrl": "https://localhost:44358" + } + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs b/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs index e497d6d5..fdad69fd 100644 --- a/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs +++ b/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs @@ -29,6 +29,7 @@ public class EShopOnAbpSharedHostingMicroservicesModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; var configuration = context.Services.GetConfiguration(); Configure(options => diff --git a/tye.yaml b/tye.yaml index 0edccd81..de0f4014 100644 --- a/tye.yaml +++ b/tye.yaml @@ -1,13 +1,13 @@ name: EShopOnAbp services: -- name: auth-server - project: apps/auth-server/src/EShopOnAbp.AuthServer/EShopOnAbp.AuthServer.csproj - bindings: - - protocol: https - port: 44330 - env: - - Kestrel__Certificates__Default__Path=../../../../etc/dev-cert/localhost.pfx - - Kestrel__Certificates__Default__Password=8b6039b6-c67a-448b-977b-0ce6d3fcfd49 +# - name: auth-server +# project: apps/auth-server/src/EShopOnAbp.AuthServer/EShopOnAbp.AuthServer.csproj +# bindings: +# - protocol: https +# port: 44330 +# env: +# - Kestrel__Certificates__Default__Path=../../../../etc/dev-cert/localhost.pfx +# - Kestrel__Certificates__Default__Password=8b6039b6-c67a-448b-977b-0ce6d3fcfd49 - name: identity-service project: services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/EShopOnAbp.IdentityService.HttpApi.Host.csproj @@ -101,4 +101,7 @@ services: port: 44335 env: - Kestrel__Certificates__Default__Path=../../../../etc/dev-cert/localhost.pfx - - Kestrel__Certificates__Default__Password=8b6039b6-c67a-448b-977b-0ce6d3fcfd49 \ No newline at end of file + - Kestrel__Certificates__Default__Password=8b6039b6-c67a-448b-977b-0ce6d3fcfd49 + +- name: keycloak-seeder + project: shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj \ No newline at end of file