Skip to content

RG-T117 Chatbot fix#426

Open
ucswift wants to merge 3 commits into
masterfrom
develop
Open

RG-T117 Chatbot fix#426
ucswift wants to merge 3 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jul 16, 2026

Copy link
Copy Markdown
Member

Pull Request Description

RG-T117: Chatbot Security Hardening — Step-up PIN Authentication and SMS Identity Verification

This pull request introduces two major security improvements to the chatbot and SMS integration:

1. Security PIN (Step-up Authentication for Destructive Actions)

Adds a new 4-digit security PIN system that acts as a second factor for confirmed destructive or department-wide chatbot/text actions:

  • Per-user opt-in: Users can enable a security PIN on their profile page and set or regenerate their own PIN.
  • Department-level enforcement: A new department setting (ForceChatbotSecurityPin) forces all members to use a PIN. When enabled, every member without a PIN automatically receives a randomly generated one.
  • Chatbot flow: After a user replies YES to confirm an action, if a PIN is required, the session enters an AwaitingPin state. The pending intent only executes after the correct PIN is supplied. After 3 failed attempts, the action is cancelled.
  • PIN security rules: PINs must be exactly 4 digits and are rejected if weak (all-same-digit or consecutive sequences). PINs are generated using a crypto RNG and stored AES-encrypted on the user profile.
  • UI: Department settings page has a new checkbox; user profile page (own profile only) shows PIN input and opt-in toggle.

2. SMS Identity Verification Hardening

Tightens trust rules so an unverified mobile number can never identify or act as a Resgrid user:

  • Existing links re-validated: On every inbound SMS, existing chatbot identity links are only trusted if the linked profile's mobile number is verified AND still matches the sender's number. Stale or unverified SMS links are removed.
  • Removed phone-link confirmation flow: The previous "Reply YES to link" confirmation is replaced by a stricter rule — only a verified mobile number may be auto-linked. Unverified numbers receive a message directing the user to verify their number on their profile.
  • Number change invalidates links: When a user changes their mobile number, all SMS-based chatbot identity links are automatically removed.
  • Controller-level enforcement: Both the Twilio and SignalWire inbound SMS controllers now reject unverified matched profiles and respond with an appropriate verification prompt.
  • Profile save protection: The SecurityPin field is preserved when a caller doesn't supply it (null = unchanged), preventing accidental PIN deletion by unrelated profile saves.

Database

  • Migration M0089 (SQL Server and PostgreSQL) adds SecurityPin (nullable string, encrypted) and SecurityPinEnabled (boolean, default false) columns to UserProfiles.

Tests

  • Added unit tests for SecurityPinUtility (format validation, weakness detection, generation) and SecurityPinService (PIN requirement logic, validation, and generation/ensuring flows).

@request-info

request-info Bot commented Jul 16, 2026

Copy link
Copy Markdown

Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details?

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

The PR adds encrypted four-digit security PINs, department-level PIN enforcement, chatbot step-up verification, verified-mobile SMS gating, PIN management UI, persistence support, and SMS identity invalidation when mobile numbers change.

Changes

Chatbot security PIN enforcement

Layer / File(s) Summary
PIN contracts and persistence
Core/Resgrid.Model/..., Providers/Resgrid.Providers.Migrations...
Adds encrypted PIN fields, department settings contracts, SMS platform constants, and database columns for SQL Server and PostgreSQL.
PIN generation and service wiring
Core/Resgrid.Framework/SecurityPinUtility.cs, Core/Resgrid.Services/...
Adds PIN validation, cryptographic generation, encrypted storage, department provisioning, caching, and dependency injection.
Profile persistence and SMS identity invalidation
Core/Resgrid.Services/UserProfileService.cs, Repositories/...
Preserves stored PINs when omitted from profile updates and removes SMS chatbot identities after mobile-number changes.
Department and profile PIN management
Web/Resgrid.Web/Areas/User/...
Adds department enforcement controls, profile PIN validation and encryption, automatic PIN generation, and form fields.
Chatbot and SMS verification flow
Core/Resgrid.Chatbot/..., Web/Resgrid.Web.Services/Controllers/...
Adds the AwaitingPin state, PIN attempt handling, confirmed-intent dispatch, phone matching, and rejection of unverified SMS sender matches.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SMS
  participant ChatbotIngressService
  participant SecurityPinService
  participant Profile
  User->>SMS: Send destructive command
  SMS->>ChatbotIngressService: Forward verified sender message
  ChatbotIngressService->>SecurityPinService: Check PIN requirement
  SecurityPinService->>Profile: Read or create encrypted PIN
  ChatbotIngressService-->>User: Request security PIN
  User->>ChatbotIngressService: Submit PIN
  ChatbotIngressService->>SecurityPinService: Validate PIN
  SecurityPinService-->>ChatbotIngressService: Validation result
  ChatbotIngressService-->>User: Dispatch intent or cancel action
Loading

Possibly related PRs

  • Resgrid/Core#391: Introduced the chatbot dialog and ingress flow extended here with PIN verification.
  • Resgrid/Core#411: Also modifies the Twilio inbound text-command pipeline.
  • Resgrid/Core#414: Also changes Twilio handling for unmapped or unusable sender profiles.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the PR but too vague to convey the main change beyond a generic chatbot fix. Rename it to summarize the primary change, such as adding security PIN enforcement for chatbot actions.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs (1)

66-105: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve dependencies via Service Locator instead of constructor injection.

As per coding guidelines, dependencies in controllers should be explicitly resolved using the Service Locator pattern (Bootstrapper.GetKernel().Resolve<T>()) to minimize constructor injection.

  • Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs#L66-L105: Remove ISecurityPinService from the constructor parameters and field initializers, and resolve it explicitly.
  • Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs#L74-L117: Remove ISecurityPinService and IEncryptionService from the constructor parameters and resolve them explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs` around lines
66 - 105, Replace constructor injection with Service Locator resolution for the
affected dependencies: in
Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs lines 66-105,
remove ISecurityPinService from the constructor and its assignment, then resolve
it via Bootstrapper.GetKernel().Resolve<ISecurityPinService>() where used; in
Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs lines 74-117, remove
ISecurityPinService and IEncryptionService from the constructor and resolve both
explicitly through the same Service Locator pattern.

Source: Coding guidelines

🧹 Nitpick comments (3)
Core/Resgrid.Services/DepartmentSettingsService.cs (1)

961-976: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use bool.TryParse to prevent potential formatting exceptions.

If a malformed string is stored in the database for the ForceChatbotSecurityPin setting, bool.Parse will throw a FormatException. Consider using bool.TryParse to provide a safe fallback.

♻️ Proposed refactor
 		public async Task<bool> GetForceChatbotSecurityPinAsync(int departmentId, bool bypassCache = false)
 		{
 			async Task<string> getSetting()
 			{
 				var s = await GetSettingByDepartmentIdType(departmentId, DepartmentSettingTypes.ForceChatbotSecurityPin);
 				return s?.Setting ?? "false";
 			}
 
 			if (Config.SystemBehaviorConfig.CacheEnabled && !bypassCache)
 			{
 				var cachedValue = await _cacheProvider.RetrieveAsync<string>(string.Format(ForceChatbotSecurityPinCacheKey, departmentId), getSetting, LongCacheLength);
-				return bool.Parse(cachedValue);
+				return bool.TryParse(cachedValue, out bool result) && result;
 			}
 
-			return bool.Parse(await getSetting());
+			return bool.TryParse(await getSetting(), out bool fallbackResult) && fallbackResult;
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/DepartmentSettingsService.cs` around lines 961 - 976,
Update GetForceChatbotSecurityPinAsync to replace both bool.Parse calls with
bool.TryParse-based handling, returning a safe false fallback when the cached or
database setting is malformed while preserving valid true/false values and
existing cache behavior.
Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs (1)

419-426: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move query inside the IsOwnProfile condition.

model.DepartmentForcesSecurityPin is only used when model.IsOwnProfile is true (both in this controller and the view). Moving this query inside the if block avoids an unnecessary database/cache retrieval when a user views another member's profile.

♻️ Proposed refactor
-			model.DepartmentForcesSecurityPin = await _departmentSettingsService.GetForceChatbotSecurityPinAsync(DepartmentId);
 			if (model.IsOwnProfile)
 			{
+				model.DepartmentForcesSecurityPin = await _departmentSettingsService.GetForceChatbotSecurityPinAsync(DepartmentId);
 				model.SecurityPinEnabled = model.Profile.SecurityPinEnabled;
 				model.SecurityPin = await _securityPinService.GetPinAsync(userId);
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs` around lines 419 -
426, Move the GetForceChatbotSecurityPinAsync call assigning
model.DepartmentForcesSecurityPin inside the existing IsOwnProfile condition in
HomeController, alongside the other owner-only security PIN assignments.
Preserve the current behavior for the profile owner while avoiding the query for
users viewing another member’s profile.
Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs (1)

45-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve ISecurityPinService through the mandated service locator.

Adding another constructor parameter conflicts with the repository’s dependency-resolution convention. Resolve it inside the constructor with Bootstrapper.GetKernel().Resolve<ISecurityPinService>().

As per coding guidelines, “Use Service Locator pattern via Bootstrapper.GetKernel().Resolve<T>() to resolve dependencies explicitly in constructors, rather than constructor injection.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 45 - 61,
Replace the injected ISecurityPinService constructor parameter in
ChatbotIngressService with service-locator resolution inside the constructor,
assigning _securityPinService from
Bootstrapper.GetKernel().Resolve<ISecurityPinService>(). Preserve the existing
field initialization and remove the constructor-injection assignment.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 239-250: Update the PIN-required flow in ChatbotIngressService so
_securityPinService.EnsurePinAsync completes successfully before setting
session.State to AwaitingPin, initializing __pinAttempts, and calling
SaveSessionAsync. Only persist the awaiting-PIN session and return the
generated-PIN response after provisioning succeeds; allow null or thrown
provisioning failures to avoid saving that state or claiming a PIN was
generated.

In `@Core/Resgrid.Services/UserProfileService.cs`:
- Around line 23-28: Update the UserProfileService constructor to stop accepting
injected dependencies and resolve IUserProfilesRepository, ICacheProvider, and
IChatbotIdentityRepository through the established service locator inside the
constructor, assigning the resolved instances to the existing fields.

In `@Web/Resgrid.Web.Services/Controllers/TwilioController.cs`:
- Line 239: Update the master-number branch in the method containing the
senderNumberUnverified parameter to apply the same MobileNumberVerified check
after refetching the profile. Ensure STOP processing does not disable messaging
when the matched profile’s mobile number is unverified, while preserving the
existing behavior for verified profiles.
- Line 152: Redact inbound mobile numbers in the SMS diagnostic logs by masking
textMessage.Msisdn or replacing it with a stable non-reversible identifier.
Apply this to Web/Resgrid.Web.Services/Controllers/TwilioController.cs lines
152-152 and 326-326, and
Web/Resgrid.Web.Services/Controllers/SignalWireController.cs lines 182-182,
while preserving the existing MessageSid and log context.

In `@Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs`:
- Around line 549-556: Reorder the ForceChatbotSecurityPin update flow in
DepartmentController so EnsurePinsForDepartmentAsync completes successfully
before SaveOrUpdateSettingAsync persists the enabled setting. Preserve the
existing transition check using forcePinWasEnabled, ensuring failures leave the
setting disabled so a later request retries PIN generation.

In `@Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs`:
- Around line 696-703: Trim model.SecurityPin before passing it to
SecurityPinUtility.IsValidFormat and SecurityPinUtility.IsWeak in the validation
block, matching the value used during encryption while preserving the existing
validation messages and flow.

In `@Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml`:
- Around line 94-104: Replace the hardcoded “Require Security PIN” label and
help text in Department Settings with entries from the injected `@localizer`. Also
update the Security PIN labels, placeholder text, and help blocks in
EditUserProfile to use localization resources, preserving the existing UI
behavior and wording through resource keys.

---

Outside diff comments:
In `@Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs`:
- Around line 66-105: Replace constructor injection with Service Locator
resolution for the affected dependencies: in
Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs lines 66-105,
remove ISecurityPinService from the constructor and its assignment, then resolve
it via Bootstrapper.GetKernel().Resolve<ISecurityPinService>() where used; in
Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs lines 74-117, remove
ISecurityPinService and IEncryptionService from the constructor and resolve both
explicitly through the same Service Locator pattern.

---

Nitpick comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 45-61: Replace the injected ISecurityPinService constructor
parameter in ChatbotIngressService with service-locator resolution inside the
constructor, assigning _securityPinService from
Bootstrapper.GetKernel().Resolve<ISecurityPinService>(). Preserve the existing
field initialization and remove the constructor-injection assignment.

In `@Core/Resgrid.Services/DepartmentSettingsService.cs`:
- Around line 961-976: Update GetForceChatbotSecurityPinAsync to replace both
bool.Parse calls with bool.TryParse-based handling, returning a safe false
fallback when the cached or database setting is malformed while preserving valid
true/false values and existing cache behavior.

In `@Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs`:
- Around line 419-426: Move the GetForceChatbotSecurityPinAsync call assigning
model.DepartmentForcesSecurityPin inside the existing IsOwnProfile condition in
HomeController, alongside the other owner-only security PIN assignments.
Preserve the current behavior for the profile owner while avoiding the query for
users viewing another member’s profile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 444d6b30-e3fd-49c1-bf69-2ce2a5ea8249

📥 Commits

Reviewing files that changed from the base of the PR and between efd0bca and cc4e3c8.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Services/SecurityPinServiceTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (24)
  • Core/Resgrid.Chatbot/Models/ChatbotPlatform.cs
  • Core/Resgrid.Chatbot/Models/ChatbotSession.cs
  • Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs
  • Core/Resgrid.Framework/SecurityPinUtility.cs
  • Core/Resgrid.Model/ChatbotIdentity.cs
  • Core/Resgrid.Model/DepartmentSettingTypes.cs
  • Core/Resgrid.Model/Services/IDepartmentSettingsService.cs
  • Core/Resgrid.Model/Services/ISecurityPinService.cs
  • Core/Resgrid.Model/UserProfile.cs
  • Core/Resgrid.Services/DepartmentSettingsService.cs
  • Core/Resgrid.Services/SecurityPinService.cs
  • Core/Resgrid.Services/ServicesModule.cs
  • Core/Resgrid.Services/UserProfileService.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0089_AddingUserSecurityPin.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0089_AddingUserSecurityPinPg.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
  • Web/Resgrid.Web.Services/Controllers/SignalWireController.cs
  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs
  • Web/Resgrid.Web/Areas/User/Models/DepartmentSettingsModel.cs
  • Web/Resgrid.Web/Areas/User/Models/EditProfileModel.cs
  • Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml

Comment thread Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs
Comment on lines +23 to +28
public UserProfileService(IUserProfilesRepository userProfileRepository, ICacheProvider cacheProvider,
IChatbotIdentityRepository chatbotIdentityRepository)
{
_userProfileRepository = userProfileRepository;
_cacheProvider = cacheProvider;
_chatbotIdentityRepository = chatbotIdentityRepository;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the Service Locator pattern instead of constructor injection.

As per coding guidelines, dependencies should be resolved explicitly in constructors using the service locator rather than being added as constructor parameters.

♻️ Proposed fix
-		public UserProfileService(IUserProfilesRepository userProfileRepository, ICacheProvider cacheProvider,
-			IChatbotIdentityRepository chatbotIdentityRepository)
+		public UserProfileService(IUserProfilesRepository userProfileRepository, ICacheProvider cacheProvider)
 		{
 			_userProfileRepository = userProfileRepository;
 			_cacheProvider = cacheProvider;
-			_chatbotIdentityRepository = chatbotIdentityRepository;
+			_chatbotIdentityRepository = Bootstrapper.GetKernel().Resolve<IChatbotIdentityRepository>();
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public UserProfileService(IUserProfilesRepository userProfileRepository, ICacheProvider cacheProvider,
IChatbotIdentityRepository chatbotIdentityRepository)
{
_userProfileRepository = userProfileRepository;
_cacheProvider = cacheProvider;
_chatbotIdentityRepository = chatbotIdentityRepository;
public UserProfileService(IUserProfilesRepository userProfileRepository, ICacheProvider cacheProvider)
{
_userProfileRepository = userProfileRepository;
_cacheProvider = cacheProvider;
_chatbotIdentityRepository = Bootstrapper.GetKernel().Resolve<IChatbotIdentityRepository>();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/UserProfileService.cs` around lines 23 - 28, Update the
UserProfileService constructor to stop accepting injected dependencies and
resolve IUserProfilesRepository, ICacheProvider, and IChatbotIdentityRepository
through the established service locator inside the constructor, assigning the
resolved instances to the existing fields.

Source: Coding guidelines

bool senderNumberUnverified = userProfile != null && userProfile.MobileNumberVerified != true;
if (senderNumberUnverified)
{
Framework.Logging.LogInfo($"[Twilio SMS] MessageSid={request.MessageSid} From={textMessage.Msisdn} matched a profile but the mobile number is not verified; not linking sender to user.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact inbound mobile numbers from SMS diagnostics.

These new logs persist complete phone numbers, exposing PII. Mask the number or replace it with a stable non-reversible identifier.

  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs#L152-L152: redact textMessage.Msisdn.
  • Web/Resgrid.Web.Services/Controllers/SignalWireController.cs#L182-L182: redact textMessage.Msisdn.
  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs#L326-L326: redact textMessage.Msisdn.
📍 Affects 2 files
  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs#L152-L152 (this comment)
  • Web/Resgrid.Web.Services/Controllers/SignalWireController.cs#L182-L182
  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs#L326-L326
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web.Services/Controllers/TwilioController.cs` at line 152, Redact
inbound mobile numbers in the SMS diagnostic logs by masking textMessage.Msisdn
or replacing it with a stable non-reversible identifier. Apply this to
Web/Resgrid.Web.Services/Controllers/TwilioController.cs lines 152-152 and
326-326, and Web/Resgrid.Web.Services/Controllers/SignalWireController.cs lines
182-182, while preserving the existing MessageSid and log context.

// and userProfile are resolved by the caller so the flag can be evaluated before dispatching here.
private async System.Threading.Tasks.Task ProcessTextCommandsAsync(TextMessage textMessage, InboundMessageEvent messageEvent,
MessagingResponse response, int? departmentId, UserProfile userProfile)
MessagingResponse response, int? departmentId, UserProfile userProfile, bool senderNumberUnverified = false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply senderNumberUnverified to the master-number branch.

The branch beginning at Line 560 refetches the profile without checking MobileNumberVerified; its STOP command can therefore disable messaging for an unverified-number match. Enforce the same verification gate before using that profile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web.Services/Controllers/TwilioController.cs` at line 239, Update
the master-number branch in the method containing the senderNumberUnverified
parameter to apply the same MobileNumberVerified check after refetching the
profile. Ensure STOP processing does not disable messaging when the matched
profile’s mobile number is unverified, while preserving the existing behavior
for verified profiles.

Comment thread Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs
Comment thread Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs
Comment on lines +94 to +104
<div class="form-group">
<label class="col-sm-2 control-label">Require Security PIN</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-10">
<input type="checkbox" asp-for="ForceChatbotSecurityPin">
</div>
</div>
<span class="help-block m-b-none">Require every member to confirm dangerous or department-wide chatbot/text actions with their personal 4-digit security PIN. Members without a PIN get a randomly generated one (viewable on their profile page).</span>
</div>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Extract hardcoded UI text into localization resources.

New UI elements have been added with hardcoded English strings instead of using the injected @localizer object, which breaks internationalization for these views.

  • Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml#L94-L104: Extract the "Require Security PIN" label and the associated help text into the localizer.
  • Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml#L299-L327: Extract the Security PIN labels, placeholder text, and help blocks into the localizer.
📍 Affects 2 files
  • Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml#L94-L104 (this comment)
  • Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml#L299-L327
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml` around lines 94
- 104, Replace the hardcoded “Require Security PIN” label and help text in
Department Settings with entries from the injected `@localizer`. Also update the
Security PIN labels, placeholder text, and help blocks in EditUserProfile to use
localization resources, preserving the existing UI behavior and wording through
resource keys.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs (2)

454-474: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid mutating session state before resolving the handler.

DispatchConfirmedIntentAsync clears session.State and session.PendingIntent before verifying if a handler exists. If no handler claims the intent, it returns null but leaves the session object inappropriately mutated, which could lead to inconsistent state tracking.

🛡️ Proposed fix to verify handler existence first
 		private async Task<ChatbotResponse> DispatchConfirmedIntentAsync(ChatbotMessage message, ChatbotSession session)
 		{
 			var pendingType = session.PendingIntent.Value;
+
+			var confirmHandler = _actionHandlers.FirstOrDefault(h => h.CanHandle(pendingType));
+			if (confirmHandler == null)
+				return null;
+
 			var confirmedIntent = new ChatbotIntent
 			{
 				Type = pendingType,
 				Parameters = new Dictionary<string, string>(session.Context) { ["__confirmed"] = "true" }
 			};
 			session.State = ChatbotDialogState.Idle;
 			session.PendingIntent = null;
 
-			var confirmHandler = _actionHandlers.FirstOrDefault(h => h.CanHandle(pendingType));
-			if (confirmHandler == null)
-				return null;
-
 			var confirmResponse = await confirmHandler.HandleAsync(message, confirmedIntent, session);
 			confirmResponse.Intent = confirmedIntent;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 454 -
474, The method DispatchConfirmedIntentAsync currently mutates session.State and
session.PendingIntent before confirming a handler exists. Resolve confirmHandler
and return null before changing either session property; only clear the pending
state after a handler is found, preserving the existing handling and persistence
flow.

326-328: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle missing handler gracefully.

If DispatchConfirmedIntentAsync returns null because no handler was found, the execution falls through to normal intent classification, which will likely result in an "unknown command" error response. Return a clear error message instead.

🛠️ Proposed fix to handle null return
 						var confirmResponse = await DispatchConfirmedIntentAsync(message, session);
 						if (confirmResponse != null)
 							return confirmResponse;
+
+						return new ChatbotResponse
+						{
+							Text = "Unable to complete action — please try again or contact support.",
+							Processed = false
+						};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 326 -
328, Update the confirmed-intent branch in the surrounding ingress method so a
null result from DispatchConfirmedIntentAsync is handled immediately with a
clear error response indicating that no handler was found. Preserve returning
confirmResponse when it is non-null, and prevent execution from falling through
to normal intent classification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 263-293: In the confirmation flow, remove the duplicate inline
handler-dispatch block and retain a single confirmResponse declaration using
DispatchConfirmedIntentAsync. If that method returns null, return the existing
explicit failure ChatbotResponse so missing handlers or unsuccessful dispatches
are handled without dereferencing or mutating session state.

---

Outside diff comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 454-474: The method DispatchConfirmedIntentAsync currently mutates
session.State and session.PendingIntent before confirming a handler exists.
Resolve confirmHandler and return null before changing either session property;
only clear the pending state after a handler is found, preserving the existing
handling and persistence flow.
- Around line 326-328: Update the confirmed-intent branch in the surrounding
ingress method so a null result from DispatchConfirmedIntentAsync is handled
immediately with a clear error response indicating that no handler was found.
Preserve returning confirmResponse when it is non-null, and prevent execution
from falling through to normal intent classification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e124e86-7880-4ec8-b32b-097ddd505457

📥 Commits

Reviewing files that changed from the base of the PR and between cc4e3c8 and 7826e61.

📒 Files selected for processing (1)
  • Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs (2)

454-474: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid mutating session state before resolving the handler.

DispatchConfirmedIntentAsync clears session.State and session.PendingIntent before verifying if a handler exists. If no handler claims the intent, it returns null but leaves the session object inappropriately mutated, which could lead to inconsistent state tracking.

🛡️ Proposed fix to verify handler existence first
 		private async Task<ChatbotResponse> DispatchConfirmedIntentAsync(ChatbotMessage message, ChatbotSession session)
 		{
 			var pendingType = session.PendingIntent.Value;
+
+			var confirmHandler = _actionHandlers.FirstOrDefault(h => h.CanHandle(pendingType));
+			if (confirmHandler == null)
+				return null;
+
 			var confirmedIntent = new ChatbotIntent
 			{
 				Type = pendingType,
 				Parameters = new Dictionary<string, string>(session.Context) { ["__confirmed"] = "true" }
 			};
 			session.State = ChatbotDialogState.Idle;
 			session.PendingIntent = null;
 
-			var confirmHandler = _actionHandlers.FirstOrDefault(h => h.CanHandle(pendingType));
-			if (confirmHandler == null)
-				return null;
-
 			var confirmResponse = await confirmHandler.HandleAsync(message, confirmedIntent, session);
 			confirmResponse.Intent = confirmedIntent;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 454 -
474, The method DispatchConfirmedIntentAsync currently mutates session.State and
session.PendingIntent before confirming a handler exists. Resolve confirmHandler
and return null before changing either session property; only clear the pending
state after a handler is found, preserving the existing handling and persistence
flow.

326-328: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle missing handler gracefully.

If DispatchConfirmedIntentAsync returns null because no handler was found, the execution falls through to normal intent classification, which will likely result in an "unknown command" error response. Return a clear error message instead.

🛠️ Proposed fix to handle null return
 						var confirmResponse = await DispatchConfirmedIntentAsync(message, session);
 						if (confirmResponse != null)
 							return confirmResponse;
+
+						return new ChatbotResponse
+						{
+							Text = "Unable to complete action — please try again or contact support.",
+							Processed = false
+						};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 326 -
328, Update the confirmed-intent branch in the surrounding ingress method so a
null result from DispatchConfirmedIntentAsync is handled immediately with a
clear error response indicating that no handler was found. Preserve returning
confirmResponse when it is non-null, and prevent execution from falling through
to normal intent classification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 263-293: In the confirmation flow, remove the duplicate inline
handler-dispatch block and retain a single confirmResponse declaration using
DispatchConfirmedIntentAsync. If that method returns null, return the existing
explicit failure ChatbotResponse so missing handlers or unsuccessful dispatches
are handled without dereferencing or mutating session state.

---

Outside diff comments:
In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs`:
- Around line 454-474: The method DispatchConfirmedIntentAsync currently mutates
session.State and session.PendingIntent before confirming a handler exists.
Resolve confirmHandler and return null before changing either session property;
only clear the pending state after a handler is found, preserving the existing
handling and persistence flow.
- Around line 326-328: Update the confirmed-intent branch in the surrounding
ingress method so a null result from DispatchConfirmedIntentAsync is handled
immediately with a clear error response indicating that no handler was found.
Preserve returning confirmResponse when it is non-null, and prevent execution
from falling through to normal intent classification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e124e86-7880-4ec8-b32b-097ddd505457

📥 Commits

Reviewing files that changed from the base of the PR and between cc4e3c8 and 7826e61.

📒 Files selected for processing (1)
  • Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs
🛑 Comments failed to post (1)
Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs (1)

263-293: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix compilation error due to duplicate confirmResponse and handle missing handlers.

The inline confirmation dispatch logic was left in place alongside the new DispatchConfirmedIntentAsync call, leading to a compilation error because confirmResponse is defined twice in the same scope. Remove the duplicate inline logic and ensure you handle cases where DispatchConfirmedIntentAsync returns null.

🐛 Proposed fix to remove duplicated logic
 						var confirmResponse = await DispatchConfirmedIntentAsync(message, session);
 						if (confirmResponse != null)
 							return confirmResponse;
-						var pendingType = session.PendingIntent.Value;
-
-						// Locate the owning handler BEFORE mutating session state. If none can handle
-						// the pending intent, leave the session parked in AwaitingConfirmation and return
-						// an explicit error rather than silently dropping the confirmation.
-						var confirmHandler = _actionHandlers.FirstOrDefault(h => h.CanHandle(pendingType));
-						if (confirmHandler == null)
-						{
-							return new ChatbotResponse
-							{
-								Text = "Unable to complete confirmation — please try again or contact support.",
-								Processed = false
-							};
-						}
-
-						var confirmedIntent = new ChatbotIntent
-						{
-							Type = pendingType,
-							Parameters = new Dictionary<string, string>(session.Context) { ["__confirmed"] = "true" }
-						};
-
-						var confirmResponse = await confirmHandler.HandleAsync(message, confirmedIntent, session);
-						confirmResponse.Intent = confirmedIntent;
-						session.Context.Clear();
-						session.State = ChatbotDialogState.Idle;
-						session.PendingIntent = null;
-						await _sessionManager.SaveSessionAsync(session);
-						return confirmResponse;
+
+						return new ChatbotResponse
+						{
+							Text = "Unable to complete confirmation — please try again or contact support.",
+							Processed = false
+						};
🧰 Tools
🪛 GitHub Actions: .NET / 2_build-and-test.txt

[error] 287-287: CS0128: A local variable or function named 'confirmResponse' is already defined in this scope.

🪛 GitHub Actions: .NET / build-and-test

[error] 287-287: CS0128: A local variable or function named 'confirmResponse' is already defined in this scope.

🪛 GitHub Check: build-and-test

[failure] 287-287:
A local variable or function named 'confirmResponse' is already defined in this scope


[failure] 287-287:
A local variable or function named 'confirmResponse' is already defined in this scope

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs` around lines 263 -
293, In the confirmation flow, remove the duplicate inline handler-dispatch
block and retain a single confirmResponse declaration using
DispatchConfirmedIntentAsync. If that method returns null, return the existing
explicit failure ChatbotResponse so missing handlers or unsuccessful dispatches
are handled without dereferencing or mutating session state.

Source: Linters/SAST tools

{
rng.GetBytes(bytes);
uint value = (uint)(bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24);
string pin = (value % 10000).ToString().PadLeft(PinLength, '0');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

The literal 10000 in 'value % 10000' is an unextracted magic number representing 10^PinLength, violating the 'Replace magic numbers with named constants' rule. Extract it to a private const PinSpace derived from PinLength.

private const int PinSpace = 10000; // 10^PinLength
// ...
string pin = (value % PinSpace).ToString().PadLeft(PinLength, '0');
Prompt for LLM

File Core/Resgrid.Framework/SecurityPinUtility.cs:

Line 64:

Violates rule 'Replace magic numbers with named constants': the literal 10000 in 'value % 10000' represents the count of possible 4-digit PIN values (10^PinLength) but is not extracted to a named constant. PinLength is already defined as a const, so the modulus could derive from it.

Suggested Code:

private const int PinSpace = 10000; // 10^PinLength
// ...
string pin = (value % PinSpace).ToString().PadLeft(PinLength, '0');

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread Core/Resgrid.Services/SecurityPinService.cs Outdated
Comment thread Core/Resgrid.Services/SecurityPinService.cs Outdated
Comment thread Web/Resgrid.Web.Services/Controllers/TwilioController.cs Outdated
@Resgrid-Bot

Copy link
Copy Markdown

PR Summary (Comment created by Kody 🤖)

Code Review Started! 🚀

✋ Hi, team! I'm already looking at the changed files and starting the review to ensure everything is in order. If you need more details, I'm here! Kody

📂 Changed Files
File Status ➕ Additions ➖ Deletions 🔄 Changes
Core/Resgrid.Chatbot/Services/ChatbotIngressService.cs modified 40 46 86
Core/Resgrid.Framework/StringHelpers.cs modified 16 0 16
Core/Resgrid.Localization/Areas/User/Department/Department.ar.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.de.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.en.resx modified 7 1 8
Core/Resgrid.Localization/Areas/User/Department/Department.es.resx modified 6 0 6
Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.it.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.resx modified 7 1 8
Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx modified 8 2 10
Core/Resgrid.Localization/Areas/User/Home/EditProfile.ar.resx modified 20 2 22
Core/Resgrid.Localization/Areas/User/Home/EditProfile.de.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.en.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.es.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.fr.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.it.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.pl.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.sv.resx modified 19 1 20
Core/Resgrid.Localization/Areas/User/Home/EditProfile.uk.resx modified 19 1 20
Core/Resgrid.Model/Repositories/IUserProfilesRepository.cs modified 10 0 10
Core/Resgrid.Services/SecurityPinService.cs modified 26 6 32
Repositories/Resgrid.Repositories.DataRepository/UserProfilesRepository.cs modified 51 0 51
Tests/Resgrid.Tests/Services/SecurityPinServiceTests.cs modified 39 7 46
Web/Resgrid.Web.Services/Controllers/SignalWireController.cs modified 1 1 2
Web/Resgrid.Web.Services/Controllers/TwilioController.cs modified 4 4 8
Web/Resgrid.Web/Areas/User/Controllers/DepartmentController.cs modified 6 3 9
Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs modified 3 0 3
Web/Resgrid.Web/Areas/User/Views/Department/Settings.cshtml modified 2 2 4
Web/Resgrid.Web/Areas/User/Views/Home/EditUserProfile.cshtml modified 6 6 12
📊 Summary of Changes
  • Total Files: 31
  • Total Lines Added: 452
  • Total Lines Removed: 101
  • Total Changes: 553

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants