982 lines
43 KiB
Plaintext
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);
|
|
}
|
|
}
|
|
}
|
|
} |