mirror of https://github.com/abpframework/abp
commit
387e247feb
@ -0,0 +1,54 @@
|
||||
# Microservice Demo, Projects Status and Road Map
|
||||
|
||||
After [the first announcement](https://abp.io/blog/abp/Abp-vNext-Announcement) on the ABP vNext, we have a lot of improvements on the codebase (1100+ commits on the [GitHub repository](https://github.com/abpframework/abp)). We've created features, samples, documentation and much more. In this post, I want to inform you about some news and the status of the project.
|
||||
|
||||
## Microservice Demo Solution
|
||||
|
||||
One of the major goals of the ABP framework is to provide a [convenient infrastructure to create microservice solutions](https://abp.io/documents/abp/latest/Microservice-Architecture).
|
||||
|
||||
We've been working to develop a microservice solution demo. Initial version was completed and [documented](https://abp.io/documents/abp/latest/Samples/Microservice-Demo). This sample solution aims to demonstrate a simple yet complete microservice solution;
|
||||
|
||||
- Has multiple, independent, self-deployable **microservices**.
|
||||
- Multiple **web applications**, each uses a different API gateway.
|
||||
- Has multiple **gateways** / BFFs (Backend for Frontends) developed using the [Ocelot](https://github.com/ThreeMammals/Ocelot) library.
|
||||
- Has an **authentication service** developed using the [IdentityServer](https://identityserver.io/) framework. It's also a SSO (Single Sign On) application with necessary UIs.
|
||||
- Has **multiple databases**. Some microservices has their own database while some services/applications shares a database (to demonstrate different use cases).
|
||||
- Has different types of databases: **SQL Server** (with **Entity Framework Core** ORM) and **MongoDB**.
|
||||
- Has a **console application** to show the simplest way of using a service by authenticating.
|
||||
- Uses [Redis](https://redis.io/) for **distributed caching**.
|
||||
- Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**.
|
||||
- Uses [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications.
|
||||
- Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)).
|
||||
|
||||
See [its documentation](https://abp.io/documents/abp/latest/Samples/Microservice-Demo) for a detailed explanation of the solution.
|
||||
|
||||
## Improvements/Features
|
||||
|
||||
We've worked on so many features including **distributed event bus** (with RabbitMQ integration), **IdentityServer4 integration** and enhancements for almost all features. We are continuously refactoring and adding tests to make the framework more stable and production ready. It is [rapidly growing](https://github.com/abpframework/abp/graphs/contributors).
|
||||
|
||||
## Road Map
|
||||
|
||||
There are still too much work to be done before the first stable release (v1.0). You can see [prioritized backlog items](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog) on the GitHub repo.
|
||||
|
||||
According to our estimation, we have planned to release v1.0 in Q2 of 2019 (probably in May or June). So, not too much time to wait. We are also very excited for the first stable release.
|
||||
|
||||
We will also work on [the documentation](https://abp.io/documents/abp/latest) since it is far from complete now.
|
||||
|
||||
First release may not include a SPA template. However, we want to prepare a simple one if it can be possible. Haven't decided yet about the SPA framework. Alternatives: **Angular, React and Blazor**. Please write your thought as a comment to this post.
|
||||
|
||||
## Chinese Web Site
|
||||
|
||||
There is a big ABP community in China. They have created a Chinese version of the abp.io web site: https://cn.abp.io/ They are keeping it up to date. Thanks to the Chinese developers and especially to [Liming Ma](https://github.com/maliming).
|
||||
|
||||
## NDC {London} 2019
|
||||
|
||||
It was a pleasure to be in [NDC {London}](https://ndc-london.com/) 2019 as a partner. We've talked to many developers about the current ASP.NET Boilerplate and the ABP vNext and we got good feedbacks.
|
||||
|
||||
We also had a chance to talk with [Scott Hanselman](https://twitter.com/shanselman) and [Jon Galloway](https://twitter.com/jongalloway). They visited our booth and we talked about the ideas for ABP vNext. They liked features, approaches and the goal of new ABP framework. See some photos and comments on twitter:
|
||||
|
||||
![scott-and-jon](scott-and-jon.png)
|
||||
|
||||
## Follow It
|
||||
|
||||
* You can star and follow the **GitHub** repository: https://github.com/abpframework/abp
|
||||
* You can follow the official **Twitter** account for news: https://twitter.com/abpframework
|
After Width: | Height: | Size: 477 KiB |
After Width: | Height: | Size: 154 KiB |
@ -1,14 +1,17 @@
|
||||
@using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Components.TenantSwitch
|
||||
@model TenantSwitchViewComponent.TenantSwitchViewModel
|
||||
<li class="nav-item">
|
||||
<a abp-button="Link" id="TenantSwitchToolbarLink" href="#">
|
||||
@if (Model.Tenant == null)
|
||||
{
|
||||
<text>@@host</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@@@Model.Tenant.Name</text>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
@if (!Model.CurrentUser.IsAuthenticated)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a abp-button="Link" id="TenantSwitchToolbarLink" href="#">
|
||||
@if (Model.Tenant == null)
|
||||
{
|
||||
<text>@@host</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@@@Model.Tenant.Name</text>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
@using Volo.Abp.UI.Navigation
|
||||
@model ApplicationMenuItem
|
||||
@{
|
||||
var elementId = string.IsNullOrEmpty(Model.ElementId) ? string.Empty : $"id=\"{Model.ElementId}\"";
|
||||
var cssClass = string.IsNullOrEmpty(Model.CssClass) ? string.Empty : Model.CssClass;
|
||||
var disabled = Model.IsDisabled ? "disabled" : string.Empty;
|
||||
}
|
||||
@if (Model.IsLeaf)
|
||||
{
|
||||
@if (Model.Url != null)
|
||||
{
|
||||
<a class="dropdown-item @cssClass @disabled" href="@(Model.Url ?? "#")" @Html.Raw(elementId)>
|
||||
@Model.DisplayName
|
||||
</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="dropdown-submenu">
|
||||
<a role="button" class="btn dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="lp-icon">
|
||||
<i class="@(Model.Icon ?? "")"></i>
|
||||
</span>
|
||||
<span class="lp-text">
|
||||
@Model.DisplayName
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
@foreach (var childMenuItem in Model.Items)
|
||||
{
|
||||
@await Html.PartialAsync("~/Themes/Basic/Components/Menu/_MenuItem.cshtml", childMenuItem)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
@page
|
||||
@using Volo.Abp.Account.Web.Pages
|
||||
@using Volo.Abp.Account.Web.Pages.Account
|
||||
@model ConsentModel
|
||||
<abp-card id="IdentityServerConsentWrapper">
|
||||
<abp-card-header>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>
|
||||
@if (Model.ClientInfo.ClientLogoUrl != null)
|
||||
{
|
||||
<img src="@Model.ClientInfo.ClientLogoUrl">
|
||||
}
|
||||
|
||||
@Model.ClientInfo.ClientName
|
||||
<small>is requesting your permission</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</abp-card-header>
|
||||
<abp-card-body>
|
||||
<form method="post" asp-page="/Consent">
|
||||
<input type="hidden" asp-for="ReturnUrl" />
|
||||
<input type="hidden" asp-for="ReturnUrlHash" />
|
||||
|
||||
<div>Uncheck the permissions you do not wish to grant.</div>
|
||||
|
||||
@if (Model.ConsentInput.IdentityScopes.Any())
|
||||
{
|
||||
<h3>Personal Information</h3>
|
||||
|
||||
<ul class="list-group">
|
||||
@for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<label asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-label">
|
||||
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-input" />
|
||||
@Model.ConsentInput.IdentityScopes[i].DisplayName
|
||||
@if (Model.ConsentInput.IdentityScopes[i].Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
|
||||
@if (Model.ConsentInput.IdentityScopes[i].Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
@Model.ConsentInput.IdentityScopes[i].Description
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (Model.ConsentInput.ApiScopes.Any())
|
||||
{
|
||||
<h3>Application Access</h3>
|
||||
|
||||
<ul class="list-group">
|
||||
@for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<label asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-label">
|
||||
<input asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-input" disabled="@Model.ConsentInput.ApiScopes[i].Required" />
|
||||
@Model.ConsentInput.ApiScopes[i].DisplayName
|
||||
@if (Model.ConsentInput.ApiScopes[i].Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
<input asp-for="@Model.ConsentInput.ApiScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
|
||||
@if (Model.ConsentInput.ApiScopes[i].Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
@Model.ConsentInput.ApiScopes[i].Description
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (Model.ClientInfo.AllowRememberConsent)
|
||||
{
|
||||
<div class="form-check">
|
||||
<label asp-for="@Model.ConsentInput.RememberConsent" class="form-check-label">
|
||||
<input asp-for="@Model.ConsentInput.RememberConsent" class="form-check-input" />
|
||||
<strong>Remember My Decision</strong>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<button name="UserDecision" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="UserDecision" value="no" class="btn">No, Do Not Allow</button>
|
||||
@if (Model.ClientInfo.ClientUrl != null)
|
||||
{
|
||||
<a class="pull-right btn btn-secondary" target="_blank" href="@Model.ClientInfo.ClientUrl">
|
||||
<strong>@Model.ClientInfo.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
||||
</form>
|
||||
</abp-card-body>
|
||||
</abp-card>
|
@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
|
||||
using Volo.Abp.UI;
|
||||
|
||||
namespace Volo.Abp.Account.Web.Pages
|
||||
{
|
||||
//TODO: Move this into the Account folder!!!
|
||||
public class ConsentModel : AbpPageModel
|
||||
{
|
||||
[HiddenInput]
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
[HiddenInput]
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string ReturnUrlHash { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public ConsentModel.ConsentInputModel ConsentInput { get; set; }
|
||||
|
||||
public ClientInfoModel ClientInfo { get; set; }
|
||||
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IClientStore _clientStore;
|
||||
private readonly IResourceStore _resourceStore;
|
||||
|
||||
public ConsentModel(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IClientStore clientStore,
|
||||
IResourceStore resourceStore)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_clientStore = clientStore;
|
||||
_resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
public virtual async Task OnGet()
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
|
||||
if (request == null)
|
||||
{
|
||||
throw new ApplicationException($"No consent request matching request: {ReturnUrl}");
|
||||
}
|
||||
|
||||
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
|
||||
if (client == null)
|
||||
{
|
||||
throw new ApplicationException($"Invalid client id: {request.ClientId}");
|
||||
}
|
||||
|
||||
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
|
||||
if (resources == null || (!resources.IdentityResources.Any() && !resources.ApiResources.Any()))
|
||||
{
|
||||
throw new ApplicationException($"No scopes matching: {request.ScopesRequested.Aggregate((x, y) => x + ", " + y)}");
|
||||
}
|
||||
|
||||
ClientInfo = new ClientInfoModel(client);
|
||||
ConsentInput = new ConsentInputModel
|
||||
{
|
||||
RememberConsent = true,
|
||||
IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, true)).ToList(),
|
||||
ApiScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, true)).ToList()
|
||||
};
|
||||
|
||||
if (resources.OfflineAccess)
|
||||
{
|
||||
ConsentInput.ApiScopes.Add(GetOfflineAccessScope(true));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> OnPost(string userDecision)
|
||||
{
|
||||
var result = await ProcessConsentAsync();
|
||||
|
||||
if (result.IsRedirect)
|
||||
{
|
||||
return Redirect(result.RedirectUri);
|
||||
}
|
||||
|
||||
if (result.HasValidationError)
|
||||
{
|
||||
//ModelState.AddModelError("", result.ValidationError);
|
||||
throw new ApplicationException("Error: " + result.ValidationError);
|
||||
}
|
||||
|
||||
throw new ApplicationException("Unknown Error!");
|
||||
}
|
||||
|
||||
protected virtual async Task<ConsentModel.ProcessConsentResult> ProcessConsentAsync()
|
||||
{
|
||||
var result = new ConsentModel.ProcessConsentResult();
|
||||
|
||||
ConsentResponse grantedConsent;
|
||||
|
||||
if (ConsentInput.UserDecision == "no")
|
||||
{
|
||||
grantedConsent = ConsentResponse.Denied;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ConsentInput.IdentityScopes.Any() || ConsentInput.ApiScopes.Any())
|
||||
{
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = ConsentInput.RememberConsent,
|
||||
ScopesConsented = ConsentInput.GetAllowedScopeNames()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this
|
||||
}
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
|
||||
if (request == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await _interaction.GrantConsentAsync(request, grantedConsent);
|
||||
|
||||
result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash?
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ConsentModel.ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
DisplayName = identity.DisplayName,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
|
||||
{
|
||||
return new ConsentModel.ScopeViewModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
Description = scope.Description,
|
||||
Emphasize = scope.Emphasize,
|
||||
Required = scope.Required,
|
||||
Checked = check || scope.Required
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual ConsentModel.ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ConsentModel.ScopeViewModel
|
||||
{
|
||||
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = "Offline Access", //TODO: Localize
|
||||
Description = "Access to your applications and resources, even when you are offline",
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
|
||||
public class ConsentInputModel
|
||||
{
|
||||
public List<ConsentModel.ScopeViewModel> IdentityScopes { get; set; }
|
||||
|
||||
public List<ConsentModel.ScopeViewModel> ApiScopes { get; set; }
|
||||
|
||||
[Required]
|
||||
public string UserDecision { get; set; }
|
||||
|
||||
public bool RememberConsent { get; set; }
|
||||
|
||||
public List<string> GetAllowedScopeNames()
|
||||
{
|
||||
return IdentityScopes.Union(ApiScopes).Where(s => s.Checked).Select(s => s.Name).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
[Required]
|
||||
[HiddenInput]
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool Checked { get; set; }
|
||||
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public bool Emphasize { get; set; }
|
||||
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
|
||||
public class ProcessConsentResult
|
||||
{
|
||||
public bool IsRedirect => RedirectUri != null;
|
||||
public string RedirectUri { get; set; }
|
||||
|
||||
public bool HasValidationError => ValidationError != null;
|
||||
public string ValidationError { get; set; }
|
||||
}
|
||||
|
||||
public class ClientInfoModel
|
||||
{
|
||||
public string ClientName { get; set; }
|
||||
|
||||
public string ClientUrl { get; set; }
|
||||
|
||||
public string ClientLogoUrl { get; set; }
|
||||
|
||||
public bool AllowRememberConsent { get; set; }
|
||||
|
||||
public ClientInfoModel(Client client)
|
||||
{
|
||||
//TODO: Automap
|
||||
ClientName = client.ClientId;
|
||||
ClientUrl = client.ClientUri;
|
||||
ClientLogoUrl = client.LogoUri;
|
||||
AllowRememberConsent = client.AllowRememberConsent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue