Files
MilkyShots/MilkStream.Client/Components/Pages/User.razor

198 lines
7.9 KiB
Plaintext

@page "/User/{UserId:guid}"
@using Butter.Dtos.User
@using Butter.Types
@using MilkStream.Client.Services
@inject LoginService LoginService
@inject UserService UserService
<h3>@(user?.Username ?? "Username")</h3>
<div class="row">
@if (isSelf || AdminAccess) {
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-envelope-at"></i> EMail:</div>
<div class="col-7 col-sm-9 col-md-10">@user?.Email</div>
}
</div>
<div class="row">
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-person-fill-lock"></i> Role:</div>
<div class="col-7 col-sm-9 col-md-10">@user?.AccessLevel.ToString()</div>
</div>
<div class="row">
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-calendar-date"></i> Creation Date:</div>
<div class="col-7 col-sm-9 col-md-10">@user?.CreatedAt.ToString("D")</div>
</div>
<div class="row">
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-door-open"></i> Last Login:</div>
<div class="col-7 col-sm-9 col-md-10">@(user?.LastLogin?.ToString("D") ?? "Never")</div>
</div>
<div class="row">
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-ban"></i>Banned:</div>
<div class="col-7 col-sm-9 col-md-10">@(user is {IsBanned: true} ? $"Yes ({user.BannedAt.ToString()})" : "No")</div>
</div>
<div class="row">
@if (AdminAccess) {
<div class="col-5 col-sm-3 col-md-2"><i class="bi bi-trash3"></i>Deleted:</div>
<div class="col-7 col-sm-9 col-md-10">@(user is not {DeletedAt: null} ? user!.DeletedAt.ToString() : "No")</div>
}
</div>
@if (isSelf) {
<hr />
@if (!string.IsNullOrEmpty(_pageMessage)) {
<div class="alert alert-success py-1">@_pageMessage</div>
}
<button class="btn btn-outline-primary btn-sm" @onclick="() => _showModal = true">
<i class="bi bi-key"></i> Change Password
</button>
}
@if (_showModal) {
<div class="modal-overlay" @onclick="CloseModal">
<div class="modal-content" @onclick:stopPropagation="true">
<div class="modal-header">
<h5><i class="bi bi-key"></i> Change Password</h5>
<button class="btn-close" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage)) {
<div class="alert alert-danger py-1">@_errorMessage</div>
}
<div class="mb-3">
<label class="form-label">Current Password</label>
<input type="password" class="form-control"
@bind="_oldPassword" @bind:event="oninput" />
</div>
<div class="mb-3">
<label class="form-label">New Password</label>
<input type="password" class="form-control"
@bind="_newPassword" @bind:event="oninput" />
<div class="mt-2" style="height: 6px; background: #333; border-radius: 3px;">
<div style="height: 100%; width: @(_passwordStrengthPercent)%; border-radius: 3px;
background: @(_passwordStrengthColor); transition: all 0.2s;"></div>
</div>
<small class="text-muted">@_passwordStrengthLabel</small>
<ul class="list-unstyled small mb-0 mt-1">
<li class="@(HasMinLength ? "text-success" : "text-danger")">
<i class="bi @(HasMinLength ? "bi-check-circle" : "bi-x-circle")"></i> At least 8 characters
</li>
<li class="@(HasUpperCase ? "text-success" : "text-danger")">
<i class="bi @(HasUpperCase ? "bi-check-circle" : "bi-x-circle")"></i> At least one uppercase letter
</li>
<li class="@(HasLowerCase ? "text-success" : "text-danger")">
<i class="bi @(HasLowerCase ? "bi-check-circle" : "bi-x-circle")"></i> At least one lowercase letter
</li>
<li class="@(HasNumberOrSpecial ? "text-success" : "text-danger")">
<i class="bi @(HasNumberOrSpecial ? "bi-check-circle" : "bi-x-circle")"></i> At least one number or special character
</li>
</ul>
</div>
<div class="mb-3">
<label class="form-label">Confirm New Password</label>
<input type="password" class="form-control"
@bind="_confirmPassword" @bind:event="oninput" />
@if (!string.IsNullOrEmpty(_confirmPassword) && _newPassword != _confirmPassword) {
<small class="text-danger">Passwords do not match</small>
}
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="CloseModal">Cancel</button>
<button class="btn btn-primary" disabled="@(!_canSubmit)" @onclick="SubmitPasswordChange">
Update Password
</button>
</div>
</div>
</div>
}
@code {
[Parameter]
public Guid UserId { get; set; }
bool isSelf;
bool AdminAccess => LoginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
UserInfoDto? user;
bool _showModal;
string _oldPassword = "";
string _newPassword = "";
string _confirmPassword = "";
string _errorMessage = "";
string _pageMessage = "";
bool HasMinLength => _newPassword.Length >= 8;
bool HasUpperCase => _newPassword.Any(char.IsUpper);
bool HasLowerCase => _newPassword.Any(char.IsLower);
bool HasNumberOrSpecial => _newPassword.Any(c => char.IsDigit(c) || char.IsPunctuation(c) || char.IsSymbol(c));
bool PasswordsMatch => _newPassword == _confirmPassword;
int _passwordStrengthPercent => _newPassword.Length == 0 ? 0 : _strengthScore * 25;
string _passwordStrengthColor => _newPassword.Length == 0 ? "transparent" : _strengthScore switch {
1 => "#dc3545",
2 => "#fd7e14",
3 => "#ffc107",
4 => "#198754",
_ => "transparent"
};
string _passwordStrengthLabel => _newPassword.Length == 0 ? "" : _strengthScore switch {
1 => "Weak",
2 => "Fair",
3 => "Good",
4 => "Strong",
_ => ""
};
int _strengthScore => (HasMinLength ? 1 : 0)
+ (HasUpperCase ? 1 : 0)
+ (HasLowerCase ? 1 : 0)
+ (HasNumberOrSpecial ? 1 : 0);
bool _canSubmit => _strengthScore == 4
&& PasswordsMatch
&& !string.IsNullOrEmpty(_oldPassword)
&& !string.IsNullOrEmpty(_newPassword);
protected async override Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (LoginService.IsLoggedIn) await LoadUser();
LoginService.AuthInfoChanged += async (_, _) => await LoadUser();
}
async Task LoadUser() {
if (UserId == LoginService.AuthInfo?.UserId) isSelf = true;
user = await UserService.GetUserAsync();
StateHasChanged();
}
async Task CloseModal() {
_showModal = false;
_oldPassword = "";
_newPassword = "";
_confirmPassword = "";
_errorMessage = "";
}
async Task SubmitPasswordChange() {
_errorMessage = "";
var dto = new UserUpdateDto {
Id = UserId,
OldPassword = _oldPassword,
Password = _newPassword
};
var (result, error) = await UserService.UpdateUserAsync(dto);
if (result != null) {
_pageMessage = "Password updated successfully.";
await CloseModal();
} else {
_errorMessage = error ?? "Failed to update password.";
}
}
}