✨ Implementation of email code and in app code
This commit is contained in:
parent
3c123be6a7
commit
af39694be6
@ -1,9 +1,14 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Email;
|
||||||
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
using DysonNetwork.Sphere.Pages.Emails;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Org.BouncyCastle.Utilities;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
@ -12,7 +17,10 @@ public class AccountService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
MagicSpellService spells,
|
MagicSpellService spells,
|
||||||
NotificationService nty,
|
NotificationService nty,
|
||||||
ICacheService cache
|
EmailService email,
|
||||||
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
|
ICacheService cache,
|
||||||
|
ILogger<AccountService> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public static void SetCultureInfo(Account account)
|
public static void SetCultureInfo(Account account)
|
||||||
@ -26,7 +34,7 @@ public class AccountService(
|
|||||||
CultureInfo.CurrentCulture = info;
|
CultureInfo.CurrentCulture = info;
|
||||||
CultureInfo.CurrentUICulture = info;
|
CultureInfo.CurrentUICulture = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string AccountCachePrefix = "account:";
|
public const string AccountCachePrefix = "account:";
|
||||||
|
|
||||||
public async Task PurgeAccountCache(Account account)
|
public async Task PurgeAccountCache(Account account)
|
||||||
@ -192,6 +200,119 @@ public class AccountService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send the auth factor verification code to users, for factors like in-app code and email.
|
||||||
|
/// Sometimes it requires a hint, like a part of the user's email address to ensure the user is who own the account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">The owner of the auth factor</param>
|
||||||
|
/// <param name="factor">The auth factor needed to send code</param>
|
||||||
|
/// <param name="hint">The part of the contact method for verification</param>
|
||||||
|
public async Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null)
|
||||||
|
{
|
||||||
|
var code = new Random().Next(100000, 999999).ToString("000000");
|
||||||
|
|
||||||
|
switch (factor.Type)
|
||||||
|
{
|
||||||
|
case AccountAuthFactorType.InAppCode:
|
||||||
|
if (await _GetFactorCode(factor) is not null)
|
||||||
|
throw new InvalidOperationException("A factor code has been sent and in active duration.");
|
||||||
|
|
||||||
|
await nty.SendNotification(
|
||||||
|
account,
|
||||||
|
"auth.verification",
|
||||||
|
localizer["AuthCodeTitle"],
|
||||||
|
null,
|
||||||
|
localizer["AuthCodeBody", code],
|
||||||
|
save: true
|
||||||
|
);
|
||||||
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.EmailCode:
|
||||||
|
if (await _GetFactorCode(factor) is not null)
|
||||||
|
throw new InvalidOperationException("A factor code has been sent and in active duration.");
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(hint);
|
||||||
|
hint = hint.Replace("@", "").Replace(".", "").Replace("+", "").Replace("%", "");
|
||||||
|
if (string.IsNullOrWhiteSpace(hint))
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Unable to send factor code to #{FactorId} with hint {Hint}, due to invalid hint...",
|
||||||
|
factor.Id,
|
||||||
|
hint
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contact = await db.AccountContacts
|
||||||
|
.Where(c => c.Type == AccountContactType.Email)
|
||||||
|
.Where(c => c.VerifiedAt != null)
|
||||||
|
.Where(c => EF.Functions.ILike(c.Content, $"%{hint}%"))
|
||||||
|
.Include(c => c.Account)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (contact is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Unable to send factor code to #{FactorId} with hint {Hint}, due to no contact method found according to hint...",
|
||||||
|
factor.Id,
|
||||||
|
hint
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await email.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
||||||
|
contact.Content,
|
||||||
|
localizer["EmailVerificationTitle"],
|
||||||
|
localizer["VerificationEmail"],
|
||||||
|
new VerificationEmailModel
|
||||||
|
{
|
||||||
|
Name = account.Name,
|
||||||
|
Code = code
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
||||||
|
break;
|
||||||
|
case AccountAuthFactorType.Password:
|
||||||
|
case AccountAuthFactorType.TimedCode:
|
||||||
|
default:
|
||||||
|
// No need to send, such as password etc...
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code)
|
||||||
|
{
|
||||||
|
switch (factor.Type)
|
||||||
|
{
|
||||||
|
case AccountAuthFactorType.EmailCode:
|
||||||
|
case AccountAuthFactorType.InAppCode:
|
||||||
|
var correctCode = await _GetFactorCode(factor);
|
||||||
|
return correctCode is not null && string.Equals(correctCode, code, StringComparison.OrdinalIgnoreCase);
|
||||||
|
case AccountAuthFactorType.Password:
|
||||||
|
case AccountAuthFactorType.TimedCode:
|
||||||
|
default:
|
||||||
|
return factor.VerifyPassword(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string AuthFactorCachePrefix = "authfactor:";
|
||||||
|
|
||||||
|
private async Task _SetFactorCode(AccountAuthFactor factor, string code, TimeSpan expires)
|
||||||
|
{
|
||||||
|
await cache.SetAsync(
|
||||||
|
$"{AuthFactorCachePrefix}{factor.Id}:code",
|
||||||
|
code,
|
||||||
|
expires
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> _GetFactorCode(AccountAuthFactor factor)
|
||||||
|
{
|
||||||
|
return await cache.GetAsync<string?>(
|
||||||
|
$"{AuthFactorCachePrefix}{factor.Id}:code"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteSession(Account account, Guid sessionId)
|
public async Task DeleteSession(Account account, Guid sessionId)
|
||||||
{
|
{
|
||||||
var session = await db.AuthSessions
|
var session = await db.AuthSessions
|
||||||
@ -204,10 +325,10 @@ public class AccountService(
|
|||||||
.Include(s => s.Challenge)
|
.Include(s => s.Challenge)
|
||||||
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
|
.Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if(session.Challenge.DeviceId is not null)
|
if (session.Challenge.DeviceId is not null)
|
||||||
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
||||||
|
|
||||||
// The current session should be included in the sessions' list
|
// The current session should be included in the sessions' list
|
||||||
db.AuthSessions.RemoveRange(sessions);
|
db.AuthSessions.RemoveRange(sessions);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
@ -67,13 +67,12 @@ public class NotificationService(
|
|||||||
string? subtitle = null,
|
string? subtitle = null,
|
||||||
string? content = null,
|
string? content = null,
|
||||||
Dictionary<string, object>? meta = null,
|
Dictionary<string, object>? meta = null,
|
||||||
bool isSilent = false
|
bool isSilent = false,
|
||||||
|
bool save = true
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (title is null && subtitle is null && content is null)
|
if (title is null && subtitle is null && content is null)
|
||||||
{
|
|
||||||
throw new ArgumentException("Unable to send notification that completely empty.");
|
throw new ArgumentException("Unable to send notification that completely empty.");
|
||||||
}
|
|
||||||
|
|
||||||
var notification = new Notification
|
var notification = new Notification
|
||||||
{
|
{
|
||||||
@ -85,8 +84,11 @@ public class NotificationService(
|
|||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(notification);
|
if (save)
|
||||||
await db.SaveChangesAsync();
|
{
|
||||||
|
db.Add(notification);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (!isSilent) _ = DeliveryNotification(notification);
|
if (!isSilent) _ = DeliveryNotification(notification);
|
||||||
|
|
||||||
|
@ -87,7 +87,11 @@ public class AuthController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("challenge/{id:guid}/factors/{factorId:guid}")]
|
[HttpPost("challenge/{id:guid}/factors/{factorId:guid}")]
|
||||||
public async Task<ActionResult> RequestFactorCode([FromRoute] Guid id, [FromRoute] Guid factorId)
|
public async Task<ActionResult> RequestFactorCode(
|
||||||
|
[FromRoute] Guid id,
|
||||||
|
[FromRoute] Guid factorId,
|
||||||
|
[FromBody] string? hint
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var challenge = await db.AuthChallenges
|
var challenge = await db.AuthChallenges
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
@ -98,7 +102,14 @@ public class AuthController(
|
|||||||
.Where(e => e.Account == challenge.Account).FirstOrDefaultAsync();
|
.Where(e => e.Account == challenge.Account).FirstOrDefaultAsync();
|
||||||
if (factor is null) return NotFound("Auth factor was not found.");
|
if (factor is null) return NotFound("Auth factor was not found.");
|
||||||
|
|
||||||
// TODO do the logic here
|
try
|
||||||
|
{
|
||||||
|
await accounts.SendFactorCode(challenge.Account, factor, hint);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@ -127,7 +138,7 @@ public class AuthController(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (factor.VerifyPassword(request.Password))
|
if (await accounts.VerifyFactorCode(factor, request.Password))
|
||||||
{
|
{
|
||||||
challenge.StepRemain--;
|
challenge.StepRemain--;
|
||||||
challenge.BlacklistFactors.Add(factor.Id);
|
challenge.BlacklistFactors.Add(factor.Id);
|
||||||
@ -226,8 +237,8 @@ public class AuthController(
|
|||||||
var tk = auth.CreateToken(session);
|
var tk = auth.CreateToken(session);
|
||||||
return Ok(new TokenExchangeResponse { Token = tk });
|
return Ok(new TokenExchangeResponse { Token = tk });
|
||||||
case "refresh_token":
|
case "refresh_token":
|
||||||
// Since we no longer need the refresh token
|
// Since we no longer need the refresh token
|
||||||
// This case is blank for now, thinking to mock it if the OIDC standard requires it
|
// This case is blank for now, thinking to mock it if the OIDC standard requires it
|
||||||
default:
|
default:
|
||||||
return BadRequest("Unsupported grant type.");
|
return BadRequest("Unsupported grant type.");
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,10 @@ public class PasswordResetEmailModel
|
|||||||
{
|
{
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public required string Link { get; set; }
|
public required string Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerificationEmailModel
|
||||||
|
{
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required string Code { get; set; }
|
||||||
}
|
}
|
62
DysonNetwork.Sphere/Pages/Emails/VerificationEmail.razor
Normal file
62
DysonNetwork.Sphere/Pages/Emails/VerificationEmail.razor
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
@using DysonNetwork.Sphere.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Sphere.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<table class="container">
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0; text-align: center;">
|
||||||
|
@(Localizer["VerificationHeader1"])
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["VerificationPara1"]) @Name,
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["VerificationPara2"])
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["VerificationPara3"])
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="background-color: #f3f4f6; padding: 1rem; border-radius: 0.5rem; display: inline-block; margin: 1rem 0;">
|
||||||
|
<span style="font-size: 1.5rem; font-weight: 600; color: #111827; letter-spacing: 0.1em;">@Code</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["VerificationPara4"])
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["VerificationPara5"])
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 2rem 0 0 0;">
|
||||||
|
@(LocalizerShared["EmailFooter1"]) <br />
|
||||||
|
@(LocalizerShared["EmailFooter2"])
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Code { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||||
|
}
|
@ -81,4 +81,25 @@
|
|||||||
<data name="PasswordResetPara4" xml:space="preserve">
|
<data name="PasswordResetPara4" xml:space="preserve">
|
||||||
<value>If you didn't request this, you can ignore this email safety.</value>
|
<value>If you didn't request this, you can ignore this email safety.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="VerificationHeader1" xml:space="preserve">
|
||||||
|
<value>Verify Your Email Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara1" xml:space="preserve">
|
||||||
|
<value>Dear, </value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara2" xml:space="preserve">
|
||||||
|
<value>Thank you for creating an account on the Solar Network. We're excited to have you join our community!</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara3" xml:space="preserve">
|
||||||
|
<value>To verify your email address and access all features of your account, please use the verification code below:</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara4" xml:space="preserve">
|
||||||
|
<value>This code will expire in 30 minutes. Please enter it on the verification page to complete your registration.</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara5" xml:space="preserve">
|
||||||
|
<value>If you didn't create this account, please ignore this email.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailVerificationTitle" xml:space="preserve">
|
||||||
|
<value>Verify your email address</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -74,4 +74,25 @@
|
|||||||
<data name="EmailPasswordResetTitle" xml:space="preserve">
|
<data name="EmailPasswordResetTitle" xml:space="preserve">
|
||||||
<value>重置您的密码</value>
|
<value>重置您的密码</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="VerificationHeader1" xml:space="preserve">
|
||||||
|
<value>验证您的电子邮箱</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara1" xml:space="preserve">
|
||||||
|
<value>尊敬的 </value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara2" xml:space="preserve">
|
||||||
|
<value>感谢您在 Solar Network 上注册账号,我们很高兴您即将加入我们的社区!</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara3" xml:space="preserve">
|
||||||
|
<value>请使用以下验证码来验证您的电子邮箱并获取账号的所有功能:</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara4" xml:space="preserve">
|
||||||
|
<value>此验证码将在30分钟后失效。请在验证页面输入此验证码以完成注册。</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationPara5" xml:space="preserve">
|
||||||
|
<value>如果您并未创建此账号,请忽略此邮件。</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailVerificationTitle" xml:space="preserve">
|
||||||
|
<value>验证您的电子邮箱</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -98,5 +98,17 @@ namespace DysonNetwork.Sphere.Resources.Localization {
|
|||||||
return ResourceManager.GetString("PostReactContentBody", resourceCulture);
|
return ResourceManager.GetString("PostReactContentBody", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string AuthCodeTitle {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AuthCodeTitle", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string AuthCodeBody {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AuthCodeBody", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,4 +45,10 @@
|
|||||||
<data name="PostReactContentBody" xml:space="preserve">
|
<data name="PostReactContentBody" xml:space="preserve">
|
||||||
<value>{0} added a reaction {1} to your post {2}</value>
|
<value>{0} added a reaction {1} to your post {2}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AuthCodeTitle" xml:space="preserve">
|
||||||
|
<value>Disposable Verification Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthCodeBody" xml:space="preserve">
|
||||||
|
<value>{0} is your disposable code, it will expires in 5 minutes</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -38,4 +38,10 @@
|
|||||||
<data name="PostReactContentBody" xml:space="preserve">
|
<data name="PostReactContentBody" xml:space="preserve">
|
||||||
<value>{0} 给你的帖子添加了一个 {1} 的反应 {2}</value>
|
<value>{0} 给你的帖子添加了一个 {1} 的反应 {2}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AuthCodeTitle" xml:space="preserve">
|
||||||
|
<value>一次性验证码</value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthCodeBody" xml:space="preserve">
|
||||||
|
<value>{0} 是你的一次性验证码,它将会在五分钟内过期</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -15,6 +15,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalScheduleBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F2b_003Ff86eadcb_003FDailyTimeIntervalScheduleBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalScheduleBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F2b_003Ff86eadcb_003FDailyTimeIntervalScheduleBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalTriggerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F5c_003F297b8312_003FDailyTimeIntervalTriggerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADailyTimeIntervalTriggerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F929ef51651404d13aacd3eb8198d2961e4800_003F5c_003F297b8312_003FDailyTimeIntervalTriggerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa0b45f29f34f594814a7b1fbc25fe5ef3c18257956ed4f4fbfa68717db58_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa0b45f29f34f594814a7b1fbc25fe5ef3c18257956ed4f4fbfa68717db58_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbFunctionsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc1c46ed28c61e1caa79185e4375a8ae7cd11cd5ba8853dcb37577f93f2ca8d5_003FDbFunctionsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADiagnosticServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F47e01f36dea14a23aaea6e0391c1347ace00_003F3c_003F140e6d8b_003FDiagnosticServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADiagnosticServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F47e01f36dea14a23aaea6e0391c1347ace00_003F3c_003F140e6d8b_003FDiagnosticServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@ -106,8 +107,8 @@
|
|||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexedValue">False</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmails_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmails_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexedValue">False</s:Boolean>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user