Ajout authentification sur Blazor

This commit is contained in:
YuukanOO 2021-04-29 15:44:39 +02:00
parent a05110becb
commit 5e71226edf
17 changed files with 692 additions and 18 deletions

View File

@ -39,6 +39,7 @@ namespace Api.Controllers
[HttpPost] [HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status201Created)]
public IActionResult Create(PublishCommentCommand cmd) public IActionResult Create(PublishCommentCommand cmd)
{ {

View File

@ -63,6 +63,7 @@ namespace Api.Controllers
/// <returns></returns> /// <returns></returns>
[HttpPost] [HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status201Created)]
public IActionResult Create(PublishLinkCommand cmd) public IActionResult Create(PublishLinkCommand cmd)
{ {

View File

@ -179,6 +179,16 @@
} }
} }
}, },
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"201": { "201": {
"description": "" "description": ""
} }
@ -237,6 +247,16 @@
} }
} }
}, },
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"201": { "201": {
"description": "" "description": ""
} }

View File

@ -1,10 +1,16 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>Oh noes</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found> </Found>
<NotFound> <NotFound>
<LayoutView Layout="@typeof(MainLayout)"> <LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p> <p>Sorry, there's nothing at this address.</p>
</LayoutView> </LayoutView>
</NotFound> </NotFound>
</Router> </Router>
</CascadingAuthenticationState>

View File

@ -5,6 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.1" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.1" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />

View File

@ -15,6 +15,372 @@ namespace Client
{ {
using System = global::System; using System = global::System;
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class AccountsClient
{
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
public AccountsClient(System.Net.Http.HttpClient httpClient)
{
_httpClient = httpClient;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
}
private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
}
protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task MeAsync()
{
return MeAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task MeAsync(System.Threading.CancellationToken cancellationToken)
{
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append("api/accounts/me");
var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("GET");
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int)response_.StatusCode;
if (status_ == 200)
{
return;
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
}
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task RegisterAsync(RegisterViewModel cmd)
{
return RegisterAsync(cmd, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task RegisterAsync(RegisterViewModel cmd, System.Threading.CancellationToken cancellationToken)
{
if (cmd == null)
throw new System.ArgumentNullException("cmd");
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append("api/accounts");
var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(cmd, _settings.Value));
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_;
request_.Method = new System.Net.Http.HttpMethod("POST");
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int)response_.StatusCode;
if (status_ == 400)
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 204)
{
return;
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
}
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<string> LoginAsync(LoginViewModel cmd)
{
return LoginAsync(cmd, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<string> LoginAsync(LoginViewModel cmd, System.Threading.CancellationToken cancellationToken)
{
if (cmd == null)
throw new System.ArgumentNullException("cmd");
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append("api/accounts/token");
var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(cmd, _settings.Value));
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_;
request_.Method = new System.Net.Http.HttpMethod("POST");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int)response_.StatusCode;
if (status_ == 400)
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<string>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
return objectResponse_.Object;
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
}
protected struct ObjectResponseResult<T>
{
public ObjectResponseResult(T responseObject, string responseText)
{
this.Object = responseObject;
this.Text = responseText;
}
public T Object { get; }
public string Text { get; }
}
public bool ReadResponseAsString { get; set; }
protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
{
if (response == null || response.Content == null)
{
return new ObjectResponseResult<T>(default(T), string.Empty);
}
if (ReadResponseAsString)
{
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
try
{
var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
return new ObjectResponseResult<T>(typedBody, responseText);
}
catch (Newtonsoft.Json.JsonException exception)
{
var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
}
}
else
{
try
{
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var streamReader = new System.IO.StreamReader(responseStream))
using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
{
var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
var typedBody = serializer.Deserialize<T>(jsonTextReader);
return new ObjectResponseResult<T>(typedBody, string.Empty);
}
}
catch (Newtonsoft.Json.JsonException exception)
{
var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
}
}
}
private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value == null)
{
return "";
}
if (value is System.Enum)
{
var name = System.Enum.GetName(value.GetType(), value);
if (name != null)
{
var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
if (field != null)
{
var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute))
as System.Runtime.Serialization.EnumMemberAttribute;
if (attribute != null)
{
return attribute.Value != null ? attribute.Value : name;
}
}
var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
return converted == null ? string.Empty : converted;
}
}
else if (value is bool)
{
return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
}
else if (value is byte[])
{
return System.Convert.ToBase64String((byte[]) value);
}
else if (value.GetType().IsArray)
{
var array = System.Linq.Enumerable.OfType<object>((System.Array) value);
return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
}
var result = System.Convert.ToString(value, cultureInfo);
return result == null ? "" : result;
}
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v12.0.0.0))")] [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class CommentsClient public partial class CommentsClient
{ {
@ -200,6 +566,16 @@ namespace Client
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
} }
else else
if (status_ == 401)
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 201) if (status_ == 201)
{ {
return; return;
@ -488,6 +864,16 @@ namespace Client
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
} }
else else
if (status_ == 401)
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 201) if (status_ == 201)
{ {
return; return;
@ -812,6 +1198,38 @@ namespace Client
} }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class RegisterViewModel
{
[Newtonsoft.Json.JsonProperty("username", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required]
public string Username { get; set; }
[Newtonsoft.Json.JsonProperty("password", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required]
public string Password { get; set; }
[Newtonsoft.Json.JsonProperty("confirmPassword", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required]
public string ConfirmPassword { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class LoginViewModel
{
[Newtonsoft.Json.JsonProperty("username", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required]
public string Username { get; set; }
[Newtonsoft.Json.JsonProperty("password", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required]
public string Password { get; set; }
} }
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")]
@ -829,6 +1247,9 @@ namespace Client
[Newtonsoft.Json.JsonProperty("downvotesCount", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [Newtonsoft.Json.JsonProperty("downvotesCount", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int DownvotesCount { get; set; } public int DownvotesCount { get; set; }
[Newtonsoft.Json.JsonProperty("createdByUsername", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string CreatedByUsername { get; set; }
} }
@ -867,6 +1288,9 @@ namespace Client
[Newtonsoft.Json.JsonProperty("commentsCount", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [Newtonsoft.Json.JsonProperty("commentsCount", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int CommentsCount { get; set; } public int CommentsCount { get; set; }
[Newtonsoft.Json.JsonProperty("createdByUsername", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string CreatedByUsername { get; set; }
} }

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
namespace Client
{
public class JwtAuthStateProvider : AuthenticationStateProvider
{
private readonly AccountsClient _accounts;
private readonly LocalStorage _storage;
private readonly HttpClient _http;
public JwtAuthStateProvider(AccountsClient accounts, LocalStorage storage, HttpClient http)
{
_accounts = accounts;
_storage = storage;
_http = http;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return AuthenticationStateFromCurrentToken();
}
public async Task TryLoginAsync(LoginViewModel cmd)
{
var token = await _accounts.LoginAsync(cmd);
await _storage.Save("token", token);
NotifyAuthenticationStateChanged(AuthenticationStateFromCurrentToken());
}
public async Task Logout()
{
await _storage.Save("token", string.Empty);
NotifyAuthenticationStateChanged(AuthenticationStateFromCurrentToken());
}
private async Task<AuthenticationState> AuthenticationStateFromCurrentToken()
{
var token = await _storage.Get("token");
if (string.IsNullOrWhiteSpace(token))
{
_http.DefaultRequestHeaders.Authorization = null;
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var claims = ParseClaimsFromJwt(token);
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt", "unique_name", null));
return new AuthenticationState(principal);
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
if (roles != null)
{
if (roles.ToString().Trim().StartsWith("["))
{
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());
foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
}
keyValuePairs.Remove(ClaimTypes.Role);
}
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
return claims;
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
}

View File

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Client
{
public class LocalStorage
{
private readonly IJSRuntime _js;
public LocalStorage(IJSRuntime js)
{
_js = js;
}
public async Task Save(string key, string value)
{
await _js.InvokeVoidAsync("setLocalItem", key, value);
}
public async Task<string> Get(string key)
{
return await _js.InvokeAsync<string>("getLocalItem", key);
}
}
}

View File

@ -32,7 +32,14 @@ else
</ul> </ul>
} }
<AuthorizeView>
<Authorized>
<CommentForm OnSubmit="PublishComment" /> <CommentForm OnSubmit="PublishComment" />
</Authorized>
<NotAuthorized>
<p>You should log in to comment!</p>
</NotAuthorized>
</AuthorizeView>
} }
@code { @code {

View File

@ -0,0 +1,44 @@
@page "/login"
@inject NavigationManager Navigation
@inject AuthenticationStateProvider Authentication
<Title Value="Login" />
<h1>Login</h1>
@if (_loginFailed)
{
<p>Could not log you in :'(</p>
}
<EditForm Model="_model" OnValidSubmit="TryLogin">
<DataAnnotationsValidator />
<ValidationSummary />
<label for="username">Username</label>
<InputText id="username" @bind-Value="_model.Username" />
<label for="password">Password</label>
<InputText id="password" type="password" @bind-Value="_model.Password" />
<button type="submit">Log me in!</button>
</EditForm>
@code {
private LoginViewModel _model = new LoginViewModel();
private bool _loginFailed;
private async Task TryLogin()
{
_loginFailed = false;
try
{
await ((JwtAuthStateProvider)Authentication).TryLoginAsync(_model);
Navigation.NavigateTo("/");
}
catch
{
_loginFailed = true;
}
}
}

View File

@ -3,6 +3,7 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject NotificationManager Notification @inject NotificationManager Notification
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@attribute [Authorize]
<h1>Publish a new link!</h1> <h1>Publish a new link!</h1>

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Components.Authorization;
namespace Client namespace Client
{ {
@ -17,9 +18,14 @@ namespace Client
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, JwtAuthStateProvider>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["BaseUrl"]) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["BaseUrl"]) });
builder.Services.AddScoped<LinksClient>(); builder.Services.AddScoped<LinksClient>();
builder.Services.AddScoped<CommentsClient>(); builder.Services.AddScoped<CommentsClient>();
builder.Services.AddScoped<AccountsClient>();
builder.Services.AddSingleton<LocalStorage>();
builder.Services.AddSingleton<NotificationManager>(); builder.Services.AddSingleton<NotificationManager>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@ -2,7 +2,7 @@
<h2>@Item.Url</h2> <h2>@Item.Url</h2>
<p> <p>
<NavLink href="@($"/links/{Item.Id}")">Show</NavLink> <NavLink href="@($"/links/{Item.Id}")">Show</NavLink>
- Published at @Item.CreatedAt.DateTime.ToLongDateString() - Published at @Item.CreatedAt.DateTime.ToLongDateString() by @Item.CreatedByUsername
- 🗨 @Item.CommentsCount - 🗨 @Item.CommentsCount
- 👍 @Item.UpvotesCount / 👎 @Item.DownvotesCount - 👍 @Item.UpvotesCount / 👎 @Item.DownvotesCount
</p> </p>

View File

@ -1,4 +1,6 @@
<div class="top-row pl-4 navbar navbar-dark"> @inject AuthenticationStateProvider Authentication
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">Client</a> <a class="navbar-brand" href="">Client</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu"> <button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -12,11 +14,28 @@
<span class="oi oi-home" aria-hidden="true"></span> Home <span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink> </NavLink>
</li> </li>
<AuthorizeView>
<Authorized>
<li class="nav-item px-3"> <li class="nav-item px-3">
<NavLink class="nav-link" href="links/new"> <NavLink class="nav-link" href="links/new">
<span class="oi oi-plus" aria-hidden="true"></span> Publish a new link <span class="oi oi-plus" aria-hidden="true"></span> Publish a new link
</NavLink> </NavLink>
</li> </li>
<li class="nav-item px-3">
<button @onclick="Logout">
Sign out
</button>
</li>
</Authorized>
<NotAuthorized>
<li class="nav-item px-3">
<NavLink class="nav-link" href="login">
<span class="oi oi-plus" aria-hidden="true"></span> Sign in
</NavLink>
</li>
</NotAuthorized>
</AuthorizeView>
<li class="nav-item px-3"> <li class="nav-item px-3">
<NavLink class="nav-link" href="counter"> <NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter <span class="oi oi-plus" aria-hidden="true"></span> Counter
@ -39,4 +58,9 @@
{ {
collapseNavMenu = !collapseNavMenu; collapseNavMenu = !collapseNavMenu;
} }
private Task Logout()
{
return ((JwtAuthStateProvider)Authentication).Logout();
}
} }

View File

@ -8,3 +8,5 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Client @using Client
@using Client.Shared @using Client.Shared
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

View File

@ -26,6 +26,14 @@
function setTitle(title) { function setTitle(title) {
document.title = title; document.title = title;
} }
function setLocalItem(key, value) {
localStorage.setItem(key, value);
}
function getLocalItem(key) {
return localStorage.getItem(key);
}
</script> </script>
<script src="_framework/blazor.webassembly.js"></script> <script src="_framework/blazor.webassembly.js"></script>
</body> </body>

Binary file not shown.