Ajout authentification sur Blazor
This commit is contained in:
parent
a05110becb
commit
5e71226edf
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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" />
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
<CommentForm OnSubmit="PublishComment" />
|
<CommentForm OnSubmit="PublishComment" />
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>You should log in to comment!</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@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 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>
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -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.
Loading…
x
Reference in New Issue
Block a user