first commit -push

This commit is contained in:
dungtt
2025-10-15 15:15:53 +07:00
parent 674ae395be
commit a9577c5756
885 changed files with 74595 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
@page "/Account/Login/Access"
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Antiforgery;
@attribute [RequireAntiforgeryToken]
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<div class="jumbotron">
<h1>Authorization</h1>
<p class="lead text-left">Do you want to grant <strong>@ApplicationName</strong> access to your data? (scopes requested: @Scope)</p>
<form action="api/Authorization/connect/authorize" method="post" >
<AntiforgeryToken />
@foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)HttpContext.Request.Form : HttpContext.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
</form>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery(Name = "request_app")]
private string ApplicationName { get; set; } = "";
[SupplyParameterFromQuery(Name = "request_scope")]
private string Scope { get; set; } = "";
}

View File

@@ -0,0 +1,355 @@
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using RobotNet.IdentityServer.Services
@using System.Text.RegularExpressions
@using System.ComponentModel.DataAnnotations
@inherits LayoutComponentBase
@inject RobotNet.IdentityServer.Services.UserImageService UserImageService
@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<ApplicationRole> RoleManager
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<MudDialogProvider />
<MudSnackbarProvider />
<div class="d-flex justify-content-center align-items-center" style="height: 90vh; overflow-y: auto;">
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-8">
@if (userInfo != null)
{
<MudCard Elevation="3" Class="rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5" Class="mb-0">Thông tin cá nhân</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">Quản lý thông tin hồ sơ của bạn</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Label="true">@string.Join(", ", userRoles)</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4" Class="d-flex flex-column align-items-center">
<div class="position-relative d-flex justify-content-center my-3">
<MudImage Class="rounded-circle"
Style="width:150px; height:150px; object-fit:cover"
Src="@avatarUrl" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Default"
Size="Size.Small"
OnClick="ChangeAvatar"
Style="position:absolute; bottom:0; right:calc(50% - 60px); background-color:white" />
</div>
<MudText Typo="Typo.h6" Class="mt-3 text-center">@userInfo.FullName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="text-center">
ID: @(userInfo.Id.Length > 10 ? userInfo.Id.Substring(0, 10) + "..." : userInfo.Id)
</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudPaper Elevation="0" Class="pa-4">
<MudForm @ref="form" Model="userInfo">
<MudTextField Label="Tên người dùng"
@bind-Value="userInfo.UserName"
Variant="Variant.Outlined"
Disabled="true"
HelperText="Tên người dùng không thể thay đổi"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
FullWidth="true" />
<MudTextField Label="Họ và tên"
@bind-Value="userInfo.FullName"
Variant="Variant.Outlined"
Required="true"
RequiredError="Họ và tên là bắt buộc"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Badge"
FullWidth="true" />
<MudTextField Label="Email"
@bind-Value="userInfo.Email"
Variant="Variant.Outlined"
Required="true"
RequiredError="Email là bắt buộc"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Email"
FullWidth="true" />
<MudTextField Label="Số điện thoại"
@bind-Value="userInfo.PhoneNumber"
Variant="Variant.Outlined"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Phone"
FullWidth="true" />
</MudForm>
@if (!isButtonDisabled)
{
<MudPaper Class="d-flex gap-3 justify-end py-2 px-0" Elevation="0">
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Cancel"
Color="Color.Error"
OnClick="ResetFields"
Size="Size.Medium">
Hủy
</MudButton>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Save"
Color="Color.Primary"
OnClick="SaveUserInfo"
Size="Size.Medium">
Lưu thay đổi
</MudButton>
</MudPaper>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
}
else
{
<MudCard Elevation="3" Class="rounded-lg pa-8">
<MudCardContent Class="d-flex flex-column align-items-center justify-center">
<MudIcon Icon="@Icons.Material.Filled.ErrorOutline" Color="Color.Error" Size="Size.Large" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-2">Vui lòng đăng nhập</MudText>
<MudText Typo="Typo.body1" Class="text-center mb-4">
Bạn cần đăng nhập để xem và chỉnh sửa thông tin cá nhân.
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => NavigationManager.NavigateTo("/Account/Login"))">
Đăng nhập ngay
</MudButton>
</MudCardContent>
</MudCard>
}
</MudContainer>
</div>
<MudDialog @bind-Visible="ChangeAvatarVisible">
<DialogContent>
<div class="d-flex flex-column align-items-center text-center px-2">
<h5 class="mb-2">Thay đổi ảnh hồ sơ</h5>
<MudText Typo="Typo.caption" Class="mb-3">
Ảnh hồ sơ giúp người khác nhận ra bạn và xác nhận rằng bạn đã đăng nhập.
</MudText>
<div class="rounded-circle overflow-hidden mb-3"
style="width: 130px; height: 130px; border: 2px solid #ccc;">
<MudImage Src="@avatarPreview"
Alt="avatar preview"
Style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<InputFile OnChange="HandleSelected" accept="image/*">
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary">
Thay đổi
</MudButton>
</InputFile>
</div>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="ConfirmChangeAvatar">
Xác nhận
</MudButton>
<MudButton Variant="Variant.Text" OnClick="@(() => ChangeAvatarVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
@code {
MudForm? form;
private string? avatarPreview;
private string? avatarUrl;
private IBrowserFile? selectedFile;
private bool ChangeAvatarVisible = false;
private bool isButtonDisabled = true;
private string originalFullName = "";
private string originalEmail = "";
private string originalPhoneNumber = "";
private string originalUserName = "";
private ApplicationUser? userInfo;
private List<string> userRoles = new List<string>();
private void EnableButtons()
{
isButtonDisabled = false;
}
private void ChangeAvatar()
{
ChangeAvatarVisible = true;
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
if (user?.Identity?.IsAuthenticated == true)
{
userInfo = await UserManager.GetUserAsync(user);
if (userInfo != null)
{
userRoles = (await UserManager.GetRolesAsync(userInfo)).ToList();
originalUserName = userInfo.UserName?? string.Empty;
originalFullName = userInfo.FullName?? string.Empty;
originalEmail = userInfo.Email?? string.Empty;
originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty;
if (userInfo.AvatarImage != null)
{
avatarUrl = $"data:{userInfo.AvatarContentType};base64,{Convert.ToBase64String(userInfo.AvatarImage)}";
avatarPreview = avatarUrl;
}
else
{
avatarUrl = "/uploads/avatars/anh.jpg";
avatarPreview = avatarUrl;
}
}
}
else
{
Snackbar.Add("Vui lòng đăng nhập để tiếp tục", Severity.Error);
}
}
private async Task HandleSelected(InputFileChangeEventArgs e)
{
selectedFile = e.File;
const long maxSize = 5 * 1024 * 1024;
if (selectedFile.Size > maxSize)
{
Snackbar.Add("⚠️ Ảnh bạn chọn vượt quá 5MB. Vui lòng chọn ảnh nhỏ hơn.", Severity.Warning);
avatarPreview = avatarUrl;
selectedFile = null;
return;
}
try
{
(byte[] buffer, string contentType) = await UserImageService.ResizeAndConvertAsync(selectedFile.OpenReadStream());
avatarPreview = $"data:{contentType};base64,{Convert.ToBase64String(buffer)}";
if (userInfo != null)
{
userInfo.AvatarImage = buffer;
userInfo.AvatarContentType = selectedFile.ContentType;
}
}
catch (Exception ex)
{
Snackbar.Add($"❌ Lỗi khi đọc ảnh: {ex.Message}", Severity.Error);
avatarPreview = avatarUrl;
}
}
private async Task ConfirmChangeAvatar()
{
if (userInfo != null && userInfo.AvatarImage != null)
{
var result = await UserManager.UpdateAsync(userInfo);
if (result.Succeeded)
{
avatarUrl = avatarPreview;
ChangeAvatarVisible = false;
await Task.Delay(200);
await UserInfoService.NotifyUserInfoChanged();
StateHasChanged();
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
Snackbar.Add("Cập nhật ảnh đại diện thành công!", Severity.Success);
}
else
{
Snackbar.Add("Lỗi khi cập nhật avatar.", Severity.Error);
}
}
}
private void ResetFields()
{
if (userInfo == null) return;
userInfo.FullName = originalFullName;
userInfo.Email = originalEmail;
userInfo.PhoneNumber = originalPhoneNumber;
userInfo.UserName = originalUserName;
isButtonDisabled = true;
}
private async Task SaveUserInfo()
{
if (userInfo != null)
{
try
{
var result = await UserManager.UpdateAsync(userInfo);
if (result.Succeeded)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userInfo = await UserManager.GetUserAsync(authState.User);
if(userInfo != null)
{
originalFullName = userInfo.FullName ?? string.Empty;
originalEmail = userInfo.Email ?? string.Empty;
originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty;
originalUserName = userInfo.UserName ?? string.Empty;
}
isButtonDisabled = true;
await Task.Delay(200);
await UserInfoService.NotifyUserInfoChanged();
StateHasChanged();
Snackbar.Add("Thông tin đã được cập nhật!", Severity.Success);
}
else
{
Snackbar.Add("Lỗi khi cập nhật thông tin.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error while saving user information: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,7 @@
.mdi {
display: inline-flex;
justify-content: center;
align-items: center;
background-size: cover;
margin-top:7px;
}

View File

@@ -0,0 +1,118 @@
@page "/Account/Login"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Log in</PageTitle>
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<h1>Log in</h1>
@if (!string.IsNullOrEmpty(errorMessage))
{
var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass" role="alert">
@errorMessage
</div>
}
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login" style="width: 300px;">
<DataAnnotationsValidator />
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Username" class="form-control" autocomplete="username" aria-required="true" />
<label for="username" class="form-label">Username</label>
<ValidationMessage For="() => Input.Username" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<label for="password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="checkbox mb-3">
<label class="form-label">
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input" />
Remember me
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</EditForm>
</div>
@code {
private string? errorMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
}
errorMessage = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
if (errorMessage is not null)
{
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
}
}
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
}
}
private sealed class InputModel
{
[Required]
public string Username { get; set; } = "";
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
@page "/Account/Logout/Confirm"
@using Microsoft.EntityFrameworkCore.Metadata.Internal
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Antiforgery;
@attribute [RequireAntiforgeryToken]
@inject NavigationManager Navigation
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<div class="jumbotron">
<h1>Log out</h1>
<p class="lead text-left">Are you sure you want to sign out?</p>
<form action="api/Authorization/connect/logout" method="post">
<AntiforgeryToken />
@foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)HttpContext.Request.Form : HttpContext.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Confirm" type="submit" value="Yes" />
</form>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private Task OnSubmitLogout()
{
Navigation.NavigateTo("/Account/Login", forceLoad: true);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,982 @@
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using OpenIddict.Abstractions
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using static OpenIddict.Abstractions.OpenIddictConstants
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject IOpenIddictApplicationManager ApplicationManager
@inject IOpenIddictScopeManager ScopeManager
<MudDialogProvider />
<MudSnackbarProvider />
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="padding: 1rem;">
<div class="app-header">
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<div>
<MudText Typo="Typo.h4" Style="font-weight: 700; margin-bottom: 8px;">
OpenIddict Manager
</MudText>
<MudText Typo="Typo.body1" Style="opacity: 0.9;">
Quản lý ứng dụng OAuth2 & OpenID Connect một cách dễ dàng
</MudText>
</div>
<MudChip T="string" Icon="@Icons.Material.Filled.Security" Color="Color.Surface" Size="Size.Large" Style="color:white">
@filteredApplications.Count Apps
</MudChip>
</MudStack>
</div>
<MudPaper Class="glass-card pa-4">
<div style="padding: 1.5rem; border-bottom: 1px solid #e2e8f0; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<MudText Typo="Typo.h6" Style="margin: 0; color: #334155; font-weight: 600;">
Danh sách Application
</MudText>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => OpenApplicationDialog())"
Class="add-scope-btn"
Style="text-transform: none; font-weight: 500;">
Thêm Application
</MudButton>
</div>
</div>
<MudTextField @bind-Value="applicationSearchTerm"
Label="Tìm kiếm application..."
Placeholder="Nhập Client ID, Display Name, Type hoặc ID để tìm kiếm"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Clearable="true"
Immediate="true"
DebounceInterval="300"
Style="margin-top: 0.5rem;" />
<MudTable Items="FilteredApplications"
Hover="true"
Dense="true"
FixedHeader="true"
Loading="@loadingApplications"
Class="compact-table"
Virtualize="true">
<HeaderContent>
<MudTh Style="width: 120px;">Client</MudTh>
<MudTh Style="width: 100px;">Type</MudTh>
<MudTh Style="width: 150px;">Display Name</MudTh>
<MudTh Style="width: 80px;">Secret</MudTh>
<MudTh Style="width: 120px;">Endpoints</MudTh>
<MudTh Style="width: 140px;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Style="font-weight: 600;">@context.ClientId</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Id[..8]...</MudText>
</MudStack>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@(context.ClientType == ClientTypes.Confidential ? Color.Primary : Color.Secondary)"
Variant="Variant.Filled">
@(context.ClientType == ClientTypes.Confidential ? "Confidential" : "Public")
</MudChip>
</MudTd>
<MudTd>
<MudText Typo="Typo.body2">@context.DisplayName</MudText>
</MudTd>
<MudTd>
<div class="status-badge">
@if (!string.IsNullOrEmpty(context.ClientSecret))
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
<span style="color: var(--mud-palette-success);">Yes</span>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small" />
<span style="color: var(--mud-palette-error);">No</span>
}
</div>
</MudTd>
<MudTd>
@if (context.RedirectUris.Any())
{
<MudTooltip Text="@string.Join(", ", context.RedirectUris)">
<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Text">
@context.RedirectUris.Count URIs
</MudChip>
</MudTooltip>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary">No URIs</MudText>
}
</MudTd>
<MudTd>
<div class="action-buttons">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewApplicationDetails(context))"
aria-label="Chi tiết" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => EditApplication(context))"
aria-label="Sửa" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeleteApplication(context.ClientId))"
aria-label="Xóa" />
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
</MudTable>
</MudPaper>
</MudContainer>
<MudDialog @bind-Visible="ShowApplicationDialog"
Options="@(new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true, CloseOnEscapeKey = true })">
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@(editingApplication != null ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="mr-2" />
@(editingApplication != null ? "Chỉnh sửa Application" : "Tạo Application Mới")
</MudText>
</TitleContent>
<DialogContent>
<MudContainer Style="max-height: 70vh; overflow-y: auto;">
<MudGrid Spacing="3">
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
Thông tin cơ bản
</MudText>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="applicationForm.ClientId"
Label="Client ID"
Required="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Key" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="applicationForm.DisplayName"
Label="Display Name"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Label" />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect Value="applicationForm.ClientType"
Label="Client Type"
Variant="Variant.Outlined"
ValueChanged="@((string value) => OnClientTypeChanged(value))"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Category">
<MudSelectItem Value="@ClientTypes.Public"> Public</MudSelectItem>
<MudSelectItem Value="@ClientTypes.Confidential"> Confidential</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="applicationForm.ConsentType"
Label="Consent Type"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.VerifiedUser">
<MudSelectItem Value="@ConsentTypes.Explicit"> Explicit</MudSelectItem>
<MudSelectItem Value="@ConsentTypes.Implicit"> Implicit</MudSelectItem>
</MudSelect>
</MudItem>
@if (applicationForm.ClientType == ClientTypes.Confidential)
{
<MudItem xs="12">
<MudTextField @bind-Value="applicationForm.ClientSecret"
Label="Client Secret"
InputType="InputType.Password"
Variant="Variant.Outlined"
Placeholder="@(editingApplication != null ? "Để trống nếu không muốn thay đổi" : "Nhập Client Secret")"
HelperText="@(editingApplication != null ? "Chỉ nhập nếu muốn thay đổi" : "Mật khẩu bí mật cho ứng dụng")"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Password" />
</MudItem>
}
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2" />
Cấu hình Endpoints
</MudText>
</MudItem>
<MudItem xs="12" md="6">
<div class="uri-input-section">
<MudText Typo="Typo.subtitle2" Class="mb-2">Redirect URIs</MudText>
<MudTextField @bind-Value="redirectUriInput"
Label="Redirect URI"
Placeholder="https://app.com/callback"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Add"
OnAdornmentClick="AddRedirectUri"
@onkeypress="@(async (KeyboardEventArgs e) => { if (e.Key == "Enter") { AddRedirectUri(); } })" />
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var uri in applicationForm.RedirectUris)
{
<MudChip T="string" Text="@uri" OnClose="@(() => RemoveRedirectUri(uri))" Color="Color.Primary" Size="Size.Small" />
}
</MudStack>
</div>
</MudItem>
<MudItem xs="12" md="6">
<div class="uri-input-section">
<MudText Typo="Typo.subtitle2" Class="mb-2">Post Logout URIs</MudText>
<MudTextField @bind-Value="postLogoutUriInput"
Label="Post Logout URI"
Placeholder="https://app.com/logout"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Add"
OnAdornmentClick="AddPostLogoutUri"
@onkeypress="@(async (KeyboardEventArgs e) => { if (e.Key == "Enter") { AddPostLogoutUri(); } })" />
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var uri in applicationForm.PostLogoutRedirectUris)
{
<MudChip T="string" Text="@uri" OnClose="@(() => RemovePostLogoutUri(uri))" Color="Color.Secondary" Size="Size.Small" />
}
</MudStack>
</div>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Security" Class="mr-2" />
Permissions & Requirements
</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudSelect T="string"
Label="Permissions"
Variant="Variant.Outlined"
MultiSelection="true"
SelectAll="true"
SelectAllText="Chọn tất cả"
Dense="true"
MaxHeight="200"
SelectedValuesChanged="@OnPermissionSelectionChanged"
SelectedValues="@GetSelectedPermissions()">
@foreach (var permission in permissionChecks)
{
<MudSelectItem Value="@permission.Key">
@permission.Key.Split('.').Last()
</MudSelectItem>
}
</MudSelect>
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var selectedPermission in GetSelectedPermissions())
{
<MudChip T="string" Text="@selectedPermission.Split('.').Last()"
OnClose="@(() => RemovePermission(selectedPermission))"
Color="Color.Primary"
Size="Size.Small" />
}
</MudStack>
</MudItem>
@if (applicationForm.ClientType == ClientTypes.Public)
{
<MudItem xs="12" md="4">
<MudText Typo="Typo.subtitle2" Class="mb-2">Requirements</MudText>
@foreach (var requirement in requirementChecks)
{
<MudCheckBox T="string" @bind-Checked="@requirementChecks[requirement.Key]"
Label="@requirement.Key.Split('.').Last()"
Dense="true" />
}
</MudItem>
}
</MudGrid>
</MudContainer>
</DialogContent>
<DialogActions>
<MudButton OnClick="CancelApplicationDialog"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.Cancel">
Hủy
</MudButton>
<MudButton OnClick="SaveApplication"
Color="Color.Primary"
Variant="Variant.Filled"
StartIcon="@(editingApplication != null ? Icons.Material.Filled.Update : Icons.Material.Filled.Save)">
@(editingApplication != null ? "Cập nhật" : "Tạo mới")
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ShowDetailsDialog"
Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true })">
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
Chi tiết Application
</MudText>
</TitleContent>
<DialogContent>
@if (selectedApplication != null)
{
<MudContainer Style="max-height: 60vh; overflow-y: auto;">
<MudGrid Spacing="2">
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f8f9ff, #e8f2ff);">
<MudText Typo="Typo.subtitle2" Color="Color.Primary" Class="mb-2"> Thông tin cơ bản</MudText>
<MudStack Spacing="1">
<div><strong>ID:</strong> @selectedApplication.Id</div>
<div><strong>Client ID:</strong> @selectedApplication.ClientId</div>
<div><strong>Display Name:</strong> @selectedApplication.DisplayName</div>
<div><strong>Type:</strong> @selectedApplication.ClientType</div>
<div><strong>Consent:</strong> @selectedApplication.ConsentType</div>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #fff8f0, #fff0e6);">
<MudText Typo="Typo.subtitle2" Color="Color.Warning" Class="mb-2">🔒 Bảo mật</MudText>
<div>
<strong>Client Secret:</strong>
@if (!string.IsNullOrEmpty(selectedApplication.ClientSecret))
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">Có</MudChip>
}
else
{
<MudChip T="string" Color="Color.Error" Size="Size.Small">Không</MudChip>
}
</div>
</MudPaper>
</MudItem>
@if (selectedApplication.RedirectUris.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f0fff8, #e6fff0);">
<MudText Typo="Typo.subtitle2" Color="Color.Success" Class="mb-2">🔗 Redirect URIs</MudText>
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var uri in selectedApplication.RedirectUris)
{
<MudChip T="string" Text="@uri" Size="Size.Small" Color="Color.Success" />
}
</MudStack>
</MudPaper>
</MudItem>
}
@if (selectedApplication.Permissions.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #fff0f8, #ffe6f0);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary" Class="mb-2">🛡️ Permissions</MudText>
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var permission in selectedApplication.Permissions)
{
<MudChip T="string" Text="@permission.Split('.').Last()" Size="Size.Small" Color="Color.Secondary" />
}
</MudStack>
</MudPaper>
</MudItem>
}
@if (selectedApplication.Requirements.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f7f0f8, #f6fef0);">
<MudText Typo="Typo.subtitle2" Color="Color.Tertiary" Class="mb-2">⚙️ Requirements</MudText>
@if (selectedApplication.Requirements.Any())
{
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var requirement in selectedApplication.Requirements)
{
<MudChip T="string" Text="@requirement" Size="Size.Small" Color="Color.Secondary" />
}
</MudStack>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Style="font-style: italic;">
Không có requirements nào được thiết lập
</MudText>
}
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDetailsDialog"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Close">
Đóng
</MudButton>
</DialogActions>
</MudDialog>
@code {
private List<ApplicationInfo> filteredApplications = new();
private bool loadingApplications = false;
private bool ShowApplicationDialog = false;
private bool ShowDetailsDialog = false;
private bool showClientSecret = false;
private ApplicationInfo? editingApplication = null;
private ApplicationInfo? selectedApplication = null;
private ApplicationForm applicationForm = new();
private string redirectUriInput = string.Empty;
private string postLogoutUriInput = string.Empty;
private string customScopeInput = string.Empty;
private HashSet<string> customScopes = new();
private Dictionary<string, bool> permissionChecks = new();
private Dictionary<string, bool> requirementChecks = new();
private List<string> availableScopes = new();
public class ApplicationInfo
{
public string Id { get; set; } = string.Empty;
public string ApplicationType { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string ClientType { get; set; } = string.Empty;
public string ConsentType { get; set; } = string.Empty;
public List<string> RedirectUris { get; set; } = new();
public List<string> PostLogoutRedirectUris { get; set; } = new();
public List<string> Permissions { get; set; } = new();
public List<string> Requirements { get; set; } = new();
public string? ClientSecret { get; set; }
}
public class ApplicationForm
{
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string ClientType { get; set; } = ClientTypes.Public;
public string ConsentType { get; set; } = ConsentTypes.Explicit;
public string? ClientSecret { get; set; }
public List<string> RedirectUris { get; set; } = new();
public List<string> PostLogoutRedirectUris { get; set; } = new();
}
private string applicationSearchTerm = "";
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
InitializePermissionChecks();
InitializeRequirementChecks();
await LoadApplicationsAsync();
await LoadAvailableScopesAsync();
}
}
private void InitializePermissionChecks()
{
permissionChecks.Clear();
permissionChecks.Add(Permissions.Endpoints.Authorization, false);
permissionChecks.Add(Permissions.Endpoints.EndSession, false);
permissionChecks.Add(Permissions.Endpoints.Token, false);
permissionChecks.Add(Permissions.Endpoints.Introspection, false);
permissionChecks.Add(Permissions.GrantTypes.AuthorizationCode, false);
permissionChecks.Add(Permissions.GrantTypes.RefreshToken, false);
permissionChecks.Add(Permissions.GrantTypes.ClientCredentials, false);
permissionChecks.Add(Permissions.ResponseTypes.Code, false);
permissionChecks.Add(Permissions.ResponseTypes.Token, false);
permissionChecks.Add(Permissions.Scopes.Email, false);
permissionChecks.Add(Permissions.Scopes.Profile, false);
permissionChecks.Add(Permissions.Scopes.Roles, false);
foreach (var scope in availableScopes)
{
var scopePermission = Permissions.Prefixes.Scope + scope;
if (!permissionChecks.ContainsKey(scopePermission))
{
permissionChecks.Add(scopePermission, false);
}
}
}
private void InitializeRequirementChecks()
{
requirementChecks = new() {
{ Requirements.Features.ProofKeyForCodeExchange, false }
};
}
private void OnClientTypeChanged(string newClientType)
{
applicationForm.ClientType = newClientType;
if (newClientType == ClientTypes.Public)
{
applicationForm.ClientSecret = null;
}
StateHasChanged();
}
private void OnPermissionSelectionChanged(IEnumerable<string> selectedValues)
{
foreach (var key in permissionChecks.Keys.ToList())
{
permissionChecks[key] = false;
}
foreach (var value in selectedValues)
{
if (permissionChecks.ContainsKey(value))
{
permissionChecks[value] = true;
}
}
StateHasChanged();
}
private IEnumerable<string> GetSelectedPermissions()
{
return permissionChecks.Where(x => x.Value).Select(x => x.Key);
}
private void RemovePermission(string permission)
{
if (permissionChecks.ContainsKey(permission))
{
permissionChecks[permission] = false;
StateHasChanged();
}
}
private IEnumerable<ApplicationInfo> FilteredApplications =>
string.IsNullOrWhiteSpace(applicationSearchTerm)
? filteredApplications
: filteredApplications.Where(r =>
(r.ClientId != null && r.ClientId.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.DisplayName != null && r.DisplayName.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.ClientType != null && r.ClientType.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.Id != null && r.Id.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)));
private async Task LoadApplicationsAsync()
{
try
{
loadingApplications = true;
filteredApplications.Clear();
await foreach (var app in ApplicationManager.ListAsync())
{
string? clientSecret = null;
try
{
var properties = await ApplicationManager.GetPropertiesAsync(app);
clientSecret = properties.ContainsKey("client_secret") ? properties["client_secret"].ToString() : null;
if (string.IsNullOrEmpty(clientSecret))
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
clientSecret = "***";
}
}
}
catch
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
clientSecret = "***";
}
}
filteredApplications.Add(new ApplicationInfo
{
Id = await ApplicationManager.GetIdAsync(app) ?? string.Empty,
ApplicationType = await ApplicationManager.GetApplicationTypeAsync(app) ?? string.Empty,
ClientId = await ApplicationManager.GetClientIdAsync(app) ?? string.Empty,
DisplayName = await ApplicationManager.GetDisplayNameAsync(app) ?? string.Empty,
ClientType = await ApplicationManager.GetClientTypeAsync(app) ?? string.Empty,
ConsentType = await ApplicationManager.GetConsentTypeAsync(app) ?? string.Empty,
RedirectUris = (await ApplicationManager.GetRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(),
PostLogoutRedirectUris = (await ApplicationManager.GetPostLogoutRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(),
Permissions = (await ApplicationManager.GetPermissionsAsync(app)).ToList(),
Requirements = (await ApplicationManager.GetRequirementsAsync(app)).ToList(),
ClientSecret = clientSecret
});
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải applications: {ex.Message}", Severity.Error);
}
finally
{
loadingApplications = false;
StateHasChanged();
}
}
private async Task LoadAvailableScopesAsync()
{
try
{
availableScopes.Clear();
await foreach (var scope in ScopeManager.ListAsync())
{
var scopeName = await ScopeManager.GetNameAsync(scope);
if (!string.IsNullOrEmpty(scopeName))
{
availableScopes.Add(scopeName);
}
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error);
}
}
private void ViewApplicationDetails(ApplicationInfo application)
{
selectedApplication = application;
showClientSecret = false;
ShowDetailsDialog = true;
}
private void CloseDetailsDialog()
{
ShowDetailsDialog = false;
selectedApplication = null;
showClientSecret = false;
}
private void ToggleClientSecretVisibility()
{
showClientSecret = !showClientSecret;
}
private async void OpenApplicationDialog(ApplicationInfo? application = null)
{
await LoadAvailableScopesAsync();
InitializePermissionChecks();
ShowApplicationDialog = true;
editingApplication = application;
ResetApplicationForm();
if (application != null)
{
applicationForm = new ApplicationForm
{
ClientId = application.ClientId,
DisplayName = application.DisplayName,
ClientType = application.ClientType,
ConsentType = application.ConsentType,
RedirectUris = new(application.RedirectUris),
PostLogoutRedirectUris = new(application.PostLogoutRedirectUris),
ClientSecret = application.ClientType == ClientTypes.Confidential ? string.Empty : null
};
foreach (var permission in application.Permissions)
{
if (permissionChecks.ContainsKey(permission)) permissionChecks[permission] = true;
else if (permission.StartsWith(Permissions.Prefixes.Scope)) customScopes.Add(permission[Permissions.Prefixes.Scope.Length..]);
}
foreach (var requirement in application.Requirements)
{
if (requirementChecks.ContainsKey(requirement)) requirementChecks[requirement] = true;
}
}
StateHasChanged();
}
private void EditApplication(ApplicationInfo application) => OpenApplicationDialog(application);
private void ResetApplicationForm()
{
applicationForm = new();
redirectUriInput = string.Empty;
postLogoutUriInput = string.Empty;
customScopeInput = string.Empty;
customScopes.Clear();
foreach (var key in permissionChecks.Keys.ToList()) permissionChecks[key] = false;
foreach (var key in requirementChecks.Keys.ToList()) requirementChecks[key] = false;
}
private void AddRedirectUri()
{
if (!string.IsNullOrWhiteSpace(redirectUriInput))
{
if (Uri.TryCreate(redirectUriInput.Trim(), UriKind.Absolute, out _))
{
if (!applicationForm.RedirectUris.Contains(redirectUriInput.Trim()))
{
applicationForm.RedirectUris.Add(redirectUriInput.Trim());
redirectUriInput = string.Empty;
StateHasChanged();
}
else
{
Snackbar.Add("URI này đã tồn tại", Severity.Warning);
}
}
else
{
Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/login-callback)", Severity.Error);
}
}
}
private void RemoveRedirectUri(string uri) => applicationForm.RedirectUris.Remove(uri);
private void AddPostLogoutUri()
{
if (!string.IsNullOrWhiteSpace(postLogoutUriInput))
{
if (Uri.TryCreate(postLogoutUriInput.Trim(), UriKind.Absolute, out _))
{
if (!applicationForm.PostLogoutRedirectUris.Contains(postLogoutUriInput.Trim()))
{
applicationForm.PostLogoutRedirectUris.Add(postLogoutUriInput.Trim());
postLogoutUriInput = string.Empty;
StateHasChanged();
}
else
{
Snackbar.Add("URI này đã tồn tại", Severity.Warning);
}
}
else
{
Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/logout-callback)", Severity.Error);
}
}
}
private void RemovePostLogoutUri(string uri) => applicationForm.PostLogoutRedirectUris.Remove(uri);
private void AddCustomScope()
{
if (!string.IsNullOrWhiteSpace(customScopeInput) && !customScopes.Contains(customScopeInput))
{
customScopes.Add(customScopeInput);
customScopeInput = string.Empty;
}
}
private void RemoveCustomScope(string scope) => customScopes.Remove(scope);
private async Task SaveApplication()
{
try
{
if (string.IsNullOrWhiteSpace(applicationForm.ClientId))
{
Snackbar.Add("Client ID là bắt buộc", Severity.Error);
return;
}
if (applicationForm.ClientType == ClientTypes.Confidential)
{
if (editingApplication == null && string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
Snackbar.Add("Client Secret là bắt buộc cho Confidential client", Severity.Error);
return;
}
}
if (editingApplication != null)
{
var existingApp = await ApplicationManager.FindByClientIdAsync(editingApplication.ClientId);
if (existingApp != null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = applicationForm.ClientId,
DisplayName = applicationForm.DisplayName,
ClientType = applicationForm.ClientType,
ConsentType = applicationForm.ConsentType
};
if (applicationForm.ClientType == ClientTypes.Confidential)
{
if (!string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
descriptor.ClientSecret = applicationForm.ClientSecret;
}
}
else if (applicationForm.ClientType == ClientTypes.Public)
{
descriptor.ClientSecret = null;
}
var currentDescriptor = new OpenIddictApplicationDescriptor();
await ApplicationManager.PopulateAsync(currentDescriptor, existingApp);
if (applicationForm.ClientType == ClientTypes.Confidential &&
string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
descriptor.ClientSecret = currentDescriptor.ClientSecret;
}
foreach (var uriString in applicationForm.RedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
else
{
Snackbar.Add($"Redirect URI không hợp lệ: {uriString}", Severity.Error);
return;
}
}
foreach (var uriString in applicationForm.PostLogoutRedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
}
else
{
Snackbar.Add($"Post Logout URI không hợp lệ: {uriString}", Severity.Error);
return;
}
}
permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key));
customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope));
requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key));
await ApplicationManager.UpdateAsync(existingApp, descriptor);
}
}
else
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = applicationForm.ClientId,
DisplayName = applicationForm.DisplayName,
ClientType = applicationForm.ClientType,
ConsentType = applicationForm.ConsentType,
ClientSecret = applicationForm.ClientType == ClientTypes.Confidential ? applicationForm.ClientSecret : null
};
foreach (var uriString in applicationForm.RedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
}
foreach (var uriString in applicationForm.PostLogoutRedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
}
}
permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key));
customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope));
requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key));
await ApplicationManager.CreateAsync(descriptor);
}
Snackbar.Add(editingApplication != null ? "Cập nhật application thành công" : "Tạo application thành công", Severity.Success);
ShowApplicationDialog = false;
await LoadApplicationsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi lưu application: {ex.Message}", Severity.Error);
}
}
private void CancelApplicationDialog()
{
ShowApplicationDialog = false;
ResetApplicationForm();
}
private async Task DeleteApplication(string clientId)
{
var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa application '{clientId}'?", yesText: "Xóa", cancelText: "Hủy");
if (confirm == true)
{
try
{
var app = await ApplicationManager.FindByClientIdAsync(clientId);
if (app != null)
{
await ApplicationManager.DeleteAsync(app);
Snackbar.Add("Xóa application thành công", Severity.Success);
await LoadApplicationsAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi xóa application: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,66 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items: center;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);
}
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.compact-table {
font-size: 0.875rem;
}
.compact-table .mud-table-cell {
padding: 8px 12px;
}
.action-buttons {
display: flex;
gap: 4px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.floating-add-btn {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.permission-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
max-height: 300px;
overflow-y: auto;
}
.uri-input-section {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
padding: 16px;
margin: 12px 0;
}

View File

@@ -0,0 +1,21 @@
@page "/Account/OpenIdDictManager"
@rendermode InteractiveServer
@using MudBlazor
<MudTabs Elevation="2" Rounded="true" Style="height:auto; max-height:100%;">
<MudTabPanel Text=" Application" Icon="@Icons.Material.Filled.Face4">
<OpenIdDictApplication />
</MudTabPanel>
<MudTabPanel Text=" Scope" Icon="@Icons.Material.Filled.Face5">
<OpenIdDictScope />
</MudTabPanel>
</MudTabs>
@code {
}

View File

@@ -0,0 +1,734 @@
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using OpenIddict.Abstractions
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using static OpenIddict.Abstractions.OpenIddictConstants
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject IOpenIddictApplicationManager ApplicationManager
@inject IOpenIddictScopeManager ScopeManager
<MudDialogProvider />
<MudSnackbarProvider />
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="padding: 1rem;">
<div class="header-gradient">
<div class="header-content">
<div style="display: flex; align-items: center;">
<MudStack Spacing="1">
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Security" Class="scope-icon" />
<MudText Typo="Typo.h4" Style="padding-right:2px; font-weight: 700;">
OpenIddict Scopes
</MudText>
</div>
<div>
<MudText Typo="Typo.subtitle2" Style="opacity: 0.7; ">
Quản lý phạm vi truy cập OAuth2 & OpenID Connect
</MudText>
</div>
</MudStack>
</div>
<div class="stats-badge">
<MudIcon Icon="@Icons.Material.Filled.Dataset" Style="margin-right: 0.5rem;" />
<MudText Typo="Typo.body1" Style="font-weight: 600;">
@filteredScopes.Count Scopes
</MudText>
</div>
</div>
</div>
<div class="scope-card">
<div style="padding: 1.5rem; border-bottom: 1px solid #e2e8f0; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<MudText Typo="Typo.h6" Style="margin: 0; color: #334155; font-weight: 600;">
Danh sách Scopes
</MudText>
<div style="display: flex; gap: 1rem; align-items: center;">
<MudButton Variant="Variant.Outlined"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="@(() => RefreshScopesAsync())"
Style="text-transform: none; font-weight: 500;">
Làm mới
</MudButton>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => OpenScopeDialog())"
Class="add-scope-btn"
Style="text-transform: none; font-weight: 500;">
Thêm Scope
</MudButton>
</div>
</div>
</div>
<MudTable Items="filteredScopes"
Class="scope-table"
Hover="true"
Dense="true"
FixedHeader="true"
Loading="@loadingScopes"
Style="background: transparent;">
<HeaderContent>
<MudTh Style="width: 30%;">Tên hiển thị</MudTh>
<MudTh Style="width: 30%;">Tên Scope</MudTh>
<MudTh Style="width: 25%;">Resources</MudTh>
<MudTh Style="width: 25%; text-align: center;">Thao tác</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Display Name">
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Style="font-weight: 500; color: #1f2937;">
@(string.IsNullOrEmpty(context.DisplayName) ? context.Name : context.DisplayName)
</MudText>
<MudText Typo="Typo.caption" Class="textid">@context.Id[..12]...</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="Name">
<MudChip T="string"
Text="@context.Name"
Size="Size.Small"
Style="background: #f3f4f6; color: #374151; font-weight: 500;" />
</MudTd>
<MudTd DataLabel="Resources">
@{
var validResources = GetValidResources(context.Resources);
}
@if (validResources.Any())
{
<MudTooltip Text="@string.Join(", ", validResources.Select(r => GetResourceDisplayName(r)))">
<MudChip T="string" Color="Color.Tertiary"
Text="@($"{validResources.Count} resource{(validResources.Count > 1 ? "s" : "")}")"
Size="Size.Small"
Class="resource-chip" />
</MudTooltip>
@if (context.Resources.Count > validResources.Count)
{
<MudTooltip Text="@($"{context.Resources.Count - validResources.Count} resource(s) không tồn tại")">
<MudChip T="string" Color="Color.Warning"
Text="@($"{context.Resources.Count - validResources.Count} invalid")"
Size="Size.Small"
Style="margin-left: 0.25rem;" />
</MudTooltip>
}
}
else if (context.Resources.Any())
{
<MudTooltip Text="Tất cả resources không tồn tại">
<MudChip T="string" Color="Color.Error"
Text="All invalid"
Size="Size.Small" />
</MudTooltip>
}
else
{
<MudText Typo="Typo.caption" Style="color: #9ca3af; font-style: italic;">
Không có resources
</MudText>
}
</MudTd>
<MudTd DataLabel="Actions">
<div class="action-buttons" style="justify-content: center;">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Style="color: #3b82f6;"
OnClick="@(() => ViewScopeDetails(context))"
aria-label="Xem chi tiết" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Style="color: #059669;"
OnClick="@(() => EditScope(context))"
aria-label="Chỉnh sửa" />
@if (HasInvalidResources(context.Resources))
{
<MudIconButton Icon="@Icons.Material.Filled.CleaningServices"
Size="Size.Small"
Style="color: #f59e0b;"
OnClick="@(() => CleanupScopeResources(context))"
aria-label="Dọn dẹp resources không hợp lệ" />
}
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Style="color: #dc2626;"
OnClick="@(() => DeleteScope(context.Name))"
aria-label="Xóa" />
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
<LoadingContent>
<div style="text-align: center; padding: 2rem;">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Typo="Typo.body1" Style="margin-top: 1rem; color: #6b7280;">
Đang tải dữ liệu...
</MudText>
</div>
</LoadingContent>
</MudTable>
</div>
<MudDialog @bind-Visible="showScopeDialog" Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true })">
<TitleContent>
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Security" Style="margin-right: 0.5rem; color: #4f46e5;" />
<MudText Typo="Typo.h6" Style="margin: 0;">
@(editingScope?.Name != null ? "Chỉnh sửa Scope" : "Thêm Scope mới")
</MudText>
</div>
</TitleContent>
<DialogContent>
<div class="dialog-content">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="scopeForm.Name"
Label="Tên Scope"
Required="true"
Variant="Variant.Outlined"
Style="background: white;" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="scopeForm.DisplayName"
Label="Tên hiển thị"
Variant="Variant.Outlined"
Style="background: white;" />
</MudItem>
<MudItem xs="12">
<div class="resource-selection">
<MudText Typo="Typo.subtitle1" Style="margin-bottom: 1rem; color: #374151; font-weight: 600;">
<MudIcon Icon="@Icons.Material.Filled.Storage" Style="margin-right: 0.5rem;" />
Chọn Resources
</MudText>
<MudSelect T="string"
Label="Chọn Resources"
Variant="Variant.Outlined"
MultiSelection="true"
SelectAll="true"
SelectAllText="Chọn tất cả"
Dense="true"
MaxHeight="250"
Style="background: white; margin-bottom: 1rem;"
SelectedValuesChanged="@OnResourceSelectionChanged"
SelectedValues="@GetSelectedResources()">
@foreach (var resource in availableResources)
{
<MudSelectItem Value="@resource.ClientId">
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Apps" Style="margin-right: 0.5rem; color: #6b7280;" />
<div>
<MudText Typo="Typo.body1">@resource.DisplayName</MudText>
<MudText Typo="Typo.caption" Style="color: #9ca3af;">@resource.ClientId</MudText>
</div>
</div>
</MudSelectItem>
}
</MudSelect>
@if (GetSelectedResources().Any())
{
<MudText Typo="Typo.subtitle2" Style="margin-bottom: 0.5rem; color: #374151;">
Resources đã chọn:
</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
@foreach (var selectedResource in GetSelectedResources())
{
var resourceInfo = availableResources.FirstOrDefault(r => r.ClientId == selectedResource);
<MudChip T="string"
Text="@(resourceInfo?.DisplayName ?? selectedResource)"
OnClose="@(() => RemoveResource(selectedResource))"
Class="selected-resource-chip"
Size="Size.Small" />
}
</div>
}
</div>
</MudItem>
</MudGrid>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="CancelScopeDialog" Style="text-transform: none;">
Hủy
</MudButton>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="SaveScope"
Style="background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); text-transform: none;">
<MudIcon Icon="@Icons.Material.Filled.Save" Style="margin-right: 0.5rem;" />
Lưu
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ShowDetailsDialog" Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true })">
<TitleContent>
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Info" Style="margin-right: 0.5rem; color: #3b82f6;" />
<MudText Typo="Typo.h6" Style="margin: 0;">Chi tiết Scope</MudText>
</div>
</TitleContent>
<DialogContent>
@if (selectedScope != null)
{
<div style="background: #f8fafc; border-radius: 12px; padding: 1.5rem;">
<MudGrid Spacing="2">
<MudItem xs="12" md="6">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #3b82f6;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">ID</MudText>
<MudText Typo="Typo.body2" Style="font-family: monospace; word-break: break-all;">@selectedScope.Id</MudText>
</div>
</MudItem>
<MudItem xs="12" md="6">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #059669;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">Tên</MudText>
<MudText Typo="Typo.body1" Style="font-weight: 500;">@selectedScope.Name</MudText>
</div>
</MudItem>
<MudItem xs="12">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #7c3aed;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">Tên hiển thị</MudText>
<MudText Typo="Typo.body1">@(string.IsNullOrEmpty(selectedScope.DisplayName) ? "Không có" : selectedScope.DisplayName)</MudText>
</div>
</MudItem>
<MudItem xs="12">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #f59e0b;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.5rem;">Resources</MudText>
<div>
@if (selectedScope.Resources.Any())
{
var validResources = GetValidResources(selectedScope.Resources);
var invalidResources = selectedScope.Resources.Except(validResources).ToList();
@if (validResources.Any())
{
<MudText Typo="Typo.caption" Style="color: #059669; margin-bottom: 0.5rem; font-weight: 600;">Resources hợp lệ:</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem;">
@foreach (var resource in validResources)
{
<MudChip T="string" Text="@GetResourceDisplayName(resource)" Size="Size.Small" Style="background: #dcfce7; color: #166534;" />
}
</div>
}
@if (invalidResources.Any())
{
<MudText Typo="Typo.caption" Style="color: #dc2626; margin-bottom: 0.5rem; font-weight: 600;">Resources không hợp lệ:</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
@foreach (var resource in invalidResources)
{
<MudChip T="string" Text="@resource" Size="Size.Small" Style="background: #fecaca; color: #991b1b;" />
}
</div>
}
}
else
{
<MudText Style="color: #9ca3af; font-style: italic;">Không có resources</MudText>
}
</div>
</div>
</MudItem>
</MudGrid>
</div>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDetailsDialog" Color="Color.Primary" Style="text-transform: none;">
<MudIcon Icon="@Icons.Material.Filled.Close" Style="margin-right: 0.5rem;" />
Đóng
</MudButton>
</DialogActions>
</MudDialog>
</MudContainer>
@code {
private string resourceInput = string.Empty;
private List<ScopeInfo> filteredScopes = new();
private List<ResourceInfo> availableResources = new();
private Dictionary<string, bool> resourceChecks = new();
private bool loadingScopes = false;
private bool showScopeDialog = false;
private bool ShowDetailsDialog = false;
private ScopeInfo? editingScope = null;
private ScopeInfo? selectedScope = null;
private ScopeForm scopeForm = new();
public class ScopeInfo
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> Resources { get; set; } = new();
public Dictionary<string, string> Properties { get; set; } = new();
}
public class ScopeForm
{
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> Resources { get; set; } = new();
}
public class ResourceInfo
{
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadAvailableResourcesAsync();
await LoadScopesAsync();
}
}
private async Task LoadAvailableResourcesAsync()
{
try
{
availableResources.Clear();
await foreach (var app in ApplicationManager.ListAsync())
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
var clientId = await ApplicationManager.GetClientIdAsync(app);
var displayName = await ApplicationManager.GetDisplayNameAsync(app);
if (!string.IsNullOrEmpty(clientId))
{
availableResources.Add(new ResourceInfo
{
ClientId = clientId,
DisplayName = string.IsNullOrEmpty(displayName) ? clientId : displayName
});
}
}
}
resourceChecks.Clear();
foreach (var resource in availableResources)
{
resourceChecks[resource.ClientId] = false;
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải resources: {ex.Message}", Severity.Error);
}
}
private List<string> GetValidResources(List<string> resources)
{
var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet();
return resources.Where(r => validResourceIds.Contains(r)).ToList();
}
private bool HasInvalidResources(List<string> resources)
{
var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet();
return resources.Any(r => !validResourceIds.Contains(r));
}
private string GetResourceDisplayName(string clientId)
{
var resource = availableResources.FirstOrDefault(r => r.ClientId == clientId);
return resource?.DisplayName ?? clientId;
}
private async Task CleanupScopeResources(ScopeInfo scope)
{
var confirm = await DialogService.ShowMessageBox(
"Xác nhận dọn dẹp",
$"Bạn có muốn xóa các resources không hợp lệ khỏi scope '{scope.Name}'?",
yesText: "Dọn dẹp",
cancelText: "Hủy"
);
if (confirm == true)
{
try
{
var existingScope = await ScopeManager.FindByNameAsync(scope.Name);
if (existingScope != null)
{
var validResources = GetValidResources(scope.Resources);
var descriptor = new OpenIddictScopeDescriptor
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description
};
foreach (var resource in validResources)
{
descriptor.Resources.Add(resource);
}
await ScopeManager.PopulateAsync(existingScope, descriptor);
await ScopeManager.UpdateAsync(existingScope);
Snackbar.Add($"Đã dọn dẹp {scope.Resources.Count - validResources.Count} resources không hợp lệ", Severity.Success);
await LoadScopesAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi dọn dẹp resources: {ex.Message}", Severity.Error);
}
}
}
private async Task RefreshScopesAsync()
{
await LoadAvailableResourcesAsync();
await LoadScopesAsync();
Snackbar.Add("Đã làm mới danh sách scopes", Severity.Success);
}
private void AddResource()
{
if (!string.IsNullOrWhiteSpace(resourceInput) && !scopeForm.Resources.Contains(resourceInput))
{
scopeForm.Resources.Add(resourceInput);
resourceInput = string.Empty;
StateHasChanged();
}
}
private void OnResourceSelectionChanged(IEnumerable<string> selectedValues)
{
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = false;
}
foreach (var value in selectedValues)
{
if (resourceChecks.ContainsKey(value))
{
resourceChecks[value] = true;
}
}
scopeForm.Resources = selectedValues.ToList();
StateHasChanged();
}
private IEnumerable<string> GetSelectedResources()
{
return resourceChecks.Where(x => x.Value).Select(x => x.Key);
}
private void RemoveResource(string resource)
{
if (resourceChecks.ContainsKey(resource))
{
resourceChecks[resource] = false;
scopeForm.Resources.Remove(resource);
StateHasChanged();
}
}
private async Task LoadScopesAsync()
{
try
{
loadingScopes = true;
filteredScopes.Clear();
await foreach (var scope in ScopeManager.ListAsync())
{
var id = await ScopeManager.GetIdAsync(scope);
var name = await ScopeManager.GetNameAsync(scope);
var displayName = await ScopeManager.GetDisplayNameAsync(scope);
var description = await ScopeManager.GetDescriptionAsync(scope);
var resources = await ScopeManager.GetResourcesAsync(scope);
var properties = await ScopeManager.GetPropertiesAsync(scope);
filteredScopes.Add(new ScopeInfo
{
Id = id ?? string.Empty,
Name = name ?? string.Empty,
DisplayName = displayName ?? string.Empty,
Description = description ?? string.Empty,
Resources = resources.ToList(),
Properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString() ?? string.Empty)
});
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error);
}
finally
{
loadingScopes = false;
StateHasChanged();
}
}
private void ViewScopeDetails(ScopeInfo scope)
{
selectedScope = scope;
ShowDetailsDialog = true;
}
private void CloseDetailsDialog()
{
ShowDetailsDialog = false;
selectedScope = null;
}
private async Task OpenScopeDialog(ScopeInfo? scope = null)
{
editingScope = scope;
ResetScopeForm();
if (scope != null)
{
scopeForm.Name = scope.Name;
scopeForm.DisplayName = scope.DisplayName;
scopeForm.Description = scope.Description;
var validResources = GetValidResources(scope.Resources);
scopeForm.Resources = new List<string>(validResources);
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = validResources.Contains(key);
}
if (scope.Resources.Count > validResources.Count)
{
var invalidCount = scope.Resources.Count - validResources.Count;
Snackbar.Add($"Đã loại bỏ {invalidCount} resource không hợp lệ khỏi form chỉnh sửa", Severity.Warning);
}
}
showScopeDialog = true;
await Task.CompletedTask;
}
private async void EditScope(ScopeInfo scope)
{
await OpenScopeDialog(scope);
}
private void ResetScopeForm()
{
scopeForm = new ScopeForm();
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = false;
}
}
private async Task SaveScope()
{
try
{
if (string.IsNullOrWhiteSpace(scopeForm.Name))
{
Snackbar.Add("Name là bắt buộc", Severity.Error);
return;
}
var descriptor = new OpenIddictScopeDescriptor
{
Name = scopeForm.Name,
DisplayName = scopeForm.DisplayName,
Description = scopeForm.Description
};
var validResources = GetValidResources(scopeForm.Resources);
foreach (var resource in validResources)
{
descriptor.Resources.Add(resource);
}
if (editingScope != null)
{
var existingScope = await ScopeManager.FindByNameAsync(editingScope.Name);
if (existingScope != null)
{
await ScopeManager.PopulateAsync(existingScope, descriptor);
await ScopeManager.UpdateAsync(existingScope);
}
}
else
{
await ScopeManager.CreateAsync(descriptor);
}
Snackbar.Add(editingScope != null ? "Cập nhật scope thành công" : "Tạo scope thành công", Severity.Success);
showScopeDialog = false;
await LoadScopesAsync();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi lưu scope: {ex.Message}", Severity.Error);
}
}
private void CancelScopeDialog()
{
showScopeDialog = false;
ResetScopeForm();
}
private async Task DeleteScope(string name)
{
var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa scope '{name}'?", yesText: "Xóa", cancelText: "Hủy");
if (confirm == true)
{
try
{
var scope = await ScopeManager.FindByNameAsync(name);
if (scope != null)
{
await ScopeManager.DeleteAsync(scope);
Snackbar.Add("Xóa scope thành công", Severity.Success);
await LoadScopesAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi xóa scope: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,142 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items: center;
}
.textid {
font-family: 'Gill Sans', 'Gill Sans MT', 'Calibri', 'Trebuchet MS', 'sans-serif';
color: #6b7280;
}
.header-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
padding: 2rem;
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
}
.header-gradient::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.header-content {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.scope-icon {
font-size: 4rem;
margin-right: 1rem;
padding-left: 1rem;
}
.stats-badge {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
}
.scope-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
overflow: hidden;
}
.scope-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.scope-table {
background: transparent;
}
.scope-table .mud-table-head {
background: #f8fafc;
border-bottom: 2px solid #e2e8f0;
}
.scope-table .mud-table-head th {
font-weight: 600;
color: #334155;
padding: 1rem 0.75rem;
}
.scope-table .mud-table-row {
border-bottom: 1px solid #f1f5f9;
transition: background-color 0.2s ease;
}
.scope-table .mud-table-row:hover {
background-color: #f8fafc;
}
.scope-table .mud-table-cell {
padding: 1rem 0.75rem;
vertical-align: middle;
}
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
.add-scope-btn {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
border: none;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
transition: all 0.3s ease;
}
.add-scope-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
}
.resource-chip {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
border: none;
font-weight: 500;
}
.dialog-content {
background: #fafbfc;
border-radius: 12px;
padding: 1.5rem;
margin: 1rem 0;
}
.resource-selection {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e2e8f0;
}
.selected-resource-chip {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
margin: 0.25rem;
}

View File

@@ -0,0 +1,285 @@
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using System.Threading
@using System.ComponentModel.DataAnnotations
@using RobotNet.IdentityServer.Services
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject PasswordStrengthService PasswordStrengthService
@inject UserManager<ApplicationUser> UserManager
@inject ISnackbar Snackbar
@inject NavigationManager NavigationManager
<MudSnackbarProvider />
<div class="password-container">
<div class="password-wrapper">
<MudCard Class="password-card" Elevation="0">
<div class="password-header">
<div class="header-content">
<MudIcon Icon="@Icons.Material.Filled.Lock" Class="header-icon" />
<div class="header-text">
<MudText Typo="Typo.h4">Đổi mật khẩu</MudText>
<MudText Typo="Typo.body1">Cập nhật mật khẩu để tăng cường bảo mật</MudText>
</div>
</div>
</div>
<MudCardContent Class="card-content">
<EditForm Model="@model" OnValidSubmit="ChangePassword" @ref="editForm">
<DataAnnotationsValidator />
<div class="form-fields">
<div class="form-group">
<MudTextField Label="Mật khẩu hiện tại"
@bind-Value="model.CurrentPassword"
InputType="@(showCurrentPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showCurrentPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showCurrentPassword = !showCurrentPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
FullWidth="true" />
<ValidationMessage For="@(() => model.CurrentPassword)" class="validation-message" />
</div>
<div class="form-group">
<MudTextField Label="Mật khẩu mới"
@bind-Value="model.NewPassword"
InputType="@(showNewPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showNewPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showNewPassword = !showNewPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
@oninput="OnNewPasswordChanged"
FullWidth="true" />
<ValidationMessage For="@(() => model.NewPassword)" class="validation-message" />
@if (!string.IsNullOrEmpty(model.NewPassword))
{
<div class="password-strength-container">
<MudText Typo="Typo.body2">Độ mạnh: @GetPasswordStrengthText()</MudText>
<MudProgressLinear Value="@GetPasswordStrength()" Color="@GetPasswordStrengthColor()" />
<div class="password-requirements">
<MudText Typo="Typo.caption">Yêu cầu:</MudText>
<div class="requirements-list">
<div class="requirement @(model.NewPassword.Length >= 6 ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Length >= 6 ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Tối thiểu 6 ký tự</span>
</div>
@* <div class="requirement @(model.NewPassword.Any(char.IsUpper) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsUpper) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Chữ hoa</span>
</div> *@
<div class="requirement @(model.NewPassword.Any(char.IsLower) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsLower) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Chữ thường</span>
</div>
@* <div class="requirement @(model.NewPassword.Any(char.IsDigit) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsDigit) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Số</span>
</div>
<div class="requirement @(model.NewPassword.Any(c => !char.IsLetterOrDigit(c)) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(c => !char.IsLetterOrDigit(c)) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Ký tự đặc biệt</span>
</div> *@
</div>
</div>
</div>
}
</div>
<div class="form-group">
<MudTextField Label="Xác nhận mật khẩu"
@bind-Value="model.ConfirmPassword"
InputType="@(showConfirmPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showConfirmPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showConfirmPassword = !showConfirmPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
FullWidth="true" />
<ValidationMessage For="@(() => model.ConfirmPassword)" class="validation-message" />
@if (!string.IsNullOrEmpty(model.NewPassword) && !string.IsNullOrEmpty(model.ConfirmPassword))
{
<MudText Color="@(model.NewPassword == model.ConfirmPassword ? Color.Success : Color.Error)">
@(model.NewPassword == model.ConfirmPassword ? "Mật khẩu khớp" : "Mật khẩu không khớp")
</MudText>
}
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-4" ShowCloseIcon="true" CloseIconClicked="() => errorMessage = string.Empty">
@errorMessage
</MudAlert>
}
</EditForm>
</MudCardContent>
<MudCardActions Class="d-flex justify-end gap-2 pb-4 px-4">
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
OnClick="Cancel"
Disabled="@isButtonDisabled"
Class="action-button"
StartIcon="@Icons.Material.Filled.Cancel">
Hủy
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SubmitForm"
Disabled="@(isButtonDisabled || isProcessing)"
Class="action-button"
StartIcon="@(isProcessing ? null : Icons.Material.Filled.Save)">
@if (isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<span class="ms-2">Đang xử lý...</span>
}
else
{
<span>Lưu</span>
}
</MudButton>
</MudCardActions>
</MudCard>
</div>
</div>
@code {
private bool isButtonDisabled = true;
private bool showCurrentPassword = false;
private bool showNewPassword = false;
private bool showConfirmPassword = false;
private ChangePasswordModel model = new();
private bool isProcessing = false;
private string errorMessage = string.Empty;
private EditForm? editForm;
private class ChangePasswordModel
{
[Required(ErrorMessage = "Vui lòng nhập mật khẩu hiện tại")]
public string CurrentPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "Vui lòng nhập mật khẩu mới")]
[StringLength(100, ErrorMessage = "Mật khẩu phải từ {2} đến {1} ký tự", MinimumLength = 8)]
public string NewPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "Vui lòng xác nhận mật khẩu mới")]
[Compare("NewPassword", ErrorMessage = "Mật khẩu xác nhận không khớp")]
public string ConfirmPassword { get; set; } = string.Empty;
}
private void EnableButtons()
{
isButtonDisabled = false;
}
private void OnNewPasswordChanged(ChangeEventArgs e)
{
model.NewPassword = e.Value?.ToString() ?? string.Empty;
StateHasChanged();
}
private async Task SubmitForm()
{
if (editForm?.EditContext?.Validate() == true)
{
await ChangePassword();
}
else
{
Snackbar.Add("Vui lòng kiểm tra lại thông tin nhập", Severity.Error);
}
}
private int GetPasswordStrength()
{
return PasswordStrengthService.EvaluatePasswordStrength(model.NewPassword);
}
private Color GetPasswordStrengthColor()
{
return PasswordStrengthService.GetStrengthColor(GetPasswordStrength());
}
private string GetPasswordStrengthText()
{
return PasswordStrengthService.GetStrengthDescription(GetPasswordStrength());
}
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState?.User?.Identity?.IsAuthenticated != true)
{
Snackbar.Add("Vui lòng đăng nhập", Severity.Error);
NavigationManager.NavigateTo("/Account/Login");
}
}
private async Task ChangePassword()
{
isProcessing = true;
errorMessage = string.Empty;
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = await UserManager.GetUserAsync(authState.User);
if (user == null)
{
Snackbar.Add("Không tìm thấy thông tin người dùng", Severity.Error);
return;
}
var result = await UserManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword);
if (result.Succeeded)
{
Snackbar.Add("Đổi mật khẩu thành công", Severity.Success);
model = new ChangePasswordModel();
isButtonDisabled = true;
}
else
{
errorMessage = string.Join(", ", result.Errors.Select(e => e.Description));
Snackbar.Add(errorMessage, Severity.Error);
}
}
catch (Exception ex)
{
errorMessage = $"Lỗi: {ex.Message}";
Snackbar.Add(errorMessage, Severity.Error);
}
finally
{
isProcessing = false;
StateHasChanged();
}
}
private void Cancel()
{
model = new ChangePasswordModel();
isButtonDisabled = true;
errorMessage = string.Empty;
StateHasChanged();
}
}

View File

@@ -0,0 +1,566 @@
.password-container {
padding: 1rem;
min-height: 90vh;
display: flex;
align-items: center;
justify-content: center;
/*background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);*/
position: relative;
overflow: hidden;
}
.password-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="20" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="80" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
pointer-events: none;
}
.password-wrapper {
width: 100%;
max-width: 650px;
position: relative;
z-index: 1;
margin: 0 auto;
}
.password-card {
border-radius: 24px;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), 0 16px 32px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6);
overflow: hidden;
width: 100%;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.password-card:hover {
transform: translateY(-8px);
box-shadow: 0 48px 96px rgba(0, 0, 0, 0.18), 0 24px 48px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.password-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem 2.5rem;
position: relative;
overflow: hidden;
}
.password-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
animation: shimmer 4s ease-in-out infinite;
}
.password-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
}
@keyframes shimmer {
0%, 100% {
transform: translateX(-100%) translateY(-100%) rotate(0deg);
}
50% {
transform: translateX(20%) translateY(20%) rotate(180deg);
}
}
.header-content {
display: flex;
align-items: center;
gap: 2.5rem;
width: 100%;
position: relative;
z-index: 2;
}
.header-icon {
font-size: 3.5rem !important;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.header-text h4 {
font-weight: 700 !important;
margin-bottom: 0.75rem !important;
color: white !important;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-size: 2.25rem !important;
line-height: 1.2;
}
.header-text .mud-typography-body1 {
color: rgba(255, 255, 255, 0.95) !important;
line-height: 1.6;
font-weight: 400;
font-size: 1.125rem;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.card-content {
padding: 3rem 2.5rem !important;
background: linear-gradient(180deg, #ffffff 0%, #fafbff 100%);
position: relative;
}
.form-fields {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.form-group {
position: relative;
width: 100%;
animation: slideInUp 0.6s ease-out;
}
.form-group:nth-child(1) {
animation-delay: 0.1s;
}
.form-group:nth-child(2) {
animation-delay: 0.2s;
}
.form-group:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Enhanced TextField Styling */
.password-field :deep(.mud-input-outlined .mud-input-root) {
border-radius: 16px !important;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(102, 126, 234, 0.2) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
min-height: 64px;
}
.password-field :deep(.mud-input-outlined:hover .mud-input-root:not(.mud-input-error)) {
border-color: #667eea !important;
background: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15);
}
.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-root:not(.mud-input-error)) {
border-color: #667eea !important;
background: white;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 8px 32px rgba(102, 126, 234, 0.2);
transform: translateY(-3px);
}
.password-field :deep(.mud-input-label) {
font-weight: 600 !important;
color: #4a5568 !important;
font-size: 1rem;
}
.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-label) {
color: #667eea !important;
}
.password-field :deep(.mud-input-control) {
padding: 0 1rem;
font-size: 1rem;
font-weight: 500;
}
.password-field :deep(.mud-input-adornment-end) {
margin-right: 0.5rem;
}
.validation-message {
color: #e53e3e;
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.5rem;
margin-left: 0.75rem;
animation: slideInUp 0.3s ease-out;
}
/* Password Strength Section */
.password-strength-container {
background: linear-gradient(135deg, #f8faff 0%, #ffffff 100%);
border-radius: 20px;
padding: 2rem;
margin-top: 1.5rem;
border: 1px solid rgba(102, 126, 234, 0.15);
animation: slideInUp 0.4s ease-out;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
}
.password-strength-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 20px 20px 0 0;
}
.password-strength-container .mud-typography-body2 {
font-weight: 600 !important;
color: #2d3748 !important;
margin-bottom: 1rem !important;
font-size: 1rem;
}
.password-strength-container :deep(.mud-progress-linear) {
height: 12px !important;
border-radius: 8px !important;
margin-bottom: 1.5rem !important;
background-color: #e2e8f0 !important;
overflow: hidden;
position: relative;
}
.password-strength-container :deep(.mud-progress-linear-bar) {
border-radius: 8px !important;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative;
}
.password-strength-container :deep(.mud-progress-linear-bar::after) {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%);
animation: shine 2s ease-in-out infinite;
}
@keyframes shine {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
.password-requirements {
margin-top: 1.5rem;
}
.password-requirements .mud-typography-caption {
font-weight: 600 !important;
color: #2d3748 !important;
margin-bottom: 1rem !important;
display: block;
font-size: 0.9375rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.requirements-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
}
.requirement {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
padding: 0.5rem 0.75rem;
border-radius: 16px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
border: 2px solid transparent;
position: relative;
overflow: hidden;
}
.requirement::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.4s ease;
}
.requirement.valid {
color: #22543d;
background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%);
border-color: #68d391;
animation: checkmark 0.5s ease-in-out;
transform: scale(1.02);
box-shadow: 0 4px 16px rgba(72, 187, 120, 0.2);
}
.requirement.valid::before {
left: 100%;
}
.requirement.invalid {
color: #718096;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-color: #e2e8f0;
}
.requirement :deep(.mud-icon-root) {
font-size: 1.25rem !important;
transition: all 0.3s ease;
}
.requirement.valid :deep(.mud-icon-root) {
animation: bounce 0.6s ease;
}
@keyframes checkmark {
0% {
transform: scale(0.8) rotate(-5deg);
opacity: 0.7;
}
50% {
transform: scale(1.1) rotate(2deg);
}
100% {
transform: scale(1.02) rotate(0deg);
opacity: 1;
}
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-4px);
}
60% {
transform: translateY(-2px);
}
}
/* Password Match Indicator */
.password-match-indicator {
margin-top: 1rem;
animation: slideInUp 0.3s ease-out;
}
.password-match-indicator .mud-typography {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9375rem;
padding: 1rem 1.25rem;
border-radius: 16px;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
}
.password-match-indicator .mud-success {
background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%);
border-color: #68d391;
color: #22543d !important;
box-shadow: 0 4px 16px rgba(72, 187, 120, 0.15);
}
.password-match-indicator .mud-error {
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%);
border-color: #fc8181;
color: #742a2a !important;
box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15);
}
.card-actions {
display:flex;
padding: 2rem 2.5rem 2.5rem !important;
background: linear-gradient(180deg, #fafbff 0%, #f4f6ff 100%);
border-top: 1px solid rgba(102, 126, 234, 0.1);
gap: 1.5rem;
justify-content: end;
}
.action-button {
min-width: 140px !important;
height: 56px !important;
border-radius: 16px !important;
font-weight: 600 !important;
font-size: 1rem !important;
text-transform: none !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
position: relative;
overflow: hidden;
}
.action-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.action-button:hover::before {
left: 100%;
}
.action-button:hover {
transform: translateY(-3px) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important;
}
.action-button:active {
transform: translateY(-1px) !important;
}
.action-button:disabled {
opacity: 0.6 !important;
transform: none !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
}
/* Error Alert */
.mud-alert {
border-radius: 16px !important;
border: 2px solid rgba(245, 101, 101, 0.2) !important;
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%) !important;
color: #742a2a !important;
font-weight: 500 !important;
box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15) !important;
margin-top: 1.5rem !important;
animation: slideInUp 0.3s ease-out;
}
/* Responsive Design */
@media (max-width: 768px) {
.password-container {
padding: 0.5rem;
}
.password-wrapper {
max-width: 100%;
}
.password-header {
padding: 2rem 1.5rem;
}
.header-content {
flex-direction: column;
gap: 1.5rem;
text-align: center;
}
.header-icon {
font-size: 2.5rem !important;
}
.header-text h4 {
font-size: 1.75rem !important;
}
.card-content {
padding: 2rem 1.5rem !important;
}
.form-fields {
gap: 2rem;
}
.password-strength-container {
padding: 1.5rem;
}
.requirements-list {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.card-actions {
padding: 1.5rem !important;
flex-direction: column;
}
.action-button {
width: 100% !important;
min-width: unset !important;
}
}
@media (max-width: 480px) {
.password-header {
padding: 1.5rem 1rem;
}
.card-content {
padding: 1.5rem 1rem !important;
}
.password-strength-container {
padding: 1rem;
}
.card-actions {
padding: 1rem !important;
}
}

View File

@@ -0,0 +1,120 @@
@page "/Account/Register"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using RobotNet.IdentityServer.Data
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject SignInManager<ApplicationUser> SignInManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject ILogger<Register> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register</PageTitle>
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<h1>Create a new account.</h1>
@if (!string.IsNullOrEmpty(errorMessage))
{
var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass" role="alert">
@errorMessage
</div>
}
<EditForm style="width:500px" Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register">
<DataAnnotationsValidator />
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3 ">
<InputText @bind-Value="Input.UserName" class="form-control" autocomplete="username" aria-required="true" placeholder="name" />
<label for="user">UserName</label>
<ValidationMessage For="() => Input.UserName" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="password">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="confirm-password">Confirm Password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</EditForm>
</div>
@code {
private string errorMessage = string.Empty;
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
public async Task RegisterUser(EditContext editContext)
{
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.UserName, CancellationToken.None);
user.NormalizedUserName = Input.UserName.ToUpperInvariant();
user.EmailConfirmed = true;
var result = await UserManager.CreateAsync(user, Input.Password);
if (!result.Succeeded)
{
identityErrors = result.Errors;
return;
}
Logger.LogInformation("User created a new account with password.");
await SignInManager.SignInAsync(user, isPersistent: false);
RedirectManager.RedirectTo(ReturnUrl);
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
}
}
private sealed class InputModel
{
[Required]
[Display(Name = "UserName")]
public string UserName { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@@ -0,0 +1,923 @@
@page "/Account/Rolemanager"
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<ApplicationRole> RoleManager
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<MudDialogProvider />
<MudSnackbarProvider />
<div class="pa-4">
<div class="role-header">
<MudGrid>
<MudItem xs="12" md="8">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.AdminPanelSettings" Size="Size.Large" Class="mr-3" />
<div>
<MudText Typo="Typo.h4" Class="mb-1">Role Management</MudText>
<MudText Typo="Typo.body1" Style="opacity: 0.9;">Quản lý vai trò và phân quyền người dùng</MudText>
</div>
</div>
</MudItem>
<MudItem xs="12" md="4" Class="text-right">
<div class="stats-card">
<MudText Typo="Typo.h5">@Roles.Count</MudText>
<MudText Typo="Typo.body2">Tổng số vai trò</MudText>
</div>
</MudItem>
</MudGrid>
</div>
<MudGrid>
<MudItem xs="12" lg="4">
<MudPaper Class="modern-card pa-4" Elevation="0">
<div class="section-title">
<MudIcon Icon="@Icons.Material.Filled.Security" />
<MudText Typo="Typo.h6">Danh Sách Vai Trò</MudText>
</div>
<div class="search-container mb-3">
<MudTextField @bind-Value="roleSearchTerm"
Label="Tìm kiếm vai trò"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Margin="Margin.Dense"
FullWidth="true" />
</div>
<div class="mb-3">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddRole"
Class="action-button"
FullWidth="true">
Tạo Vai Trò Mới
</MudButton>
</div>
<div class="table-container">
<MudTable Items="@FilteredRoles" Hover="true" Dense="true" Striped="true">
<HeaderContent>
<MudTh><MudText Typo="Typo.subtitle2">Tên Vai Trò</MudText></MudTh>
<MudTh Style="width: 100px;"><MudText Typo="Typo.subtitle2">Thao Tác</MudText></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(context.Name ?? string.Empty)" Size="Size.Small" Class="mr-2" Color="@GetRoleColor(context.Name ?? string.Empty)" />
<MudText Typo="Typo.body2">@context.Name</MudText>
</div>
</MudTd>
<MudTd>
@if (LoggedInUserRoles.Contains(context.Name ?? string.Empty))
{
<MudTooltip Text="Vai trò không thể chỉnh sửa ">
<MudIconButton Icon="@Icons.Material.Filled.Lock"
Size="Size.Small"
Color="Color.Default"
Disabled="true" />
</MudTooltip>
}
else
{
<MudTooltip Text="Chỉnh sửa">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
OnClick="() => EditRole(context)"
Class="action-button mr-1" />
</MudTooltip>
<MudTooltip Text="Xóa">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="() => DelRole(context.Id, context.Name ?? string.Empty)"
Class="action-button" />
</MudTooltip>
}
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }"
RowsPerPageString="@rowsPerPageString" />
</PagerContent>
</MudTable>
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="8">
<MudPaper Class="modern-card pa-4" Elevation="0">
<div class="section-title">
<MudIcon Icon="@Icons.Material.Filled.People" />
<MudText Typo="Typo.h6">Quản Lý Người Dùng</MudText>
</div>
<div class="search-container mb-3">
<MudTextField @bind-Value="userSearchTerm"
Label="Tìm kiếm người dùng"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Margin="Margin.Dense"
FullWidth="true" />
</div>
<div class="table-container">
<MudTable Items="@FilteredUsers" Hover="true" Dense="true" Striped="true">
<HeaderContent>
<MudTh><MudText Typo="Typo.subtitle2">Người Dùng</MudText></MudTh>
<MudTh><MudText Typo="Typo.subtitle2">Vai Trò</MudText></MudTh>
<MudTh Style="width: 100px;"><MudText Typo="Typo.subtitle2">Thao Tác</MudText></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<div class="d-flex align-center">
<div class="user-avatar">
@(context.UserName?.Substring(0, 1).ToUpper())
</div>
<div>
<MudText Typo="Typo.body2" Class="font-weight-medium">@context.UserName</MudText>
@if (context.UserId == LoggedInUserId)
{
<MudChip T="string"
Size="Size.Small"
Color="Color.Info"
Class="mt-1">
Bạn
</MudChip>
}
</div>
</div>
</MudTd>
<MudTd>
<div class="d-flex flex-wrap">
@if (context.Roles.Any())
{
foreach (var role in context.Roles)
{
<MudChip T="string"
Size="Size.Small"
Color="@GetRoleChipColor(role)"
Icon="@GetRoleIcon(role)"
Class="role-chip">
@role
</MudChip>
}
}
else
{
<MudChip T="string"
Size="Size.Small"
Color="Color.Default"
Class="role-chip">
Chưa có vai trò
</MudChip>
}
</div>
</MudTd>
<MudTd>
@if (context.UserId == LoggedInUserId)
{
<MudTooltip Text="Không thể chỉnh sửa vai trò của chính mình">
<MudIconButton Icon="@Icons.Material.Filled.Lock"
Size="Size.Small"
Color="Color.Default"
Disabled="true" />
</MudTooltip>
}
else
{
<MudTooltip Text="Quản lý vai trò">
<MudIconButton Icon="@Icons.Material.Filled.ManageAccounts"
Size="Size.Small"
Color="Color.Primary"
OnClick="() => ManageUserRoles(context.UserId ?? string.Empty, context.UserName ?? string.Empty)"
Class="action-button" />
</MudTooltip>
}
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
</MudTable>
</div>
</MudPaper>
</MudItem>
</MudGrid>
</div>
<MudDialog @bind-Visible="CreateRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Add" Class="mr-3" />
<MudText Typo="Typo.h6">Tạo Vai Trò Mới</MudText>
</div>
</TitleContent>
<DialogContent>
<MudTextField Label="Tên vai trò"
@bind-Value="NewRoleName"
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Margin="Margin.Dense"
Style="overflow:hidden;" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="CreateRole"
StartIcon="@Icons.Material.Filled.Save">
Tạo
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => CreateRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="EditRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Edit" Class="mr-3" />
<MudText Typo="Typo.h6">Chỉnh Sửa Vai Trò</MudText>
</div>
</TitleContent>
<DialogContent>
<MudTextField Label="Tên vai trò mới"
@bind-Value="EditRoleName"
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Margin="Margin.Dense" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="SaveEditRole"
StartIcon="@Icons.Material.Filled.Save">
Lưu
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => EditRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="DelRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-3" />
<MudText Typo="Typo.h6">Xác Nhận Xóa</MudText>
</div>
</TitleContent>
<DialogContent>
<MudAlert Severity="Severity.Warning" Class="mb-3">
Bạn có chắc chắn muốn xóa vai trò <strong>@RoleNameToDelete</strong>?
</MudAlert>
<MudText Typo="Typo.body2">Hành động này không thể hoàn tác.</MudText>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="ConfirmDelRole"
StartIcon="@Icons.Material.Filled.Delete">
Xóa
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => DelRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ManageUserRolesVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.ManageAccounts" Class="mr-3" />
<MudText Typo="Typo.h6">Quản Lý Vai Trò: @SelectedUserName</MudText>
</div>
</TitleContent>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudPaper Class="pa-4" Style="background: #f8fafc; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Badge" Size="Size.Small" Class="mr-2" />
Vai Trò Hiện Tại
</MudText>
<div class="d-flex flex-wrap">
@if (AssignedRoles.Any())
{
foreach (var role in AssignedRoles)
{
if (role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
<MudChip T="string"
Color="Color.Warning"
Variant="Variant.Filled"
Icon="@Icons.Material.Filled.Lock"
Class="ma-1">
@role (Được bảo vệ)
</MudChip>
}
else
{
<MudChip T="string"
Color="@GetRoleChipColor(role)"
Variant="Variant.Filled"
Icon="@GetRoleIcon(role)"
Class="ma-1">
@role
</MudChip>
}
}
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">
Chưa có vai trò nào
</MudText>
}
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Style="border: 2px dashed #e2e8f0; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3" Color="Color.Success">
<MudIcon Icon="@Icons.Material.Filled.Add" Size="Size.Small" Class="mr-2" />
Thêm Vai Trò
</MudText>
<MudSelect T="string"
Label="Chọn vai trò để thêm"
MultiSelection="true"
@bind-SelectedValues="selectedRolesToAdd"
Variant="Variant.Outlined"
Clearable="true"
Dense="true"
MaxHeight="200"
FullWidth="true">
@foreach (var role in AvailableRoles)
{
<MudSelectItem T="string" Value="@role">
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(role)" Size="Size.Small" Class="mr-2" />
@role
</div>
</MudSelectItem>
}
</MudSelect>
<MudButton Color="Color.Success"
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddSelectedRolesToUser"
Disabled="@(!selectedRolesToAdd.Any())"
Class="mt-3"
FullWidth="true">
Thêm (@selectedRolesToAdd.Count())
</MudButton>
</MudPaper>
</MudItem>
@{
var removableRoles = AssignedRoles
.Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
.ToList();
}
@if (removableRoles.Any())
{
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Style="border: 2px dashed #fed7d4; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3" Color="Color.Error">
<MudIcon Icon="@Icons.Material.Filled.Remove" Size="Size.Small" Class="mr-2" />
Xóa Vai Trò
</MudText>
<MudSelect T="string"
Label="Chọn vai trò để xóa"
MultiSelection="true"
@bind-SelectedValues="selectedRolesToRemove"
Variant="Variant.Outlined"
Clearable="true"
Dense="true"
MaxHeight="200"
FullWidth="true">
@foreach (var role in removableRoles)
{
<MudSelectItem T="string" Value="@role">
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(role)" Size="Size.Small" Class="mr-2" />
@role
</div>
</MudSelectItem>
}
</MudSelect>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Remove"
OnClick="RemoveSelectedRolesFromUser"
Disabled="@(!selectedRolesToRemove.Any())"
Class="mt-3"
FullWidth="true">
Xóa (@selectedRolesToRemove.Count())
</MudButton>
</MudPaper>
</MudItem>
}
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text"
OnClick="@(() => ManageUserRolesVisible = false)">
Đóng
</MudButton>
</DialogActions>
</MudDialog>
@code {
private bool CreateRoleVisible { get; set; }
private bool DelRoleVisible { get; set; }
private bool EditRoleVisible { get; set; }
private bool ManageUserRolesVisible { get; set; } = false;
private string CurrentRoleId { get; set; } = "";
private string EditRoleName { get; set; } = "";
private string NewRoleName { get; set; } = "";
private string RoleNameToDelete { get; set; } = "";
private string RoleIdToDelete { get; set; } = "";
private string SelectedUserName { get; set; } = string.Empty;
private string UserIdToManageRoles { get; set; } = string.Empty;
private string LoggedInUserId { get; set; } = string.Empty;
private List<string> LoggedInUserRoles { get; set; } = new();
private List<string> AllRoles { get; set; } = new List<string>();
private List<string> AvailableRoles { get; set; } = new List<string>();
private List<string> AssignedRoles { get; set; } = new List<string>();
private List<ApplicationRole> Roles { get; set; } = new List<ApplicationRole>();
private List<UserRoleModel> UsersWithRoles { get; set; } = new List<UserRoleModel>();
private IEnumerable<string> selectedRolesToAdd = new HashSet<string>();
private IEnumerable<string> selectedRolesToRemove = new HashSet<string>();
private string rowsPerPageString = "Rows:";
private string roleSearchTerm = "";
private string userSearchTerm = "";
private IEnumerable<ApplicationRole> FilteredRoles =>
string.IsNullOrWhiteSpace(roleSearchTerm)
? Roles
: Roles?.Where(r => r.Name != null && r.Name.Contains(roleSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty<ApplicationRole>();
private IEnumerable<UserRoleModel> FilteredUsers =>
string.IsNullOrWhiteSpace(userSearchTerm)
? UsersWithRoles
: UsersWithRoles?.Where(u => u.UserName != null && u.UserName.Contains(userSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty<UserRoleModel>();
private string GetRoleIcon(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Icons.Material.Filled.SupervisorAccount,
"user" => Icons.Material.Filled.Person,
"guest" => Icons.Material.Filled.PersonOutline,
_ => Icons.Material.Filled.Security
};
}
private Color GetRoleColor(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Color.Error,
"user" => Color.Primary,
"guest" => Color.Default,
_ => Color.Info
};
}
private Color GetRoleChipColor(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Color.Error,
"user" => Color.Primary,
"guest" => Color.Default,
_ => Color.Info
};
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var currentUser = authenticationState.User;
if (currentUser.Identity?.IsAuthenticated == true)
{
LoggedInUserId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var currentUserObj = await UserManager.GetUserAsync(currentUser);
if (currentUserObj != null)
{
LoggedInUserRoles = (await UserManager.GetRolesAsync(currentUserObj)).ToList();
}
Roles = await RoleManager.Roles.OrderBy(r => r.CreatedDate).ToListAsync();
await LoadAllRoles();
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add("User is not authenticated.", Severity.Error);
}
}
private async Task LoadUsersWithRoles()
{
try
{
var users = await UserManager.Users.ToListAsync();
if (users == null || !users.Any())
{
Snackbar.Add("No users found.", Severity.Error);
return;
}
var userRoleList = new List<UserRoleModel>();
foreach (var user in users)
{
var userRoles = await UserManager.GetRolesAsync(user);
userRoleList.Add(new UserRoleModel
{
UserName = user.UserName,
Roles = userRoles.ToList(),
UserId = user.Id
});
}
UsersWithRoles = userRoleList.OrderBy(u => u.UserId == LoggedInUserId ? 0 : 1).ToList();
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải người dùng và role: {ex.Message}", Severity.Error);
UsersWithRoles = new List<UserRoleModel>();
}
}
private async Task LoadAllRoles()
{
try
{
var roles = await RoleManager.Roles
.Select(role => role.Name)
.ToListAsync();
AllRoles = roles.Where(name => !string.IsNullOrEmpty(name)).Cast<string>().ToList() ?? new List<string>();
if (!AllRoles.Any())
{
Snackbar.Add("Không tìm thấy vai trò nào.", Severity.Warning);
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải danh sách vai trò: {ex.Message}", Severity.Error);
AllRoles = new List<string>();
}
}
private void AddRole()
{
CreateRoleVisible = true;
StateHasChanged();
}
private void DelRole(string roleId, string roleName)
{
RoleIdToDelete = roleId;
RoleNameToDelete = roleName;
DelRoleVisible = true;
StateHasChanged();
}
private void EditRole(ApplicationRole role)
{
if (role?.Name is not null)
{
CurrentRoleId = role.Id;
EditRoleName = role.Name;
EditRoleVisible = true;
StateHasChanged();
}
else
{
Snackbar.Add("Role information is incomplete or invalid.", Severity.Error);
}
}
private async Task ManageUserRoles(string userId, string userName)
{
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền quản lý role của user khác.", Severity.Error);
return;
}
if (!AllRoles.Any())
{
Snackbar.Add("Không có vai trò nào có thể chỉ định", Severity.Warning);
return;
}
SelectedUserName = userName;
UserIdToManageRoles = userId;
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
{
var userRoles = await UserManager.GetRolesAsync(user);
AssignedRoles = userRoles.ToList();
AvailableRoles = AllRoles
.Except(userRoles)
.Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
.ToList();
}
selectedRolesToAdd = new HashSet<string>();
selectedRolesToRemove = new HashSet<string>();
ManageUserRolesVisible = true;
StateHasChanged();
}
private async Task AddSelectedRolesToUser()
{
foreach (var role in selectedRolesToAdd.ToList())
{
await AddRoleToSelectedUser(role);
}
selectedRolesToAdd = new HashSet<string>();
StateHasChanged();
}
private async Task RemoveSelectedRolesFromUser()
{
foreach (var role in selectedRolesToRemove.ToList())
{
await RemoveRoleFromSelectedUser(role);
}
selectedRolesToRemove = new HashSet<string>();
StateHasChanged();
}
private async Task AddRoleToSelectedUser(string roleName)
{
if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
Snackbar.Add("Không thể gán role Admin cho user khác.", Severity.Error);
return;
}
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error);
return;
}
var user = await UserManager.FindByIdAsync(UserIdToManageRoles);
if (user != null)
{
var result = await UserManager.AddToRoleAsync(user, roleName);
if (result.Succeeded)
{
AssignedRoles.Add(roleName);
AvailableRoles.Remove(roleName);
selectedRolesToAdd = selectedRolesToAdd.Where(r => r != roleName);
Snackbar.Add($"Thêm role '{roleName}' cho {SelectedUserName} Thành Công.", Severity.Success);
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add($"Failed to add role '{roleName}' to user.", Severity.Error);
}
}
}
private async Task RemoveRoleFromSelectedUser(string roleName)
{
if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
Snackbar.Add("Không thể xóa role Admin của user.", Severity.Error);
return;
}
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error);
return;
}
var user = await UserManager.FindByIdAsync(UserIdToManageRoles);
if (user != null)
{
var result = await UserManager.RemoveFromRoleAsync(user, roleName);
if (result.Succeeded)
{
AssignedRoles.Remove(roleName);
AvailableRoles.Add(roleName);
selectedRolesToRemove = selectedRolesToRemove.Where(r => r != roleName);
Snackbar.Add($"Thành công xoá role '{roleName}' của {SelectedUserName}.", Severity.Success);
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add($"Failed to remove role '{roleName}' from user.", Severity.Error);
}
}
}
private async Task CreateRole()
{
if (string.IsNullOrWhiteSpace(NewRoleName))
{
Snackbar.Add(" Tên Role không được để trống.", Severity.Error);
StateHasChanged();
return;
}
var roleExist = await RoleManager.RoleExistsAsync(NewRoleName.ToUpper());
if (roleExist)
{
Snackbar.Add(" Role đã tồn tại.", Severity.Warning);
CreateRoleVisible = false;
NewRoleName = "";
StateHasChanged();
return;
}
var newRole = new ApplicationRole
{
Name = NewRoleName,
NormalizedName = NewRoleName.ToUpper(),
CreatedDate = DateTime.UtcNow
};
var result = await RoleManager.CreateAsync(newRole);
if (result.Succeeded)
{
Roles.Add(newRole);
Snackbar.Add(" Tạo Role thành công!", Severity.Success);
await LoadAllRoles();
CreateRoleVisible = false;
NewRoleName = "";
StateHasChanged();
}
else
{
Snackbar.Add(" Tạo Role thất bại.", Severity.Error);
StateHasChanged();
}
}
private async Task SaveEditRole()
{
if (string.IsNullOrWhiteSpace(EditRoleName))
{
Snackbar.Add("Tên Role không được để trống.", Severity.Error);
StateHasChanged();
return;
}
var role = await RoleManager.FindByIdAsync(CurrentRoleId);
if (role != null)
{
role.Name = EditRoleName;
role.NormalizedName = EditRoleName.ToUpper();
var result = await RoleManager.UpdateAsync(role);
if (result.Succeeded)
{
var existingRole = Roles.FirstOrDefault(r => r.Id == role.Id);
if (existingRole != null)
{
existingRole.Name = role.Name;
}
Snackbar.Add("Role đã được sửa thành công!", Severity.Success);
await LoadAllRoles();
await LoadUsersWithRoles();
EditRoleVisible = false;
EditRoleName = "";
StateHasChanged();
}
else
{
Snackbar.Add("Sửa Role thất bại.", Severity.Error);
EditRoleVisible = false;
StateHasChanged();
}
}
else
{
Snackbar.Add("Không tìm thấy role với ID đã cho.", Severity.Error);
EditRoleVisible = false;
StateHasChanged();
}
}
private async Task ConfirmDelRole()
{
if (string.IsNullOrEmpty(RoleIdToDelete))
{
Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error);
return;
}
var role = await RoleManager.FindByIdAsync(RoleIdToDelete);
if (role != null)
{
var result = await RoleManager.DeleteAsync(role);
if (result.Succeeded)
{
Snackbar.Add(" Đã xóa Role thành công.", Severity.Success);
Roles = await RoleManager.Roles
.OrderBy(r => r.CreatedDate)
.ToListAsync();
await LoadAllRoles();
await LoadUsersWithRoles();
}
else
{
Snackbar.Add(" Xóa Role thất bại.", Severity.Error);
}
}
else
{
Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error);
}
DelRoleVisible = false;
RoleIdToDelete = "";
StateHasChanged();
}
public class UserRoleModel
{
public string? UserName { get; set; }
public List<string> Roles { get; set; } = new List<string>();
public string? UserId { get; set; }
}
}

View File

@@ -0,0 +1,87 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items:center;
}
.pa-4{
overflow:hidden;
}
.role-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
}
.modern-card {
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.06);
transition: all 0.3s ease;
}
.modern-card:hover {
box-shadow: 0 8px 40px rgba(0,0,0,0.12);
transform: translateY(-2px);
}
.section-title {
font-weight: 600;
color: #2d3748;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.stats-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.role-chip {
margin: 2px;
font-size: 12px;
}
.action-button {
border-radius: 8px;
transition: all 0.2s ease;
}
.action-button:hover {
transform: scale(1.05);
}
.table-container {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.search-container {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}

View File

@@ -0,0 +1,17 @@
@page "/Account/Usermanager"
@rendermode InteractiveServer
<MudTabs Elevation="2" Rounded="true" Style="height:auto; max-height:100%; overflow:hidden">
<MudTabPanel Icon="@Icons.Material.Filled.RamenDining">
<Infor/>
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.Build" >
<Password/>
</MudTabPanel>
</MudTabs>
@code {
}

View File

@@ -0,0 +1,73 @@
.mdi {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.user-manager-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
}
.header-section {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.modern-tabs {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.modern-tabs .mud-tabs-toolbar {
background: transparent;
border-radius: 12px;
padding: 8px;
margin-bottom: 24px;
background: rgba(21, 101, 192, 0.05);
}
.modern-tabs .mud-tab {
border-radius: 8px;
margin: 0 4px;
transition: all 0.3s ease;
font-weight: 500;
}
.modern-tabs .mud-tab:hover {
background: rgba(21, 101, 192, 0.1);
transform: translateY(-2px);
}
.modern-tabs .mud-tab.mud-tab-active {
background: rgba(21, 101, 192, 0.15);
color: #1565C0;
font-weight: 600;
}
.tab-content {
animation: fadeInUp 0.5s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tab-panel {
padding: 0 !important;
}

View File

@@ -0,0 +1,2 @@
@using RobotNet.IdentityServer.Components.Account.Shared
@attribute [ExcludeFromInteractiveRouting]