username change panel added

This commit is contained in:
2024-11-06 00:11:45 +01:00
Unverified
parent 26a5e1e558
commit 4cbc44f9be
21 changed files with 305 additions and 42 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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<ActionResult> 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<ActionResult> PatchAccountUsername([FromBody]AccountUsernameRequest data) => await accountsControllerService.PatchAccountUsername(data);
#endregion
[HttpGet("{id}/movies")]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<MovieRatedResponse>), StatusCodes.Status200OK)]

View File

@@ -240,7 +240,9 @@ public class AccountsControllerService(
}
#endregion
#region Info
public async Task<RequestResult> 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<RequestResult> 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<RequestResult> GetAccountRatedMovies(long id, MovieRatedQueryParameters query)
{

View File

@@ -21,6 +21,7 @@ public interface IAccountsControllerService
Task<RequestResult> DeleteAccountProfileBackground();
Task<RequestResult> GetAccountInfo(long id);
Task<RequestResult> PutAccountProfileInfo(AccountProfileInfoRequest data);
Task<RequestResult> PatchAccountUsername(AccountUsernameRequest data);
Task<RequestResult> GetAccountRatedMovies(long id, MovieRatedQueryParameters query);
Task<RequestResult> GetAccountRatedSeries(long id, SeriesRatedQueryParameters query);
Task<RequestResult> GetAccountRatedPersons(long id, PersonRatedQueryParameters query);

View File

@@ -0,0 +1,16 @@
using FluentValidation;
using WatchIt.Common.Model.Accounts;
using WatchIt.Database;
namespace WatchIt.WebAPI.Validators.Accounts;
public class AccountUsernameRequestValidator : AbstractValidator<AccountUsernameRequest>
{
public AccountUsernameRequestValidator(DatabaseContext database)
{
RuleFor(x => x.NewUsername).MinimumLength(5)
.MaximumLength(50)
.CannotBeIn(database.Accounts, x => x.Username)
.WithMessage("Username is already used");
}
}

View File

@@ -176,6 +176,21 @@ public class AccountsClientService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task PatchAccountUsername(AccountUsernameRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? 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<IEnumerable<MovieRatedResponse>>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.GetAccountRatedMovies, id);

View File

@@ -20,6 +20,7 @@ public interface IAccountsClientService
Task DeleteAccountProfileBackground(Action? successAction = null, Action? unauthorizedAction = null);
Task GetAccountInfo(long id, Action<AccountResponse>? successAction = null, Action? notFoundAction = null);
Task PutAccountProfileInfo(AccountProfileInfoRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null);
Task PatchAccountUsername(AccountUsernameRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null);
Task GetAccountRatedMovies(long id, MovieRatedQueryParameters query, Action<IEnumerable<MovieRatedResponse>>? successAction = null, Action? notFoundAction = null);
Task GetAccountRatedSeries(long id, SeriesRatedQueryParameters query, Action<IEnumerable<SeriesRatedResponse>>? successAction = null, Action? notFoundAction = null);
Task GetAccountRatedPersons(long id, PersonRatedQueryParameters query, Action<IEnumerable<PersonRatedResponse>>? successAction = null, Action? notFoundAction = null);

View File

@@ -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; }

View File

@@ -0,0 +1,5 @@
<div class="panel panel-section-header">
<div class="d-flex">
<h3 class="fw-bold m-0">Account settings</h3>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<div class="panel">
<div class="vstack gap-3">
<h4 class="fw-bold">Change username</h4>
@if (_data is not null)
{
<EditForm Model="@(_data)">
<AntiforgeryToken/>
<div class="container-grid">
<div class="row form-group my-1">
<label for="username" class="col-2 col-form-label">New username</label>
<div class="col-10">
<InputText id="username" class="form-control" @bind-Value="_data!.NewUsername"/>
</div>
</div>
<div class="row form-group my-1">
<label for="password" class="col-2 col-form-label">Password</label>
<div class="col-10">
<InputText id="password" type="password" class="form-control" @bind-Value="_data!.Password"/>
</div>
</div>
<div class="row mt-2">
<div class="col align-self-center">
@if (!string.IsNullOrWhiteSpace(_error))
{
<span class="text-danger">@(_error)</span>
}
else if (_saved)
{
<span class="text-success">New username saved!</span>
}
</div>
<div class="col-auto">
<button type="submit" class="btn btn-secondary" disabled="@(_saving)" @onclick="@(Save)">
<LoadingButtonContentComponent IsLoading="@(_saving)" Content="Save" LoadingContent="Saving..."/>
</button>
</div>
</div>
</div>
</EditForm>
}
else
{
<LoadingComponent Color="@(LoadingComponent.LoadingComponentColors.Light)"/>
}
</div>
</div>

View File

@@ -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<string, string[]> 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
}

View File

@@ -90,7 +90,7 @@
<div class="container-grid">
<div class="row gx-3 mb-2">
<div class="col">
<PictureComponent Picture="@(_selectedPhoto)" AlternativeText="background" Width="500" Placeholder="/assets/photo.png" AspectRatio="PictureComponent.PictureComponentAspectRatio.Photo"/>
<PictureComponent Class="w-100" Picture="@(_selectedPhoto)" AlternativeText="background" Placeholder="/assets/photo.png" AspectRatio="PictureComponent.PictureComponentAspectRatio.Photo"/>
</div>
<div class="col">
<div class="rounded-3 border h-100" style="height: 30px; background: linear-gradient(45deg, @($"#{Convert.ToHexString(_selectedPhoto.Background!.FirstGradientColor)}, #{Convert.ToHexString(_selectedPhoto.Background!.SecondGradientColor)}"));"></div>

View File

@@ -39,12 +39,12 @@
<button type="submit" class="btn btn-secondary" disabled="@(_saving)" @onclick="@(Save)">
@if (!_saving)
{
<span>Save</span>
<span>Save</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Saving...</span>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Saving...</span>
}
</button>
</div>

View File

@@ -1,8 +1,8 @@
<div class="panel" role="button" @onclick="@(() => NavigationManager.NavigateTo("/user"))">
<div class="d-flex gap-3 align-items-center">
<AccountPictureComponent @ref="_accountPicture" Id="@(User.Id)" Size="50"/>
<AccountPictureComponent @ref="_accountPicture" Id="@(User.Id)" Size="60"/>
<div class="d-flex-inline flex-column">
<h2 id="username" class="fw-bold m-0">@(User.Username)</h2>
<h2 id="username" class="fw-bold m-0">@(_username ?? "Loading...")</h2>
<span id="secondaryText" class="text-secondary">User settings</span>
</div>
</div>

View File

@@ -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
}

View File

@@ -56,7 +56,7 @@
<Button Color="Color.Default" Clicked="@(() => NavigationManager.NavigateTo("/user"))">
<div class="d-flex gap-2 align-items-center">
<AccountPictureComponent @ref="_profilePicture" Id="@(_user.Id)" Size="30"/>
<span>@(_user.Username)</span>
<span>@(_accountData!.Username)</span>
</div>
</Button>
<DropdownToggle Color="Color.Default" Split />

View File

@@ -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<Task> endTasks = new List<Task>();
// 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();

View File

@@ -37,40 +37,41 @@
</Items>
<Content>
<TabPanel Name="profile">
<div class="mt-default">
<div class="container-grid">
<div class="row">
<div class="col">
<ProfileEditHeaderPanelComponent/>
</div>
<div class="container-grid mt-default">
<div class="row">
<div class="col">
<ProfileEditHeaderPanelComponent/>
</div>
<div class="row mt-default gx-default">
<div class="col-auto">
<PictureEditorPanelComponent Id="@(_user.Id)"
Class="h-100"
PicturePlaceholder="assets/user_placeholder.png"
Circle="true"
PictureGetTask="@((id, action) => AccountsClientService.GetAccountProfilePicture(id, action))"
PicturePutTask="@((_, picture, action) => AccountsClientService.PutAccountProfilePicture(new AccountProfilePictureRequest(picture), action))"
PictureDeleteTask="@((_, action) => AccountsClientService.DeleteAccountProfilePicture(action))"
OnPictureChanged="@(async (_) => await PictureChanged())"/>
</div>
<div class="col">
<ProfileEditFormPanelComponent Id="@(_user.Id)"
Class="h-100"/>
</div>
</div>
<div class="row mt-default gx-default">
<div class="col-auto">
<PictureEditorPanelComponent Id="@(_user.Id)"
Class="h-100"
PicturePlaceholder="assets/user_placeholder.png"
Circle="true"
PictureGetTask="@((id, action) => AccountsClientService.GetAccountProfilePicture(id, action))"
PicturePutTask="@((_, picture, action) => AccountsClientService.PutAccountProfilePicture(new AccountProfilePictureRequest(picture), action))"
PictureDeleteTask="@((_, action) => AccountsClientService.DeleteAccountProfilePicture(action))"
OnPictureChanged="@(async (_) => await PictureChanged())"/>
</div>
<div class="row mt-default">
<div class="col">
<ProfileBackgroundEditorPanelComponent Id="@(_user.Id)"
OnBackgroundChanged="BackgroundChanged"/>
</div>
<div class="col">
<ProfileEditFormPanelComponent Id="@(_user.Id)"
Class="h-100"/>
</div>
</div>
<div class="row mt-default">
<div class="col">
<ProfileBackgroundEditorPanelComponent Id="@(_user.Id)"
OnBackgroundChanged="BackgroundChanged"/>
</div>
</div>
</div>
</TabPanel>
<TabPanel Name="account">
<div class="vstack mt-default gap-default">
<AccountEditHeaderPanelComponent/>
<NewUsernamePanelComponent Id="@(_user.Id)"/>
</div>
</TabPanel>
</Content>
</Tabs>

View File

@@ -29,6 +29,7 @@
"DeleteAccountProfileBackground": "/profile_background",
"GetAccountInfo": "/{0}/info",
"PutAccountProfileInfo": "/profile_info",
"PatchAccountUsername": "/username",
"GetAccountRatedMovies": "/{0}/movies",
"GetAccountRatedSeries": "/{0}/series",
"GetAccountRatedPersons": "/{0}/persons"