Ajout authentification sur Blazor
This commit is contained in:
parent
a05110becb
commit
5e71226edf
@ -39,6 +39,7 @@ namespace Api.Controllers
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public IActionResult Create(PublishCommentCommand cmd)
|
||||
{
|
||||
|
||||
@ -63,6 +63,7 @@ namespace Api.Controllers
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public IActionResult Create(PublishLinkCommand cmd)
|
||||
{
|
||||
|
||||
@ -179,6 +179,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
@ -237,6 +247,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<p>Oh noes</p>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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.DevServer" Version="5.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
|
||||
@ -15,6 +15,372 @@ namespace Client
|
||||
{
|
||||
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))")]
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
return;
|
||||
@ -488,6 +864,16 @@ namespace Client
|
||||
throw new ApiException<ProblemDetails>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)")]
|
||||
@ -829,6 +1247,9 @@ namespace Client
|
||||
[Newtonsoft.Json.JsonProperty("downvotesCount", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
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)]
|
||||
public int CommentsCount { get; set; }
|
||||
|
||||
[Newtonsoft.Json.JsonProperty("createdByUsername", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
public string CreatedByUsername { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
104
Apps/Client/JwtAuthStateProvider.cs
Normal file
104
Apps/Client/JwtAuthStateProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Apps/Client/LocalStorage.cs
Normal file
25
Apps/Client/LocalStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,14 @@ else
|
||||
</ul>
|
||||
}
|
||||
|
||||
<CommentForm OnSubmit="PublishComment" />
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<CommentForm OnSubmit="PublishComment" />
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<p>You should log in to comment!</p>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
44
Apps/Client/Pages/Login.razor
Normal file
44
Apps/Client/Pages/Login.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationManager Notification
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [Authorize]
|
||||
|
||||
<h1>Publish a new link!</h1>
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
@ -17,9 +18,14 @@ namespace Client
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
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<LinksClient>();
|
||||
builder.Services.AddScoped<CommentsClient>();
|
||||
builder.Services.AddScoped<AccountsClient>();
|
||||
builder.Services.AddSingleton<LocalStorage>();
|
||||
builder.Services.AddSingleton<NotificationManager>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<h2>@Item.Url</h2>
|
||||
<p>
|
||||
<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.UpvotesCount / 👎 @Item.DownvotesCount
|
||||
</p>
|
||||
|
||||
@ -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>
|
||||
<button class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -12,11 +14,28 @@
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="links/new">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Publish a new link
|
||||
</NavLink>
|
||||
</li>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="links/new">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Publish a new link
|
||||
</NavLink>
|
||||
</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">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||
@ -39,4 +58,9 @@
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
private Task Logout()
|
||||
{
|
||||
return ((JwtAuthStateProvider)Authentication).Logout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,3 +8,5 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using Client
|
||||
@using Client.Shared
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@ -26,6 +26,14 @@
|
||||
function setTitle(title) {
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
function setLocalItem(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function getLocalItem(key) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
</script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user