Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Core/Resgrid.Config/OidcConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ public static class OidcConfig

public static string Key = "";

public static string ConnectionString = "Server=rgdevserver;Database=ResgridOIDC;User Id=resgrid_odic;Password=resgrid123;MultipleActiveResultSets=True;TrustServerCertificate=True;";
public static string ConnectionString = "";

public static int AccessTokenExpiryMinutes = 1440;

public static int RefreshTokenExpiryDays = 365;

public static int NonMobileRefreshTokenExpiryDays = 2;

/// <summary>
/// Comma-separated, registered client IDs allowed to receive the longer mobile
/// refresh-token lifetime. Anonymous requests and caller-supplied scopes never qualify.
/// </summary>
public static string TrustedLongLivedClientIds = "";

public static string EncryptionCert = "";

public static string SigningCert = "";
Expand Down
24 changes: 24 additions & 0 deletions Core/Resgrid.Config/SessionSecurityConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Resgrid.Config
{
public static class SessionSecurityConfig
{
// Session tracking is required by the Web BFF and is safe for pre-feature
// credentials because they are adopted lazily by the validation middleware.
public static bool TrackingEnabled = true;

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 high

Immutable configuration state in Core/Resgrid.Config/SessionSecurityConfig.cs is expressed as a mutable static field, which obscures intent and permits accidental reassignment. Mark TrackingEnabled as const, and apply the same immutability pattern to the related literals at Core/Resgrid.Config/SessionSecurityConfig.cs:8-8, :13-13, :14-14, :15-15, :16-16, :17-17, :18-18, :19-19, :20-20, Core/Resgrid.Config/OidcConfig.cs:16-16, :28-28, Core/Resgrid.Model/Identity/IdentityUser.cs:138-138, Core/Resgrid.Services/DepartmentSettingsService.cs:27-27, and Core/Resgrid.Services/LocalIpLocationProvider.cs:22-22.

Kody rule violation: Use `readonly` or `const` for Immutable Data

public const bool TrackingEnabled = true;
Prompt for LLM

File Core/Resgrid.Config/SessionSecurityConfig.cs:

Line 7:

Immutable configuration state in Core/Resgrid.Config/SessionSecurityConfig.cs is expressed as a mutable static field, which obscures intent and permits accidental reassignment. Mark TrackingEnabled as const, and apply the same immutability pattern to the related literals at Core/Resgrid.Config/SessionSecurityConfig.cs:8-8, :13-13, :14-14, :15-15, :16-16, :17-17, :18-18, :19-19, :20-20, Core/Resgrid.Config/OidcConfig.cs:16-16, :28-28, Core/Resgrid.Model/Identity/IdentityUser.cs:138-138, Core/Resgrid.Services/DepartmentSettingsService.cs:27-27, and Core/Resgrid.Services/LocalIpLocationProvider.cs:22-22.

Suggested Code:

		public const bool TrackingEnabled = true;

Talk to Kody by mentioning @kody

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

public static bool LegacyAdoptionEnabled = true;
public static string RequireSessionClaimForCredentialsIssuedAfterUtc = "";
// Blank is intentionally disabled at launch. Set to an ISO-8601 UTC timestamp
// only after previewing stored DepartmentSecurityPolicy session values.
public static string DepartmentSessionPolicyEnforcementAfterUtc = "";
public static int LastActivityWriteIntervalMinutes = 5;
public static int RevokedSessionRetentionDays = 90;
public static int PublicResetLinkLifetimeMinutes = 30;
public static int PublicResetAccountLimitPerHour = 3;
public static int PublicResetIpLimitPerHour = 10;
public static int WebBffAccessTokenLifetimeMinutes = 5;
public static int ClientMetadataMaximumLength = 256;
public static int UserAgentMaximumLength = 1024;
// Optional local JSON CIDR database. Leave blank to display location as unavailable.
public static string IpLocationDatabasePath = "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1080,4 +1080,10 @@
<data name="UnitStatusBaseType_8" xml:space="preserve">
<value>Returning</value>
</data>
</root>
<data name="RequirePasswordResetViaEmail" xml:space="preserve">
<value>Require password resets by email</value>
</data>
<data name="RequirePasswordResetViaEmailHelp" xml:space="preserve">
<value>Prevents department and group administrators from choosing or seeing a member's new password. Reset actions send the member a short-lived, single-use email link instead. Existing sessions are revoked after the member successfully changes the password.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,10 @@
<data name="UnitStatusBaseType_8" xml:space="preserve">
<value>Returning</value>
</data>
</root>
<data name="RequirePasswordResetViaEmail" xml:space="preserve">
<value>Require password resets by email</value>
</data>
<data name="RequirePasswordResetViaEmailHelp" xml:space="preserve">
<value>Prevents department and group administrators from choosing or seeing a member's new password. Reset actions send the member a short-lived, single-use email link instead. Existing sessions are revoked after the member successfully changes the password.</value>
</data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.ar.resx
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>على الأقل {0} أحرف</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>رقم واحد على الأقل</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>حرف كبير وحرف صغير على الأقل</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>إرسال بريد إلكتروني لإعادة تعيين كلمة المرور</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>إرسال بريد إلكتروني لإعادة تعيين كلمة المرور</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>سيرسل Resgrid إلى هذا المستخدم رابطًا قصير الصلاحية يُستخدم مرة واحدة إلى عنوان بريده الإلكتروني المؤكد. لن يختار المسؤول كلمة المرور الجديدة ولن يراها.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Mindestens {0} Zeichen lang</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Mindestens eine Ziffer (Zahl)</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Mindestens ein Groß- und ein Kleinbuchstabe</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>E-Mail zum Zurücksetzen des Passworts senden</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>E-Mail zum Zurücksetzen des Passworts senden</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid sendet diesem Benutzer einen kurzlebigen, einmal verwendbaren Link an seine bestätigte E-Mail-Adresse. Der Administrator wählt oder sieht das neue Passwort nicht.</value></data>
</root>
5 changes: 4 additions & 1 deletion Core/Resgrid.Localization/Areas/User/Profile/Profile.el.resx
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Τουλάχιστον {0} χαρακτήρες</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Τουλάχιστον ένα ψηφίο (αριθμός)</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Τουλάχιστον ένα κεφαλαίο και ένα πεζό γράμμα</value></data>
</root>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Αποστολή email επαναφοράς κωδικού πρόσβασης</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Αποστολή email επαναφοράς κωδικού πρόσβασης</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Το Resgrid θα στείλει σε αυτόν τον χρήστη έναν σύνδεσμο σύντομης διάρκειας και μίας χρήσης στην επιβεβαιωμένη διεύθυνση email του. Ο διαχειριστής δεν θα επιλέξει ούτε θα δει τον νέο κωδικό πρόσβασης.</value></data>
</root>
5 changes: 4 additions & 1 deletion Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>At least {0} characters long</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>At least one digit (number)</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>At least one uppercase and one lowercase letter</value></data>
</root>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Send Password Reset Email</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Send Password Reset Email</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid will send this user a short-lived, single-use link at their confirmed email address. The administrator will not choose or see the new password.</value></data>
</root>
5 changes: 4 additions & 1 deletion Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Al menos {0} caracteres</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Al menos un dígito (número)</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Al menos una letra mayúscula y una minúscula</value></data>
</root>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Enviar correo de restablecimiento de contraseña</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Enviar correo de restablecimiento de contraseña</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid enviará a este usuario un enlace de corta duración y de un solo uso a su dirección de correo confirmada. El administrador no elegirá ni verá la nueva contraseña.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Au moins {0} caractères</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Au moins un chiffre</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Au moins une lettre majuscule et une minuscule</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Envoyer l’e-mail de réinitialisation du mot de passe</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Envoyer l’e-mail de réinitialisation du mot de passe</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid enverra à cet utilisateur un lien à courte durée de vie et à usage unique à son adresse e-mail confirmée. L’administrateur ne choisira ni ne verra le nouveau mot de passe.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.it.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Almeno {0} caratteri</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Almeno una cifra (numero)</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Almeno una lettera maiuscola e una minuscola</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Invia email di reimpostazione password</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Invia email di reimpostazione password</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid invierà a questo utente un link di breve durata e monouso al suo indirizzo email confermato. L’amministratore non sceglierà né vedrà la nuova password.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.pl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Co najmniej {0} znaków</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Co najmniej jedna cyfra</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Co najmniej jedna wielka i jedna mała litera</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Wyślij wiadomość e-mail do resetowania hasła</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Wyślij wiadomość e-mail do resetowania hasła</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid wyśle temu użytkownikowi krótkotrwały, jednorazowy link na potwierdzony adres e-mail. Administrator nie wybierze ani nie zobaczy nowego hasła.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.sv.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Minst {0} tecken</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Minst en siffra</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Minst en stor och en liten bokstav</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Skicka e-post för lösenordsåterställning</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Skicka e-post för lösenordsåterställning</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid skickar en kortlivad engångslänk till användarens bekräftade e-postadress. Administratören väljer eller ser inte det nya lösenordet.</value></data>
</root>
3 changes: 3 additions & 0 deletions Core/Resgrid.Localization/Areas/User/Profile/Profile.uk.resx
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,7 @@
<data name="PasswordRequirementsLength" xml:space="preserve"><value>Щонайменше {0} символів</value></data>
<data name="PasswordRequirementsDigit" xml:space="preserve"><value>Щонайменше одна цифра</value></data>
<data name="PasswordRequirementsCase" xml:space="preserve"><value>Щонайменше одна велика та одна мала літера</value></data>
<data name="SendPasswordResetEmailHeader" xml:space="preserve"><value>Надіслати електронний лист для скидання пароля</value></data>
<data name="SendPasswordResetEmail" xml:space="preserve"><value>Надіслати електронний лист для скидання пароля</value></data>
<data name="SendPasswordResetEmailInfo" xml:space="preserve"><value>Resgrid надішле цьому користувачеві короткочасне одноразове посилання на підтверджену електронну адресу. Адміністратор не вибиратиме й не бачитиме новий пароль.</value></data>
</root>
4 changes: 3 additions & 1 deletion Core/Resgrid.Model/AuditLogTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ public enum AuditLogTypes
ModerationReportSubmitted,
ModerationRequestReopened,
ModerationRequestCompleted,
ModerationEvidenceDownloaded
ModerationEvidenceDownloaded,
PasswordResetByAdministrator,
UserAuthenticationSessionsRevoked
}
}
6 changes: 6 additions & 0 deletions Core/Resgrid.Model/DepartmentSettingTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,11 @@ public enum DepartmentSettingTypes
/// before the board highlights it.
/// </summary>
UnitStatusThresholds = 62,

/// <summary>
/// When enabled, department and group administrators cannot choose a member's new password.
/// Their reset action sends the member the hardened, single-use password recovery link instead.
/// </summary>
RequirePasswordResetViaEmail = 63,
}
}
11 changes: 11 additions & 0 deletions Core/Resgrid.Model/ExternalIdentityLinkMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Resgrid.Model
{
public enum ExternalIdentityLinkMethod
{
Subject = 0,
VerifiedEmail = 1,
TrustedSamlEmail = 2,
Scim = 3,
Administrator = 4
}
}
15 changes: 15 additions & 0 deletions Core/Resgrid.Model/Identity/IdentityUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ public IdentityUser(string userName) : this()
//[ProtoMember(15)]
public override int AccessFailedCount { get; set; }

/// <summary>
/// Monotonically increases whenever all credentials for this account must be invalidated.
/// Existing accounts begin at zero so installing the session schema does not sign them out.
/// </summary>
public long AuthenticationGeneration { get; set; }

/// <summary>
/// Credentials issued at or before this UTC instant are invalid. This is also the
/// compatibility boundary for cookies and tokens issued before session tracking existed.
/// </summary>
public DateTime? CredentialsValidAfterUtc { get; set; }

/// <summary>UTC timestamp of the latest account-wide authentication state change.</summary>
public DateTime? AuthenticationStateChangedOn { get; set; }

[System.ComponentModel.DataAnnotations.Schema.NotMapped]
public string UserId
{
Expand Down
6 changes: 4 additions & 2 deletions Core/Resgrid.Model/Providers/IEmailProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ public interface IEmailProvider
{
void Configure(object sender, string fromAddress);

Task<bool> SendWelcomeMail(string name, string departmentName, string userName, string password, string email, int departmentId);
Task<bool> SendPasswordResetMail(string name, string password, string userName, string email, string departmentName);
Task<bool> SendWelcomeMail(string name, string departmentName, string userName, string email, int departmentId);
Task<bool> SendPasswordRecoveryMail(string name, string email, string departmentName,
string resetUrl, string ipAddress, string userAgent, string requestedOn, bool isSsoManaged);
Task<bool> SendPasswordChangedByAdministratorMail(string name, string userName, string email, string departmentName);
Task<bool> SendSignupMail(string name, string departmentName, string email);
Task<bool> SendMessageMail(string email, string subject, string messageSubject, string messageBody, string senderEmail, string senderName, string sentOn, int messageId);
Task<bool> SendCallMail(string email, string subject, string title, string priority, string natureOfCall, string mapPage,
Expand Down
19 changes: 18 additions & 1 deletion Core/Resgrid.Model/Providers/IGifProvider.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ProtoBuf;

namespace Resgrid.Model.Providers
{
/// <summary>A GIF search hit returned to chat clients; urls point at the GIF CDN, never at Resgrid.</summary>
/// <summary>
/// A GIF search hit returned to chat clients; urls point at the GIF CDN, never at Resgrid.
/// Search results are cached, and the cache provider serializes with protobuf-net, so this type needs a
/// contract — without one every cache write throws and the per-query cache silently never populates.
/// </summary>
[ProtoContract]
public class GifSearchResult
{
[ProtoMember(1)]
public string Id { get; set; }

[ProtoMember(2)]
public string Title { get; set; }

/// <summary>Small preview/thumbnail url for the picker grid.</summary>
[ProtoMember(3)]
public string PreviewUrl { get; set; }

/// <summary>Full GIF url embedded in the message metadata.</summary>
[ProtoMember(4)]
public string GifUrl { get; set; }

[ProtoMember(5)]
public int Width { get; set; }

[ProtoMember(6)]
public int Height { get; set; }
}

Expand Down
14 changes: 0 additions & 14 deletions Core/Resgrid.Model/Repositories/IIdentityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ public interface IIdentityRepository
/// <returns>IdentityUser.</returns>
IdentityUser Update(IdentityUser user);

/// <summary>
/// Updates the username.
/// </summary>
/// <param name="oldUsername">The old username.</param>
/// <param name="newUsername">The new username.</param>
void UpdateUsername(string oldUsername, string newUsername);

/// <summary>
/// Adds the user to role.
/// </summary>
Expand Down Expand Up @@ -111,13 +104,6 @@ public interface IIdentityRepository
/// <returns>List&lt;UserGroupRole&gt;.</returns>
Task<List<UserGroupRole>> GetAllUsersGroupsAndRolesAsync(int departmentId, bool retrieveHidden, bool retrieveDisabled, bool retrieveDeleted);

/// <summary>
/// Updates the email.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <param name="newEmail">The new email.</param>
void UpdateEmail(string userId, string newEmail);

/// <summary>
/// Gets the user by user name asynchronous.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Resgrid.Model.Repositories
{
public interface IUserExternalIdentityLinksRepository : IRepository<UserExternalIdentityLink>
{
Task<UserExternalIdentityLink> GetActiveBySubjectAsync(string departmentSsoConfigId, string externalSubject);
Task<UserExternalIdentityLink> GetActiveByUserAndConfigAsync(string userId, string departmentSsoConfigId);
Task<IReadOnlyList<UserExternalIdentityLink>> GetActiveByUserAsync(string userId);
}
}
22 changes: 22 additions & 0 deletions Core/Resgrid.Model/Repositories/IUserSessionsRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Resgrid.Model.Repositories
{
public interface IUserSessionsRepository : IRepository<UserSession>
{
Task<IReadOnlyList<UserSession>> GetActiveByUserAsync(string userId, DateTime utcNow);
Task<UserSession> GetByAuthorizationIdAsync(string authorizationId);
Task<int> TouchAsync(string sessionId, DateTime occurredOn, DateTime writeBefore, string ipAddress,
string country, string region, string city, string userAgent, CancellationToken cancellationToken);
Task<int> UpdateDepartmentAsync(string targetUserId, string sessionId, int departmentId,
CancellationToken cancellationToken);
Task<int> RevokeAsync(string targetUserId, string sessionId, string actorUserId, int reason, DateTime revokedOn, CancellationToken cancellationToken);
Task<int> RevokeOthersAsync(string userId, string currentSessionId, int reason, DateTime revokedOn, CancellationToken cancellationToken);
Task<int> RevokeAllAsync(string targetUserId, string actorUserId, int reason, DateTime revokedOn, CancellationToken cancellationToken);
Task<int> RevokeDepartmentAsync(string targetUserId, int departmentId, int reason, DateTime revokedOn, CancellationToken cancellationToken);
Task<int> PurgeInactiveBeforeAsync(DateTime historyBeforeUtc, CancellationToken cancellationToken);
}
}
Loading