Skip to content

Commit 9827d47

Browse files
committed
Update 10-publish-and-deploy
1 parent e93c900 commit 9827d47

31 files changed

+463
-247
lines changed

docs/10-publish-and-deploy.md

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,109 @@ Right-click on the Server project in the solution and select Publish. The ASP.NE
1616

1717
![Publish from VS](https://user-images.githubusercontent.com/1874516/51885818-2501ac80-2385-11e9-8025-4d1477083a8d.png)
1818

19-
In the "Pick a publish target" dialog:
20-
- Select "App Service"
21-
- Select the "Create New" option
22-
- Select "Create Profile" in the button drop down before clicking it
19+
In the Publish wizard, select "Azure" and then select Next:
2320

24-
![Pick a publish target](https://user-images.githubusercontent.com/1874516/51885912-7f027200-2385-11e9-8707-0e2f82b543fd.png)
21+
![Pick a publish target](https://user-images.githubusercontent.com/1874516/78459197-31118a00-766c-11ea-9d41-470ea772e34f.png)
22+
23+
Select "Azure App Service (Windows)" for the specific target, and then select Next:
24+
25+
![Publish to App Service](https://user-images.githubusercontent.com/1874516/78459246-8baae600-766c-11ea-9600-b01e168bf71a.png)
26+
27+
Wait for your subscriptions to load, and then select the subscription to use for the Azure App Service. After selecting your subscription, select "Create a new Azure App Service..." at the bottom of the dialog.
28+
29+
![Select existing or create new](https://user-images.githubusercontent.com/1874516/78459794-7041da00-7670-11ea-96ab-d103b8f21739.png)
30+
31+
In the "App Service: Create New" dialog:
2532

26-
In the "Create App Service" dialog:
2733
- Make sure that the correct account that you want to use for your new Azure App Service is selected in the account drop down in the upper right
2834
- Pick a unique name for your app (which becomes part of the app's default URL)
2935
- Select the Azure subscription you want to use along with the Resource Group and Hosting Plan
3036
- Resource groups are a convenient way to group related resources on Azure, so consider creating one specific to the pizza store app.
31-
- For the hosting plan, using a free plan is fine.
37+
- For the hosting plan, you'll need to select a Basic tier hosting plan or higher.
3238

33-
![Create App Service](https://user-images.githubusercontent.com/1874516/51886115-4e6f0800-2386-11e9-9da1-82cc910aad3b.png)
39+
![Create new App Service](https://user-images.githubusercontent.com/1874516/78463095-e0ab2400-768d-11ea-8ec3-f8885368118d.png)
3440

35-
At this point you could also create a production database for your app. Since the app uses SQLite and deploys its own database, creating a database isn't necessary, but for a real app it would be.
41+
Click Create to create the app service. This may take a couple of minutes.
3642

37-
Click Create to create the App Service. This may take a couple of minutes. Once the App Service is created you should see your publish profile in the Publish page:
43+
Once the app service has been created, make sure it is selected and then click Finish in the Publish dialog
3844

39-
![Publish profile](https://user-images.githubusercontent.com/1874516/51886256-ee2c9600-2386-11e9-9da7-d80d2500b0ea.png)
45+
![Finish Publish](https://user-images.githubusercontent.com/1874516/78459868-0d047780-7671-11ea-87d5-0a72ca9e5d36.png)
4046

41-
Before we publish, we first we need to do some configuration for our app to run. The pizza store app requires a Twitter app consumer key and secret to handle authentication. During development, these values are stored in `appsettings.Development.json`. We need to configure these values in the App Service environment, or the app will fail to run.
47+
Once the App Service is created you should see your publish profile in the Publish page:
4248

43-
To register your app service as a Twitter app, you'll need to use the [Twitter Developer Console](https://developer.twitter.com/apps) and signup up for a Twitter developer account. Or you can use dummy values for now, which will break authentication, but at least allow the app to run.
49+
![Publish profile](https://user-images.githubusercontent.com/1874516/78460244-0e836f00-7674-11ea-975a-f582d6af9942.png)
4450

45-
Click on the "Edit App Service Settings" link. Add two settings: `Authentication:Twitter:ConsumerKey`, and `Authentication:Twitter:ConsumerSecret`. Specify the correct values for your Twitter app, or just put in some dummy strings if you don't care about getting authentication working. Click OK to save the app settings.
51+
At this point you could create a production database for your app. Since the app uses SQLite and deploys its own database, creating a database isn't necessary, but for a real app it would be.
4652

47-
![Add app settings](https://user-images.githubusercontent.com/1874516/51886491-fc2ee680-2387-11e9-9c16-5f1fc47365fa.png)
53+
If we publish the app at this point, it will return a server error and fail to start. This is because we first need to configure a signing key for IdentityServer. During development, we used a development key (see *BlazingPizza.Server/appsettings.Development.json*), but in production we need to configure an actual certificate for issuing tokens. We'll do that using Azure Key Vault.
4854

49-
You're ready to publish! Click Publish.
55+
## Setup a signing certificate with Azure Key Vault
56+
57+
You can create a signing certificate using an existing key vault, or create a new one.
58+
59+
To create a new key vault:
60+
61+
- Sign in to the Azure portal at https://portal.azure.com.
62+
- From the Azure portal menu, or from the Home page, select **Create a resource**.
63+
- In the Search box, enter **Key Vault**.
64+
- From the results list, choose **Key Vault**.
65+
- On the Key Vault section, choose **Create**.
66+
- On the **Create key vault** section, provide the following information:
67+
- **Name**: A unique name is required.
68+
- **Subscription**: Choose your subscription.
69+
- **Resource Group**: Choose the resource group for your key vault.
70+
- In the **Location** pull-down menu, choose a location.
71+
- Leave the other options to their defaults.
72+
- After providing the information above, select **Create**.
73+
74+
Browse to your key vault in the Azure portal and select **Certificates**. Select **Generate/Import** to create a new certificate.
75+
76+
![Generate key vault certificate](https://user-images.githubusercontent.com/1874516/78463378-ba3ab800-7690-11ea-9744-6850c2d1a7e6.png)
77+
78+
Generate a self-signed certificate with a name of your choice and a matching subject name (prefixed with "CN=") and the select **Create**.
5079

51-
![image](https://user-images.githubusercontent.com/1874516/51886932-a52a1100-2389-11e9-8b58-6ea3ae5a4291.png)
80+
![Create certificate](https://user-images.githubusercontent.com/1874516/78463413-17cf0480-7691-11ea-91dc-343cdea5aa79.png)
5281

82+
Browse to your app service in the portal, select **TLS/SSL Settings**. Select the **Private Key Certificates (.pfx)** tab and then select **Import Key Vault Certificate**.
83+
84+
![Import key vault certificate](https://user-images.githubusercontent.com/1874516/78463445-890eb780-7691-11ea-949a-d7dd38b43550.png)
85+
86+
Select the certificate you previously created to import it into the app service.
87+
88+
![Select certificate](https://user-images.githubusercontent.com/1874516/78463454-ae9bc100-7691-11ea-9ca4-64d27582f699.png)
89+
90+
Select the imported certificate and copy its thumbprint.
91+
92+
![Copy certificate thumbprint](https://user-images.githubusercontent.com/1874516/78463487-1520df00-7692-11ea-93ae-697406bfdd86.png)
93+
94+
Select **Configuration** in the left nav for the app service. Add the `WEBSITE_LOAD_CERTIFICATES` application setting with its value set to the certificate thumbprint you copied previously. This setting will make the certificate available to your app using the Windows certificate store.
95+
96+
![Load certificates setting](https://user-images.githubusercontent.com/1874516/78463547-e8b99280-7692-11ea-9d02-394b20c653cd.png)
97+
98+
Now update **appsettings.json* in the server project configure the app to use the certificate in production.
99+
100+
```json
101+
"IdentityServer": {
102+
"Key": {
103+
"Type": "Store",
104+
"StoreName": "My",
105+
"StoreLocation": "CurrentUser",
106+
"Name": "CN=BlazingPizzaCertificate"
107+
},
108+
"Clients": {
109+
"BlazingPizza.Client": {
110+
"Profile": "IdentityServerSPA"
111+
}
112+
}
113+
}
114+
```
115+
116+
You're ready to publish! Click Publish.
53117

54118
Publishing the app may take a few minutes. Once the app has finished deploying it should automatically load in the browser.
55119

56-
![Published app](https://user-images.githubusercontent.com/1874516/51886593-5a5bc980-2388-11e9-9329-7e015901e45d.png)
120+
![Published app](https://user-images.githubusercontent.com/1874516/78463636-09ceb300-7694-11ea-9d3c-57b52b982186.png)
57121

58122
Congrats!
123+
124+
Once your done showing off your completed Blazor app to your friend, be sure to clean up any Azure resources that you no longer wish to maintain.

src/BlazingPizza.Client/App.razor

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
<Found>
44
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
55
<NotAuthorized>
6-
<div class="main">
7-
<h2>You're signed out</h2>
8-
<p>To continue, please sign in.</p>
9-
<a class="btn btn-danger" href="user/signin">Sign in</a>
10-
</div>
6+
<RedirectToLogin />
117
</NotAuthorized>
128
<Authorizing>
139
<div class="main">Please wait...</div>
@@ -16,7 +12,7 @@
1612
</Found>
1713
<NotFound>
1814
<LayoutView Layout="typeof(MainLayout)">
19-
<div class="main">Page not found</div>
15+
<div class="main">Sorry, there's nothing at this address.</div>
2016
</LayoutView>
2117
</NotFound>
2218
</Router>

src/BlazingPizza.Client/BlazingPizza.Client.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
<ItemGroup>
1010
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(BlazorVersion)" />
1111
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="$(BlazorVersion)" PrivateAssets="all" />
12-
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="$(BlazorVersion)" />
13-
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="$(AspNetCoreVersion)" />
12+
<PackageReference Include="System.Net.Http.Json" Version="$(SystemNetHttpJsonVersion)" />
13+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(BlazorVersion)" />
1414
</ItemGroup>
1515

1616
<ItemGroup>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@page "/authentication/{action}"
2+
@inject OrderState OrderState
3+
@inject NavigationManager NavigationManager
4+
5+
<RemoteAuthenticatorViewCore
6+
TAuthenticationState="PizzaAuthenticationState"
7+
AuthenticationState="RemoteAuthenticationState"
8+
OnLogInSucceeded="RestorePizza"
9+
Action="@Action" />
10+
11+
@code{
12+
[Parameter] public string Action { get; set; }
13+
14+
public PizzaAuthenticationState RemoteAuthenticationState { get; set; } = new PizzaAuthenticationState();
15+
16+
protected override void OnInitialized()
17+
{
18+
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
19+
{
20+
// Preserve the current order so that we don't loose it
21+
RemoteAuthenticationState.Order = OrderState.Order;
22+
}
23+
}
24+
25+
private void RestorePizza(PizzaAuthenticationState pizzaState)
26+
{
27+
if (pizzaState.Order != null)
28+
{
29+
OrderState.ReplaceOrder(pizzaState.Order);
30+
}
31+
}
32+
}
Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,80 @@
11
@page "/checkout"
2+
@attribute [Authorize]
23
@inject OrderState OrderState
34
@inject HttpClient HttpClient
45
@inject NavigationManager NavigationManager
6+
@inject IAccessTokenProvider TokenProvider
57
@inject IJSRuntime JSRuntime
68

79
<div class="main">
8-
<AuthorizeView Context="authContext">
9-
<NotAuthorized>
10-
<h2>Redirecting you...</h2>
11-
</NotAuthorized>
12-
<Authorized>
13-
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
14-
<div class="checkout-cols">
15-
<div class="checkout-order-details">
16-
<h4>Review order</h4>
17-
<OrderReview Order="OrderState.Order" />
18-
</div>
10+
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
11+
<div class="checkout-cols">
12+
<div class="checkout-order-details">
13+
<h4>Review order</h4>
14+
<OrderReview Order="OrderState.Order" />
15+
</div>
1916

20-
<div class="checkout-delivery-address">
21-
<h4>Deliver to...</h4>
22-
<AddressEditor Address="OrderState.Order.DeliveryAddress" />
23-
</div>
24-
</div>
17+
<div class="checkout-delivery-address">
18+
<h4>Deliver to...</h4>
19+
<AddressEditor Address="OrderState.Order.DeliveryAddress" />
20+
</div>
21+
</div>
2522

26-
<button type="submit" class="checkout-button btn btn-warning">
27-
Place order
28-
</button>
23+
<button type="submit" class="checkout-button btn btn-warning" disabled="@isSubmitting">
24+
Place order
25+
</button>
2926

30-
<DataAnnotationsValidator />
31-
</EditForm>
32-
</Authorized>
33-
</AuthorizeView>
27+
<DataAnnotationsValidator />
28+
</EditForm>
3429
</div>
3530

3631
@code {
37-
[CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; }
32+
bool isSubmitting;
3833

39-
protected override async Task OnInitializedAsync()
34+
protected override void OnInitialized()
4035
{
41-
var authState = await AuthenticationStateTask;
42-
if (!authState.User.Identity.IsAuthenticated)
43-
{
44-
// The server won't accept orders from unauthenticated users, so avoid
45-
// an error by making them log in at this point
46-
await LocalStorage.SetAsync(JSRuntime, "currentorder", OrderState.Order);
47-
NavigationManager.NavigateTo("user/signin?redirectUri=/checkout", true);
48-
}
36+
// In the background, ask if they want to be notified about order updates
37+
_ = RequestNotificationSubscriptionAsync();
38+
}
4939

50-
// Try to recover any temporary saved order
51-
if (!OrderState.Order.Pizzas.Any())
40+
async Task RequestNotificationSubscriptionAsync()
41+
{
42+
var tokenResult = await TokenProvider.RequestAccessToken();
43+
if (tokenResult.TryGetToken(out var accessToken))
5244
{
53-
var savedOrder = await LocalStorage.GetAsync<Order>(JSRuntime, "currentorder");
54-
if (savedOrder != null)
45+
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
46+
if (subscription != null)
5547
{
56-
OrderState.ReplaceOrder(savedOrder);
57-
await LocalStorage.DeleteAsync(JSRuntime, "currentorder");
58-
}
59-
else
60-
{
61-
// There's nothing check out - go to home
62-
NavigationManager.NavigateTo("");
48+
var request = new HttpRequestMessage(HttpMethod.Put, "notifications/subscribe");
49+
request.Content = JsonContent.Create(subscription);
50+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
51+
await HttpClient.SendAsync(request);
6352
}
6453
}
65-
66-
// In the background, ask if they want to be notified about order updates
67-
_ = RequestNotificationSubscriptionAsync();
54+
else
55+
{
56+
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
57+
}
6858
}
6959

7060
async Task PlaceOrder()
7161
{
72-
var newOrderId = await HttpClient.PostJsonAsync<int>("orders", OrderState.Order);
73-
OrderState.ResetOrder();
74-
NavigationManager.NavigateTo($"myorders/{newOrderId}");
75-
}
62+
isSubmitting = true;
7663

77-
async Task RequestNotificationSubscriptionAsync()
78-
{
79-
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
80-
if (subscription != null)
64+
var tokenResult = await TokenProvider.RequestAccessToken();
65+
if (tokenResult.TryGetToken(out var accessToken))
66+
{
67+
var request = new HttpRequestMessage(HttpMethod.Post, "orders");
68+
request.Content = JsonContent.Create(OrderState.Order);
69+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
70+
var response = await HttpClient.SendAsync(request);
71+
var newOrderId = await response.Content.ReadFromJsonAsync<int>();
72+
OrderState.ResetOrder();
73+
NavigationManager.NavigateTo($"myorders/{newOrderId}");
74+
}
75+
else
8176
{
82-
await HttpClient.PutJsonAsync<object>("notifications/subscribe", subscription);
77+
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
8378
}
8479
}
8580
}

src/BlazingPizza.Client/Pages/Index.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161

6262
protected override async Task OnInitializedAsync()
6363
{
64-
specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("specials");
64+
specials = await HttpClient.GetFromJsonAsync<List<PizzaSpecial>>("specials");
6565
}
6666

6767
async Task RemovePizza(Pizza configuredPizza)

src/BlazingPizza.Client/Pages/MyOrders.razor

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@page "/myorders"
22
@attribute [Authorize]
33
@inject HttpClient HttpClient
4+
@inject NavigationManager NavigationManager
5+
@inject IAccessTokenProvider TokenProvider
46

57
<div class="main">
68
<TemplatedList Loader="@LoadOrders" ListGroupClass="orders-list">
@@ -32,6 +34,19 @@
3234
@code {
3335
async Task<List<OrderWithStatus>> LoadOrders()
3436
{
35-
return await HttpClient.GetJsonAsync<List<OrderWithStatus>>("orders");
37+
var ordersWithStatus = new List<OrderWithStatus>();
38+
var tokenResult = await TokenProvider.RequestAccessToken();
39+
if (tokenResult.TryGetToken(out var accessToken))
40+
{
41+
var request = new HttpRequestMessage(HttpMethod.Get, "orders");
42+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
43+
var response = await HttpClient.SendAsync(request);
44+
ordersWithStatus = await response.Content.ReadFromJsonAsync<List<OrderWithStatus>>();
45+
}
46+
else
47+
{
48+
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
49+
}
50+
return ordersWithStatus;
3651
}
3752
}

0 commit comments

Comments
 (0)