From cdd6e4e9a864d8c682f431e14f54bd7bf02cb6cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:15:40 +0000 Subject: [PATCH 01/10] Bump the dotnet group with 20 updates Bumps AWSSDK.Core from 4.0.100 to 4.0.100.8 Bumps ClosedXML from 0.105.0 to 0.105.1 Bumps dotnet-ef from 10.0.9 to 10.0.10 Bumps DotNetDiag.HealthChecks.UI from 10.0.7 to 10.0.11 Bumps DotNetDiag.HealthChecks.UI.Client from 10.0.7 to 10.0.11 Bumps DotNetDiag.HealthChecks.UI.InMemory.Storage from 10.0.7 to 10.0.11 Bumps Hangfire.AspNetCore from 1.8.23 to 1.8.24 Bumps Hangfire.Core from 1.8.23 to 1.8.24 Bumps Hangfire.SqlServer from 1.8.23 to 1.8.24 Bumps HtmlSanitizer from 9.0.892 to 9.0.967 Bumps jetbrains.resharper.globaltools from 2026.1.3 to 2026.2.0 Bumps Microsoft.NET.Test.Sdk from 18.7.0 to 18.8.1 Bumps Microsoft.Testing.Platform from 2.2.3 to 2.3.2 Bumps NLog.MailKit from 6.1.4 to 6.1.5 Bumps NLog.Web.AspNetCore from 6.1.3 to 6.1.4 Bumps NSubstitute from 5.3.0 to 6.0.0 Bumps QuestPDF from 2026.6.0 to 2026.7.1 Bumps SonarAnalyzer.CSharp from 10.27.0.140913 to 10.30.0.144632 Bumps SQLitePCLRaw.bundle_e_sqlite3 from 3.0.3 to 3.0.4 Bumps System.Linq.Dynamic.Core from 1.7.2 to 1.7.3 --- updated-dependencies: - dependency-name: AWSSDK.Core dependency-version: 4.0.100.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: ClosedXML dependency-version: 0.105.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: dotnet-ef dependency-version: 10.0.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: DotNetDiag.HealthChecks.UI dependency-version: 10.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: DotNetDiag.HealthChecks.UI.Client dependency-version: 10.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: DotNetDiag.HealthChecks.UI.InMemory.Storage dependency-version: 10.0.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: Hangfire.AspNetCore dependency-version: 1.8.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: Hangfire.Core dependency-version: 1.8.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: Hangfire.SqlServer dependency-version: 1.8.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: HtmlSanitizer dependency-version: 9.0.967 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: jetbrains.resharper.globaltools dependency-version: 2026.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dotnet - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.8.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dotnet - dependency-name: Microsoft.Testing.Platform dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dotnet - dependency-name: NLog.MailKit dependency-version: 6.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: NLog.Web.AspNetCore dependency-version: 6.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: NSubstitute dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: dotnet - dependency-name: QuestPDF dependency-version: 2026.7.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dotnet - dependency-name: SonarAnalyzer.CSharp dependency-version: 10.30.0.144632 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dotnet - dependency-name: SQLitePCLRaw.bundle_e_sqlite3 dependency-version: 3.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet - dependency-name: System.Linq.Dynamic.Core dependency-version: 1.7.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dotnet ... Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 4 ++-- test/Viper.test.csproj | 14 +++++++------- web/Viper.csproj | 26 +++++++++++++------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 80cf99d50..29684d05c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.9", + "version": "10.0.10", "commands": [ "dotnet-ef" ] @@ -15,7 +15,7 @@ ] }, "jetbrains.resharper.globaltools": { - "version": "2026.1.3", + "version": "2026.2.0", "commands": [ "jb" ] diff --git a/test/Viper.test.csproj b/test/Viper.test.csproj index af5cccc12..2d0a6bb30 100644 --- a/test/Viper.test.csproj +++ b/test/Viper.test.csproj @@ -22,23 +22,23 @@ - - + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/web/Viper.csproj b/web/Viper.csproj index 7b8e781c2..122ae6711 100644 --- a/web/Viper.csproj +++ b/web/Viper.csproj @@ -42,20 +42,20 @@ - - - + + + - - + + - + - + - + @@ -71,18 +71,18 @@ - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + From b64d9a24d04985471f71d0713c36c0083da8b2b3 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 01:54:09 -0700 Subject: [PATCH 02/10] fix(deps): hold NSubstitute at 5.3.0 pending 6.0.1 - 6.0.0 types Arg.Is as Predicate, emitting CS8602/CS8604 across the suite; upstream nsubstitute/NSubstitute#973 and #976 are open --- test/Viper.test.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Viper.test.csproj b/test/Viper.test.csproj index 2d0a6bb30..a2c0e746d 100644 --- a/test/Viper.test.csproj +++ b/test/Viper.test.csproj @@ -25,7 +25,9 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 8985f49bcd50dc5f4e1a01d42ab74d261fed6ab8 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 01:55:30 -0700 Subject: [PATCH 03/10] refactor: clear new SonarAnalyzer 10.30 warnings - bound regex execution to 1s so pathological input cannot pin a request thread (S6444) - fold Where predicates into First/FirstOrDefault (S2971) --- test/CTS/AssessmentControllerTest.cs | 8 ++++---- test/CTS/SetupAssessments.cs | 6 +++--- .../Services/CourseClassificationService.cs | 2 +- .../RAPS/Controllers/AdGroupRolesController.cs | 3 +-- web/Areas/RAPS/Services/RAPSCacheService.cs | 2 +- web/Areas/RAPS/Services/VMACSExport.cs | 2 +- web/Areas/Students/Services/PhotoExportService.cs | 2 +- web/Areas/Students/Services/PhotoService.cs | 2 +- web/ViteProxyHelpers.cs | 15 ++++++++++----- 9 files changed, 23 insertions(+), 19 deletions(-) diff --git a/test/CTS/AssessmentControllerTest.cs b/test/CTS/AssessmentControllerTest.cs index 3f166ad2d..a5c8141da 100644 --- a/test/CTS/AssessmentControllerTest.cs +++ b/test/CTS/AssessmentControllerTest.cs @@ -123,10 +123,10 @@ public async Task GetAssessmentCheck() var actrlAsChief = GetAssessmentController(SetupUsers.UserType.Chief); var encounterIdExists = SetupAssessments.GetEncounters().First().EncounterId; - var encounterIdOtherFac = SetupAssessments.GetEncounters().Where(e => e.EnteredBy != SetupUsers.facultyUser.AaudUserId).First().EncounterId; - var encounterIdOtherStd = SetupAssessments.GetEncounters().Where(e => e.StudentUserId != SetupUsers.studentUser1.AaudUserId).First().EncounterId; - var encounterIdChief = SetupAssessments.GetEncounters().Where(e => e.ServiceId == SetupServices.ServiceChiefs[0].ServiceId).First().EncounterId; - var encounterIdNotChief = SetupAssessments.GetEncounters().Where(e => e.ServiceId != SetupServices.ServiceChiefs[0].ServiceId).First().EncounterId; + var encounterIdOtherFac = SetupAssessments.GetEncounters().First(e => e.EnteredBy != SetupUsers.facultyUser.AaudUserId).EncounterId; + var encounterIdOtherStd = SetupAssessments.GetEncounters().First(e => e.StudentUserId != SetupUsers.studentUser1.AaudUserId).EncounterId; + var encounterIdChief = SetupAssessments.GetEncounters().First(e => e.ServiceId == SetupServices.ServiceChiefs[0].ServiceId).EncounterId; + var encounterIdNotChief = SetupAssessments.GetEncounters().First(e => e.ServiceId != SetupServices.ServiceChiefs[0].ServiceId).EncounterId; var encounterIdNotExists = 99999; //act diff --git a/test/CTS/SetupAssessments.cs b/test/CTS/SetupAssessments.cs index a32b41d57..f329ffb1f 100644 --- a/test/CTS/SetupAssessments.cs +++ b/test/CTS/SetupAssessments.cs @@ -23,7 +23,7 @@ internal static class SetupAssessments FullName = SetupUsers.studentUser1.DisplayLastName + ", " + SetupUsers.studentUser1.DisplayFirstName, MailId = "", }, - EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.facultyUser.AaudUserId).FirstOrDefault(), + EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.facultyUser.AaudUserId), ServiceId = 1, }, new Encounter @@ -38,7 +38,7 @@ internal static class SetupAssessments FullName = SetupUsers.studentUser1.DisplayLastName + ", " + SetupUsers.studentUser1.DisplayFirstName, MailId = "", }, - EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.facultyUser.AaudUserId).FirstOrDefault(), + EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.facultyUser.AaudUserId), ServiceId = 2, }, new Encounter @@ -53,7 +53,7 @@ internal static class SetupAssessments FullName = SetupUsers.studentUser2.DisplayLastName + ", " + SetupUsers.studentUser2.DisplayFirstName, MailId = "", }, - EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.otherFacultyUser.AaudUserId).FirstOrDefault(), + EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.otherFacultyUser.AaudUserId), ServiceId = 3, }, }; diff --git a/web/Areas/Effort/Services/CourseClassificationService.cs b/web/Areas/Effort/Services/CourseClassificationService.cs index e4865b212..02b2f9bc3 100644 --- a/web/Areas/Effort/Services/CourseClassificationService.cs +++ b/web/Areas/Effort/Services/CourseClassificationService.cs @@ -34,7 +34,7 @@ public bool Is199299Course(string? crseNumb) // Match 199 or 299 followed by non-digit or end of string // Valid: "199", "299", "199A", "299R", "199B" // Invalid: "1990", "2995", "19900" - return Regex.IsMatch(crseNumb.Trim(), @"^(199|299)([^0-9]|$)", RegexOptions.IgnoreCase); + return Regex.IsMatch(crseNumb.Trim(), @"^(199|299)([^0-9]|$)", RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); } /// diff --git a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs index 8412ff84e..fd492f430 100644 --- a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs +++ b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs @@ -27,8 +27,7 @@ public AdGroupRolesController(RAPSContext context) private bool GroupExists(int groupId) { var group = _context.OuGroups - .Where(gr => gr.OugroupId == groupId) - .FirstOrDefault(); + .FirstOrDefault(gr => gr.OugroupId == groupId); return group != null; } diff --git a/web/Areas/RAPS/Services/RAPSCacheService.cs b/web/Areas/RAPS/Services/RAPSCacheService.cs index 3156d05c0..a9b2bdf7c 100644 --- a/web/Areas/RAPS/Services/RAPSCacheService.cs +++ b/web/Areas/RAPS/Services/RAPSCacheService.cs @@ -16,7 +16,7 @@ public RAPSCacheService(RAPSContext rapsContext, AAUDContext aaudContext, IUserH public void ClearCachedRolesAndPermissionsForUser(string mothraId) { - AaudUser? user = aaudContext.AaudUsers.Where(u => u.MothraId == mothraId).FirstOrDefault(); + AaudUser? user = aaudContext.AaudUsers.FirstOrDefault(u => u.MothraId == mothraId); if (user != null) { userHelper.ClearCachedRolesAndPermissions(user); diff --git a/web/Areas/RAPS/Services/VMACSExport.cs b/web/Areas/RAPS/Services/VMACSExport.cs index 64c465b21..c088b8233 100644 --- a/web/Areas/RAPS/Services/VMACSExport.cs +++ b/web/Areas/RAPS/Services/VMACSExport.cs @@ -214,7 +214,7 @@ join role in _RAPSContext.TblRoles on rm.RoleId equals role.RoleId //Get a single record with their ids and blank role so that an empty permissions array can be pushed. if (userList.Count == 0 && loginId.Length > 0) { - var user = _RAPSContext.VwAaudUser.Where(a => a.LoginId == loginId).FirstOrDefault(); + var user = _RAPSContext.VwAaudUser.FirstOrDefault(a => a.LoginId == loginId); if (user != null) { userList.Add(new UserList diff --git a/web/Areas/Students/Services/PhotoExportService.cs b/web/Areas/Students/Services/PhotoExportService.cs index 3729c75eb..db4aaa89b 100644 --- a/web/Areas/Students/Services/PhotoExportService.cs +++ b/web/Areas/Students/Services/PhotoExportService.cs @@ -64,7 +64,7 @@ public class PhotoExportService : IPhotoExportService // and our own home-grown checker flag 3+ consecutive whitespace chars // ("RepeatedBlanks"); collapsing them on the way out is a safe no-op for // clean data and a structural improvement for messy data. - private static readonly Regex RepeatedWhitespaceRegex = new(@"\s{2,}", RegexOptions.Compiled); + private static readonly Regex RepeatedWhitespaceRegex = new(@"\s{2,}", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private static string CollapseWhitespace(string? text) => string.IsNullOrEmpty(text) ? string.Empty : RepeatedWhitespaceRegex.Replace(text, " ").Trim(); diff --git a/web/Areas/Students/Services/PhotoService.cs b/web/Areas/Students/Services/PhotoService.cs index 93ef0788e..3f8f45c4b 100644 --- a/web/Areas/Students/Services/PhotoService.cs +++ b/web/Areas/Students/Services/PhotoService.cs @@ -27,7 +27,7 @@ public class PhotoService : IPhotoService private const int DefaultCacheDurationHours = 24; // Compiled regex for mailId validation - compiled once for performance - private static readonly Regex MailIdValidationRegex = new("^[a-z0-9.-]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex MailIdValidationRegex = new("^[a-z0-9.-]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1)); public PhotoService(IConfiguration configuration, IMemoryCache cache, ILogger logger, IWebHostEnvironment webHostEnvironment) { diff --git a/web/ViteProxyHelpers.cs b/web/ViteProxyHelpers.cs index 0e4e4f9d4..186ab91b9 100644 --- a/web/ViteProxyHelpers.cs +++ b/web/ViteProxyHelpers.cs @@ -41,10 +41,15 @@ internal static class ViteProxyHelpers private const string VueAppRoutePattern = @"^/({0})(/.*)?$"; private const string VueAppAssetPattern = @"^/({0})/.*\.(js|ts|css|map|vue|json)$|^/({0})\.(js|ts|css|map|vue|json)$"; + // Bounds matching so a pathological request path can't pin a request thread. + // Must stay above the regex fields: static initialisers run in textual order. + private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); + // Regex for built asset files with Vite hashes (used to skip proxying built assets) private static readonly Regex AssetHashRegex = new Regex( $@"{Regex.Escape(ViteAssetsBasePath)}{AssetHashPattern}{SupportedAssetExtensions}$", - RegexOptions.IgnoreCase | RegexOptions.Compiled + RegexOptions.IgnoreCase | RegexOptions.Compiled, + RegexTimeout ); // Cached compiled regexes - initialized once at startup @@ -76,15 +81,15 @@ private static void InitializeRegexes(string[] vueAppNames) if (string.IsNullOrEmpty(vueAppsPattern)) { // @"(?!)" is a negative lookahead that never matches any input; used here to ensure no routes/assets match when no app names are provided. - _vueAppRouteRegex = new Regex(@"(?!)", RegexOptions.Compiled); - _vueAppAssetRegex = new Regex(@"(?!)", RegexOptions.Compiled); + _vueAppRouteRegex = new Regex(@"(?!)", RegexOptions.Compiled, RegexTimeout); + _vueAppAssetRegex = new Regex(@"(?!)", RegexOptions.Compiled, RegexTimeout); } else { _vueAppRouteRegex = new Regex(string.Format(VueAppRoutePattern, vueAppsPattern), - RegexOptions.Compiled | RegexOptions.IgnoreCase); + RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout); _vueAppAssetRegex = new Regex(string.Format(VueAppAssetPattern, vueAppsPattern), - RegexOptions.Compiled | RegexOptions.IgnoreCase); + RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout); } } } From 736a1587056e63c9c8144a6f2a43a87c2338fc20 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 14:17:18 -0700 Subject: [PATCH 04/10] chore(deps): drop unused System.Linq.Dynamic.Core - no usages anywhere; a runtime string-to-expression-tree parser is attack surface with no offsetting benefit here --- web/Viper.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/web/Viper.csproj b/web/Viper.csproj index 122ae6711..ad780ee0d 100644 --- a/web/Viper.csproj +++ b/web/Viper.csproj @@ -82,7 +82,6 @@ - From 74ce8de79de277490752d370559c8c22d60b30c6 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 15:07:31 -0700 Subject: [PATCH 05/10] fix(raps): keep entity objects defined when viperFetch 404s - viperFetch resolves to undefined on 4xx/5xx, overwriting the initial {} and throwing in templates that dereference it, e.g. {{group.name}} - applied to all 15 views sharing the pattern, not only the one found --- web/Areas/RAPS/Views/Groups/Members.cshtml | 2 +- web/Areas/RAPS/Views/Groups/Roles.cshtml | 4 +++- web/Areas/RAPS/Views/Members/History.cshtml | 2 +- web/Areas/RAPS/Views/Members/Permissions.cshtml | 2 +- web/Areas/RAPS/Views/Members/RSOP.cshtml | 2 +- web/Areas/RAPS/Views/Members/Roles.cshtml | 2 +- web/Areas/RAPS/Views/Permissions/AllMembers.cshtml | 2 +- web/Areas/RAPS/Views/Permissions/Members.cshtml | 2 +- web/Areas/RAPS/Views/Permissions/Roles.cshtml | 2 +- web/Areas/RAPS/Views/Permissions/RolesRO.cshtml | 2 +- web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml | 2 +- web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml | 2 +- web/Areas/RAPS/Views/Roles/Members.cshtml | 2 +- web/Areas/RAPS/Views/Roles/Permissions.cshtml | 2 +- web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml | 2 +- 15 files changed, 17 insertions(+), 15 deletions(-) diff --git a/web/Areas/RAPS/Views/Groups/Members.cshtml b/web/Areas/RAPS/Views/Groups/Members.cshtml index 5f1e78fb8..96ca05249 100644 --- a/web/Areas/RAPS/Views/Groups/Members.cshtml +++ b/web/Areas/RAPS/Views/Groups/Members.cshtml @@ -113,7 +113,7 @@ }, methods: { loadGroup: async function () { - this.group = await viperFetch(this, "Groups/" + this.groupId) + this.group = (await viperFetch(this, "Groups/" + this.groupId)) ?? {} }, fdate: function (d) { return formatDate(d) diff --git a/web/Areas/RAPS/Views/Groups/Roles.cshtml b/web/Areas/RAPS/Views/Groups/Roles.cshtml index 172c13beb..1f0f539a8 100644 --- a/web/Areas/RAPS/Views/Groups/Roles.cshtml +++ b/web/Areas/RAPS/Views/Groups/Roles.cshtml @@ -103,7 +103,9 @@ }, methods: { loadGroup: async function () { - this.group = await viperFetch(this, "Groups/" + this.groupId) + // viperFetch resolves to undefined on 4xx/5xx; keep group an object + // so the {{group.name}} heading renders blank instead of throwing. + this.group = (await viperFetch(this, "Groups/" + this.groupId)) ?? {} }, addGroupRole: async function (groupRole) { viperFetch(this, diff --git a/web/Areas/RAPS/Views/Members/History.cshtml b/web/Areas/RAPS/Views/Members/History.cshtml index f762c555e..f224eae00 100644 --- a/web/Areas/RAPS/Views/Members/History.cshtml +++ b/web/Areas/RAPS/Views/Members/History.cshtml @@ -55,7 +55,7 @@ }, async mounted() { this.memberId = this.urlParams.get("memberId") - this.member = await viperFetch(this, "members/" + this.memberId) + this.member = (await viperFetch(this, "members/" + this.memberId)) ?? {} this.historyDates = (await viperFetch(this, "members/" + this.memberId + "/history/dates")) .map(d => { return {label: formatDate(d), value: d} diff --git a/web/Areas/RAPS/Views/Members/Permissions.cshtml b/web/Areas/RAPS/Views/Members/Permissions.cshtml index 84e57c2b1..24b924e56 100644 --- a/web/Areas/RAPS/Views/Members/Permissions.cshtml +++ b/web/Areas/RAPS/Views/Members/Permissions.cshtml @@ -251,7 +251,7 @@ this.selectedPermissions = [] }, loadMember: async function () { - this.member = await viperFetch(this, "members/" + this.memberId) + this.member = (await viperFetch(this, "members/" + this.memberId)) ?? {} } }, mounted() { diff --git a/web/Areas/RAPS/Views/Members/RSOP.cshtml b/web/Areas/RAPS/Views/Members/RSOP.cshtml index ceae260bf..f3479ddec 100644 --- a/web/Areas/RAPS/Views/Members/RSOP.cshtml +++ b/web/Areas/RAPS/Views/Members/RSOP.cshtml @@ -46,7 +46,7 @@ }, methods: { loadMember: async function () { - this.member = await viperFetch(this, "Members/" + this.memberId) + this.member = (await viperFetch(this, "Members/" + this.memberId)) ?? {} } }, async mounted() { diff --git a/web/Areas/RAPS/Views/Members/Roles.cshtml b/web/Areas/RAPS/Views/Members/Roles.cshtml index 7ef9a8bca..39b3b2558 100644 --- a/web/Areas/RAPS/Views/Members/Roles.cshtml +++ b/web/Areas/RAPS/Views/Members/Roles.cshtml @@ -265,7 +265,7 @@ this.selectedRoles = [] }, loadMember: async function () { - this.member = await viperFetch(this, "Members/" + this.memberId) + this.member = (await viperFetch(this, "Members/" + this.memberId)) ?? {} }, pushToVMACS: async function (skipToggleCheck) { if (skipToggleCheck || this.updateVMACS) { diff --git a/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml b/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml index 8654d27f3..604263e91 100644 --- a/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml +++ b/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml @@ -56,7 +56,7 @@ }, methods: { loadPermission: async function () { - this.permission = await viperFetch(this, "Permissions/" + this.permissionId) + this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {} } }, async mounted() { diff --git a/web/Areas/RAPS/Views/Permissions/Members.cshtml b/web/Areas/RAPS/Views/Permissions/Members.cshtml index e8aa02346..7db52a135 100644 --- a/web/Areas/RAPS/Views/Permissions/Members.cshtml +++ b/web/Areas/RAPS/Views/Permissions/Members.cshtml @@ -170,7 +170,7 @@ }, methods: { loadPermission: async function () { - this.permission = await viperFetch(this, "Permissions/" + this.permissionId) + this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {} }, memberSearch: function (val, update, abort) { if (val.length < 3) { diff --git a/web/Areas/RAPS/Views/Permissions/Roles.cshtml b/web/Areas/RAPS/Views/Permissions/Roles.cshtml index 95d512689..60e736627 100644 --- a/web/Areas/RAPS/Views/Permissions/Roles.cshtml +++ b/web/Areas/RAPS/Views/Permissions/Roles.cshtml @@ -102,7 +102,7 @@ }, methods: { loadPermission: async function () { - this.permission = await viperFetch(this, "Permissions/" + this.permissionId) + this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {} }, updateRolePermissions: async function (allowAccess = 1) { var roleIds = this.selectedPermissions.reduce( (ids, sp) => {ids.push(sp.roleId); return ids;}, [] ) diff --git a/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml b/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml index f7d3c8b98..28303c8ac 100644 --- a/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml +++ b/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml @@ -49,7 +49,7 @@ }, methods: { loadPermission: async function () { - this.permission = await viperFetch(this, "Permissions/" + this.permissionId) + this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {} }, loadTables: async function () { await this.rolePermissionTable.load(this) diff --git a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml index 03b7c86d4..2c7ff9c41 100644 --- a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml +++ b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml @@ -142,7 +142,7 @@ }, async mounted() { this.roleTemplateId = this.urlParams.get("roleTemplateId") - this.roleTemplate = await viperFetch(this, "RoleTemplates/" + this.roleTemplateId) + this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)) ?? {} this.VMACSPush = this.urlPath.indexOf("/VMACS.") > 0 }, watch: { diff --git a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml index 0f18480aa..fa5aefe79 100644 --- a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml +++ b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml @@ -58,7 +58,7 @@ }, async mounted() { this.loadRoles() - this.role = await viperFetch(this, "Roles/" + this.urlParams.get("roleId")) + this.role = (await viperFetch(this, "Roles/" + this.urlParams.get("roleId"))) ?? {} } }) diff --git a/web/Areas/RAPS/Views/Roles/Members.cshtml b/web/Areas/RAPS/Views/Roles/Members.cshtml index d852e60d9..ea43c333f 100644 --- a/web/Areas/RAPS/Views/Roles/Members.cshtml +++ b/web/Areas/RAPS/Views/Roles/Members.cshtml @@ -162,7 +162,7 @@ }, methods: { loadRole: async function () { - this.role = await viperFetch(this, "Roles/" + this.roleId) + this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? {} }, memberSearch: function (val, update, abort) { if (val.length < 3) { diff --git a/web/Areas/RAPS/Views/Roles/Permissions.cshtml b/web/Areas/RAPS/Views/Roles/Permissions.cshtml index 2e1b7614e..6bfbc4e05 100644 --- a/web/Areas/RAPS/Views/Roles/Permissions.cshtml +++ b/web/Areas/RAPS/Views/Roles/Permissions.cshtml @@ -80,7 +80,7 @@ }, methods: { loadRole: async function () { - this.role = await viperFetch(this, "Roles/" + this.roleId) + this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? {} }, loadRolePermissions: async function() { await this.rolePermissionTable.load(this) diff --git a/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml b/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml index 0a0f33925..89f742c15 100644 --- a/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml +++ b/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml @@ -66,7 +66,7 @@ }, async mounted() { this.loadRoles() - this.roleTemplate = await viperFetch(this, "RoleTemplates/" + this.urlParams.get("roleTemplateId")) + this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.urlParams.get("roleTemplateId"))) ?? {} } }) From 76bc02650c18f8b549423dfcfb3f4fca997438d5 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 21:05:41 -0700 Subject: [PATCH 06/10] fix(raps): make fetch fallbacks match downstream usage - ?? {} stopped templates throwing but left callers dereferencing undefined properties: role.instance.indexOf, roleTemplateRoles.reduce - guard Sync/Edit actions so a failed group fetch cannot post to Groups/undefined/Sync or link to roleId=undefined - default array-returning fetches to [] where .map/.filter follow - AsNoTracking on two read-only lookups --- web/Areas/RAPS/Controllers/AdGroupRolesController.cs | 1 + web/Areas/RAPS/Services/RAPSCacheService.cs | 3 ++- web/Areas/RAPS/Views/Groups/List.cshtml | 4 ++-- web/Areas/RAPS/Views/Groups/Members.cshtml | 4 ++-- web/Areas/RAPS/Views/Members/History.cshtml | 4 ++-- web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml | 3 ++- web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml | 4 ++-- web/Areas/RAPS/Views/Roles/Members.cshtml | 3 ++- 8 files changed, 15 insertions(+), 11 deletions(-) diff --git a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs index fd492f430..80be33037 100644 --- a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs +++ b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs @@ -27,6 +27,7 @@ public AdGroupRolesController(RAPSContext context) private bool GroupExists(int groupId) { var group = _context.OuGroups + .AsNoTracking() .FirstOrDefault(gr => gr.OugroupId == groupId); return group != null; } diff --git a/web/Areas/RAPS/Services/RAPSCacheService.cs b/web/Areas/RAPS/Services/RAPSCacheService.cs index a9b2bdf7c..1590c1fb6 100644 --- a/web/Areas/RAPS/Services/RAPSCacheService.cs +++ b/web/Areas/RAPS/Services/RAPSCacheService.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using Viper.Classes.SQLContext; using Viper.Models.AAUD; @@ -16,7 +17,7 @@ public RAPSCacheService(RAPSContext rapsContext, AAUDContext aaudContext, IUserH public void ClearCachedRolesAndPermissionsForUser(string mothraId) { - AaudUser? user = aaudContext.AaudUsers.FirstOrDefault(u => u.MothraId == mothraId); + AaudUser? user = aaudContext.AaudUsers.AsNoTracking().FirstOrDefault(u => u.MothraId == mothraId); if (user != null) { userHelper.ClearCachedRolesAndPermissions(user); diff --git a/web/Areas/RAPS/Views/Groups/List.cshtml b/web/Areas/RAPS/Views/Groups/List.cshtml index f5b73eab9..d0c2c6a57 100644 --- a/web/Areas/RAPS/Views/Groups/List.cshtml +++ b/web/Areas/RAPS/Views/Groups/List.cshtml @@ -136,11 +136,11 @@ }, async mounted() { this.groupTable.load(this) - this.ouGroups = await viperFetch(this, "Groups/OU") + this.ouGroups = (await viperFetch(this, "Groups/OU")) ?? [] this.ouGroups = this.ouGroups .map(g => ({ label: "OU: " + g.cn, value: g.distinguishedName })) - this.adGroups = await viperFetch(this, "Groups/AD") + this.adGroups = (await viperFetch(this, "Groups/AD")) ?? [] this.adGroups = this.adGroups .map(g => ({ label: "AD: " + (g.displayName ?? g.samAccountName), value: g.distinguishedName })) diff --git a/web/Areas/RAPS/Views/Groups/Members.cshtml b/web/Areas/RAPS/Views/Groups/Members.cshtml index 96ca05249..4ffe0b6ba 100644 --- a/web/Areas/RAPS/Views/Groups/Members.cshtml +++ b/web/Areas/RAPS/Views/Groups/Members.cshtml @@ -9,8 +9,8 @@ - - + + { return {label: formatDate(d), value: d} }) diff --git a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml index 2c7ff9c41..dee968e21 100644 --- a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml +++ b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml @@ -142,7 +142,8 @@ }, async mounted() { this.roleTemplateId = this.urlParams.get("roleTemplateId") - this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)) ?? {} + // roleTemplateRoles must be an array: the VMACS push path calls .reduce() on it. + this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)) ?? { roleTemplateRoles: [] } this.VMACSPush = this.urlPath.indexOf("/VMACS.") > 0 }, watch: { diff --git a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml index fa5aefe79..8eda9bec8 100644 --- a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml +++ b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml @@ -34,8 +34,8 @@ }, methods: { loadRoles: async function() { - var childRoles = await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId")) - var allRoles = (await viperFetch(this, "Roles?Application=0")) + var childRoles = (await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId"))) ?? [] + var allRoles = ((await viperFetch(this, "Roles?Application=0")) ?? []) .filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined) this.selectedRoles = childRoles;//.reduce((result, cr) => { result.push(cr.roleId); return result }, []) this.roles = childRoles.concat(allRoles) diff --git a/web/Areas/RAPS/Views/Roles/Members.cshtml b/web/Areas/RAPS/Views/Roles/Members.cshtml index ea43c333f..3ce720a0d 100644 --- a/web/Areas/RAPS/Views/Roles/Members.cshtml +++ b/web/Areas/RAPS/Views/Roles/Members.cshtml @@ -162,7 +162,8 @@ }, methods: { loadRole: async function () { - this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? {} + // instance must be a string: mounted() calls role.instance.indexOf(). + this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? { instance: "" } }, memberSearch: function (val, update, abort) { if (val.length < 3) { From 49afda9e32de340ddb64798d9355056ee7e39c1d Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 22:48:45 -0700 Subject: [PATCH 07/10] fix(raps): guard clearable history filter, add AsNoTracking - clearing the date select leaves startDate null, so getAuditLog threw on .value before the request ran; omit the filter to show full history - AsNoTracking on the VMACS export fallback user lookup --- web/Areas/RAPS/Services/VMACSExport.cs | 4 +++- web/Areas/RAPS/Views/Members/History.cshtml | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/Areas/RAPS/Services/VMACSExport.cs b/web/Areas/RAPS/Services/VMACSExport.cs index c088b8233..062df033e 100644 --- a/web/Areas/RAPS/Services/VMACSExport.cs +++ b/web/Areas/RAPS/Services/VMACSExport.cs @@ -214,7 +214,9 @@ join role in _RAPSContext.TblRoles on rm.RoleId equals role.RoleId //Get a single record with their ids and blank role so that an empty permissions array can be pushed. if (userList.Count == 0 && loginId.Length > 0) { - var user = _RAPSContext.VwAaudUser.FirstOrDefault(a => a.LoginId == loginId); + var user = _RAPSContext.VwAaudUser + .AsNoTracking() + .FirstOrDefault(a => a.LoginId == loginId); if (user != null) { userList.Add(new UserList diff --git a/web/Areas/RAPS/Views/Members/History.cshtml b/web/Areas/RAPS/Views/Members/History.cshtml index ac27efa99..451bc800f 100644 --- a/web/Areas/RAPS/Views/Members/History.cshtml +++ b/web/Areas/RAPS/Views/Members/History.cshtml @@ -50,7 +50,11 @@ }, methods: { getAuditLog: async function() { - this.auditLog = (await viperFetch(this, "members/" + this.memberId + "/history?startDate=" + this.startDate.value)) ?? [] + // The date select is clearable, so startDate can be null; omit the + // filter entirely in that case to show the full history. + const startDate = this.startDate?.value + const query = startDate ? "?startDate=" + encodeURIComponent(startDate) : "" + this.auditLog = (await viperFetch(this, "members/" + this.memberId + "/history" + query)) ?? [] } }, async mounted() { From f4e87962fd72d5e874511ab328e1229c7b85138a Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 4 Aug 2026 23:52:01 -0700 Subject: [PATCH 08/10] fix(raps): do not treat failed reads as empty data - ?? [] made a failed load indistinguishable from "nothing selected", so Submit changes could PUT an empty list and wipe delegated roles - an unread role template likewise pushed an empty role list to VMACS - track load failure and disable the mutating action in both cases --- .../RAPS/Views/Roles/ApplyTemplate.cshtml | 9 ++++++-- .../RAPS/Views/Roles/DelegateRoles.cshtml | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml index dee968e21..d3e3dd6bc 100644 --- a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml +++ b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml @@ -17,7 +17,7 @@ Review Changes - + @@ -50,6 +50,7 @@ membersFound: false, //show table if a matching user was found roleTemplateId: 0, roleTemplate: {}, + templateLoadFailed: false, loginids: "", userSearch: "", userSearchResults: [], @@ -143,7 +144,11 @@ async mounted() { this.roleTemplateId = this.urlParams.get("roleTemplateId") // roleTemplateRoles must be an array: the VMACS push path calls .reduce() on it. - this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)) ?? { roleTemplateRoles: [] } + // Track load failure separately so an unread template is never mistaken for + // an empty one, which would push an empty role list to VMACS. + const template = await viperFetch(this, "RoleTemplates/" + this.roleTemplateId) + this.templateLoadFailed = template === undefined + this.roleTemplate = template ?? { roleTemplateRoles: [] } this.VMACSPush = this.urlPath.indexOf("/VMACS.") > 0 }, watch: { diff --git a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml index 8eda9bec8..71e63ccfd 100644 --- a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml +++ b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml @@ -12,7 +12,7 @@ :pagination="{rowsPerPage:0}" :loading="loadingRoles"> @@ -26,6 +26,7 @@ roles: [], selectedRoles: [], loadingRoles: false, + loadFailed: false, columns: [ { name: "role", label: "Role", field: "friendlyName", align: "left" }, { name: "description", label: "Description", field: "description", align: "left" } @@ -34,13 +35,22 @@ }, methods: { loadRoles: async function() { - var childRoles = (await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId"))) ?? [] - var allRoles = ((await viperFetch(this, "Roles?Application=0")) ?? []) - .filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined) - this.selectedRoles = childRoles;//.reduce((result, cr) => { result.push(cr.roleId); return result }, []) - this.roles = childRoles.concat(allRoles) + var childRoles = await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId")) + var allRoles = await viperFetch(this, "Roles?Application=0") + // A failed read must not look like "nothing is selected". Submitting + // that would PUT an empty list and wipe the existing delegations. + this.loadFailed = childRoles === undefined || allRoles === undefined + if (this.loadFailed) { + return + } + this.selectedRoles = childRoles + this.roles = childRoles.concat( + allRoles.filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined)) }, submitChanges: async function() { + if (this.loadFailed) { + return + } var roleIds = this.selectedRoles.reduce( (result, role) => { result.push(role.roleId) return result From 6eacddd668f982b4bfec0e4a114ee4ca92f8d995 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Wed, 5 Aug 2026 00:03:32 -0700 Subject: [PATCH 09/10] fix(vite-proxy): treat a regex timeout as a non-match - adding match timeouts meant a pathological path could surface as an unhandled RegexMatchTimeoutException instead of simply not matching - fall through to normal handling, preserving pre-timeout behaviour --- web/ViteProxyHelpers.cs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/web/ViteProxyHelpers.cs b/web/ViteProxyHelpers.cs index 186ab91b9..a1f8a00d7 100644 --- a/web/ViteProxyHelpers.cs +++ b/web/ViteProxyHelpers.cs @@ -103,7 +103,7 @@ public static bool ShouldProxyToVite(HttpContext context, string[] vueAppNames) var path = context.Request.Path; // Skip proxying built asset files with hashes - serve as static files - if (path.HasValue && AssetHashRegex.IsMatch(path.Value)) + if (path.HasValue && IsMatchWithinTimeout(AssetHashRegex, path.Value)) { return false; } @@ -127,19 +127,36 @@ public static bool ShouldProxyToVite(HttpContext context, string[] vueAppNames) var pathValue = path.Value; // Match Vue app routes: /CTS, /Computing, /Students (exact match or with sub-paths) - if (_vueAppRouteRegex!.IsMatch(pathValue)) + if (IsMatchWithinTimeout(_vueAppRouteRegex!, pathValue)) { return true; } // Match: /AppName/file.ext (assets in app subdirectories) // OR: /appname.ext (root-level entry files like /cts.ts) - return _vueAppAssetRegex!.IsMatch(pathValue); + return IsMatchWithinTimeout(_vueAppAssetRegex!, pathValue); } return false; } + /// + /// Runs a match under the configured timeout. A timed-out match is treated as a + /// non-match so the request falls through to normal handling, which is how it + /// behaved before timeouts were added, rather than surfacing as a 500. + /// + private static bool IsMatchWithinTimeout(Regex regex, string input) + { + try + { + return regex.IsMatch(input); + } + catch (RegexMatchTimeoutException) + { + return false; + } + } + /// /// Builds the target URL for proxying to Vite development server /// From b7a3b53028030d2d6a3f8bc4b85b316c21dde19c Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Wed, 5 Aug 2026 00:21:47 -0700 Subject: [PATCH 10/10] fix(raps): disable mutating actions until the read succeeds - the previous flags defaulted to "not failed", so Submit changes and Apply template were live during the initial fetch; a fast click could still PUT an empty list or push an empty role set - invert to rolesLoaded/templateLoaded, false until a read succeeds - await loadRoles in mounted so the flag settles before other setup --- .../RAPS/Views/Roles/ApplyTemplate.cshtml | 7 ++++--- .../RAPS/Views/Roles/DelegateRoles.cshtml | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml index d3e3dd6bc..fa4231afa 100644 --- a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml +++ b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml @@ -17,7 +17,7 @@ Review Changes - + @@ -50,7 +50,8 @@ membersFound: false, //show table if a matching user was found roleTemplateId: 0, roleTemplate: {}, - templateLoadFailed: false, + // Starts false so applying is disabled until the template is read. + templateLoaded: false, loginids: "", userSearch: "", userSearchResults: [], @@ -147,7 +148,7 @@ // Track load failure separately so an unread template is never mistaken for // an empty one, which would push an empty role list to VMACS. const template = await viperFetch(this, "RoleTemplates/" + this.roleTemplateId) - this.templateLoadFailed = template === undefined + this.templateLoaded = template !== undefined this.roleTemplate = template ?? { roleTemplateRoles: [] } this.VMACSPush = this.urlPath.indexOf("/VMACS.") > 0 }, diff --git a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml index 71e63ccfd..2b1818408 100644 --- a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml +++ b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml @@ -12,7 +12,7 @@ :pagination="{rowsPerPage:0}" :loading="loadingRoles"> @@ -26,7 +26,8 @@ roles: [], selectedRoles: [], loadingRoles: false, - loadFailed: false, + // Starts false so the page is disabled until the initial read succeeds. + rolesLoaded: false, columns: [ { name: "role", label: "Role", field: "friendlyName", align: "left" }, { name: "description", label: "Description", field: "description", align: "left" } @@ -35,20 +36,22 @@ }, methods: { loadRoles: async function() { + this.rolesLoaded = false var childRoles = await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId")) var allRoles = await viperFetch(this, "Roles?Application=0") - // A failed read must not look like "nothing is selected". Submitting - // that would PUT an empty list and wipe the existing delegations. - this.loadFailed = childRoles === undefined || allRoles === undefined - if (this.loadFailed) { + // A failed or still-pending read must not look like "nothing is + // selected": submitting that would PUT an empty list and wipe the + // existing delegations. Stay disabled until both reads succeed. + if (childRoles === undefined || allRoles === undefined) { return } this.selectedRoles = childRoles this.roles = childRoles.concat( allRoles.filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined)) + this.rolesLoaded = true }, submitChanges: async function() { - if (this.loadFailed) { + if (!this.rolesLoaded) { return } var roleIds = this.selectedRoles.reduce( (result, role) => { @@ -67,7 +70,7 @@ } }, async mounted() { - this.loadRoles() + await this.loadRoles() this.role = (await viperFetch(this, "Roles/" + this.urlParams.get("roleId"))) ?? {} }