diff --git a/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountProfileBackgroundRequest.cs b/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountProfileBackgroundRequest.cs index 4bcb781..6f8ce7b 100644 --- a/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountProfileBackgroundRequest.cs +++ b/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountProfileBackgroundRequest.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace WatchIt.Common.Model.Accounts; @@ -6,6 +7,7 @@ public class AccountProfileBackgroundRequest { #region PROPERTIES + [JsonPropertyName("id")] public required Guid Id { get; set; } #endregion diff --git a/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountUsernameRequest.cs b/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountUsernameRequest.cs new file mode 100644 index 0000000..5219594 --- /dev/null +++ b/WatchIt.Common/WatchIt.Common.Model/Accounts/AccountUsernameRequest.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace WatchIt.Common.Model.Accounts; + +public class AccountUsernameRequest +{ + #region PROPERTIES + + [JsonPropertyName("new_username")] + public string NewUsername { get; set; } + + [JsonPropertyName("password")] + public string Password { get; set; } + + #endregion + + + + #region PUBLIC METHODS + + public void UpdateAccount(Database.Model.Account.Account account) + { + account.Username = NewUsername; + } + + #endregion +} \ No newline at end of file diff --git a/WatchIt.WebAPI/WatchIt.WebAPI.Controllers/AccountsController.cs b/WatchIt.WebAPI/WatchIt.WebAPI.Controllers/AccountsController.cs index 714d4e4..1b93fe9 100644 --- a/WatchIt.WebAPI/WatchIt.WebAPI.Controllers/AccountsController.cs +++ b/WatchIt.WebAPI/WatchIt.WebAPI.Controllers/AccountsController.cs @@ -92,6 +92,8 @@ public class AccountsController(IAccountsControllerService accountsControllerSer #endregion + #region Info + [HttpGet("{id}/info")] [AllowAnonymous] [ProducesResponseType(typeof(AccountResponse), StatusCodes.Status200OK)] @@ -105,6 +107,15 @@ public class AccountsController(IAccountsControllerService accountsControllerSer [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PutAccountProfileInfo([FromBody]AccountProfileInfoRequest data) => await accountsControllerService.PutAccountProfileInfo(data); + [HttpPatch("username")] + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task PatchAccountUsername([FromBody]AccountUsernameRequest data) => await accountsControllerService.PatchAccountUsername(data); + + #endregion + [HttpGet("{id}/movies")] [AllowAnonymous] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/AccountsControllerService.cs b/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/AccountsControllerService.cs index e4a6879..67d41c1 100644 --- a/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/AccountsControllerService.cs +++ b/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/AccountsControllerService.cs @@ -240,7 +240,9 @@ public class AccountsControllerService( } #endregion - + + #region Info + public async Task GetAccountInfo(long id) { Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id); @@ -266,6 +268,23 @@ public class AccountsControllerService( return RequestResult.Ok(); } + + public async Task PatchAccountUsername(AccountUsernameRequest data) + { + Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId()); + + if (!ComputeHash(data.Password, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password)) + { + return RequestResult.Unauthorized(); + } + + data.UpdateAccount(account); + await database.SaveChangesAsync(); + + return RequestResult.Ok(); + } + + #endregion public async Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query) { diff --git a/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/IAccountsControllerService.cs b/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/IAccountsControllerService.cs index 5213ab4..481603e 100644 --- a/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/IAccountsControllerService.cs +++ b/WatchIt.WebAPI/WatchIt.WebAPI.Services/WatchIt.WebAPI.Services.Controllers/WatchIt.WebAPI.Services.Controllers.Accounts/IAccountsControllerService.cs @@ -21,6 +21,7 @@ public interface IAccountsControllerService Task DeleteAccountProfileBackground(); Task GetAccountInfo(long id); Task PutAccountProfileInfo(AccountProfileInfoRequest data); + Task PatchAccountUsername(AccountUsernameRequest data); Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query); Task GetAccountRatedSeries(long id, SeriesRatedQueryParameters query); Task GetAccountRatedPersons(long id, PersonRatedQueryParameters query); diff --git a/WatchIt.WebAPI/WatchIt.WebAPI.Validators/Accounts/AccountUsernameRequestValidator.cs b/WatchIt.WebAPI/WatchIt.WebAPI.Validators/Accounts/AccountUsernameRequestValidator.cs new file mode 100644 index 0000000..6c0ad30 --- /dev/null +++ b/WatchIt.WebAPI/WatchIt.WebAPI.Validators/Accounts/AccountUsernameRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using WatchIt.Common.Model.Accounts; +using WatchIt.Database; + +namespace WatchIt.WebAPI.Validators.Accounts; + +public class AccountUsernameRequestValidator : AbstractValidator +{ + public AccountUsernameRequestValidator(DatabaseContext database) + { + RuleFor(x => x.NewUsername).MinimumLength(5) + .MaximumLength(50) + .CannotBeIn(database.Accounts, x => x.Username) + .WithMessage("Username is already used"); + } +} \ No newline at end of file diff --git a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/AccountsClientService.cs b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/AccountsClientService.cs index 1dab7b6..d73728e 100644 --- a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/AccountsClientService.cs +++ b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/AccountsClientService.cs @@ -176,6 +176,21 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig .ExecuteAction(); } + public async Task PatchAccountUsername(AccountUsernameRequest data, Action? successAction = null, Action>? badRequestAction = null, Action? unauthorizedAction = null) + { + string url = GetUrl(EndpointsConfiguration.Accounts.PatchAccountUsername); + HttpRequest request = new HttpRequest(HttpMethodType.Patch, url) + { + Body = data, + }; + + HttpResponse response = await httpClientService.SendRequestAsync(request); + response.RegisterActionFor2XXSuccess(successAction) + .RegisterActionFor400BadRequest(badRequestAction) + .RegisterActionFor401Unauthorized(unauthorizedAction) + .ExecuteAction(); + } + public async Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query, Action>? successAction = null, Action? notFoundAction = null) { string url = GetUrl(EndpointsConfiguration.Accounts.GetAccountRatedMovies, id); diff --git a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/IAccountsClientService.cs b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/IAccountsClientService.cs index 48110fe..4a86d8f 100644 --- a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/IAccountsClientService.cs +++ b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Client/Accounts/IAccountsClientService.cs @@ -20,6 +20,7 @@ public interface IAccountsClientService Task DeleteAccountProfileBackground(Action? successAction = null, Action? unauthorizedAction = null); Task GetAccountInfo(long id, Action? successAction = null, Action? notFoundAction = null); Task PutAccountProfileInfo(AccountProfileInfoRequest data, Action? successAction = null, Action>? badRequestAction = null, Action? unauthorizedAction = null); + Task PatchAccountUsername(AccountUsernameRequest data, Action? successAction = null, Action>? badRequestAction = null, Action? unauthorizedAction = null); Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query, Action>? successAction = null, Action? notFoundAction = null); Task GetAccountRatedSeries(long id, SeriesRatedQueryParameters query, Action>? successAction = null, Action? notFoundAction = null); Task GetAccountRatedPersons(long id, PersonRatedQueryParameters query, Action>? successAction = null, Action? notFoundAction = null); diff --git a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Configuration/Model/Accounts.cs b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Configuration/Model/Accounts.cs index f035f6d..04e8c7f 100644 --- a/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Configuration/Model/Accounts.cs +++ b/WatchIt.Website/WatchIt.Website.Services/WatchIt.Website.Services.Configuration/Model/Accounts.cs @@ -15,6 +15,7 @@ public class Accounts public string DeleteAccountProfileBackground { get; set; } public string GetAccountInfo { get; set; } public string PutAccountProfileInfo { get; set; } + public string PatchAccountUsername { get; set; } public string GetAccountRatedMovies { get; set; } public string GetAccountRatedSeries { get; set; } public string GetAccountRatedPersons { get; set; } diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/AccountEditHeaderPanelComponent.razor b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/AccountEditHeaderPanelComponent.razor new file mode 100644 index 0000000..a051670 --- /dev/null +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/AccountEditHeaderPanelComponent.razor @@ -0,0 +1,5 @@ +
+
+

Account settings

+
+
\ No newline at end of file diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor new file mode 100644 index 0000000..62a6a9b --- /dev/null +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor @@ -0,0 +1,46 @@ +
+
+

Change username

+ @if (_data is not null) + { + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ @if (!string.IsNullOrWhiteSpace(_error)) + { + @(_error) + } + else if (_saved) + { + New username saved! + } +
+
+ +
+
+
+
+ } + else + { + + } +
+
\ No newline at end of file diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor.cs b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor.cs new file mode 100644 index 0000000..0a797fd --- /dev/null +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/NewUsernamePanelComponent.razor.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Components; +using WatchIt.Common.Model.Accounts; +using WatchIt.Website.Services.Authentication; +using WatchIt.Website.Services.Client.Accounts; + +namespace WatchIt.Website.Components.Pages.UserEditPage.Panels; + +public partial class NewUsernamePanelComponent : ComponentBase +{ + #region SERVICES + + [Inject] private IAuthenticationService AuthenticationService { get; set; } = default!; + [Inject] private IAccountsClientService AccountsClientService { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; + + #endregion + + + + #region PARAMETERS + + [Parameter] public required long Id { get; set; } + + #endregion + + + + #region FIELDS + + private AccountUsernameRequest? _data; + private string? _error; + private bool _saving; + private bool _saved; + + #endregion + + + + #region PRIVATE METHODS + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + User? user = await AuthenticationService.GetUserAsync(); + + if (user is null) + { + return; + } + + await AccountsClientService.GetAccountInfo(user.Id, data => + { + _data = new AccountUsernameRequest + { + NewUsername = data.Username + }; + StateHasChanged(); + }); + } + } + + private async Task Save() + { + void Success() + { + _saved = true; + _saving = false; + _data = new AccountUsernameRequest + { + NewUsername = _data!.NewUsername + }; + NavigationManager.Refresh(true); + } + + void BadRequest(IDictionary errors) + { + _error = errors.SelectMany(x => x.Value).FirstOrDefault() ?? "Unknown error"; + _saving = false; + } + + void Unauthorized() + { + _error = "Incorrect password"; + _saving = false; + } + + _saving = true; + _saved = false; + _error = null; + await AccountsClientService.PatchAccountUsername(_data!, Success, BadRequest, Unauthorized); + } + + #endregion +} \ No newline at end of file diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileBackgroundEditorPanelComponent.razor b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileBackgroundEditorPanelComponent.razor index 0a2e598..6281e1a 100644 --- a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileBackgroundEditorPanelComponent.razor +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileBackgroundEditorPanelComponent.razor @@ -90,7 +90,7 @@
- +
diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditFormPanelComponent.razor b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditFormPanelComponent.razor index 438fa9c..8f9869a 100644 --- a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditFormPanelComponent.razor +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditFormPanelComponent.razor @@ -39,12 +39,12 @@
diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditHeaderPanelComponent.razor.css b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/ProfileEditHeaderPanelComponent.razor.css deleted file mode 100644 index e69de29..0000000 diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor index 2b2c701..2fd8305 100644 --- a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor @@ -1,8 +1,8 @@
- +
-

@(User.Username)

+

@(_username ?? "Loading...")

User settings
diff --git a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor.cs b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor.cs index 9b44909..83f7d8e 100644 --- a/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor.cs +++ b/WatchIt.Website/WatchIt.Website/Components/Pages/UserEditPage/Panels/UserEditPageHeaderPanelComponent.razor.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; using WatchIt.Website.Components.Common.Subcomponents; using WatchIt.Website.Services.Authentication; +using WatchIt.Website.Services.Client.Accounts; namespace WatchIt.Website.Components.Pages.UserEditPage.Panels; @@ -8,6 +9,7 @@ public partial class UserEditPageHeaderPanelComponent : ComponentBase { #region SERVICES + [Inject] public IAccountsClientService AccountsClientService { get; set; } = default!; [Inject] public NavigationManager NavigationManager { get; set; } = default!; #endregion @@ -25,6 +27,7 @@ public partial class UserEditPageHeaderPanelComponent : ComponentBase #region FIELDS private AccountPictureComponent _accountPicture = default!; + private string? _username; #endregion @@ -35,4 +38,22 @@ public partial class UserEditPageHeaderPanelComponent : ComponentBase public async Task ReloadPicture() => await _accountPicture.Reload(); #endregion + + + + #region PRIVATE METHODS + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await AccountsClientService.GetAccountInfo(User.Id, data => + { + _username = data.Username; + StateHasChanged(); + }); + } + } + + #endregion } \ No newline at end of file diff --git a/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor b/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor index 3b64a23..736e311 100644 --- a/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor +++ b/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor @@ -56,7 +56,7 @@ diff --git a/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor.cs b/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor.cs index a73056b..00657c8 100644 --- a/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor.cs +++ b/WatchIt.Website/WatchIt.Website/Layout/MainLayout.razor.cs @@ -21,6 +21,7 @@ public partial class MainLayout : LayoutComponentBase [Inject] public IAuthenticationService AuthenticationService { get; set; } = default!; [Inject] public IMediaClientService MediaClientService { get; set; } = default!; [Inject] public IPhotosClientService PhotosClientService { get; set; } = default!; + [Inject] public IAccountsClientService AccountsClientService { get; set; } = default!; #endregion @@ -33,6 +34,7 @@ public partial class MainLayout : LayoutComponentBase private bool _loaded; private User? _user; + private AccountResponse? _accountData; private PhotoResponse? _defaultBackgroundPhoto; private bool _searchbarVisible; @@ -81,17 +83,16 @@ public partial class MainLayout : LayoutComponentBase { if (firstRender) { - List endTasks = new List(); - - // STEP 0 - endTasks.AddRange( + await Task.WhenAll( [ Task.Run(async () => _user = await AuthenticationService.GetUserAsync()), PhotosClientService.GetPhotoRandomBackground(data => _defaultBackgroundPhoto = data) ]); - - // END - await Task.WhenAll(endTasks); + + if (_user is not null) + { + await AccountsClientService.GetAccountInfo(_user.Id, data => _accountData = data); + } _loaded = true; StateHasChanged(); diff --git a/WatchIt.Website/WatchIt.Website/Pages/UserEditPage.razor b/WatchIt.Website/WatchIt.Website/Pages/UserEditPage.razor index ae3c2c8..77dadc7 100644 --- a/WatchIt.Website/WatchIt.Website/Pages/UserEditPage.razor +++ b/WatchIt.Website/WatchIt.Website/Pages/UserEditPage.razor @@ -37,40 +37,41 @@ -
-
-
-
- -
+
+
+
+
-
-
- -
-
- -
+
+
+
+
-
-
- -
+
+ +
+
+
+
+
- +
+ + +
diff --git a/WatchIt.Website/WatchIt.Website/appsettings.json b/WatchIt.Website/WatchIt.Website/appsettings.json index b6820ca..a4eb5eb 100644 --- a/WatchIt.Website/WatchIt.Website/appsettings.json +++ b/WatchIt.Website/WatchIt.Website/appsettings.json @@ -29,6 +29,7 @@ "DeleteAccountProfileBackground": "/profile_background", "GetAccountInfo": "/{0}/info", "PutAccountProfileInfo": "/profile_info", + "PatchAccountUsername": "/username", "GetAccountRatedMovies": "/{0}/movies", "GetAccountRatedSeries": "/{0}/series", "GetAccountRatedPersons": "/{0}/persons"