diff --git a/Core/Resgrid.Config/SessionSecurityConfig.cs b/Core/Resgrid.Config/SessionSecurityConfig.cs index a08d7a77a..1eba5f571 100644 --- a/Core/Resgrid.Config/SessionSecurityConfig.cs +++ b/Core/Resgrid.Config/SessionSecurityConfig.cs @@ -1,4 +1,4 @@ -namespace Resgrid.Config +namespace Resgrid.Config { public static class SessionSecurityConfig { @@ -16,6 +16,13 @@ public static class SessionSecurityConfig public static int PublicResetAccountLimitPerHour = 3; public static int PublicResetIpLimitPerHour = 10; public static int WebBffAccessTokenLifetimeMinutes = 5; + // Eventing tokens are deliberately longer-lived than API tokens. Every hub sets + // CloseOnAuthenticationExpiration, so this value is how often each connected client tears the + // connection down and renegotiates -- a token mint plus an introspection round trip per client, + // per expiry. SessionValidationHubFilter revalidates the session on every hub invocation, so a + // revoked session is cut off well before its token expires; this lifetime only bounds how long a + // purely passive connection can keep receiving broadcasts after revocation. + public static int WebEventingAccessTokenLifetimeMinutes = 30; public static int ClientMetadataMaximumLength = 256; public static int UserAgentMaximumLength = 1024; // Optional local JSON CIDR database. Leave blank to display location as unavailable. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx index 678564de6..420c8d92a 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx index c8a7de28a..8befe4a97 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx @@ -127,7 +127,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx index 2837d903e..39c0f78ca 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx index 07e2deb15..03a154616 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx index 941b65ac3..0b6185542 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx index c76db586e..b3cd3c416 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx index 8da57f8b3..6cadbfba5 100644 --- a/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx @@ -78,7 +78,7 @@ Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. - Mapping Settings + Mapping and BigBoard Settings Successfully saved Mapping Settings. diff --git a/Core/Resgrid.Services/SystemAuditsService.cs b/Core/Resgrid.Services/SystemAuditsService.cs index 6be172bc5..48e0a0953 100644 --- a/Core/Resgrid.Services/SystemAuditsService.cs +++ b/Core/Resgrid.Services/SystemAuditsService.cs @@ -23,6 +23,16 @@ public SystemAuditsService(ISystemAuditsRepository systemAuditsRepository) if (auditLog.Data == null) auditLog.Data = ""; + // IpAddress and ServerName are NOT NULL on both the SQL Server and PostgreSQL SystemAudits + // tables. Worker- and scheduler-originated audits have no client request behind them and so + // have no IP to record, and leaving the field unset threw the whole audit away on insert. + // Empty means "no client", which is what the ServerName beside it already says. + if (auditLog.IpAddress == null) + auditLog.IpAddress = ""; + + if (auditLog.ServerName == null) + auditLog.ServerName = ""; + // Several fields carry caller-controlled values (route ids, X-Forwarded-For). Clamp to // the SystemAudits column sizes so a hostile over-length value degrades to a truncated // audit row instead of a SqlException that loses the audit entirely. diff --git a/Web/Resgrid.Web.Eventing/Hubs/ChatHub.cs b/Web/Resgrid.Web.Eventing/Hubs/ChatHub.cs index dc49ad72e..b0c6917e4 100644 --- a/Web/Resgrid.Web.Eventing/Hubs/ChatHub.cs +++ b/Web/Resgrid.Web.Eventing/Hubs/ChatHub.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Concurrent; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using OpenIddict.Validation.AspNetCore; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Resgrid.Config; @@ -20,7 +20,11 @@ namespace Resgrid.Web.Eventing.Hubs /// per channel (the version rotates on authorization changes), /// chatuser:{deptId}:{userId} for personal events, chatdept:{deptId} for channel-list updates. /// - [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = ResgridResources.Chat_View)] + // Validated through OpenIddict, not the JwtBearer handler: the API registers an encryption + // certificate, so every access token it issues is an encrypted JWE. The JwtBearer handler here + // has no decryption key (and validates against resgrid.local issuer/audience defaults), so it + // rejects every one of them. The other two hubs already validate through introspection. + [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, Policy = ResgridResources.Chat_View)] public class ChatHub : Hub { private readonly IChatChannelService _chatChannelService; diff --git a/Web/Resgrid.Web.Eventing/Startup.cs b/Web/Resgrid.Web.Eventing/Startup.cs index 4c2354472..4e3683dcf 100644 --- a/Web/Resgrid.Web.Eventing/Startup.cs +++ b/Web/Resgrid.Web.Eventing/Startup.cs @@ -1,4 +1,4 @@ -using System; +using System; using Autofac; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -35,6 +35,7 @@ using Resgrid.Repositories.DataRepository; using Resgrid.Services; using Resgrid.Web.Eventing.Hubs; +using OpenIddict.Validation.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; using System.Security.Cryptography.X509Certificates; using Resgrid.Web.Services.Models; @@ -321,7 +322,7 @@ public void ConfigureServices(IServiceCollection services) services.AddAuthorization(options => { options.AddPolicy(ResgridResources.Chat_View, policy => policy - .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .AddAuthenticationSchemes(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme) .RequireChatAccessClaims()); }); diff --git a/Web/Resgrid.Web.Services/Controllers/v4/ConnectController.cs b/Web/Resgrid.Web.Services/Controllers/v4/ConnectController.cs index af058c7df..d289e77d2 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/ConnectController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/ConnectController.cs @@ -384,8 +384,9 @@ public async Task Token() eventingIdentity.AddClaim(new Claim(SessionClaimTypes.WebEventingOnly, "true")); foreach (var claim in principal.Claims) claim.SetDestinations(GetDestinations(claim, principal)); - principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(eventingOnly ? 2 : - Math.Max(1, SessionSecurityConfig.WebBffAccessTokenLifetimeMinutes))); + principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(eventingOnly + ? Math.Max(1, SessionSecurityConfig.WebEventingAccessTokenLifetimeMinutes) + : Math.Max(1, SessionSecurityConfig.WebBffAccessTokenLifetimeMinutes))); principal.SetResources(JwtConfig.EventsClientId); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs b/Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs index 401abdf9c..f88312699 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs @@ -244,9 +244,6 @@ public async Task Settings() else model.Use24HourTime = false; - model.NewCallFields = await BuildNewCallFieldRowsAsync(); - model.UnitStatusThresholds = await BuildUnitStatusThresholdRowsAsync(); - var address = await _departmentSettingsService.GetBigBoardCenterAddressDepartmentAsync(DepartmentId); var zoomLevel = await _departmentSettingsService.GetBigBoardMapZoomLevelForDepartmentAsync(DepartmentId); var refreshTimer = await _departmentSettingsService.GetBigBoardRefreshTimeForDepartmentAsync(DepartmentId); @@ -576,27 +573,6 @@ await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.Pe await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.UnitsSort.ToString(), DepartmentSettingTypes.UnitsSortOrder, cancellationToken); await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.CallsSort.ToString(), DepartmentSettingTypes.CallsSortOrder, cancellationToken); - await _departmentSettingsService.SaveUnitStatusThresholdsAsync(DepartmentId, new UnitStatusThresholds - { - Thresholds = (model.UnitStatusThresholds ?? new List()) - .Select(x => new UnitStatusThreshold - { - BaseType = x.BaseType, - // Entered in minutes, stored in seconds — the board works in seconds and a - // dispatcher should never have to think in them. - WarnSeconds = Math.Max(0, x.WarnMinutes) * 60, - AlertSeconds = Math.Max(0, x.AlertMinutes) * 60 - }) - .ToList() - }, cancellationToken); - - await _departmentSettingsService.SaveNewCallFieldPolicyAsync(DepartmentId, new NewCallFieldPolicy - { - Rules = (model.NewCallFields ?? new List()) - .Select(x => new NewCallFieldRule { Key = x.Key, Visible = x.Visible, Required = x.Required }) - .ToList() - }, cancellationToken); - // Coordinates the operator typed are stored verbatim. Leaving both blank means "work it // out from our address" -- the service geocodes it and stores the result, so a department // that never touches these fields still gets every map centred on itself instead of on @@ -1874,6 +1850,7 @@ public async Task DispatchSettings() model.UnitClearStatus = await _departmentSettingsService.GetUnitCallReleaseStatusToSetAsync(DepartmentId); model.PersonnelOnUnitSetUnitStatus = await _departmentSettingsService.GetPersonnelOnUnitSetUnitStatusAsync(DepartmentId); model.AutoEnableCheckInTimers = await _departmentSettingsService.GetCheckInTimersAutoEnableForNewCallsAsync(DepartmentId); + model.NewCallFields = await BuildNewCallFieldRowsAsync(); await PopulateDispatchSettingsSupportingDataAsync(model); await PopulateDispatchSettingsUnitTypeOverridesAsync(model, @@ -1927,6 +1904,13 @@ await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.Pe await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.AutoEnableCheckInTimers.ToString(), DepartmentSettingTypes.CheckInTimersAutoEnableForNewCalls, cancellationToken); + await _departmentSettingsService.SaveNewCallFieldPolicyAsync(DepartmentId, new NewCallFieldPolicy + { + Rules = (model.NewCallFields ?? new List()) + .Select(x => new NewCallFieldRule { Key = x.Key, Visible = x.Visible, Required = x.Required }) + .ToList() + }, cancellationToken); + if (await _featureToggleService.IsEnabledAsync(FeatureFlagKeys.DispatchRunCards, DepartmentId)) { var mode = Enum.IsDefined(typeof(DispatchRecommendationModes), model.DispatchRecommendationMode) @@ -1959,6 +1943,12 @@ await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.Au await PopulateDispatchSettingsUnitTypeOverridesAsync(model); await PopulateRunCardRecommendationSettingsAsync(model, false); + + // A rejected post redisplays the operator's own ticks -- they round-trip through model + // binding. Only rebuild from storage when nothing came back at all. + if (model.NewCallFields == null || model.NewCallFields.Count == 0) + model.NewCallFields = await BuildNewCallFieldRowsAsync(); + model.SaveSuccess = false; return View(model); } @@ -2443,6 +2433,7 @@ public async Task MappingSettings() model.UseMapboxOverride = await _departmentSettingsService.GetMappingUseMapboxOverrideAsync(DepartmentId); model.MapboxStyleUrl = await _departmentSettingsService.GetMappingMapboxStyleUrlAsync(DepartmentId); model.MapboxAccessToken = await _departmentSettingsService.GetMappingMapboxAccessTokenAsync(DepartmentId); + model.UnitStatusThresholds = await BuildUnitStatusThresholdRowsAsync(); return View(model); } @@ -2469,6 +2460,22 @@ public async Task MappingSettings(MappingSettingsView model, Canc if (ModelState.IsValid) { + await _departmentSettingsService.SaveUnitStatusThresholdsAsync(DepartmentId, new UnitStatusThresholds + { + Thresholds = (model.UnitStatusThresholds ?? new List()) + .Select(x => new UnitStatusThreshold + { + BaseType = x.BaseType, + // Entered in minutes, stored in seconds — the board works in seconds and a + // dispatcher should never have to think in them. Clamped to the largest minute + // count that still converts: the form binds a raw int, and without the ceiling a + // posted value over int.MaxValue / 60 wraps negative and silently reads as 0. + WarnSeconds = Math.Min(Math.Max(0, x.WarnMinutes), int.MaxValue / 60) * 60, + AlertSeconds = Math.Min(Math.Max(0, x.AlertMinutes), int.MaxValue / 60) * 60 + }) + .ToList() + }, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.PersonnelLocationTTL.ToString(), DepartmentSettingTypes.MappingPersonnelLocationTTL, cancellationToken); await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.UnitLocationTTL.ToString(), @@ -2497,6 +2504,11 @@ await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.Ma return View(model); } + // A rejected post redisplays the operator's own numbers -- they round-trip through model + // binding. Only rebuild from storage when nothing came back at all. + if (model.UnitStatusThresholds == null || model.UnitStatusThresholds.Count == 0) + model.UnitStatusThresholds = await BuildUnitStatusThresholdRowsAsync(); + model.SaveSuccess = false; return View(model); } diff --git a/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs b/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs index e55230de2..52d6f9964 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs @@ -378,6 +378,12 @@ public async Task PersonnelActionButtonsPartial() [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public async Task EditUserProfile(string userId) { + // No userId means "my profile" -- links that come back here from a sub-page (Active + // Sessions, Change Password) have no user to name. Without this the authorization + // check below is handed a null target and answers 401 for the user's own profile. + if (string.IsNullOrWhiteSpace(userId)) + userId = UserId; + var model = new EditProfileModel(); model.ApiUrl = Config.SystemBehaviorConfig.ResgridApiBaseUrl; model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); diff --git a/Web/Resgrid.Web/Areas/User/Models/DepartmentSettingsModel.cs b/Web/Resgrid.Web/Areas/User/Models/DepartmentSettingsModel.cs index f3584b23c..715bf7943 100644 --- a/Web/Resgrid.Web/Areas/User/Models/DepartmentSettingsModel.cs +++ b/Web/Resgrid.Web/Areas/User/Models/DepartmentSettingsModel.cs @@ -19,18 +19,6 @@ public class DepartmentSettingsModel : BaseUserModel public string MapZoomLevel { get; set; } public string RefreshTime { get; set; } - /// - /// New-call field policy, one row per configurable built-in field. Bound as a list so the admin - /// screen can render a Visible/Required grid without a per-field property explosion. - /// - public List NewCallFields { get; set; } = new List(); - - /// - /// Time-in-status thresholds, one row per canonical status meaning. Entered in minutes because - /// that is how dispatchers talk about them ("four minutes without reporting departed"). - /// - public List UnitStatusThresholds { get; set; } = new List(); - [StringLength(500, ErrorMessage = "Street address cannot exceed 500 characters.")] public string MapCenterPointAddressAddress1 { get; set; } @@ -100,7 +88,7 @@ public void SetUsers(List users, List names) } } } - /// One configurable built-in new-call field on the department settings screen. + /// One configurable built-in new-call field on the call and dispatch settings screen. public class NewCallFieldPolicyRow { /// Stable key from Resgrid.Model.NewCallFieldKeys. @@ -110,7 +98,7 @@ public class NewCallFieldPolicyRow public bool Required { get; set; } } - /// One time-in-status threshold row on the department settings screen. + /// One time-in-status threshold row on the mapping and big board settings screen. public class UnitStatusThresholdRow { /// The ActionBaseTypes value this row configures. diff --git a/Web/Resgrid.Web/Areas/User/Models/Departments/DispatchSettingsView.cs b/Web/Resgrid.Web/Areas/User/Models/Departments/DispatchSettingsView.cs index 6e9cd9606..8630b5af9 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Departments/DispatchSettingsView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Departments/DispatchSettingsView.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; using Resgrid.Model; @@ -6,6 +6,12 @@ namespace Resgrid.Web.Areas.User.Models.Departments { public class DispatchSettingsView { + /// + /// New-call field policy, one row per configurable built-in field. Bound as a list so the screen + /// can render a Visible/Required grid without a per-field property explosion. + /// + public List NewCallFields { get; set; } = new List(); + public SelectList StatusLevels { get; set; } public SelectList UnitStatusLevels { get; set; } public int ShiftDispatchStatus { get; set; } diff --git a/Web/Resgrid.Web/Areas/User/Models/Departments/MappingSettingsView.cs b/Web/Resgrid.Web/Areas/User/Models/Departments/MappingSettingsView.cs index 5b231164a..1f3cc4f7a 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Departments/MappingSettingsView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Departments/MappingSettingsView.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Resgrid.Web.Areas.User.Models.Departments { @@ -7,6 +8,12 @@ public class MappingSettingsView public bool? SaveSuccess { get; set; } public string Message { get; set; } + /// + /// Time-in-status thresholds driving Big Board highlighting, one row per canonical status + /// meaning. Entered in minutes because that is how dispatchers talk about them. + /// + public List UnitStatusThresholds { get; set; } = new List(); + public int PersonnelLocationTTL { get; set; } public int UnitLocationTTL { get; set; } diff --git a/Web/Resgrid.Web/Areas/User/Views/Department/DispatchSettings.cshtml b/Web/Resgrid.Web/Areas/User/Views/Department/DispatchSettings.cshtml index c608bc031..72194d95e 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Department/DispatchSettings.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Department/DispatchSettings.cshtml @@ -1,4 +1,4 @@ -@model Resgrid.Web.Areas.User.Models.Departments.DispatchSettingsView +@model Resgrid.Web.Areas.User.Models.Departments.DispatchSettingsView @inject IStringLocalizer localizer @{ ViewBag.Title = "Resgrid | " + localizer["CallDispatchSettingsHeader"]; @@ -238,6 +238,37 @@ } +
+

@localizer["NewCallFieldsLabel"]

+ +
+
+ @localizer["NewCallFieldsHelp"] + + + + + + + + + + @for (var i = 0; i < Model.NewCallFields.Count; i++) + { + + + + + + } + +
@localizer["NewCallFieldsFieldColumn"]@localizer["NewCallFieldsVisibleColumn"]@localizer["NewCallFieldsRequiredColumn"]
+ @localizer["NewCallField_" + Model.NewCallFields[i].Key] + +
+
+
+
@commonLocalizer["Cancel"] diff --git a/Web/Resgrid.Web/Areas/User/Views/Department/MappingSettings.cshtml b/Web/Resgrid.Web/Areas/User/Views/Department/MappingSettings.cshtml index 8578540c7..dcbeadd6d 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Department/MappingSettings.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Department/MappingSettings.cshtml @@ -113,6 +113,35 @@
+

@localizer["UnitStatusThresholdsLabel"]

+
+
+ @localizer["UnitStatusThresholdsHelp"] + + + + + + + + + + @for (var i = 0; i < Model.UnitStatusThresholds.Count; i++) + { + + + + + + } + +
@localizer["UnitStatusThresholdsStatusColumn"]@localizer["UnitStatusThresholdsWarnColumn"]@localizer["UnitStatusThresholdsAlertColumn"]
+ @localizer["UnitStatusBaseType_" + Model.UnitStatusThresholds[i].BaseType] + +
+
+
+
@commonLocalizer["Cancel"] diff --git a/Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml b/Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml index 7612abcbe..c57467098 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml @@ -221,72 +221,6 @@
-
- -
- -
- @localizer["NewCallFieldsHelp"] - - - - - - - - - - @for (var i = 0; i < Model.NewCallFields.Count; i++) - { - - - - - - } - -
@localizer["NewCallFieldsFieldColumn"]@localizer["NewCallFieldsVisibleColumn"]@localizer["NewCallFieldsRequiredColumn"]
- @localizer["NewCallField_" + Model.NewCallFields[i].Key] - -
-
-
- -
- -
- -
- @localizer["UnitStatusThresholdsHelp"] - - - - - - - - - - @for (var i = 0; i < Model.UnitStatusThresholds.Count; i++) - { - - - - - - } - -
@localizer["UnitStatusThresholdsStatusColumn"]@localizer["UnitStatusThresholdsWarnColumn"]@localizer["UnitStatusThresholdsAlertColumn"]
- @localizer["UnitStatusBaseType_" + Model.UnitStatusThresholds[i].BaseType] - -
-
-
-
@commonLocalizer["Cancel"] diff --git a/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml b/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml index b6f16e050..6ec23f58f 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml @@ -35,11 +35,6 @@ { @localizer["ReportDelivery"] Active Sessions - @if (Model.CanManageLocalCredentials) - { - Change Username - Change Password - } } else if (Model.CanResetPassword && !Model.IsSsoManaged) { @@ -459,7 +454,6 @@ } else diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_Navigation.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_Navigation.cshtml index c370fc39b..fb8cb32c2 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Shared/_Navigation.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Shared/_Navigation.cshtml @@ -5,42 +5,42 @@ var chatSystemEnabled = await featureToggleService.IsEnabledAsync(FeatureFlagKeys.ChatSystem, ClaimsAuthorizationHelper.GetDepartmentId()); }