diff --git a/Apps/Api/Controllers/CommentsController.cs b/Apps/Api/Controllers/CommentsController.cs index aeccb36..1f18a1b 100644 --- a/Apps/Api/Controllers/CommentsController.cs +++ b/Apps/Api/Controllers/CommentsController.cs @@ -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) { diff --git a/Apps/Api/Controllers/LinksController.cs b/Apps/Api/Controllers/LinksController.cs index 7e00dcc..26e04a9 100644 --- a/Apps/Api/Controllers/LinksController.cs +++ b/Apps/Api/Controllers/LinksController.cs @@ -63,6 +63,7 @@ namespace Api.Controllers /// [HttpPost] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status201Created)] public IActionResult Create(PublishLinkCommand cmd) { diff --git a/Apps/Api/swagger.json b/Apps/Api/swagger.json index da3c7a6..415d93b 100644 --- a/Apps/Api/swagger.json +++ b/Apps/Api/swagger.json @@ -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": "" } diff --git a/Apps/Client/App.razor b/Apps/Client/App.razor index b941644..cadab7f 100644 --- a/Apps/Client/App.razor +++ b/Apps/Client/App.razor @@ -1,10 +1,16 @@ - - - - - - - Sorry, there's nothing at this address. - - - + + + + + + Oh noes + + + + + + Sorry, there's nothing at this address. + + + + \ No newline at end of file diff --git a/Apps/Client/Client.csproj b/Apps/Client/Client.csproj index 2204e7d..69601cd 100644 --- a/Apps/Client/Client.csproj +++ b/Apps/Client/Client.csproj @@ -5,6 +5,7 @@ + diff --git a/Apps/Client/HNApiClient.cs b/Apps/Client/HNApiClient.cs index 3b8fabf..ce7cfd9 100644 --- a/Apps/Client/HNApiClient.cs +++ b/Apps/Client/HNApiClient.cs @@ -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 _settings; + + public AccountsClient(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + _settings = new System.Lazy(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); + /// A server side error occurred. + public System.Threading.Tasks.Task MeAsync() + { + return MeAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A server side error occurred. + 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(); + } + } + + /// A server side error occurred. + public System.Threading.Tasks.Task RegisterAsync(RegisterViewModel cmd) + { + return RegisterAsync(cmd, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A server side error occurred. + 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(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("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(); + } + } + + /// A server side error occurred. + public System.Threading.Tasks.Task LoginAsync(LoginViewModel cmd) + { + return LoginAsync(cmd, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A server side error occurred. + public async System.Threading.Tasks.Task 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(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("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(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 + { + 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> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText, JsonSerializerSettings); + return new ObjectResponseResult(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(jsonTextReader); + return new ObjectResponseResult(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((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("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); } else + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(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("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("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); } else + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(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("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; } + } diff --git a/Apps/Client/JwtAuthStateProvider.cs b/Apps/Client/JwtAuthStateProvider.cs new file mode 100644 index 0000000..5cf159b --- /dev/null +++ b/Apps/Client/JwtAuthStateProvider.cs @@ -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 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 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 ParseClaimsFromJwt(string jwt) + { + var claims = new List(); + var payload = jwt.Split('.')[1]; + var jsonBytes = ParseBase64WithoutPadding(payload); + var keyValuePairs = JsonSerializer.Deserialize>(jsonBytes); + + keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles); + + if (roles != null) + { + if (roles.ToString().Trim().StartsWith("[")) + { + var parsedRoles = JsonSerializer.Deserialize(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); + } + } +} \ No newline at end of file diff --git a/Apps/Client/LocalStorage.cs b/Apps/Client/LocalStorage.cs new file mode 100644 index 0000000..1d86d11 --- /dev/null +++ b/Apps/Client/LocalStorage.cs @@ -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 Get(string key) + { + return await _js.InvokeAsync("getLocalItem", key); + } + } +} \ No newline at end of file diff --git a/Apps/Client/Pages/Detail.razor b/Apps/Client/Pages/Detail.razor index 947cec7..fda358a 100644 --- a/Apps/Client/Pages/Detail.razor +++ b/Apps/Client/Pages/Detail.razor @@ -32,7 +32,14 @@ else } - + + + + + + You should log in to comment! + + } @code { diff --git a/Apps/Client/Pages/Login.razor b/Apps/Client/Pages/Login.razor new file mode 100644 index 0000000..a6f2627 --- /dev/null +++ b/Apps/Client/Pages/Login.razor @@ -0,0 +1,44 @@ +@page "/login" +@inject NavigationManager Navigation +@inject AuthenticationStateProvider Authentication + + + +Login + +@if (_loginFailed) +{ + Could not log you in :'( +} + + + + + + Username + + + Password + + + Log me in! + + +@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; + } + } +} \ No newline at end of file diff --git a/Apps/Client/Pages/PublishLink.razor b/Apps/Client/Pages/PublishLink.razor index 70074fd..aa04036 100644 --- a/Apps/Client/Pages/PublishLink.razor +++ b/Apps/Client/Pages/PublishLink.razor @@ -3,6 +3,7 @@ @inject NavigationManager Navigation @inject NotificationManager Notification @using System.ComponentModel.DataAnnotations +@attribute [Authorize] Publish a new link! diff --git a/Apps/Client/Program.cs b/Apps/Client/Program.cs index d3054c4..1a5c5e4 100644 --- a/Apps/Client/Program.cs +++ b/Apps/Client/Program.cs @@ -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"); + builder.Services.AddAuthorizationCore(); + builder.Services.AddScoped(); + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["BaseUrl"]) }); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); await builder.Build().RunAsync(); diff --git a/Apps/Client/Shared/LinkItem.razor b/Apps/Client/Shared/LinkItem.razor index 65868dd..b111a45 100644 --- a/Apps/Client/Shared/LinkItem.razor +++ b/Apps/Client/Shared/LinkItem.razor @@ -2,7 +2,7 @@ @Item.Url Show - - Published at @Item.CreatedAt.DateTime.ToLongDateString() + - Published at @Item.CreatedAt.DateTime.ToLongDateString() by @Item.CreatedByUsername - 🗨 @Item.CommentsCount - 👍 @Item.UpvotesCount / 👎 @Item.DownvotesCount diff --git a/Apps/Client/Shared/NavMenu.razor b/Apps/Client/Shared/NavMenu.razor index 38a28fd..a03f1c7 100644 --- a/Apps/Client/Shared/NavMenu.razor +++ b/Apps/Client/Shared/NavMenu.razor @@ -1,4 +1,6 @@ - +@inject AuthenticationStateProvider Authentication + + Client @@ -12,11 +14,28 @@ Home - - - Publish a new link - - + + + + + Publish a new link + + + + + Sign out + + + + + + + Sign in + + + + + Counter @@ -39,4 +58,9 @@ { collapseNavMenu = !collapseNavMenu; } + + private Task Logout() + { + return ((JwtAuthStateProvider)Authentication).Logout(); + } } diff --git a/Apps/Client/_Imports.razor b/Apps/Client/_Imports.razor index c07d903..5e4d444 100644 --- a/Apps/Client/_Imports.razor +++ b/Apps/Client/_Imports.razor @@ -8,3 +8,5 @@ @using Microsoft.JSInterop @using Client @using Client.Shared +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization \ No newline at end of file diff --git a/Apps/Client/wwwroot/index.html b/Apps/Client/wwwroot/index.html index 889c804..bfe5e2e 100644 --- a/Apps/Client/wwwroot/index.html +++ b/Apps/Client/wwwroot/index.html @@ -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); + }
Sorry, there's nothing at this address.
Oh noes
You should log in to comment!
Could not log you in :'(
Show - - Published at @Item.CreatedAt.DateTime.ToLongDateString() + - Published at @Item.CreatedAt.DateTime.ToLongDateString() by @Item.CreatedByUsername - 🗨 @Item.CommentsCount - 👍 @Item.UpvotesCount / 👎 @Item.DownvotesCount