using IdentityServer8.EntityFramework.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.EntityFrameworkCore;
using Yavsc.Server.Helpers;
namespace Yavsc.Controllers;
///
/// Partial class that adds the per-collection edit pages for an OAuth2
/// client. See ClientController.cs for the scalar edit flow and
/// the seed/secret management. The collection pages are deliberately
/// factored into a separate file so the controller stays navigable.
///
/// Each collection has three actions:
///
/// - GET Edit{Collection}(int id) — render the page
/// - POST Add{Collection}(int id, …) — append a row
/// - POST Remove{Collection}(int id, int rowId) — delete a row
///
///
[Authorize("AdministratorOnly")]
public partial class ClientController
{
// ---- Redirect URIs ------------------------------------------------
readonly IHtmlLocalizer _localizer;
// ClientController has a single constructor declared in
// ClientController.cs; this partial shares its fields.
[HttpGet]
public async Task EditRedirectUris(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.RedirectUris.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddRedirectUri(int id, string redirectUri)
{
return await AddCollectionRowAsync(
id, redirectUri,
(client, uri) => new ClientRedirectUri { ClientId = client.Id, RedirectUri = uri });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveRedirectUri(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditRedirectUris");
// ---- Post-logout Redirect URIs -----------------------------------
[HttpGet]
public async Task EditPostLogoutRedirectUris(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.PostLogoutRedirectUris.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddPostLogoutRedirectUri(int id, string postLogoutRedirectUri)
{
return await AddCollectionRowAsync(
id, postLogoutRedirectUri,
(client, uri) => new ClientPostLogoutRedirectUri { ClientId = client.Id, PostLogoutRedirectUri = uri });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemovePostLogoutRedirectUri(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditPostLogoutRedirectUris");
// ---- Allowed Scopes ----------------------------------------------
[HttpGet]
public async Task EditScopes(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.AllowedScopes.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddScope(int id, string scope)
{
return await AddCollectionRowAsync(
id, scope,
(client, s) => new ClientScope { ClientId = client.Id, Scope = s });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveScope(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditScopes");
// ---- Allowed Grant Types -----------------------------------------
[HttpGet]
public async Task EditGrantTypes(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.AllowedGrantTypes.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddGrantType(int id, string grantType)
{
return await AddCollectionRowAsync(
id, grantType,
(client, g) => new ClientGrantType { ClientId = client.Id, GrantType = g });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveGrantType(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditGrantTypes");
// ---- Allowed CORS Origins ----------------------------------------
[HttpGet]
public async Task EditCorsOrigins(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.AllowedCorsOrigins.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddCorsOrigin(int id, string origin)
{
return await AddCollectionRowAsync(
id, origin,
(client, o) => new ClientCorsOrigin { ClientId = client.Id, Origin = o });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveCorsOrigin(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditCorsOrigins");
// ---- IdentityProvider Restrictions -------------------------------
[HttpGet]
public async Task EditIdPRestrictions(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.IdentityProviderRestrictions.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddIdPRestriction(int id, string provider)
{
return await AddCollectionRowAsync(
id, provider,
(client, p) => new ClientIdPRestriction { ClientId = client.Id, Provider = p });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveIdPRestriction(int id, int rowId)
=> await RemoveCollectionRowAsync(id, rowId, "EditIdPRestrictions");
// ---- Claims ------------------------------------------------------
[HttpGet]
public async Task EditClaims(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.Claims.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddClaim(int id, string type, string value)
{
if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(value))
{
TempData["Error"] = _localizer["BothTypeAndValueRequired"].Value;
return RedirectToAction("EditClaims", new { id });
}
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
dbContext.Set().Add(new ClientClaim { ClientId = client.Id, Type = type, Value = value });
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditClaims", new { id });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveClaim(int id, int rowId)
{
var row = await dbContext.Set().FindAsync(rowId);
if (row is null || row.ClientId != id) return NotFound();
dbContext.Set().Remove(row);
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditClaims", new { id });
}
// ---- Properties (key/value) --------------------------------------
[HttpGet]
public async Task EditProperties(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.Properties.ToList());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddProperty(int id, string key, string value)
{
if (string.IsNullOrWhiteSpace(key))
{
TempData["Error"] = _localizer["KeyRequired"].Value;
return RedirectToAction("EditProperties", new { id });
}
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
dbContext.Set().Add(new ClientProperty { ClientId = client.Id, Key = key, Value = value });
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditProperties", new { id });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveProperty(int id, int rowId)
{
var row = await dbContext.Set().FindAsync(rowId);
if (row is null || row.ClientId != id) return NotFound();
dbContext.Set().Remove(row);
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditProperties", new { id });
}
// ---- Secrets -----------------------------------------------------
[HttpGet]
public async Task EditSecrets(int id)
{
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
SetAppTypesInputValues();
return View(client.ClientSecrets?.ToList() ?? new List());
}
[HttpPost, ValidateAntiForgeryToken]
public async Task AddSecret(int id, string value, string description, DateTime? expiration)
{
if (string.IsNullOrWhiteSpace(value))
{
TempData["Error"] = _localizer["SecretValueRequired"].Value;
return RedirectToAction("EditSecrets", new { id });
}
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
dbContext.ClientSecrets.Add(new ClientSecret
{
ClientId = client.Id,
Type = "SharedSecret",
Value = value,
Description = description,
Created = DateTime.UtcNow,
Expiration = expiration
});
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditSecrets", new { id });
}
[HttpPost, ValidateAntiForgeryToken]
public async Task RemoveSecret(int id, int rowId)
{
var row = await dbContext.ClientSecrets.FindAsync(rowId);
if (row is null || row.ClientId != id) return NotFound();
dbContext.ClientSecrets.Remove(row);
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction("EditSecrets", new { id });
}
// ---- Helpers -----------------------------------------------------
private async Task LoadClientAsync(int id)
=> await dbContext.Clients
.Include(c => c.RedirectUris)
.Include(c => c.PostLogoutRedirectUris)
.Include(c => c.AllowedScopes)
.Include(c => c.AllowedGrantTypes)
.Include(c => c.AllowedCorsOrigins)
.Include(c => c.IdentityProviderRestrictions)
.Include(c => c.Claims)
.Include(c => c.Properties)
.Include(c => c.ClientSecrets)
.SingleOrDefaultAsync(c => c.Id == id);
private async Task AddCollectionRowAsync(
int id,
string value,
Func factory)
where TEntity : class
{
if (string.IsNullOrWhiteSpace(value))
{
TempData["Error"] = _localizer["ValueRequired"].Value;
return RedirectToAction(RedirectTargetFor(), new { id });
}
var client = await LoadClientAsync(id);
if (client is null) return NotFound();
dbContext.Add(factory(client, value));
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction(RedirectTargetFor(), new { id });
}
private async Task RemoveCollectionRowAsync(int id, int rowId, string redirectAction)
where TEntity : class
{
var row = await dbContext.FindAsync(rowId);
if (row is null) return NotFound();
// IdentityServer8 navigation properties are not always populated
// by FindAsync; rely on the FK check on the caller side.
var fk = (row as dynamic).ClientId as int?;
if (fk is null || fk != id) return NotFound();
dbContext.Remove(row);
await dbContext.SaveChangesAsync(User.GetUserId());
return RedirectToAction(redirectAction, new { id });
}
private static string RedirectTargetFor() => typeof(TEntity).Name switch
{
nameof(ClientRedirectUri) => nameof(EditRedirectUris),
nameof(ClientPostLogoutRedirectUri) => nameof(EditPostLogoutRedirectUris),
nameof(ClientScope) => nameof(EditScopes),
nameof(ClientGrantType) => nameof(EditGrantTypes),
nameof(ClientCorsOrigin) => nameof(EditCorsOrigins),
nameof(ClientIdPRestriction) => nameof(EditIdPRestrictions),
_ => "Edit",
};
}