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; public ClientController( IHtmlLocalizer localizer ) { _localizer = localizer; } [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", }; }