first commit -push
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal static class IdentityComponentsEndpointRouteBuilderExtensions
|
||||
{
|
||||
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
|
||||
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
|
||||
var accountGroup = endpoints.MapGroup("/Account");
|
||||
|
||||
accountGroup.MapPost("/Logout", async (
|
||||
ClaimsPrincipal user,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
[FromForm] string returnUrl) =>
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
return TypedResults.LocalRedirect($"~/{returnUrl}");
|
||||
});
|
||||
|
||||
return accountGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
|
||||
namespace RobotNet.IdentityServer.Components.Account
|
||||
{
|
||||
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
|
||||
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
|
||||
{
|
||||
private readonly IEmailSender emailSender = new NoOpEmailSender();
|
||||
|
||||
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
|
||||
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
|
||||
|
||||
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
|
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
|
||||
|
||||
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
|
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace RobotNet.IdentityServer.Components.Account
|
||||
{
|
||||
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
|
||||
{
|
||||
public const string StatusCookieName = "Identity.StatusMessage";
|
||||
|
||||
private static readonly CookieBuilder StatusCookieBuilder = new()
|
||||
{
|
||||
SameSite = SameSiteMode.Strict,
|
||||
HttpOnly = true,
|
||||
IsEssential = true,
|
||||
MaxAge = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
|
||||
[DoesNotReturn]
|
||||
public void RedirectTo(string? uri)
|
||||
{
|
||||
uri ??= "";
|
||||
|
||||
// Prevent open redirects.
|
||||
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
|
||||
{
|
||||
uri = navigationManager.ToBaseRelativePath(uri);
|
||||
}
|
||||
|
||||
// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
|
||||
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
|
||||
navigationManager.NavigateTo(uri);
|
||||
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
|
||||
{
|
||||
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
|
||||
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
|
||||
RedirectTo(newUri);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public void RedirectToWithStatus(string uri, string message, HttpContext context)
|
||||
{
|
||||
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
|
||||
RedirectTo(uri);
|
||||
}
|
||||
|
||||
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
|
||||
|
||||
[DoesNotReturn]
|
||||
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
|
||||
|
||||
[DoesNotReturn]
|
||||
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
|
||||
=> RedirectToWithStatus(CurrentPath, message, context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace RobotNet.IdentityServer.Components.Account
|
||||
{
|
||||
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
|
||||
// every 30 minutes an interactive circuit is connected.
|
||||
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<IdentityOptions> options)
|
||||
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
|
||||
{
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (!userManager.SupportsUserSecurityStamp)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
|
||||
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||
return principalStamp == userStamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
|
||||
namespace RobotNet.IdentityServer.Components.Account
|
||||
{
|
||||
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
|
||||
{
|
||||
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(context.User);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
}
|
||||
355
RobotNet.IdentityServer/Components/Account/Pages/Infor.razor
Normal file
355
RobotNet.IdentityServer/Components/Account/Pages/Infor.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.mdi {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-size: cover;
|
||||
margin-top:7px;
|
||||
}
|
||||
118
RobotNet.IdentityServer/Components/Account/Pages/Login.razor
Normal file
118
RobotNet.IdentityServer/Components/Account/Pages/Login.razor
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
285
RobotNet.IdentityServer/Components/Account/Pages/Password.razor
Normal file
285
RobotNet.IdentityServer/Components/Account/Pages/Password.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
120
RobotNet.IdentityServer/Components/Account/Pages/Register.razor
Normal file
120
RobotNet.IdentityServer/Components/Account/Pages/Register.razor
Normal 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; } = "";
|
||||
}
|
||||
}
|
||||
923
RobotNet.IdentityServer/Components/Account/Pages/Role.razor
Normal file
923
RobotNet.IdentityServer/Components/Account/Pages/Role.razor
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@using RobotNet.IdentityServer.Components.Account.Shared
|
||||
@attribute [ExcludeFromInteractiveRouting]
|
||||
@@ -0,0 +1,8 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
30
RobotNet.IdentityServer/Components/App.razor
Normal file
30
RobotNet.IdentityServer/Components/App.razor
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="stylesheet" href="@Assets["mud/fonts.googleapis.com.css"]" />
|
||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/css/bootstrap.min.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["lib/mdi/font/css/materialdesignicons.min.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["RobotNet.IdentityServer.styles.css"]" />
|
||||
<ImportMap @rendermode="InteractiveServer" />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
|
||||
<body >
|
||||
|
||||
|
||||
<Routes />
|
||||
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="@Assets["lib/bootstrap/js/bootstrap.bundle.min.js"]"></script>
|
||||
<script src="@Assets["lib/bootstrap/js/bootstrap.min.js"]"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
RobotNet.IdentityServer/Components/Layout/MainLayout.razor
Normal file
29
RobotNet.IdentityServer/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
@using MudBlazor
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider @rendermode="InteractiveServer" />
|
||||
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar-container">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<article class="content px-2">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
123
RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css
Normal file
123
RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,123 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f7f7f7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.content px-2{
|
||||
overflow:hidden;
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
border-radius: 15px;
|
||||
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
margin-left: 0;
|
||||
width: 80%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 270px;
|
||||
height: calc(100vh - 20px);
|
||||
position: fixed;
|
||||
top:10px;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 0.75rem !important;
|
||||
padding-right: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
214
RobotNet.IdentityServer/Components/Layout/NavMenu.razor
Normal file
214
RobotNet.IdentityServer/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,214 @@
|
||||
@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.Security.Claims
|
||||
|
||||
@implements IDisposable
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@inject RobotNet.IdentityServer.Services.IdentityService IdentityService
|
||||
@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
|
||||
@inject RoleManager<ApplicationRole> RoleManager
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="top-row px-4 py-3 bg-gradient-to-r from-blue-400 to-purple-500 flex items-center">
|
||||
<a class="navbar-brand text-white font-bold text-lg" href="">User Management</a>
|
||||
<button class="navbar-toggler md:hidden">
|
||||
<span class="mdi mdi-menu text-white text-2xl"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-scrollable">
|
||||
<nav class="nav flex-column p-2 space-y-2">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="nav-item">
|
||||
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Usermanager">
|
||||
<span class="mdi mdi-account-cog text-xl"></span>
|
||||
<span class="text-nav">User Info Management</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<AuthorizeView Roles="Administrator">
|
||||
<Authorized>
|
||||
<div class="nav-item">
|
||||
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Rolemanager">
|
||||
<span class="mdi mdi-account-cog text-xl"></span>
|
||||
<span class="text-nav">Role Manager</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="/Account/OpenIdDictManager">
|
||||
<span class="mdi mdi-database-import-outline text-xl"></span>
|
||||
<span class="text-nav">OpenIdDict</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
<AuthorizeView>
|
||||
|
||||
<NotAuthorized>
|
||||
<div class="nav-item">
|
||||
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Login">
|
||||
<span class="mdi mdi-account-arrow-right text-xl"></span>
|
||||
<span class="text-nav">Login</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Register">
|
||||
<span class="mdi mdi-account-plus text-xl"></span>
|
||||
<span class="text-nav">Register</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</nav>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="user-profile mt-auto">
|
||||
<div class="user-profile-inner">
|
||||
<div class="avatar">
|
||||
@if (string.IsNullOrEmpty(userImageUrl))
|
||||
{
|
||||
<div class="avatar-placeholder">
|
||||
<span class="mdi mdi-account text-3xl"></span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="@userImageUrl" alt="User Avatar" class="avatar-image" />
|
||||
}
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="username">@userName</div>
|
||||
<div class="user-email">@userEmail</div>
|
||||
</div>
|
||||
<form action="Account/Logout" method="post" class="logout-form">
|
||||
<AntiforgeryToken />
|
||||
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
|
||||
<button type="submit" class="logout-button" title="Logout">
|
||||
<span class=" mdi mdi-account-arrow-left text-xl"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
@code {
|
||||
private Func<Task>? _userInfoChangedHandler;
|
||||
private string cacheBuster = "";
|
||||
private string? currentUrl;
|
||||
private ApplicationUser? currentUser;
|
||||
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
|
||||
private string userName = string.Empty;
|
||||
private string userEmail = string.Empty;
|
||||
private string userImageUrl = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
||||
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
|
||||
_userInfoChangedHandler = async () =>
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadUserInfoAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
};
|
||||
UserInfoService.RegisterHandler(_userInfoChangedHandler);
|
||||
await LoadUserInfoAsync();
|
||||
}
|
||||
|
||||
private async Task UserInfoChangedHandler()
|
||||
{
|
||||
await LoadUserInfoAsync();
|
||||
|
||||
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
cacheBuster = $"?v={DateTime.Now.Ticks}";
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadUserInfoAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
|
||||
if (user?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
|
||||
currentUser = await IdentityService.GetUserByIdAsync(userId);
|
||||
if (currentUser != null)
|
||||
{
|
||||
userName = currentUser.UserName ?? string.Empty; ;
|
||||
userEmail = currentUser.Email ?? string.Empty; ;
|
||||
|
||||
if (currentUser.AvatarImage != null)
|
||||
{
|
||||
|
||||
userImageUrl = $"data:{currentUser.AvatarContentType ?? "image/jpeg"};base64,{Convert.ToBase64String(currentUser.AvatarImage)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
userImageUrl = "/uploads/avatars/anh.jpg";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading user info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
|
||||
StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
|
||||
if (UserInfoService != null && _userInfoChangedHandler != null)
|
||||
{
|
||||
UserInfoService.UnregisterHandler(_userInfoChangedHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
202
RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css
Normal file
202
RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,202 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: turquoise;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
border-radius: 15px 15px 0 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.mdi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 26px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 1.05rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #4a5568;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(79, 70, 229, 0.2);
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(79, 70, 229, 0.1);
|
||||
color: #4338ca;
|
||||
}
|
||||
.text-nav {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 3.5rem);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.user-profile {
|
||||
margin-top: auto;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background-color: #e8f0fe;
|
||||
border-radius: 0 0 15px 15px;
|
||||
}
|
||||
|
||||
.user-profile-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 0.75rem;
|
||||
background-color: rgba(79, 70, 229, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
color: #718096;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.logout-form {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #718096;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background-color: rgba(79, 70, 229, 0.1);
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100% - 3.5rem);
|
||||
overflow-y: auto;
|
||||
border-radius: 0 0 15px 15px;
|
||||
background: #e8f0fe;
|
||||
}
|
||||
}
|
||||
36
RobotNet.IdentityServer/Components/Pages/Error.razor
Normal file
36
RobotNet.IdentityServer/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
27
RobotNet.IdentityServer/Components/Pages/Home.razor
Normal file
27
RobotNet.IdentityServer/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,27 @@
|
||||
@page "/"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
<h1>Hello</h1>
|
||||
|
||||
|
||||
<AuthorizeView>
|
||||
<NotAuthorized>
|
||||
Vui lòng đăng nhập
|
||||
</NotAuthorized>
|
||||
<Authorized>
|
||||
Hello @context.User.Identity?.Name!
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
12
RobotNet.IdentityServer/Components/Routes.razor
Normal file
12
RobotNet.IdentityServer/Components/Routes.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@using RobotNet.IdentityServer.Components.Account.Shared
|
||||
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
14
RobotNet.IdentityServer/Components/_Imports.razor
Normal file
14
RobotNet.IdentityServer/Components/_Imports.razor
Normal file
@@ -0,0 +1,14 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using RobotNet.IdentityServer
|
||||
@using RobotNet.IdentityServer.Components
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
402
RobotNet.IdentityServer/Controllers/AuthorizationController.cs
Normal file
402
RobotNet.IdentityServer/Controllers/AuthorizationController.cs
Normal file
@@ -0,0 +1,402 @@
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using RobotNet.IdentityServer.Helpers;
|
||||
using System.Security.Claims;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
namespace RobotNet.IdentityServer.Controllers;
|
||||
|
||||
[EnableCors("RequestAuthorize")]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AuthorizationController(
|
||||
IOpenIddictApplicationManager applicationManager,
|
||||
IOpenIddictAuthorizationManager authorizationManager,
|
||||
IOpenIddictScopeManager scopeManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
UserManager<ApplicationUser> userManager) : ControllerBase
|
||||
{
|
||||
[HttpGet("connect/authorize")]
|
||||
[HttpPost("connect/authorize")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> Authorize()
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest() ??
|
||||
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
|
||||
|
||||
// Try to retrieve the user principal stored in the authentication cookie and redirect
|
||||
// the user agent to the login page (or to an external provider) in the following cases:
|
||||
//
|
||||
// - If the user principal can't be extracted or the cookie is too old.
|
||||
// - If prompt=login was specified by the client application.
|
||||
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
|
||||
//
|
||||
// For scenarios where the default authentication handler configured in the ASP.NET Core
|
||||
// authentication options shouldn't be used, a specific scheme can be specified here.
|
||||
var result = await HttpContext.AuthenticateAsync();
|
||||
if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login) ||
|
||||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
|
||||
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
|
||||
{
|
||||
// If the client application requested promptless authentication,
|
||||
// return an error indicating that the user is not logged in.
|
||||
if (request.HasPromptValue(PromptValues.None))
|
||||
{
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
|
||||
}));
|
||||
}
|
||||
|
||||
// To avoid endless login -> authorization redirects, the prompt=login flag
|
||||
// is removed from the authorization request payload before redirecting the user.
|
||||
var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login));
|
||||
|
||||
var parameters = Request.HasFormContentType ?
|
||||
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
|
||||
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
|
||||
|
||||
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
|
||||
|
||||
// For scenarios where the default challenge handler configured in the ASP.NET Core
|
||||
// authentication options shouldn't be used, a specific scheme can be specified here.
|
||||
return Challenge(new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve the profile of the logged in user.
|
||||
var user = await userManager.GetUserAsync(result.Principal) ??
|
||||
throw new InvalidOperationException("The user details cannot be retrieved.");
|
||||
|
||||
// Retrieve the application details from the database.
|
||||
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ??
|
||||
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
|
||||
|
||||
// Retrieve the permanent authorizations associated with the user and the calling client application.
|
||||
var authorizations = await authorizationManager.FindAsync(
|
||||
subject: await userManager.GetUserIdAsync(user),
|
||||
client: await applicationManager.GetIdAsync(application),
|
||||
status: Statuses.Valid,
|
||||
type: AuthorizationTypes.Permanent,
|
||||
scopes: request.GetScopes()).ToListAsync();
|
||||
|
||||
switch (await applicationManager.GetConsentTypeAsync(application))
|
||||
{
|
||||
// If the consent is external (e.g when authorizations are granted by a sysadmin),
|
||||
// immediately return an error if no authorization can be found in the database.
|
||||
case ConsentTypes.External when authorizations.Count is 0:
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
||||
"The logged in user is not allowed to access this client application."
|
||||
}));
|
||||
|
||||
// If the consent is implicit or if an authorization was found,
|
||||
// return an authorization response without displaying the consent form.
|
||||
case ConsentTypes.Implicit:
|
||||
case ConsentTypes.External when authorizations.Count is not 0:
|
||||
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent):
|
||||
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
|
||||
var identity = new ClaimsIdentity(
|
||||
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
|
||||
nameType: Claims.Name,
|
||||
roleType: Claims.Role);
|
||||
|
||||
// Add the claims that will be persisted in the tokens.
|
||||
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
|
||||
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
|
||||
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
|
||||
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
|
||||
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
|
||||
|
||||
// Note: in this sample, the granted scopes match the requested scope
|
||||
// but you may want to allow the user to uncheck specific scopes.
|
||||
// For that, simply restrict the list of scopes before calling SetScopes.
|
||||
identity.SetScopes(request.GetScopes());
|
||||
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
|
||||
|
||||
// Automatically create a permanent authorization to avoid requiring explicit consent
|
||||
// for future authorization or token requests containing the same scopes.
|
||||
var authorization = authorizations.LastOrDefault();
|
||||
authorization ??= await authorizationManager.CreateAsync(
|
||||
identity: identity,
|
||||
subject: await userManager.GetUserIdAsync(user),
|
||||
client: await applicationManager.GetIdAsync(application) ?? "",
|
||||
type: AuthorizationTypes.Permanent,
|
||||
scopes: identity.GetScopes());
|
||||
|
||||
identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
|
||||
identity.SetDestinations(GetDestinations);
|
||||
|
||||
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
|
||||
// At this point, no authorization was found in the database and an error must be returned
|
||||
// if the client application specified prompt=none in the authorization request.
|
||||
case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None):
|
||||
case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None):
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
||||
"Interactive user consent is required."
|
||||
}));
|
||||
|
||||
// In every other case, render the consent form.
|
||||
default:
|
||||
return Redirect($"/Account/Login/Access{Request.QueryString}&request_app={await applicationManager.GetLocalizedDisplayNameAsync(application)}&request_scope={request.Scope}");
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize, FormValueRequired("submit.Accept")]
|
||||
[HttpPost("connect/authorize"), ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Accept()
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest() ??
|
||||
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
|
||||
|
||||
// Retrieve the profile of the logged in user.
|
||||
var user = await userManager.GetUserAsync(User) ??
|
||||
throw new InvalidOperationException("The user details cannot be retrieved.");
|
||||
|
||||
// Retrieve the application details from the database.
|
||||
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ??
|
||||
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
|
||||
|
||||
// Retrieve the permanent authorizations associated with the user and the calling client application.
|
||||
var authorizations = await authorizationManager.FindAsync(
|
||||
subject: await userManager.GetUserIdAsync(user),
|
||||
client: await applicationManager.GetIdAsync(application),
|
||||
status: Statuses.Valid,
|
||||
type: AuthorizationTypes.Permanent,
|
||||
scopes: request.GetScopes()).ToListAsync();
|
||||
|
||||
// Note: the same check is already made in the other action but is repeated
|
||||
// here to ensure a malicious user can't abuse this POST-only endpoint and
|
||||
// force it to return a valid response without the external authorization.
|
||||
if (authorizations.Count is 0 && await applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
|
||||
{
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
||||
"The logged in user is not allowed to access this client application."
|
||||
}));
|
||||
}
|
||||
|
||||
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
|
||||
var identity = new ClaimsIdentity(
|
||||
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
|
||||
nameType: Claims.Name,
|
||||
roleType: Claims.Role);
|
||||
|
||||
// Add the claims that will be persisted in the tokens.
|
||||
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
|
||||
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
|
||||
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
|
||||
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
|
||||
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
|
||||
|
||||
// Note: in this sample, the granted scopes match the requested scope
|
||||
// but you may want to allow the user to uncheck specific scopes.
|
||||
// For that, simply restrict the list of scopes before calling SetScopes.
|
||||
identity.SetScopes(request.GetScopes());
|
||||
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
|
||||
|
||||
// Automatically create a permanent authorization to avoid requiring explicit consent
|
||||
// for future authorization or token requests containing the same scopes.
|
||||
var authorization = authorizations.LastOrDefault();
|
||||
authorization ??= await authorizationManager.CreateAsync(
|
||||
identity: identity,
|
||||
subject: await userManager.GetUserIdAsync(user),
|
||||
client: await applicationManager.GetIdAsync(application) ?? "",
|
||||
type: AuthorizationTypes.Permanent,
|
||||
scopes: identity.GetScopes());
|
||||
|
||||
identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
|
||||
identity.SetDestinations(GetDestinations);
|
||||
|
||||
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
|
||||
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
[Authorize, FormValueRequired("submit.Deny")]
|
||||
[HttpPost("connect/authorize"), ValidateAntiForgeryToken]
|
||||
// Notify OpenIddict that the authorization grant has been denied by the resource owner
|
||||
// to redirect the user agent to the client application using the appropriate response_mode.
|
||||
public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
|
||||
[HttpGet("connect/logout")]
|
||||
public IActionResult Logout() => Redirect($"/Account/Logout/Confirm{Request.QueryString}");
|
||||
|
||||
//[ActionName(nameof(Logout)), HttpPost("connect/logout"), ValidateAntiForgeryToken]
|
||||
[Authorize, FormValueRequired("submit.Confirm")]
|
||||
[HttpPost("connect/logout"), ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LogoutPost()
|
||||
{
|
||||
// Ask ASP.NET Core Identity to delete the local and external cookies created
|
||||
// when the user agent is redirected from the external identity provider
|
||||
// after a successful authentication flow (e.g Google or Facebook).
|
||||
await signInManager.SignOutAsync();
|
||||
|
||||
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
|
||||
// to the post_logout_redirect_uri specified by the client application or to
|
||||
// the RedirectUri specified in the authentication properties if none was set.
|
||||
return SignOut(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = "/"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
|
||||
public async Task<IActionResult> Exchange()
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest() ??
|
||||
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
|
||||
|
||||
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
|
||||
{
|
||||
// Retrieve the claims principal stored in the authorization code/refresh token.
|
||||
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
|
||||
// Retrieve the user profile corresponding to the authorization code/refresh token.
|
||||
var user = await userManager.FindByIdAsync(result.Principal?.GetClaim(Claims.Subject) ?? "");
|
||||
if (user is null)
|
||||
{
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
|
||||
}));
|
||||
}
|
||||
|
||||
// Ensure the user is still allowed to sign in.
|
||||
if (!await signInManager.CanSignInAsync(user))
|
||||
{
|
||||
return Forbid(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
|
||||
}));
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(result.Principal?.Claims,
|
||||
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
|
||||
nameType: Claims.Name,
|
||||
roleType: Claims.Role);
|
||||
|
||||
// Override the user claims present in the principal in case they
|
||||
// changed since the authorization code/refresh token was issued.
|
||||
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
|
||||
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
|
||||
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
|
||||
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
|
||||
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
|
||||
|
||||
identity.SetDestinations(GetDestinations);
|
||||
|
||||
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
|
||||
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
else if (request.IsClientCredentialsGrantType())
|
||||
{
|
||||
// Xử lý Client Credentials Flow
|
||||
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "");
|
||||
if (application == null) throw new InvalidOperationException("The application details cannot be found in the database.");
|
||||
|
||||
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
|
||||
var identity = new ClaimsIdentity(
|
||||
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
|
||||
nameType: Claims.Name,
|
||||
roleType: Claims.Role);
|
||||
|
||||
// Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
|
||||
identity.SetClaim(Claims.Subject, await applicationManager.GetClientIdAsync(application));
|
||||
identity.SetClaim(Claims.Name, await applicationManager.GetDisplayNameAsync(application));
|
||||
|
||||
// Note: In the original OAuth 2.0 specification, the client credentials grant
|
||||
// doesn't return an identity token, which is an OpenID Connect concept.
|
||||
//
|
||||
// As a non-standardized extension, OpenIddict allows returning an id_token
|
||||
// to convey information about the client application when the "openid" scope
|
||||
// is granted (i.e specified when calling principal.SetScopes()). When the "openid"
|
||||
// scope is not explicitly set, no identity token is returned to the client application.
|
||||
|
||||
// Set the list of scopes granted to the client application in access_token.
|
||||
identity.SetScopes(request.GetScopes());
|
||||
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
|
||||
identity.SetDestinations(GetDestinations);
|
||||
|
||||
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The specified grant type is not supported.");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetDestinations(Claim claim)
|
||||
{
|
||||
// Note: by default, claims are NOT automatically included in the access and identity tokens.
|
||||
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
|
||||
// whether they should be included in access tokens, in identity tokens or in both.
|
||||
|
||||
switch (claim.Type)
|
||||
{
|
||||
case Claims.Name or Claims.PreferredUsername:
|
||||
yield return Destinations.AccessToken;
|
||||
|
||||
if (claim.Subject?.HasScope(Scopes.Profile) ?? false)
|
||||
yield return Destinations.IdentityToken;
|
||||
|
||||
yield break;
|
||||
|
||||
case Claims.Email:
|
||||
yield return Destinations.AccessToken;
|
||||
|
||||
if (claim.Subject?.HasScope(Scopes.Email) ?? false)
|
||||
yield return Destinations.IdentityToken;
|
||||
|
||||
yield break;
|
||||
|
||||
case Claims.Role:
|
||||
yield return Destinations.AccessToken;
|
||||
|
||||
if (claim.Subject?.HasScope(Scopes.Roles) ?? false)
|
||||
yield return Destinations.IdentityToken;
|
||||
|
||||
yield break;
|
||||
|
||||
// Never include the security stamp in the access and identity tokens, as it's a secret value.
|
||||
case "AspNet.Identity.SecurityStamp": yield break;
|
||||
|
||||
default:
|
||||
yield return Destinations.AccessToken;
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RobotNet.IdentityServer.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
public class IdentityServerLoggerController(ILogger<IdentityServerLoggerController> Logger) : ControllerBase
|
||||
{
|
||||
private readonly string LoggerDirectory = "identityServerlogs";
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
|
||||
{
|
||||
string temp = "";
|
||||
try
|
||||
{
|
||||
string fileName = $"{date:yyyy-MM-dd}.log";
|
||||
string path = Path.Combine(LoggerDirectory, fileName);
|
||||
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(path))
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
|
||||
return [];
|
||||
}
|
||||
|
||||
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
|
||||
System.IO.File.Copy(path, temp);
|
||||
|
||||
return await System.IO.File.ReadAllLinesAsync(temp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
RobotNet.IdentityServer/Controllers/UserinfoController.cs
Normal file
63
RobotNet.IdentityServer/Controllers/UserinfoController.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
namespace RobotNet.IdentityServer.Controllers;
|
||||
|
||||
[EnableCors("RequestAuthorize")]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class UserinfoController(UserManager<ApplicationUser> userManager) : ControllerBase
|
||||
{// GET: /api/userinfo
|
||||
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
||||
[HttpGet(""), HttpPost(""), Produces("application/json")]
|
||||
public async Task<IActionResult> Userinfo()
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(User.GetClaim(Claims.Subject) ?? "");
|
||||
if (user == null)
|
||||
{
|
||||
return Challenge(
|
||||
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
||||
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
||||
"The specified access token is bound to an account that no longer exists."
|
||||
}));
|
||||
}
|
||||
|
||||
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
|
||||
{
|
||||
// Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
|
||||
[Claims.Subject] = await userManager.GetUserIdAsync(user)
|
||||
};
|
||||
|
||||
if (User.HasScope(Scopes.Email))
|
||||
{
|
||||
claims[Claims.Email] = await userManager.GetEmailAsync(user) ?? "";
|
||||
claims[Claims.EmailVerified] = await userManager.IsEmailConfirmedAsync(user);
|
||||
}
|
||||
|
||||
if (User.HasScope(Scopes.Phone))
|
||||
{
|
||||
claims[Claims.PhoneNumber] = await userManager.GetPhoneNumberAsync(user) ?? "";
|
||||
claims[Claims.PhoneNumberVerified] = await userManager.IsPhoneNumberConfirmedAsync(user);
|
||||
}
|
||||
|
||||
if (User.HasScope(Scopes.Roles))
|
||||
{
|
||||
claims[Claims.Role] = await userManager.GetRolesAsync(user);
|
||||
}
|
||||
|
||||
// Note: the complete list of standard claims supported by the OpenID Connect specification
|
||||
// can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
|
||||
return Ok(claims);
|
||||
}
|
||||
}
|
||||
13
RobotNet.IdentityServer/Data/ApplicationDbContext.cs
Normal file
13
RobotNet.IdentityServer/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RobotNet.IdentityServer.Data
|
||||
{
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
190
RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs
Normal file
190
RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OpenIddict.Abstractions;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
namespace RobotNet.IdentityServer.Data;
|
||||
|
||||
public static class ApplicationDbExtensions
|
||||
{
|
||||
public static async Task SeedApplicationDbAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
using var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||
|
||||
using var appDb = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
||||
await appDb.Database.MigrateAsync();
|
||||
//await appDb.Database.EnsureCreatedAsync();
|
||||
await appDb.SaveChangesAsync();
|
||||
|
||||
await scope.ServiceProvider.SeedRolesAsync();
|
||||
await scope.ServiceProvider.SeedUsersAsync();
|
||||
await scope.ServiceProvider.SeedOpenIddictApplicationAsync();
|
||||
await scope.ServiceProvider.SeedOpenIddictScopesAsync();
|
||||
}
|
||||
|
||||
private static async Task SeedRolesAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
var roleManager = serviceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||
if (!await roleManager.RoleExistsAsync("Administrator"))
|
||||
{
|
||||
await roleManager.CreateAsync(new ApplicationRole()
|
||||
{
|
||||
Name = "Administrator",
|
||||
NormalizedName = "ADMINISTRATOR",
|
||||
CreatedDate = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedUsersAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
using var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
if (await userManager.FindByNameAsync("admin") is null)
|
||||
{
|
||||
var admin = new ApplicationUser()
|
||||
{
|
||||
UserName = "admin",
|
||||
Email = "administrator@phenikaa-x.com",
|
||||
NormalizedUserName = "ADMINISTRATOR",
|
||||
NormalizedEmail = "ADMINISTRATOR@PHENIKAA-X.COM",
|
||||
EmailConfirmed = true,
|
||||
};
|
||||
|
||||
await userManager.CreateAsync(admin, "robotics");
|
||||
await userManager.AddToRoleAsync(admin, "Administrator");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreateIfNotExistAsync(this IOpenIddictApplicationManager manager, OpenIddictApplicationDescriptor desciptor)
|
||||
{
|
||||
if (desciptor.ClientId == null) return;
|
||||
if (await manager.FindByClientIdAsync(desciptor.ClientId) == null)
|
||||
{
|
||||
await manager.CreateAsync(desciptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedOpenIddictApplicationAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
var manager = serviceProvider.GetRequiredService<IOpenIddictApplicationManager>();
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
|
||||
{
|
||||
ClientId = "robotnet-webapp",
|
||||
ConsentType = ConsentTypes.Explicit,
|
||||
DisplayName = "RobotNet WebApp",
|
||||
ClientType = ClientTypes.Public,
|
||||
PostLogoutRedirectUris =
|
||||
{
|
||||
new Uri("https://localhost:7035/authentication/logout-callback")
|
||||
},
|
||||
RedirectUris =
|
||||
{
|
||||
new Uri("https://localhost:7035/authentication/login-callback")
|
||||
},
|
||||
Permissions =
|
||||
{
|
||||
Permissions.Endpoints.Authorization,
|
||||
Permissions.Endpoints.EndSession,
|
||||
Permissions.Endpoints.Token,
|
||||
Permissions.GrantTypes.AuthorizationCode,
|
||||
Permissions.GrantTypes.RefreshToken,
|
||||
Permissions.ResponseTypes.Code,
|
||||
Permissions.Scopes.Email,
|
||||
Permissions.Scopes.Profile,
|
||||
Permissions.Scopes.Roles,
|
||||
Permissions.Prefixes.Scope + "robotnet-script-api",
|
||||
Permissions.Prefixes.Scope + "robotnet-robot-api",
|
||||
Permissions.Prefixes.Scope + "robotnet-map-api",
|
||||
},
|
||||
Requirements =
|
||||
{
|
||||
Requirements.Features.ProofKeyForCodeExchange,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
|
||||
{
|
||||
ClientId = "robotnet-script-manager",
|
||||
ClientSecret = "05594ECB-BBAE-4246-8EED-4F0841C3B475",
|
||||
Permissions =
|
||||
{
|
||||
Permissions.Endpoints.Introspection,
|
||||
Permissions.GrantTypes.ClientCredentials,
|
||||
Permissions.Endpoints.Token,
|
||||
Permissions.Prefixes.Scope + "robotnet-robot-api",
|
||||
Permissions.Prefixes.Scope + "robotnet-map-api",
|
||||
}
|
||||
});
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
|
||||
{
|
||||
ClientId = "robotnet-map-manager",
|
||||
ClientSecret = "72B36E68-2F2B-455B-858A-77B1DCC79979",
|
||||
Permissions =
|
||||
{
|
||||
Permissions.Endpoints.Introspection,
|
||||
}
|
||||
});
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
|
||||
{
|
||||
ClientId = "robotnet-robot-manager",
|
||||
ClientSecret = "469B2DEB-660E-4C91-97C7-D69550D9969D",
|
||||
Permissions =
|
||||
{
|
||||
Permissions.Endpoints.Introspection,
|
||||
Permissions.GrantTypes.ClientCredentials,
|
||||
Permissions.Endpoints.Token,
|
||||
Permissions.Prefixes.Scope + "robotnet-map-api",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task CreateIfNotExistAsync(this IOpenIddictScopeManager manager, OpenIddictScopeDescriptor desciptor)
|
||||
{
|
||||
if (desciptor.Name == null) return;
|
||||
if (await manager.FindByNameAsync(desciptor.Name) is null)
|
||||
{
|
||||
await manager.CreateAsync(desciptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedOpenIddictScopesAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
var manager = serviceProvider.GetRequiredService<IOpenIddictScopeManager>();
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
|
||||
{
|
||||
DisplayName = "RobotNet Script Manager API Access",
|
||||
Name = "robotnet-script-api",
|
||||
Resources =
|
||||
{
|
||||
"robotnet-script-manager"
|
||||
}
|
||||
});
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
|
||||
{
|
||||
DisplayName = "RobotNet Map Manager API Access",
|
||||
Name = "robotnet-map-api",
|
||||
Resources =
|
||||
{
|
||||
"robotnet-map-manager"
|
||||
}
|
||||
});
|
||||
|
||||
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
|
||||
{
|
||||
DisplayName = "RobotNet Robot Manager API Access",
|
||||
Name = "robotnet-robot-api",
|
||||
Resources =
|
||||
{
|
||||
"robotnet-robot-manager"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
9
RobotNet.IdentityServer/Data/ApplicationRole.cs
Normal file
9
RobotNet.IdentityServer/Data/ApplicationRole.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace RobotNet.IdentityServer.Data
|
||||
{
|
||||
public class ApplicationRole : IdentityRole
|
||||
{
|
||||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
13
RobotNet.IdentityServer/Data/ApplicationUser.cs
Normal file
13
RobotNet.IdentityServer/Data/ApplicationUser.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace RobotNet.IdentityServer.Data
|
||||
{
|
||||
// Add profile data for application users by adding properties to the ApplicationUser class
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public string FullName { get; set; } = "";
|
||||
public byte[]? AvatarImage { get; set; }
|
||||
public string AvatarContentType { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
540
RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs
generated
Normal file
540
RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs
generated
Normal file
@@ -0,0 +1,540 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.IdentityServer.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20250716085859_InitializeApplicationDb")]
|
||||
partial class InitializeApplicationDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClientType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ConsentType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("JsonWebKeySet")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PostLogoutRedirectUris")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RedirectUris")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Requirements")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Settings")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId")
|
||||
.IsUnique()
|
||||
.HasFilter("[ClientId] IS NOT NULL");
|
||||
|
||||
b.ToTable("OpenIddictApplications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime?>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Scopes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("nvarchar(400)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictAuthorizations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Descriptions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Resources")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasFilter("[Name] IS NOT NULL");
|
||||
|
||||
b.ToTable("OpenIddictScopes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("AuthorizationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime?>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("RedemptionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("nvarchar(400)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorizationId");
|
||||
|
||||
b.HasIndex("ReferenceId")
|
||||
.IsUnique()
|
||||
.HasFilter("[ReferenceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AvatarContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<byte[]>("AvatarImage")
|
||||
.HasColumnType("varbinary(max)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("FullName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
|
||||
.WithMany("Authorizations")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.Navigation("Application");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
|
||||
{
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("AuthorizationId");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("Authorization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
|
||||
{
|
||||
b.Navigation("Authorizations");
|
||||
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.IdentityServer.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitializeApplicationDb : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
FullName = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
AvatarImage = table.Column<byte[]>(type: "varbinary(max)", nullable: true),
|
||||
AvatarContentType = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictApplications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ApplicationType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
ClientId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
ClientSecret = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClientType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
ConsentType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
JsonWebKeySet = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Permissions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PostLogoutRedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
RedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Requirements = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Settings = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Descriptions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Resources = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictAuthorizations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Scopes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
|
||||
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
AuthorizationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ExpirationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Payload = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
RedemptionDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ReferenceId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
|
||||
Type = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
|
||||
column: x => x.AuthorizationId,
|
||||
principalTable: "OpenIddictAuthorizations",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true,
|
||||
filter: "[NormalizedName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true,
|
||||
filter: "[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictApplications_ClientId",
|
||||
table: "OpenIddictApplications",
|
||||
column: "ClientId",
|
||||
unique: true,
|
||||
filter: "[ClientId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
|
||||
table: "OpenIddictAuthorizations",
|
||||
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictScopes_Name",
|
||||
table: "OpenIddictScopes",
|
||||
column: "Name",
|
||||
unique: true,
|
||||
filter: "[Name] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type",
|
||||
table: "OpenIddictTokens",
|
||||
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_AuthorizationId",
|
||||
table: "OpenIddictTokens",
|
||||
column: "AuthorizationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_ReferenceId",
|
||||
table: "OpenIddictTokens",
|
||||
column: "ReferenceId",
|
||||
unique: true,
|
||||
filter: "[ReferenceId] IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictAuthorizations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictApplications");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.IdentityServer.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClientType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ConsentType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("JsonWebKeySet")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PostLogoutRedirectUris")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RedirectUris")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Requirements")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Settings")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId")
|
||||
.IsUnique()
|
||||
.HasFilter("[ClientId] IS NOT NULL");
|
||||
|
||||
b.ToTable("OpenIddictApplications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime?>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Scopes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("nvarchar(400)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictAuthorizations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Descriptions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DisplayNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Resources")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasFilter("[Name] IS NOT NULL");
|
||||
|
||||
b.ToTable("OpenIddictScopes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("AuthorizationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime?>("CreationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("RedemptionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("nvarchar(400)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorizationId");
|
||||
|
||||
b.HasIndex("ReferenceId")
|
||||
.IsUnique()
|
||||
.HasFilter("[ReferenceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AvatarContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<byte[]>("AvatarImage")
|
||||
.HasColumnType("varbinary(max)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("FullName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
|
||||
.WithMany("Authorizations")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.Navigation("Application");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
|
||||
{
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("AuthorizationId");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("Authorization");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
|
||||
{
|
||||
b.Navigation("Authorizations");
|
||||
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
|
||||
{
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
56
RobotNet.IdentityServer/Dockerfile
Normal file
56
RobotNet.IdentityServer/Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
||||
FROM alpine:3.22 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["RobotNet.IdentityServer/RobotNet.IdentityServer.csproj", "RobotNet.IdentityServer/"]
|
||||
COPY ["RobotNet.IdentityServer/libman.json", "RobotNet.IdentityServer/"]
|
||||
COPY ["RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj", "RobotNet.ServiceDefaults/"]
|
||||
|
||||
# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj"
|
||||
RUN dotnet restore "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj"
|
||||
|
||||
WORKDIR /src/RobotNet.IdentityServer
|
||||
RUN dotnet tool install -g Microsoft.Web.LibraryManager.Cli
|
||||
ENV PATH="${PATH}:/root/.dotnet/tools"
|
||||
# RUN libman restore
|
||||
|
||||
WORKDIR /src
|
||||
COPY RobotNet.IdentityServer/ RobotNet.IdentityServer/
|
||||
COPY RobotNet.ServiceDefaults/ RobotNet.ServiceDefaults/
|
||||
|
||||
RUN rm -rf ./RobotNet.IdentityServer/bin
|
||||
RUN rm -rf ./RobotNet.IdentityServer/obj
|
||||
RUN rm -rf ./RobotNet.ServiceDefaults/bin
|
||||
RUN rm -rf ./RobotNet.ServiceDefaults/obj
|
||||
|
||||
WORKDIR "/src/RobotNet.IdentityServer"
|
||||
RUN dotnet build -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
WORKDIR /src/RobotNet.IdentityServer
|
||||
RUN dotnet publish "RobotNet.IdentityServer.csproj" \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--runtime linux-musl-x64 \
|
||||
--self-contained true \
|
||||
/p:PublishTrimmed=false \
|
||||
/p:PublishReadyToRun=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish ./
|
||||
|
||||
RUN apk add --no-cache icu-libs tzdata ca-certificates
|
||||
|
||||
RUN echo '#!/bin/sh' >> ./start.sh
|
||||
RUN echo 'update-ca-certificates' >> ./start.sh
|
||||
RUN echo 'exec ./RobotNet.IdentityServer' >> ./start.sh
|
||||
|
||||
RUN chmod +x ./RobotNet.IdentityServer
|
||||
RUN chmod +x ./start.sh
|
||||
|
||||
# Use the start script to ensure certificates are updated before starting the application
|
||||
EXPOSE 443
|
||||
ENTRYPOINT ["./start.sh"]
|
||||
21
RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs
Normal file
21
RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace RobotNet.IdentityServer.Helpers;
|
||||
|
||||
public static class AsyncEnumerableExtensions
|
||||
{
|
||||
public static Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> source)
|
||||
{
|
||||
return source == null ? throw new ArgumentNullException(nameof(source)) : ExecuteAsync();
|
||||
|
||||
async Task<List<T>> ExecuteAsync()
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
await foreach (var element in source)
|
||||
{
|
||||
list.Add(element);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||
|
||||
namespace RobotNet.IdentityServer.Helpers;
|
||||
|
||||
public sealed class FormValueRequiredAttribute(string name) : ActionMethodSelectorAttribute
|
||||
{
|
||||
private readonly string _name = name;
|
||||
|
||||
public override bool IsValidForRequest(RouteContext context, ActionDescriptor action)
|
||||
{
|
||||
if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]);
|
||||
}
|
||||
}
|
||||
196
RobotNet.IdentityServer/Program.cs
Normal file
196
RobotNet.IdentityServer/Program.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using BlazorComponentBus;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MudBlazor.Services;
|
||||
using NLog.Web;
|
||||
using Quartz;
|
||||
using RobotNet.IdentityServer.Components;
|
||||
using RobotNet.IdentityServer.Components.Account;
|
||||
using RobotNet.IdentityServer.Components.Layout;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using RobotNet.IdentityServer.Services;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseNLog();
|
||||
// builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddMudServices();
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddScoped<ComponentBus>();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddScoped<NavMenu>();
|
||||
builder.Services.AddSingleton<PasswordStrengthService>();
|
||||
builder.Services.AddSingleton<UserInfoService>();
|
||||
builder.Services.AddScoped<UserImageService>();
|
||||
builder.Services.AddScoped<IdentityService>();
|
||||
builder.Services.AddScoped<IdentityUserAccessor>();
|
||||
builder.Services.AddScoped<IdentityRedirectManager>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
options.UseSqlServer(connectionString);
|
||||
options.UseOpenIddict();
|
||||
});
|
||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
|
||||
{
|
||||
options.SignIn.RequireConfirmedAccount = true;
|
||||
options.Lockout.AllowedForNewUsers = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireDigit = false;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
|
||||
|
||||
|
||||
builder.Services.AddQuartz(options =>
|
||||
{
|
||||
options.UseSimpleTypeLoader();
|
||||
options.UseInMemoryStore();
|
||||
});
|
||||
|
||||
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
.AddCore(options =>
|
||||
{
|
||||
// Configure OpenIddict to use the Entity Framework Core stores and models.
|
||||
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
|
||||
options.UseEntityFrameworkCore()
|
||||
.UseDbContext<ApplicationDbContext>();
|
||||
|
||||
// Enable Quartz.NET integration.
|
||||
options.UseQuartz();
|
||||
})
|
||||
.AddServer(options =>
|
||||
{
|
||||
options.SetIssuer(builder.Configuration["OpenIddictCertificate:Issuer"] ?? throw new InvalidOperationException("OpenIddictCertificate Issuer is not configured."));
|
||||
|
||||
// Enable the authorization, logout, token and userinfo endpoints.
|
||||
options.SetAuthorizationEndpointUris("api/Authorization/connect/authorize")
|
||||
.SetEndSessionEndpointUris("api/Authorization/connect/logout")
|
||||
.SetIntrospectionEndpointUris("connect/introspect")
|
||||
.SetTokenEndpointUris("api/Authorization/connect/token")
|
||||
.AllowClientCredentialsFlow()
|
||||
.SetUserInfoEndpointUris("api/Userinfo")
|
||||
.SetEndUserVerificationEndpointUris("connect/verify");
|
||||
|
||||
// Mark the "email", "profile" and "roles" scopes as supported scopes.
|
||||
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
|
||||
|
||||
// Note: this sample only uses the authorization code and refresh token
|
||||
// flows but you can enable the other flows if you need to support
|
||||
// implicit, password or client credentials.
|
||||
options.AllowAuthorizationCodeFlow()
|
||||
.AllowRefreshTokenFlow()
|
||||
.AllowClientCredentialsFlow();
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
// Register the signing and encryption credentials.
|
||||
options.AddDevelopmentEncryptionCertificate()
|
||||
.AddDevelopmentSigningCertificate();
|
||||
|
||||
// Thêm ephemeral encryption key
|
||||
//options.AddEphemeralEncryptionKey()
|
||||
// .AddEphemeralSigningKey(); // Thêm signing key tạm thời
|
||||
}
|
||||
else if (builder.Environment.IsProduction())
|
||||
{
|
||||
// Thêm ephemeral encryption key
|
||||
// Sử dụng chứng chỉ thực tế
|
||||
var path = builder.Configuration["OpenIddictCertificate:Path"] ?? throw new InvalidOperationException("Certificate path is not configured.");
|
||||
var password = builder.Configuration["OpenIddictCertificate:Password"] ?? throw new InvalidOperationException("Certificate password is not configured.");
|
||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
throw new InvalidOperationException("Certificate path or password is not configured.");
|
||||
}
|
||||
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile(path, password);
|
||||
options.AddEncryptionCertificate(certificate)
|
||||
.AddSigningCertificate(certificate);
|
||||
}
|
||||
|
||||
options.UseDataProtection()
|
||||
.PreferDefaultAccessTokenFormat()
|
||||
.PreferDefaultAuthorizationCodeFormat()
|
||||
.PreferDefaultRefreshTokenFormat();
|
||||
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
|
||||
options.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableEndSessionEndpointPassthrough()
|
||||
.EnableTokenEndpointPassthrough()
|
||||
.EnableUserInfoEndpointPassthrough()
|
||||
.EnableStatusCodePagesIntegration();
|
||||
// Can thiệp vào sự kiện logging
|
||||
})
|
||||
.AddValidation(options =>
|
||||
{
|
||||
// Import the configuration from the local OpenIddict server instance.
|
||||
options.UseLocalServer();
|
||||
|
||||
// Register the ASP.NET Core host.
|
||||
options.UseAspNetCore();
|
||||
});
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("RequestAuthorize", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
await app.Services.SeedApplicationDbAsync();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseMigrationsEndPoint();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors("RequestAuthorize");
|
||||
|
||||
app.MapControllers();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.MapAdditionalIdentityEndpoints();
|
||||
|
||||
app.Run();
|
||||
15
RobotNet.IdentityServer/Properties/launchSettings.json
Normal file
15
RobotNet.IdentityServer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"workingDirectory": "$(TargetDir)",
|
||||
"applicationUrl": "https://localhost:7061",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mssql1": {
|
||||
"type": "mssql",
|
||||
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mssql1": {
|
||||
"type": "mssql.local",
|
||||
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
RobotNet.IdentityServer/RobotNet.IdentityServer.csproj
Normal file
41
RobotNet.IdentityServer/RobotNet.IdentityServer.csproj
Normal file
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-RobotNet.IdentityServer-e398adbb-379f-421d-8396-f36f060aca5f</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlazorComponentBus" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MudBlazor" Version="8.11.0" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.0.0" />
|
||||
<PackageReference Include="OpenIddict.Quartz" Version="7.0.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="NLog" Version="6.0.3" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="nlog.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
69
RobotNet.IdentityServer/Services/IdentityService.cs
Normal file
69
RobotNet.IdentityServer/Services/IdentityService.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// IdentityService.cs - Tạo dịch vụ này để tránh lỗi DbContext
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RobotNet.IdentityServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RobotNet.IdentityServer.Services;
|
||||
public class IdentityService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public IdentityService(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser?> GetUserByIdAsync(string userId)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
|
||||
var user = await userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser?> GetUserByNameAsync(string userName)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
return await userManager.FindByNameAsync(userName);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetUserRolesAsync(ApplicationUser user)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
return roles.ToList();
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> UpdateUserAsync(ApplicationUser user)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
|
||||
|
||||
var existingUser = await userManager.FindByIdAsync(user.Id);
|
||||
if (existingUser != null)
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
context.Entry(existingUser).State = EntityState.Detached;
|
||||
}
|
||||
|
||||
return await userManager.UpdateAsync(user);
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
return await userManager.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
RobotNet.IdentityServer/Services/PasswordStrengthService.cs
Normal file
61
RobotNet.IdentityServer/Services/PasswordStrengthService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using MudBlazor;
|
||||
|
||||
namespace RobotNet.IdentityServer.Services;
|
||||
|
||||
public class PasswordStrengthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Đánh giá độ mạnh của mật khẩu (thang điểm 0-100)
|
||||
/// </summary>
|
||||
/// <param name="password">Mật khẩu cần đánh giá</param>
|
||||
/// <returns>Điểm đánh giá từ 0-100</returns>
|
||||
public int EvaluatePasswordStrength(string password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password))
|
||||
return 0;
|
||||
|
||||
int strength = 0;
|
||||
|
||||
// Đánh giá dựa trên độ dài
|
||||
if (password.Length >= 1) strength += 5;
|
||||
if (password.Length >= 3) strength += 5;
|
||||
if (password.Length >= 6) strength += 10;
|
||||
if (password.Length >= 8) strength += 10;
|
||||
if (password.Length >= 10) strength += 10;
|
||||
|
||||
// Đánh giá dựa trên độ phức tạp
|
||||
if (password.Any(char.IsUpper)) strength += 15;
|
||||
if (password.Any(char.IsLower)) strength += 15;
|
||||
if (password.Any(char.IsDigit)) strength += 15;
|
||||
if (password.Any(c => !char.IsLetterOrDigit(c))) strength += 15;
|
||||
|
||||
return System.Math.Min(strength, 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy màu tương ứng với độ mạnh của mật khẩu
|
||||
/// </summary>
|
||||
/// <param name="strength">Điểm đánh giá độ mạnh (0-100)</param>
|
||||
/// <returns>Color tương ứng</returns>
|
||||
public Color GetStrengthColor(int strength)
|
||||
{
|
||||
if (strength < 30) return Color.Error;
|
||||
if (strength < 60) return Color.Warning;
|
||||
if (strength < 80) return Color.Info;
|
||||
return Color.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy mô tả tương ứng với độ mạnh của mật khẩu
|
||||
/// </summary>
|
||||
/// <param name="strength">Điểm đánh giá độ mạnh (0-100)</param>
|
||||
/// <returns>Mô tả dạng văn bản</returns>
|
||||
public string GetStrengthDescription(int strength)
|
||||
{
|
||||
if (strength == 0) return "Chưa nhập mật khẩu";
|
||||
if (strength < 30) return "Mật khẩu yếu";
|
||||
if (strength < 60) return "Mật khẩu trung bình";
|
||||
if (strength < 80) return "Mật khẩu tốt";
|
||||
return "Mật khẩu mạnh";
|
||||
}
|
||||
}
|
||||
26
RobotNet.IdentityServer/Services/UserImageService.cs
Normal file
26
RobotNet.IdentityServer/Services/UserImageService.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
|
||||
namespace RobotNet.IdentityServer.Services;
|
||||
|
||||
public class UserImageService
|
||||
{
|
||||
public async Task<(byte[] ImageBytes, string ContentType)> ResizeAndConvertAsync(Stream input)
|
||||
{
|
||||
using var image = await Image.LoadAsync(input);
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(300, 300),
|
||||
Mode = ResizeMode.Crop
|
||||
}));
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = 90 });
|
||||
|
||||
return (ms.ToArray(), "image/jpeg");
|
||||
}
|
||||
}
|
||||
41
RobotNet.IdentityServer/Services/UserInfoService.cs
Normal file
41
RobotNet.IdentityServer/Services/UserInfoService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace RobotNet.IdentityServer.Services;
|
||||
|
||||
public class UserInfoService
|
||||
{
|
||||
|
||||
private readonly List<Func<Task>> _handlers = [];
|
||||
|
||||
|
||||
public void RegisterHandler(Func<Task> handler)
|
||||
{
|
||||
if (handler != null && !_handlers.Contains(handler))
|
||||
{
|
||||
_handlers.Add(handler);
|
||||
}
|
||||
}
|
||||
public void UnregisterHandler(Func<Task> handler)
|
||||
{
|
||||
if (handler != null && _handlers.Contains(handler))
|
||||
{
|
||||
_handlers.Remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task NotifyUserInfoChanged()
|
||||
{
|
||||
var handlers = new List<Func<Task>>(_handlers);
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in user info change handler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
RobotNet.IdentityServer/appsettings.json
Normal file
18
RobotNet.IdentityServer/appsettings.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=172.20.235.170;Database=RobotNet.Identity;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"OpenIddictCertificate": {
|
||||
"Issuer": "https://localhost:7061",
|
||||
"Path": "/app/certs/robotnet.pfx",
|
||||
"Password": "RobotNet@2024"
|
||||
}
|
||||
}
|
||||
15
RobotNet.IdentityServer/libman.json
Normal file
15
RobotNet.IdentityServer/libman.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "3.0",
|
||||
"defaultProvider": "cdnjs",
|
||||
"libraries": [
|
||||
{
|
||||
"library": "bootstrap@5.3.3",
|
||||
"destination": "wwwroot/lib/bootstrap/"
|
||||
},
|
||||
{
|
||||
"provider": "jsdelivr",
|
||||
"library": "@mdi/font@7.4.47",
|
||||
"destination": "wwwroot/lib/mdi/font/"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
RobotNet.IdentityServer/nlog.config
Normal file
25
RobotNet.IdentityServer/nlog.config
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
autoReload="true">
|
||||
|
||||
<extensions>
|
||||
<add assembly="NLog.Web.AspNetCore"/>
|
||||
</extensions>
|
||||
|
||||
<targets>
|
||||
<target xsi:type="File" name="identityLogFile" fileName="${basedir}/identityServerlogs/${shortdate}.log" maxArchiveFiles="90" archiveEvery="Day" >
|
||||
<layout type='JsonLayout'>
|
||||
<attribute name='time' layout='${date:format=HH\:mm\:ss.ffff}' />
|
||||
<attribute name='level' layout='${level:upperCase=true}'/>
|
||||
<attribute name='logger' layout='${logger}' />
|
||||
<attribute name='message' layout='${message}' />
|
||||
<attribute name='exception' layout='${exception:format=tostring}' />
|
||||
</layout>
|
||||
</target>
|
||||
</targets>
|
||||
<rules>
|
||||
<logger name="OpenIddict.*" minlevel="Debug" writeto="identityLogFile" />
|
||||
<logger name="RobotNet.IdentityServer.*" minlevel="Debug" writeto="identityLogFile" />
|
||||
</rules>
|
||||
</nlog>
|
||||
60
RobotNet.IdentityServer/wwwroot/app.css
Normal file
60
RobotNet.IdentityServer/wwwroot/app.css
Normal file
@@ -0,0 +1,60 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
5
RobotNet.IdentityServer/wwwroot/favicon.svg
Normal file
5
RobotNet.IdentityServer/wwwroot/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path d="M31.81,1.79,21.29,14.32l-2.48-3a2.6,2.6,0,0,1,0-3.34l4.45-5.31a2.64,2.64,0,0,1,2-.93Z" fill="#e06e2e"/>
|
||||
<path d="M.19,1.8,10.71,14.34l2.48-2.95a2.6,2.6,0,0,0,0-3.34L8.75,2.74a2.66,2.66,0,0,0-2-.94Z" fill="#e06e2e"/>
|
||||
<path d="M32,30.21H25.36a2.61,2.61,0,0,1-2-.92L16.5,21.1a.65.65,0,0,0-1,0L8.63,29.29a2.58,2.58,0,0,1-2,.92H0L12.07,15.83,14,13.57a2.67,2.67,0,0,1,4.08,0l1.89,2.26Z" fill="#233871"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
81
RobotNet.IdentityServer/wwwroot/mud/fonts.googleapis.com.css
Normal file
81
RobotNet.IdentityServer/wwwroot/mud/fonts.googleapis.com.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||
}
|
||||
/* symbols */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
RobotNet.IdentityServer/wwwroot/uploads/avatars/anh.jpg
Normal file
BIN
RobotNet.IdentityServer/wwwroot/uploads/avatars/anh.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
Reference in New Issue
Block a user