✨ Auth factors APIs
This commit is contained in:
parent
e62b2cc5ff
commit
49d5ee6184
@ -5,6 +5,7 @@ using DysonNetwork.Sphere.Permission;
|
|||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using OtpNet;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
@ -32,18 +33,19 @@ public class Account : ModelBase
|
|||||||
|
|
||||||
public abstract class Leveling
|
public abstract class Leveling
|
||||||
{
|
{
|
||||||
public static readonly List<int> ExperiencePerLevel = [
|
public static readonly List<int> ExperiencePerLevel =
|
||||||
0, // Level 0
|
[
|
||||||
100, // Level 1
|
0, // Level 0
|
||||||
250, // Level 2
|
100, // Level 1
|
||||||
500, // Level 3
|
250, // Level 2
|
||||||
1000, // Level 4
|
500, // Level 3
|
||||||
2000, // Level 5
|
1000, // Level 4
|
||||||
4000, // Level 6
|
2000, // Level 5
|
||||||
8000, // Level 7
|
4000, // Level 6
|
||||||
16000, // Level 8
|
8000, // Level 7
|
||||||
32000, // Level 9
|
16000, // Level 8
|
||||||
64000, // Level 10
|
32000, // Level 9
|
||||||
|
64000, // Level 10
|
||||||
128000, // Level 11
|
128000, // Level 11
|
||||||
256000, // Level 12
|
256000, // Level 12
|
||||||
512000, // Level 13
|
512000, // Level 13
|
||||||
@ -65,9 +67,12 @@ public class Profile : ModelBase
|
|||||||
|
|
||||||
public int Experience { get; set; } = 0;
|
public int Experience { get; set; } = 0;
|
||||||
[NotMapped] public int Level => Leveling.ExperiencePerLevel.Count(xp => Experience >= xp) - 1;
|
[NotMapped] public int Level => Leveling.ExperiencePerLevel.Count(xp => Experience >= xp) - 1;
|
||||||
[NotMapped] public double LevelingProgress => Level >= Leveling.ExperiencePerLevel.Count - 1 ? 100 :
|
|
||||||
(Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 /
|
[NotMapped]
|
||||||
(Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]);
|
public double LevelingProgress => Level >= Leveling.ExperiencePerLevel.Count - 1
|
||||||
|
? 100
|
||||||
|
: (Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 /
|
||||||
|
(Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]);
|
||||||
|
|
||||||
// Outdated fields, for backward compability
|
// Outdated fields, for backward compability
|
||||||
[MaxLength(32)] public string? PictureId { get; set; }
|
[MaxLength(32)] public string? PictureId { get; set; }
|
||||||
@ -102,7 +107,18 @@ public class AccountAuthFactor : ModelBase
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public AccountAuthFactorType Type { get; set; }
|
public AccountAuthFactorType Type { get; set; }
|
||||||
[MaxLength(8196)] public string? Secret { get; set; } = null;
|
[MaxLength(8196)] public string? Secret { get; set; }
|
||||||
|
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Config { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The trustworthy stands for how safe is this auth factor.
|
||||||
|
/// Basically, it affects how many steps it can complete in authentication.
|
||||||
|
/// Besides, users may need to use some high trustworthy level auth factors when confirming some dangerous operations.
|
||||||
|
/// </summary>
|
||||||
|
public int Trustworthy { get; set; } = 1;
|
||||||
|
|
||||||
|
public Instant? EnabledAt { get; set; }
|
||||||
|
public Instant? ExpiredAt { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account Account { get; set; } = null!;
|
[JsonIgnore] public Account Account { get; set; } = null!;
|
||||||
@ -118,8 +134,24 @@ public class AccountAuthFactor : ModelBase
|
|||||||
{
|
{
|
||||||
if (Secret == null)
|
if (Secret == null)
|
||||||
throw new InvalidOperationException("Auth factor with no secret cannot be verified with password.");
|
throw new InvalidOperationException("Auth factor with no secret cannot be verified with password.");
|
||||||
return BCrypt.Net.BCrypt.Verify(password, Secret);
|
switch (Type)
|
||||||
|
{
|
||||||
|
case AccountAuthFactorType.Password:
|
||||||
|
return BCrypt.Net.BCrypt.Verify(password, Secret);
|
||||||
|
case AccountAuthFactorType.TimedCode:
|
||||||
|
var otp = new Totp(Base32Encoding.ToBytes(Secret));
|
||||||
|
return otp.VerifyTotp(DateTime.UtcNow, password, out _, new VerificationWindow(previous: 5, future: 5));
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unsupported verification type, use CheckDeliveredCode instead.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This dictionary will be returned to the client and should only be set when it just created.
|
||||||
|
/// Useful for passing the client some data to finishing setup and recovery code.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public Dictionary<string, object>? CreatedResponse { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AccountAuthFactorType
|
public enum AccountAuthFactorType
|
||||||
|
@ -15,7 +15,6 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
public class AccountCurrentController(
|
public class AccountCurrentController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
FileService fs,
|
|
||||||
FileReferenceService fileRefService,
|
FileReferenceService fileRefService,
|
||||||
AccountEventService events,
|
AccountEventService events,
|
||||||
AuthService auth
|
AuthService auth
|
||||||
@ -93,8 +92,10 @@ public class AccountCurrentController(
|
|||||||
var profileResourceId = $"profile:{profile.Id}";
|
var profileResourceId = $"profile:{profile.Id}";
|
||||||
|
|
||||||
// Remove old references for the profile picture
|
// Remove old references for the profile picture
|
||||||
if (profile.Picture is not null) {
|
if (profile.Picture is not null)
|
||||||
var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
{
|
||||||
|
var oldPictureRefs =
|
||||||
|
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
||||||
foreach (var oldRef in oldPictureRefs)
|
foreach (var oldRef in oldPictureRefs)
|
||||||
{
|
{
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
@ -119,8 +120,10 @@ public class AccountCurrentController(
|
|||||||
var profileResourceId = $"profile:{profile.Id}";
|
var profileResourceId = $"profile:{profile.Id}";
|
||||||
|
|
||||||
// Remove old references for the profile background
|
// Remove old references for the profile background
|
||||||
if (profile.Background is not null) {
|
if (profile.Background is not null)
|
||||||
var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
{
|
||||||
|
var oldBackgroundRefs =
|
||||||
|
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
||||||
foreach (var oldRef in oldBackgroundRefs)
|
foreach (var oldRef in oldBackgroundRefs)
|
||||||
{
|
{
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
@ -334,8 +337,48 @@ public class AccountCurrentController(
|
|||||||
return Ok(factors);
|
return Ok(factors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AuthFactorRequest
|
||||||
|
{
|
||||||
|
public AccountAuthFactorType Type { get; set; }
|
||||||
|
public string? Secret { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("factors")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
|
||||||
|
return BadRequest($"Auth factor with type {request.Type} is already exists.");
|
||||||
|
|
||||||
|
var factor = await accounts.CreateAuthFactor(currentUser, request.Type, request.Secret);
|
||||||
|
return Ok(factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("factors/{id:guid}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor(Guid id, [FromBody] string code)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var factor = await db.AccountAuthFactors
|
||||||
|
.Where(f => f.AccountId == id && f.Id == id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if(factor is null) return NotFound();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
factor = await accounts.EnableAuthFactor(factor, code);
|
||||||
|
return Ok(factor);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("sessions")]
|
[HttpGet("sessions")]
|
||||||
public async Task<ActionResult<List<Auth.Session>>> GetSessions(
|
public async Task<ActionResult<List<Session>>> GetSessions(
|
||||||
[FromQuery] int take = 20,
|
[FromQuery] int take = 20,
|
||||||
[FromQuery] int offset = 0
|
[FromQuery] int offset = 0
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ using DysonNetwork.Sphere.Storage;
|
|||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using OtpNet;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
@ -64,6 +65,91 @@ public class AccountService(
|
|||||||
await spells.NotifyMagicSpell(spell);
|
await spells.NotifyMagicSpell(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type)
|
||||||
|
{
|
||||||
|
var isExists = await db.AccountAuthFactors
|
||||||
|
.Where(x => x.AccountId == account.Id && x.Type == type)
|
||||||
|
.AnyAsync();
|
||||||
|
return isExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccountAuthFactor?> CreateAuthFactor(Account account, AccountAuthFactorType type, string? secret)
|
||||||
|
{
|
||||||
|
AccountAuthFactor? factor = null;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case AccountAuthFactorType.Password:
|
||||||
|
if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret));
|
||||||
|
factor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.Password,
|
||||||
|
Trustworthy = 1,
|
||||||
|
AccountId = account.Id,
|
||||||
|
Secret = secret,
|
||||||
|
EnabledAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
|
}.HashSecret();
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.EmailCode:
|
||||||
|
factor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.EmailCode,
|
||||||
|
Trustworthy = 2,
|
||||||
|
EnabledAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.InAppCode:
|
||||||
|
factor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.InAppCode,
|
||||||
|
Trustworthy = 1,
|
||||||
|
EnabledAt = SystemClock.Instance.GetCurrentInstant()
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.TimedCode:
|
||||||
|
var skOtp = KeyGeneration.GenerateRandomKey(20);
|
||||||
|
var skOtp32 = Base32Encoding.ToString(skOtp);
|
||||||
|
factor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Secret = skOtp32,
|
||||||
|
Type = AccountAuthFactorType.InAppCode,
|
||||||
|
Trustworthy = 2,
|
||||||
|
EnabledAt = null, // It needs to be tired once to enable
|
||||||
|
CreatedResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["uri"] = new OtpUri(
|
||||||
|
OtpType.Totp,
|
||||||
|
skOtp32,
|
||||||
|
account.Id.ToString(),
|
||||||
|
"Solar Network"
|
||||||
|
).ToString(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factor is null) throw new InvalidOperationException("Unable to create auth factor.");
|
||||||
|
db.AccountAuthFactors.Add(factor);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string code)
|
||||||
|
{
|
||||||
|
if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled.");
|
||||||
|
if (!factor.VerifyPassword(code))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Invalid code, you need to enter the correct code to enable the factor."
|
||||||
|
);
|
||||||
|
|
||||||
|
factor.EnabledAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
db.Update(factor);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return factor;
|
||||||
|
}
|
||||||
|
|
||||||
/// Maintenance methods for server administrator
|
/// Maintenance methods for server administrator
|
||||||
public async Task EnsureAccountProfileCreated()
|
public async Task EnsureAccountProfileCreated()
|
||||||
{
|
{
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||||
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||||
|
@ -76,6 +76,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATusDiskStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fe1_003Fefd9af34_003FTusDiskStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATusDiskStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fe1_003Fefd9af34_003FTusDiskStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATusDiskStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F1c_003F21999acd_003FTusDiskStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATusDiskStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F1c_003F21999acd_003FTusDiskStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5d2c480da9be415dab9be535bb6d08713cc00_003Fd0_003Fffc36a51_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5d2c480da9be415dab9be535bb6d08713cc00_003Fd0_003Fffc36a51_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user