♻️ Still don't know what I am doing
This commit is contained in:
@ -402,16 +402,17 @@ public class AccountService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mailer.SendTemplatedEmailAsync<DysonNetwork.Pass.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
await mailer
|
||||||
account.Nick,
|
.SendTemplatedEmailAsync<Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
||||||
contact.Content,
|
account.Nick,
|
||||||
localizer["VerificationEmail"],
|
contact.Content,
|
||||||
new VerificationEmailModel
|
localizer["VerificationEmail"],
|
||||||
{
|
new VerificationEmailModel
|
||||||
Name = account.Name,
|
{
|
||||||
Code = code
|
Name = account.Name,
|
||||||
}
|
Code = code
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
||||||
break;
|
break;
|
||||||
@ -496,7 +497,10 @@ public class AccountService(
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (session.Challenge.DeviceId is not null)
|
if (session.Challenge.DeviceId is not null)
|
||||||
await pusher.UnsubscribePushNotifications(session.Challenge.DeviceId);
|
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
|
||||||
|
{
|
||||||
|
DeviceId = session.Challenge.DeviceId
|
||||||
|
});
|
||||||
|
|
||||||
// The current session should be included in the sessions' list
|
// The current session should be included in the sessions' list
|
||||||
await db.AuthSessions
|
await db.AuthSessions
|
||||||
@ -655,7 +659,8 @@ public class AccountService(
|
|||||||
|
|
||||||
if (missingId.Count != 0)
|
if (missingId.Count != 0)
|
||||||
{
|
{
|
||||||
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id }).ToList();
|
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id })
|
||||||
|
.ToList();
|
||||||
await db.BulkInsertAsync(newProfiles);
|
await db.BulkInsertAsync(newProfiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,24 @@ public class AccountServiceGrpc(
|
|||||||
return account.ToProtoValue();
|
return account.ToProtoValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<GetAccountBatchResponse> GetAccountBatch(GetAccountBatchRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var accountIds = request.Id
|
||||||
|
.Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null)
|
||||||
|
.Where(id => id.HasValue)
|
||||||
|
.Select(id => id!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var accounts = await _db.Accounts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => accountIds.Contains(a.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var response = new GetAccountBatchResponse();
|
||||||
|
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<Shared.Proto.Account> CreateAccount(CreateAccountRequest request,
|
public override async Task<Shared.Proto.Account> CreateAccount(CreateAccountRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
@ -6,18 +6,11 @@ using Microsoft.AspNetCore.Components;
|
|||||||
namespace DysonNetwork.Pass.Email;
|
namespace DysonNetwork.Pass.Email;
|
||||||
|
|
||||||
public class EmailService(
|
public class EmailService(
|
||||||
IEtcdClient etcd,
|
PusherService.PusherServiceClient pusher,
|
||||||
RazorViewRenderer viewRenderer,
|
RazorViewRenderer viewRenderer,
|
||||||
IConfiguration configuration,
|
|
||||||
ILogger<EmailService> logger
|
ILogger<EmailService> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private readonly PusherService.PusherServiceClient _client = GrpcClientHelper.CreatePusherServiceClient(
|
|
||||||
etcd,
|
|
||||||
configuration["Service:CertPath"]!,
|
|
||||||
configuration["Service:KeyPath"]!
|
|
||||||
).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
public async Task SendEmailAsync(
|
public async Task SendEmailAsync(
|
||||||
string? recipientName,
|
string? recipientName,
|
||||||
string recipientEmail,
|
string recipientEmail,
|
||||||
@ -27,7 +20,7 @@ public class EmailService(
|
|||||||
{
|
{
|
||||||
subject = $"[Solarpass] {subject}";
|
subject = $"[Solarpass] {subject}";
|
||||||
|
|
||||||
await _client.SendEmailAsync(
|
await pusher.SendEmailAsync(
|
||||||
new SendEmailRequest()
|
new SendEmailRequest()
|
||||||
{
|
{
|
||||||
Email = new EmailMessage()
|
Email = new EmailMessage()
|
||||||
|
@ -2,7 +2,7 @@ namespace DysonNetwork.Pass.Permission;
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
||||||
{
|
{
|
||||||
public string Area { get; set; } = area;
|
public string Area { get; set; } = area;
|
||||||
|
@ -19,6 +19,7 @@ using DysonNetwork.Pass.Handlers;
|
|||||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.GeoIp;
|
using DysonNetwork.Shared.GeoIp;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Startup;
|
namespace DysonNetwork.Pass.Startup;
|
||||||
|
|
||||||
@ -48,8 +49,7 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register gRPC reflection for service discovery
|
services.AddPusherService();
|
||||||
services.AddGrpc();
|
|
||||||
|
|
||||||
// Register gRPC services
|
// Register gRPC services
|
||||||
services.AddScoped<AccountServiceGrpc>();
|
services.AddScoped<AccountServiceGrpc>();
|
||||||
@ -194,7 +194,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<ActionLogService>();
|
services.AddScoped<ActionLogService>();
|
||||||
services.AddScoped<RelationshipService>();
|
services.AddScoped<RelationshipService>();
|
||||||
services.AddScoped<MagicSpellService>();
|
services.AddScoped<MagicSpellService>();
|
||||||
services.AddScoped<NotificationService>();
|
|
||||||
services.AddScoped<AuthService>();
|
services.AddScoped<AuthService>();
|
||||||
services.AddScoped<AccountUsernameService>();
|
services.AddScoped<AccountUsernameService>();
|
||||||
services.AddScoped<WalletService>();
|
services.AddScoped<WalletService>();
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Wallet;
|
namespace DysonNetwork.Pass.Wallet;
|
||||||
|
|
||||||
public class PaymentService(
|
public class PaymentService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
WalletService wat,
|
WalletService wat,
|
||||||
NotificationService nty,
|
PusherService.PusherServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer
|
IStringLocalizer<NotificationResource> localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -205,16 +206,19 @@ public class PaymentService(
|
|||||||
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
||||||
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
|
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
|
||||||
|
|
||||||
await nty.SendNotification(
|
|
||||||
account,
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
"wallets.orders.paid",
|
new SendPushNotificationToUserRequest
|
||||||
localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
|
||||||
null,
|
|
||||||
localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
|
||||||
readableOrderRemark],
|
|
||||||
new Dictionary<string, object>()
|
|
||||||
{
|
{
|
||||||
["order_id"] = order.Id.ToString()
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "wallets.orders.paid",
|
||||||
|
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
||||||
|
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
||||||
|
readableOrderRemark],
|
||||||
|
IsSavable = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Account;
|
|
||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||||
|
using Duration = NodaTime.Duration;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Wallet;
|
namespace DysonNetwork.Pass.Wallet;
|
||||||
|
|
||||||
@ -13,7 +16,7 @@ public class SubscriptionService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PaymentService payment,
|
PaymentService payment,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
NotificationService nty,
|
PusherService.PusherServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
@ -352,15 +355,19 @@ public class SubscriptionService(
|
|||||||
? subscription.EndedAt.Value.Minus(subscription.BegunAt).Days.ToString()
|
? subscription.EndedAt.Value.Minus(subscription.BegunAt).Days.ToString()
|
||||||
: "infinite";
|
: "infinite";
|
||||||
|
|
||||||
await nty.SendNotification(
|
var notification = new PushNotification
|
||||||
account,
|
{
|
||||||
"subscriptions.begun",
|
Topic = "subscriptions.begun",
|
||||||
localizer["SubscriptionAppliedTitle", humanReadableName],
|
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
||||||
null,
|
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
||||||
localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
IsSavable = false,
|
||||||
new Dictionary<string, object>()
|
};
|
||||||
|
notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString()));
|
||||||
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
{
|
{
|
||||||
["subscription_id"] = subscription.Id.ToString(),
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = notification
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,31 +20,6 @@ public class WebSocketService
|
|||||||
|
|
||||||
private static readonly ConcurrentDictionary<string, string> ActiveSubscriptions = new(); // deviceId -> chatRoomId
|
private static readonly ConcurrentDictionary<string, string> ActiveSubscriptions = new(); // deviceId -> chatRoomId
|
||||||
|
|
||||||
public void SubscribeToChatRoom(string chatRoomId, string deviceId)
|
|
||||||
{
|
|
||||||
ActiveSubscriptions[deviceId] = chatRoomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnsubscribeFromChatRoom(string deviceId)
|
|
||||||
{
|
|
||||||
ActiveSubscriptions.TryRemove(deviceId, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsUserSubscribedToChatRoom(string accountId, string chatRoomId)
|
|
||||||
{
|
|
||||||
var userDeviceIds = ActiveConnections.Keys.Where(k => k.AccountId == accountId).Select(k => k.DeviceId);
|
|
||||||
foreach (var deviceId in userDeviceIds)
|
|
||||||
{
|
|
||||||
if (ActiveSubscriptions.TryGetValue(deviceId, out var subscribedChatRoomId) &&
|
|
||||||
subscribedChatRoomId == chatRoomId)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAdd(
|
public bool TryAdd(
|
||||||
(string AccountId, string DeviceId) key,
|
(string AccountId, string DeviceId) key,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
@ -67,7 +42,11 @@ public class WebSocketService
|
|||||||
);
|
);
|
||||||
data.Cts.Cancel();
|
data.Cts.Cancel();
|
||||||
ActiveConnections.TryRemove(key, out _);
|
ActiveConnections.TryRemove(key, out _);
|
||||||
UnsubscribeFromChatRoom(key.DeviceId);
|
}
|
||||||
|
|
||||||
|
public bool GetDeviceIsConnected(string deviceId)
|
||||||
|
{
|
||||||
|
return ActiveConnections.Any(c => c.Key.DeviceId == deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetAccountIsConnected(string accountId)
|
public bool GetAccountIsConnected(string accountId)
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using AccountService = DysonNetwork.Shared.Proto.AccountService;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Notification;
|
namespace DysonNetwork.Pusher.Notification;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/notifications")]
|
[Route("/api/notifications")]
|
||||||
public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase
|
public class NotificationController(
|
||||||
|
AppDatabase db,
|
||||||
|
PushService nty,
|
||||||
|
AccountService.AccountServiceClient accounts) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("count")]
|
[HttpGet("count")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@ -17,9 +22,10 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var count = await db.Notifications
|
var count = await db.Notifications
|
||||||
.Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null)
|
.Where(s => s.AccountId == accountId && s.ViewedAt == null)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
return Ok(count);
|
return Ok(count);
|
||||||
}
|
}
|
||||||
@ -30,24 +36,25 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
[FromQuery] int offset = 0,
|
[FromQuery] int offset = 0,
|
||||||
// The page size set to 5 is to avoid the client pulled the notification
|
// The page size set to 5 is to avoid the client pulled the notification
|
||||||
// but didn't render it in the screen-viewable region.
|
// but didn't render it in the screen-viewable region.
|
||||||
[FromQuery] int take = 5
|
[FromQuery] int take = 8
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var totalCount = await db.Notifications
|
var totalCount = await db.Notifications
|
||||||
.Where(s => s.AccountId == currentUser.Id)
|
.Where(s => s.AccountId == accountId)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
var notifications = await db.Notifications
|
var notifications = await db.Notifications
|
||||||
.Where(s => s.AccountId == currentUser.Id)
|
.Where(s => s.AccountId == accountId)
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
Response.Headers["X-Total"] = totalCount.ToString();
|
Response.Headers["X-Total"] = totalCount.ToString();
|
||||||
await nty.MarkNotificationsViewed(notifications);
|
await nty.MarkNotificationsViewed(notifications.ToList());
|
||||||
|
|
||||||
return Ok(notifications);
|
return Ok(notifications);
|
||||||
}
|
}
|
||||||
@ -55,14 +62,15 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
public class PushNotificationSubscribeRequest
|
public class PushNotificationSubscribeRequest
|
||||||
{
|
{
|
||||||
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
|
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
|
||||||
public NotificationPushProvider Provider { get; set; }
|
public PushProvider Provider { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("subscription")]
|
[HttpPut("subscription")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<NotificationPushSubscription>> SubscribeToPushNotification(
|
public async Task<ActionResult<PushSubscription>>
|
||||||
[FromBody] PushNotificationSubscribeRequest request
|
SubscribeToPushNotification(
|
||||||
)
|
[FromBody] PushNotificationSubscribeRequest request
|
||||||
|
)
|
||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
@ -72,8 +80,12 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
if (currentSession == null) return Unauthorized();
|
if (currentSession == null) return Unauthorized();
|
||||||
|
|
||||||
var result =
|
var result =
|
||||||
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
await nty.SubscribeDevice(
|
||||||
request.DeviceToken);
|
currentSession.Challenge.DeviceId!,
|
||||||
|
request.DeviceToken,
|
||||||
|
request.Provider,
|
||||||
|
currentUser
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
@ -88,10 +100,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
if (currentUser == null) return Unauthorized();
|
if (currentUser == null) return Unauthorized();
|
||||||
var currentSession = currentSessionValue as AuthSession;
|
var currentSession = currentSessionValue as AuthSession;
|
||||||
if (currentSession == null) return Unauthorized();
|
if (currentSession == null) return Unauthorized();
|
||||||
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var affectedRows = await db.NotificationPushSubscriptions
|
var affectedRows = await db.PushSubscriptions
|
||||||
.Where(s =>
|
.Where(s =>
|
||||||
s.AccountId == currentUser.Id &&
|
s.AccountId == accountId &&
|
||||||
s.DeviceId == currentSession.Challenge.DeviceId
|
s.DeviceId == currentSession.Challenge.DeviceId
|
||||||
).ExecuteDeleteAsync();
|
).ExecuteDeleteAsync();
|
||||||
return Ok(affectedRows);
|
return Ok(affectedRows);
|
||||||
@ -107,31 +120,6 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
public int Priority { get; set; } = 10;
|
public int Priority { get; set; } = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("broadcast")]
|
|
||||||
[Authorize]
|
|
||||||
[RequiredPermission("global", "notifications.broadcast")]
|
|
||||||
public async Task<ActionResult> BroadcastNotification(
|
|
||||||
[FromBody] NotificationRequest request,
|
|
||||||
[FromQuery] bool save = false
|
|
||||||
)
|
|
||||||
{
|
|
||||||
await nty.BroadcastNotification(
|
|
||||||
new Notification
|
|
||||||
{
|
|
||||||
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
|
||||||
UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
|
||||||
Topic = request.Topic,
|
|
||||||
Title = request.Title,
|
|
||||||
Subtitle = request.Subtitle,
|
|
||||||
Content = request.Content,
|
|
||||||
Meta = request.Meta,
|
|
||||||
Priority = request.Priority,
|
|
||||||
},
|
|
||||||
save
|
|
||||||
);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotificationWithAimRequest : NotificationRequest
|
public class NotificationWithAimRequest : NotificationRequest
|
||||||
{
|
{
|
||||||
[Required] public List<Guid> AccountId { get; set; } = null!;
|
[Required] public List<Guid> AccountId { get; set; } = null!;
|
||||||
@ -145,7 +133,6 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
[FromQuery] bool save = false
|
[FromQuery] bool save = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var accounts = await db.Accounts.Where(a => request.AccountId.Contains(a.Id)).ToListAsync();
|
|
||||||
await nty.SendNotificationBatch(
|
await nty.SendNotificationBatch(
|
||||||
new Notification
|
new Notification
|
||||||
{
|
{
|
||||||
@ -157,7 +144,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
Content = request.Content,
|
Content = request.Content,
|
||||||
Meta = request.Meta,
|
Meta = request.Meta,
|
||||||
},
|
},
|
||||||
accounts,
|
request.AccountId,
|
||||||
save
|
save
|
||||||
);
|
);
|
||||||
return Ok();
|
return Ok();
|
||||||
|
@ -12,14 +12,14 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto
|
|||||||
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
||||||
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
||||||
|
|
||||||
public async Task UnsubscribePushNotifications(string deviceId)
|
public async Task UnsubscribeDevice(string deviceId)
|
||||||
{
|
{
|
||||||
await db.PushSubscriptions
|
await db.PushSubscriptions
|
||||||
.Where(s => s.DeviceId == deviceId)
|
.Where(s => s.DeviceId == deviceId)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PushSubscription> SubscribePushNotification(
|
public async Task<PushSubscription> SubscribeDevice(
|
||||||
string deviceId,
|
string deviceId,
|
||||||
string deviceToken,
|
string deviceToken,
|
||||||
PushProvider provider,
|
PushProvider provider,
|
||||||
|
@ -9,7 +9,7 @@ namespace DysonNetwork.Pusher.Services;
|
|||||||
|
|
||||||
public class PusherServiceGrpc(
|
public class PusherServiceGrpc(
|
||||||
EmailService emailService,
|
EmailService emailService,
|
||||||
WebSocketService webSocketService,
|
WebSocketService websocket,
|
||||||
PushService pushService
|
PushService pushService
|
||||||
) : PusherService.PusherServiceBase
|
) : PusherService.PusherServiceBase
|
||||||
{
|
{
|
||||||
@ -32,7 +32,7 @@ public class PusherServiceGrpc(
|
|||||||
Data = request.Packet.Data,
|
Data = request.Packet.Data,
|
||||||
ErrorMessage = request.Packet.ErrorMessage
|
ErrorMessage = request.Packet.ErrorMessage
|
||||||
};
|
};
|
||||||
webSocketService.SendPacketToAccount(request.UserId, packet);
|
websocket.SendPacketToAccount(request.UserId, packet);
|
||||||
return Task.FromResult(new Empty());
|
return Task.FromResult(new Empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ public class PusherServiceGrpc(
|
|||||||
ErrorMessage = request.Packet.ErrorMessage
|
ErrorMessage = request.Packet.ErrorMessage
|
||||||
};
|
};
|
||||||
foreach (var userId in request.UserIds)
|
foreach (var userId in request.UserIds)
|
||||||
webSocketService.SendPacketToAccount(userId, packet);
|
websocket.SendPacketToAccount(userId, packet);
|
||||||
|
|
||||||
return Task.FromResult(new Empty());
|
return Task.FromResult(new Empty());
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ public class PusherServiceGrpc(
|
|||||||
Data = request.Packet.Data,
|
Data = request.Packet.Data,
|
||||||
ErrorMessage = request.Packet.ErrorMessage
|
ErrorMessage = request.Packet.ErrorMessage
|
||||||
};
|
};
|
||||||
webSocketService.SendPacketToDevice(request.DeviceId, packet);
|
websocket.SendPacketToDevice(request.DeviceId, packet);
|
||||||
return Task.FromResult(new Empty());
|
return Task.FromResult(new Empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ public class PusherServiceGrpc(
|
|||||||
ErrorMessage = request.Packet.ErrorMessage
|
ErrorMessage = request.Packet.ErrorMessage
|
||||||
};
|
};
|
||||||
foreach (var deviceId in request.DeviceIds)
|
foreach (var deviceId in request.DeviceIds)
|
||||||
webSocketService.SendPacketToDevice(deviceId, packet);
|
websocket.SendPacketToDevice(deviceId, packet);
|
||||||
|
|
||||||
return Task.FromResult(new Empty());
|
return Task.FromResult(new Empty());
|
||||||
}
|
}
|
||||||
@ -159,4 +159,22 @@ public class PusherServiceGrpc(
|
|||||||
await pushService.SendNotificationBatch(notification, accounts, request.Notification.IsSavable);
|
await pushService.SendNotificationBatch(notification, accounts, request.Notification.IsSavable);
|
||||||
return new Empty();
|
return new Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> UnsubscribePushNotifications(UnsubscribePushNotificationsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
await pushService.UnsubscribeDevice(request.DeviceId);
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<GetWebsocketConnectionStatusResponse> GetWebsocketConnectionStatus(GetWebsocketConnectionStatusRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var isConnected = request.IdCase switch
|
||||||
|
{
|
||||||
|
GetWebsocketConnectionStatusRequest.IdOneofCase.DeviceId => websocket.GetDeviceIsConnected(request.DeviceId),
|
||||||
|
GetWebsocketConnectionStatusRequest.IdOneofCase.UserId => websocket.GetAccountIsConnected(request.UserId),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(new GetWebsocketConnectionStatusResponse { IsConnected = isConnected });
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,10 +16,9 @@ public class DysonTokenAuthHandler(
|
|||||||
IOptionsMonitor<DysonTokenAuthOptions> options,
|
IOptionsMonitor<DysonTokenAuthOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
ISystemClock clock,
|
|
||||||
AuthService.AuthServiceClient auth
|
AuthService.AuthServiceClient auth
|
||||||
)
|
)
|
||||||
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder, clock)
|
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
||||||
{
|
{
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
|
72
DysonNetwork.Shared/Auth/PermissionMiddleware.cs
Normal file
72
DysonNetwork.Shared/Auth/PermissionMiddleware.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Auth
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
||||||
|
{
|
||||||
|
public string Area { get; set; } = area;
|
||||||
|
public string Key { get; } = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PermissionMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService, ILogger<PermissionMiddleware> logger)
|
||||||
|
{
|
||||||
|
var endpoint = httpContext.GetEndpoint();
|
||||||
|
|
||||||
|
var attr = endpoint?.Metadata
|
||||||
|
.OfType<RequiredPermissionAttribute>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (attr != null)
|
||||||
|
{
|
||||||
|
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
|
{
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await httpContext.Response.WriteAsync("Unauthorized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming Account proto has a bool field 'is_superuser' which is generated as 'IsSuperuser'
|
||||||
|
if (currentUser.IsSuperuser)
|
||||||
|
{
|
||||||
|
// Bypass the permission check for performance
|
||||||
|
await next(httpContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor = $"user:{currentUser.Id}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest
|
||||||
|
{
|
||||||
|
Actor = actor,
|
||||||
|
Area = attr.Area,
|
||||||
|
Key = attr.Key
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permResp.HasPermission)
|
||||||
|
{
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} was required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (RpcException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "gRPC call to PermissionService failed while checking permission {Area}/{Key} for actor {Actor}", attr.Area, attr.Key, actor);
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
await httpContext.Response.WriteAsync("Error checking permissions.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,9 +16,9 @@ public static class DysonAuthStartup
|
|||||||
{
|
{
|
||||||
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
var config = sp.GetRequiredService<IConfiguration>();
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
var clientCertPath = config["ClientCert:Path"];
|
var clientCertPath = config["Service:ClientCert"];
|
||||||
var clientKeyPath = config["ClientKey:Path"];
|
var clientKeyPath = config["Service:ClientKey"];
|
||||||
var clientCertPassword = config["ClientCert:Password"];
|
var clientCertPassword = config["Service:CertPassword"];
|
||||||
|
|
||||||
return GrpcClientHelper
|
return GrpcClientHelper
|
||||||
.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
||||||
|
@ -19,8 +19,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||||
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
@ -33,17 +33,6 @@ public static class GrpcClientHelper
|
|||||||
return response.Kvs[0].Value.ToStringUtf8();
|
return response.Kvs[0].Value.ToStringUtf8();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AccountService.AccountServiceClient CreateAccountServiceClient(
|
|
||||||
string url,
|
|
||||||
string clientCertPath,
|
|
||||||
string clientKeyPath,
|
|
||||||
string? clientCertPassword = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
|
||||||
clientCertPassword));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
|
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@ -51,22 +40,11 @@ public static class GrpcClientHelper
|
|||||||
string? clientCertPassword = null
|
string? clientCertPassword = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService");
|
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
|
||||||
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AuthService.AuthServiceClient CreateAuthServiceClient(
|
|
||||||
string url,
|
|
||||||
string clientCertPath,
|
|
||||||
string clientKeyPath,
|
|
||||||
string? clientCertPassword = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
|
||||||
clientCertPassword));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
|
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@ -74,22 +52,11 @@ public static class GrpcClientHelper
|
|||||||
string? clientCertPassword = null
|
string? clientCertPassword = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService");
|
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
|
||||||
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PusherService.PusherServiceClient CreatePusherServiceClient(
|
|
||||||
string url,
|
|
||||||
string clientCertPath,
|
|
||||||
string clientKeyPath,
|
|
||||||
string? clientCertPassword = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
|
||||||
clientCertPassword));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
|
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
@ -97,7 +64,7 @@ public static class GrpcClientHelper
|
|||||||
string? clientCertPassword = null
|
string? clientCertPassword = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService");
|
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pusher");
|
||||||
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,7 @@ message LevelingInfo {
|
|||||||
service AccountService {
|
service AccountService {
|
||||||
// Account Operations
|
// Account Operations
|
||||||
rpc GetAccount(GetAccountRequest) returns (Account) {}
|
rpc GetAccount(GetAccountRequest) returns (Account) {}
|
||||||
|
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||||
rpc CreateAccount(CreateAccountRequest) returns (Account) {}
|
rpc CreateAccount(CreateAccountRequest) returns (Account) {}
|
||||||
rpc UpdateAccount(UpdateAccountRequest) returns (Account) {}
|
rpc UpdateAccount(UpdateAccountRequest) returns (Account) {}
|
||||||
rpc DeleteAccount(DeleteAccountRequest) returns (google.protobuf.Empty) {}
|
rpc DeleteAccount(DeleteAccountRequest) returns (google.protobuf.Empty) {}
|
||||||
@ -243,6 +244,14 @@ message GetAccountRequest {
|
|||||||
string id = 1; // Account ID to retrieve
|
string id = 1; // Account ID to retrieve
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetAccountBatchRequest {
|
||||||
|
repeated string id = 1; // Account ID to retrieve
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAccountBatchResponse {
|
||||||
|
repeated Account accounts = 1; // List of accounts
|
||||||
|
}
|
||||||
|
|
||||||
message CreateAccountRequest {
|
message CreateAccountRequest {
|
||||||
string name = 1; // Required: Unique username
|
string name = 1; // Required: Unique username
|
||||||
string nick = 2; // Optional: Display name
|
string nick = 2; // Optional: Display name
|
||||||
|
@ -36,6 +36,12 @@ service PusherService {
|
|||||||
|
|
||||||
// Sends a push notification to a list of users.
|
// Sends a push notification to a list of users.
|
||||||
rpc SendPushNotificationToUsers(SendPushNotificationToUsersRequest) returns (google.protobuf.Empty) {}
|
rpc SendPushNotificationToUsers(SendPushNotificationToUsersRequest) returns (google.protobuf.Empty) {}
|
||||||
|
|
||||||
|
// Unsubscribes a device from push notifications.
|
||||||
|
rpc UnsubscribePushNotifications(UnsubscribePushNotificationsRequest) returns (google.protobuf.Empty) {}
|
||||||
|
|
||||||
|
// Gets the WebSocket connection status for a device or user.
|
||||||
|
rpc GetWebsocketConnectionStatus(GetWebsocketConnectionStatusRequest) returns (GetWebsocketConnectionStatusResponse) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents an email message.
|
// Represents an email message.
|
||||||
@ -108,3 +114,18 @@ message SendPushNotificationToUsersRequest {
|
|||||||
repeated string user_ids = 1;
|
repeated string user_ids = 1;
|
||||||
PushNotification notification = 2;
|
PushNotification notification = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UnsubscribePushNotificationsRequest {
|
||||||
|
string device_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetWebsocketConnectionStatusRequest {
|
||||||
|
oneof id {
|
||||||
|
string device_id = 1;
|
||||||
|
string user_id = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetWebsocketConnectionStatusResponse {
|
||||||
|
bool is_connected = 1;
|
||||||
|
}
|
||||||
|
47
DysonNetwork.Shared/Registry/ServiceHelper.cs
Normal file
47
DysonNetwork.Shared/Registry/ServiceHelper.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using dotnet_etcd.interfaces;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
|
public static class ServiceHelper
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddPusherService(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<PusherService.PusherServiceClient>(sp =>
|
||||||
|
{
|
||||||
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var clientCertPath = config["Service:ClientCert"];
|
||||||
|
var clientKeyPath = config["Service:ClientKey"];
|
||||||
|
var clientCertPassword = config["Service:CertPassword"];
|
||||||
|
|
||||||
|
return GrpcClientHelper
|
||||||
|
.CreatePusherServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddAccountService(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<AccountService.AccountServiceClient>(sp =>
|
||||||
|
{
|
||||||
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var clientCertPath = config["Service:ClientCert"];
|
||||||
|
var clientKeyPath = config["Service:ClientKey"];
|
||||||
|
var clientCertPassword = config["Service:CertPassword"];
|
||||||
|
|
||||||
|
return GrpcClientHelper
|
||||||
|
.CreateAccountServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
@ -8,16 +8,20 @@ namespace DysonNetwork.Shared.Registry;
|
|||||||
|
|
||||||
public class ServiceRegistry(IEtcdClient etcd, ILogger<ServiceRegistry> logger)
|
public class ServiceRegistry(IEtcdClient etcd, ILogger<ServiceRegistry> logger)
|
||||||
{
|
{
|
||||||
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60, CancellationToken cancellationToken = default)
|
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var key = $"/services/{serviceName}";
|
var key = $"/services/{serviceName}";
|
||||||
var leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds });
|
var leaseResponse = await etcd.LeaseGrantAsync(
|
||||||
|
new LeaseGrantRequest { TTL = leaseTtlSeconds },
|
||||||
|
cancellationToken: cancellationToken
|
||||||
|
);
|
||||||
await etcd.PutAsync(new PutRequest
|
await etcd.PutAsync(new PutRequest
|
||||||
{
|
{
|
||||||
Key = ByteString.CopyFrom(key, Encoding.UTF8),
|
Key = ByteString.CopyFrom(key, Encoding.UTF8),
|
||||||
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
|
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
|
||||||
Lease = leaseResponse.ID
|
Lease = leaseResponse.ID
|
||||||
});
|
}, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
@ -190,7 +190,6 @@ public class NotificationService(
|
|||||||
Content = notification.Content,
|
Content = notification.Content,
|
||||||
Meta = notification.Meta,
|
Meta = notification.Meta,
|
||||||
Priority = notification.Priority,
|
Priority = notification.Priority,
|
||||||
Account = x,
|
|
||||||
AccountId = x.Id
|
AccountId = x.Id
|
||||||
};
|
};
|
||||||
return newNotification;
|
return newNotification;
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<PackageReference Include="Markdig" Version="0.41.3" />
|
<PackageReference Include="Markdig" Version="0.41.3" />
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAny_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F67_003F87f868e3_003FAny_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAny_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F67_003F87f868e3_003FAny_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F0f_003F51443844_003FApnSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F0f_003F51443844_003FApnSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentNullException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003Faf_003F30ff0e5c_003FArgumentNullException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1f1354e4dbf943ecb04840af5ff9a527fa20_003F5d_003F1fb111f6_003FAuthenticationHandler_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1f1354e4dbf943ecb04840af5ff9a527fa20_003F5d_003F1fb111f6_003FAuthenticationHandler_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9b24a56e61ae4d86a9e8ba13482a2db924600_003F5b_003F9e854504_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9b24a56e61ae4d86a9e8ba13482a2db924600_003F5b_003F9e854504_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@ -28,6 +29,7 @@
|
|||||||
<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>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointHttpContextExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc181aff8c6ec418494a7efcfec578fc154e00_003F81_003F048fd513_003FEndpointHttpContextExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@ -81,6 +83,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcbafb95b4df34952928f87356db00c8f2fe00_003F9b_003F8ba036bb_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcbafb95b4df34952928f87356db00c8f2fe00_003F9b_003F8ba036bb_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARazorPage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F81d2924a2bbd4b0c864a1d23cbf5f0893d200_003F5f_003Fc110be1c_003FRazorPage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARazorPage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F81d2924a2bbd4b0c864a1d23cbf5f0893d200_003F5f_003Fc110be1c_003FRazorPage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARepeatedField_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003Fc1_003F67c16263_003FRepeatedField_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Reference in New Issue
Block a user