RobotNet/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor
2025-10-15 15:15:53 +07:00

982 lines
43 KiB
Plaintext

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