734 lines
31 KiB
Plaintext
734 lines
31 KiB
Plaintext
@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);
|
|
}
|
|
}
|
|
}
|
|
} |