From cde55eb237154f59089ed265175e69ae9470ce50 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 13 Jul 2025 23:38:57 +0800 Subject: [PATCH] :recycle: Still don't know what I am doing --- DysonNetwork.Pass/Account/AccountService.cs | 29 ++++---- .../Account/AccountServiceGrpc.cs | 18 +++++ DysonNetwork.Pass/Email/EmailService.cs | 11 +-- .../Permission/PermissionMiddleware.cs | 2 +- .../Startup/ServiceCollectionExtensions.cs | 7 +- DysonNetwork.Pass/Wallet/PaymentService.cs | 26 ++++--- .../Wallet/SubscriptionService.cs | 27 ++++--- .../Connection/WebSocketService.cs | 31 ++------ .../Notification/NotificationController.cs | 71 ++++++++---------- .../Notification/PushService.cs | 4 +- .../Services/PusherServiceGrpc.cs | 28 ++++++-- DysonNetwork.Shared/Auth/AuthScheme.cs | 3 +- .../Auth/PermissionMiddleware.cs | 72 +++++++++++++++++++ DysonNetwork.Shared/Auth/Startup.cs | 6 +- .../DysonNetwork.Shared.csproj | 3 +- DysonNetwork.Shared/Proto/GrpcClientHelper.cs | 39 +--------- DysonNetwork.Shared/Proto/account.proto | 9 +++ DysonNetwork.Shared/Proto/pusher.proto | 21 ++++++ DysonNetwork.Shared/Registry/ServiceHelper.cs | 47 ++++++++++++ .../Registry/ServiceRegistry.cs | 10 ++- .../Account/NotificationService.cs | 1 - .../DysonNetwork.Sphere.csproj | 2 +- DysonNetwork.sln.DotSettings.user | 3 + 23 files changed, 300 insertions(+), 170 deletions(-) create mode 100644 DysonNetwork.Shared/Auth/PermissionMiddleware.cs create mode 100644 DysonNetwork.Shared/Registry/ServiceHelper.cs diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index 5aea9c6..d8ff6b6 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -402,16 +402,17 @@ public class AccountService( return; } - await mailer.SendTemplatedEmailAsync( - account.Nick, - contact.Content, - localizer["VerificationEmail"], - new VerificationEmailModel - { - Name = account.Name, - Code = code - } - ); + await mailer + .SendTemplatedEmailAsync( + account.Nick, + contact.Content, + localizer["VerificationEmail"], + new VerificationEmailModel + { + Name = account.Name, + Code = code + } + ); await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); break; @@ -496,7 +497,10 @@ public class AccountService( .ToListAsync(); 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 await db.AuthSessions @@ -655,7 +659,8 @@ public class AccountService( 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); } } diff --git a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs index 7450de1..d79d8ac 100644 --- a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs +++ b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs @@ -36,6 +36,24 @@ public class AccountServiceGrpc( return account.ToProtoValue(); } + public override async Task 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 CreateAccount(CreateAccountRequest request, ServerCallContext context) { diff --git a/DysonNetwork.Pass/Email/EmailService.cs b/DysonNetwork.Pass/Email/EmailService.cs index 7cf34ee..6384a1a 100644 --- a/DysonNetwork.Pass/Email/EmailService.cs +++ b/DysonNetwork.Pass/Email/EmailService.cs @@ -6,18 +6,11 @@ using Microsoft.AspNetCore.Components; namespace DysonNetwork.Pass.Email; public class EmailService( - IEtcdClient etcd, + PusherService.PusherServiceClient pusher, RazorViewRenderer viewRenderer, - IConfiguration configuration, ILogger logger ) { - private readonly PusherService.PusherServiceClient _client = GrpcClientHelper.CreatePusherServiceClient( - etcd, - configuration["Service:CertPath"]!, - configuration["Service:KeyPath"]! - ).GetAwaiter().GetResult(); - public async Task SendEmailAsync( string? recipientName, string recipientEmail, @@ -27,7 +20,7 @@ public class EmailService( { subject = $"[Solarpass] {subject}"; - await _client.SendEmailAsync( + await pusher.SendEmailAsync( new SendEmailRequest() { Email = new EmailMessage() diff --git a/DysonNetwork.Pass/Permission/PermissionMiddleware.cs b/DysonNetwork.Pass/Permission/PermissionMiddleware.cs index e1011fc..4764c06 100644 --- a/DysonNetwork.Pass/Permission/PermissionMiddleware.cs +++ b/DysonNetwork.Pass/Permission/PermissionMiddleware.cs @@ -2,7 +2,7 @@ namespace DysonNetwork.Pass.Permission; using System; -[AttributeUsage(AttributeTargets.Method, Inherited = true)] +[AttributeUsage(AttributeTargets.Method)] public class RequiredPermissionAttribute(string area, string key) : Attribute { public string Area { get; set; } = area; diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index af8e0f9..3f539e9 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -19,6 +19,7 @@ using DysonNetwork.Pass.Handlers; using DysonNetwork.Pass.Wallet.PaymentHandlers; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Shared.Registry; namespace DysonNetwork.Pass.Startup; @@ -48,9 +49,8 @@ public static class ServiceCollectionExtensions options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB }); - // Register gRPC reflection for service discovery - services.AddGrpc(); - + services.AddPusherService(); + // Register gRPC services services.AddScoped(); services.AddScoped(); @@ -194,7 +194,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Pass/Wallet/PaymentService.cs b/DysonNetwork.Pass/Wallet/PaymentService.cs index c01cd62..abaf064 100644 --- a/DysonNetwork.Pass/Wallet/PaymentService.cs +++ b/DysonNetwork.Pass/Wallet/PaymentService.cs @@ -1,17 +1,18 @@ using System.Globalization; -using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Localization; +using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Localization; using NodaTime; +using AccountService = DysonNetwork.Pass.Account.AccountService; namespace DysonNetwork.Pass.Wallet; public class PaymentService( AppDatabase db, WalletService wat, - NotificationService nty, + PusherService.PusherServiceClient pusher, IStringLocalizer localizer ) { @@ -205,16 +206,19 @@ public class PaymentService( var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; - await nty.SendNotification( - account, - "wallets.orders.paid", - localizer["OrderPaidTitle", $"#{readableOrderId}"], - null, - localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency, - readableOrderRemark], - new Dictionary() + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest { - ["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 + } } ); } diff --git a/DysonNetwork.Pass/Wallet/SubscriptionService.cs b/DysonNetwork.Pass/Wallet/SubscriptionService.cs index b999d0f..f6e625e 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionService.cs +++ b/DysonNetwork.Pass/Wallet/SubscriptionService.cs @@ -1,11 +1,14 @@ using System.Text.Json; -using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Wallet.PaymentHandlers; using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Proto; +using Google.Protobuf.WellKnownTypes; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using NodaTime; +using AccountService = DysonNetwork.Pass.Account.AccountService; +using Duration = NodaTime.Duration; namespace DysonNetwork.Pass.Wallet; @@ -13,7 +16,7 @@ public class SubscriptionService( AppDatabase db, PaymentService payment, AccountService accounts, - NotificationService nty, + PusherService.PusherServiceClient pusher, IStringLocalizer localizer, IConfiguration configuration, ICacheService cache, @@ -352,15 +355,19 @@ public class SubscriptionService( ? subscription.EndedAt.Value.Minus(subscription.BegunAt).Days.ToString() : "infinite"; - await nty.SendNotification( - account, - "subscriptions.begun", - localizer["SubscriptionAppliedTitle", humanReadableName], - null, - localizer["SubscriptionAppliedBody", duration, humanReadableName], - new Dictionary() + var notification = new PushNotification + { + Topic = "subscriptions.begun", + Title = localizer["SubscriptionAppliedTitle", humanReadableName], + Body = localizer["SubscriptionAppliedBody", duration, humanReadableName], + IsSavable = false, + }; + 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 } ); } diff --git a/DysonNetwork.Pusher/Connection/WebSocketService.cs b/DysonNetwork.Pusher/Connection/WebSocketService.cs index 9022a3a..c9d9af9 100644 --- a/DysonNetwork.Pusher/Connection/WebSocketService.cs +++ b/DysonNetwork.Pusher/Connection/WebSocketService.cs @@ -20,31 +20,6 @@ public class WebSocketService private static readonly ConcurrentDictionary 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( (string AccountId, string DeviceId) key, WebSocket socket, @@ -67,7 +42,11 @@ public class WebSocketService ); data.Cts.Cancel(); 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) diff --git a/DysonNetwork.Pusher/Notification/NotificationController.cs b/DysonNetwork.Pusher/Notification/NotificationController.cs index 9616b72..f8b240e 100644 --- a/DysonNetwork.Pusher/Notification/NotificationController.cs +++ b/DysonNetwork.Pusher/Notification/NotificationController.cs @@ -1,15 +1,20 @@ using System.ComponentModel.DataAnnotations; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; +using AccountService = DysonNetwork.Shared.Proto.AccountService; namespace DysonNetwork.Pusher.Notification; [ApiController] [Route("/api/notifications")] -public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase +public class NotificationController( + AppDatabase db, + PushService nty, + AccountService.AccountServiceClient accounts) : ControllerBase { [HttpGet("count")] [Authorize] @@ -17,9 +22,10 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); if (currentUserValue is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); var count = await db.Notifications - .Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null) + .Where(s => s.AccountId == accountId && s.ViewedAt == null) .CountAsync(); return Ok(count); } @@ -30,24 +36,25 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C [FromQuery] int offset = 0, // The page size set to 5 is to avoid the client pulled the notification // 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); if (currentUserValue is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); var totalCount = await db.Notifications - .Where(s => s.AccountId == currentUser.Id) + .Where(s => s.AccountId == accountId) .CountAsync(); var notifications = await db.Notifications - .Where(s => s.AccountId == currentUser.Id) + .Where(s => s.AccountId == accountId) .OrderByDescending(e => e.CreatedAt) .Skip(offset) .Take(take) .ToListAsync(); Response.Headers["X-Total"] = totalCount.ToString(); - await nty.MarkNotificationsViewed(notifications); + await nty.MarkNotificationsViewed(notifications.ToList()); return Ok(notifications); } @@ -55,14 +62,15 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C public class PushNotificationSubscribeRequest { [MaxLength(4096)] public string DeviceToken { get; set; } = null!; - public NotificationPushProvider Provider { get; set; } + public PushProvider Provider { get; set; } } [HttpPut("subscription")] [Authorize] - public async Task> SubscribeToPushNotification( - [FromBody] PushNotificationSubscribeRequest request - ) + public async Task> + SubscribeToPushNotification( + [FromBody] PushNotificationSubscribeRequest request + ) { HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); @@ -72,8 +80,12 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C if (currentSession == null) return Unauthorized(); var result = - await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!, - request.DeviceToken); + await nty.SubscribeDevice( + currentSession.Challenge.DeviceId!, + request.DeviceToken, + request.Provider, + currentUser + ); return Ok(result); } @@ -88,10 +100,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C if (currentUser == null) return Unauthorized(); var currentSession = currentSessionValue as AuthSession; if (currentSession == null) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); - var affectedRows = await db.NotificationPushSubscriptions + var affectedRows = await db.PushSubscriptions .Where(s => - s.AccountId == currentUser.Id && + s.AccountId == accountId && s.DeviceId == currentSession.Challenge.DeviceId ).ExecuteDeleteAsync(); return Ok(affectedRows); @@ -107,36 +120,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C public int Priority { get; set; } = 10; } - [HttpPost("broadcast")] - [Authorize] - [RequiredPermission("global", "notifications.broadcast")] - public async Task 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 { [Required] public List AccountId { get; set; } = null!; } - + [HttpPost("send")] [Authorize] [RequiredPermission("global", "notifications.send")] @@ -145,7 +133,6 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C [FromQuery] bool save = false ) { - var accounts = await db.Accounts.Where(a => request.AccountId.Contains(a.Id)).ToListAsync(); await nty.SendNotificationBatch( new Notification { @@ -157,7 +144,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C Content = request.Content, Meta = request.Meta, }, - accounts, + request.AccountId, save ); return Ok(); diff --git a/DysonNetwork.Pusher/Notification/PushService.cs b/DysonNetwork.Pusher/Notification/PushService.cs index 050107e..5a859e0 100644 --- a/DysonNetwork.Pusher/Notification/PushService.cs +++ b/DysonNetwork.Pusher/Notification/PushService.cs @@ -12,14 +12,14 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto private readonly string _notifyTopic = config["Notifications:Topic"]!; private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); - public async Task UnsubscribePushNotifications(string deviceId) + public async Task UnsubscribeDevice(string deviceId) { await db.PushSubscriptions .Where(s => s.DeviceId == deviceId) .ExecuteDeleteAsync(); } - public async Task SubscribePushNotification( + public async Task SubscribeDevice( string deviceId, string deviceToken, PushProvider provider, diff --git a/DysonNetwork.Pusher/Services/PusherServiceGrpc.cs b/DysonNetwork.Pusher/Services/PusherServiceGrpc.cs index a4c87ef..0ff7d05 100644 --- a/DysonNetwork.Pusher/Services/PusherServiceGrpc.cs +++ b/DysonNetwork.Pusher/Services/PusherServiceGrpc.cs @@ -9,7 +9,7 @@ namespace DysonNetwork.Pusher.Services; public class PusherServiceGrpc( EmailService emailService, - WebSocketService webSocketService, + WebSocketService websocket, PushService pushService ) : PusherService.PusherServiceBase { @@ -32,7 +32,7 @@ public class PusherServiceGrpc( Data = request.Packet.Data, ErrorMessage = request.Packet.ErrorMessage }; - webSocketService.SendPacketToAccount(request.UserId, packet); + websocket.SendPacketToAccount(request.UserId, packet); return Task.FromResult(new Empty()); } @@ -46,7 +46,7 @@ public class PusherServiceGrpc( ErrorMessage = request.Packet.ErrorMessage }; foreach (var userId in request.UserIds) - webSocketService.SendPacketToAccount(userId, packet); + websocket.SendPacketToAccount(userId, packet); return Task.FromResult(new Empty()); } @@ -60,7 +60,7 @@ public class PusherServiceGrpc( Data = request.Packet.Data, ErrorMessage = request.Packet.ErrorMessage }; - webSocketService.SendPacketToDevice(request.DeviceId, packet); + websocket.SendPacketToDevice(request.DeviceId, packet); return Task.FromResult(new Empty()); } @@ -74,7 +74,7 @@ public class PusherServiceGrpc( ErrorMessage = request.Packet.ErrorMessage }; foreach (var deviceId in request.DeviceIds) - webSocketService.SendPacketToDevice(deviceId, packet); + websocket.SendPacketToDevice(deviceId, packet); return Task.FromResult(new Empty()); } @@ -159,4 +159,22 @@ public class PusherServiceGrpc( await pushService.SendNotificationBatch(notification, accounts, request.Notification.IsSavable); return new Empty(); } + + public override async Task UnsubscribePushNotifications(UnsubscribePushNotificationsRequest request, ServerCallContext context) + { + await pushService.UnsubscribeDevice(request.DeviceId); + return new Empty(); + } + + public override Task 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 }); + } } \ No newline at end of file diff --git a/DysonNetwork.Shared/Auth/AuthScheme.cs b/DysonNetwork.Shared/Auth/AuthScheme.cs index 69d4873..7ddf4b0 100644 --- a/DysonNetwork.Shared/Auth/AuthScheme.cs +++ b/DysonNetwork.Shared/Auth/AuthScheme.cs @@ -16,10 +16,9 @@ public class DysonTokenAuthHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - ISystemClock clock, AuthService.AuthServiceClient auth ) - : AuthenticationHandler(options, logger, encoder, clock) + : AuthenticationHandler(options, logger, encoder) { protected override async Task HandleAuthenticateAsync() { diff --git a/DysonNetwork.Shared/Auth/PermissionMiddleware.cs b/DysonNetwork.Shared/Auth/PermissionMiddleware.cs new file mode 100644 index 0000000..30711be --- /dev/null +++ b/DysonNetwork.Shared/Auth/PermissionMiddleware.cs @@ -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 logger) + { + var endpoint = httpContext.GetEndpoint(); + + var attr = endpoint?.Metadata + .OfType() + .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); + } + } +} diff --git a/DysonNetwork.Shared/Auth/Startup.cs b/DysonNetwork.Shared/Auth/Startup.cs index 6f29d03..7da4185 100644 --- a/DysonNetwork.Shared/Auth/Startup.cs +++ b/DysonNetwork.Shared/Auth/Startup.cs @@ -16,9 +16,9 @@ public static class DysonAuthStartup { var etcdClient = sp.GetRequiredService(); var config = sp.GetRequiredService(); - var clientCertPath = config["ClientCert:Path"]; - var clientKeyPath = config["ClientKey:Path"]; - var clientCertPassword = config["ClientCert:Password"]; + var clientCertPath = config["Service:ClientCert"]; + var clientKeyPath = config["Service:ClientKey"]; + var clientCertPassword = config["Service:CertPassword"]; return GrpcClientHelper .CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) diff --git a/DysonNetwork.Shared/DysonNetwork.Shared.csproj b/DysonNetwork.Shared/DysonNetwork.Shared.csproj index 38296a3..92e78ee 100644 --- a/DysonNetwork.Shared/DysonNetwork.Shared.csproj +++ b/DysonNetwork.Shared/DysonNetwork.Shared.csproj @@ -19,8 +19,7 @@ - - + diff --git a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs index f891317..df42e78 100644 --- a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs +++ b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs @@ -33,17 +33,6 @@ public static class GrpcClientHelper 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 CreateAccountServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -51,22 +40,11 @@ public static class GrpcClientHelper string? clientCertPassword = null ) { - var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService"); + var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass"); return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, 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 CreateAuthServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -74,22 +52,11 @@ public static class GrpcClientHelper string? clientCertPassword = null ) { - var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService"); + var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass"); return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, 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 CreatePusherServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -97,7 +64,7 @@ public static class GrpcClientHelper string? clientCertPassword = null ) { - var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService"); + var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pusher"); return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } diff --git a/DysonNetwork.Shared/Proto/account.proto b/DysonNetwork.Shared/Proto/account.proto index 142f52e..b7fad42 100644 --- a/DysonNetwork.Shared/Proto/account.proto +++ b/DysonNetwork.Shared/Proto/account.proto @@ -195,6 +195,7 @@ message LevelingInfo { service AccountService { // Account Operations rpc GetAccount(GetAccountRequest) returns (Account) {} + rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc CreateAccount(CreateAccountRequest) returns (Account) {} rpc UpdateAccount(UpdateAccountRequest) returns (Account) {} rpc DeleteAccount(DeleteAccountRequest) returns (google.protobuf.Empty) {} @@ -243,6 +244,14 @@ message GetAccountRequest { 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 { string name = 1; // Required: Unique username string nick = 2; // Optional: Display name diff --git a/DysonNetwork.Shared/Proto/pusher.proto b/DysonNetwork.Shared/Proto/pusher.proto index cf5d815..65d71a3 100644 --- a/DysonNetwork.Shared/Proto/pusher.proto +++ b/DysonNetwork.Shared/Proto/pusher.proto @@ -36,6 +36,12 @@ service PusherService { // Sends a push notification to a list of users. 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. @@ -108,3 +114,18 @@ message SendPushNotificationToUsersRequest { repeated string user_ids = 1; 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; +} diff --git a/DysonNetwork.Shared/Registry/ServiceHelper.cs b/DysonNetwork.Shared/Registry/ServiceHelper.cs new file mode 100644 index 0000000..47353ee --- /dev/null +++ b/DysonNetwork.Shared/Registry/ServiceHelper.cs @@ -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(sp => + { + var etcdClient = sp.GetRequiredService(); + var config = sp.GetRequiredService(); + 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(sp => + { + var etcdClient = sp.GetRequiredService(); + var config = sp.GetRequiredService(); + 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; + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Registry/ServiceRegistry.cs b/DysonNetwork.Shared/Registry/ServiceRegistry.cs index 2266f8b..535326a 100644 --- a/DysonNetwork.Shared/Registry/ServiceRegistry.cs +++ b/DysonNetwork.Shared/Registry/ServiceRegistry.cs @@ -8,16 +8,20 @@ namespace DysonNetwork.Shared.Registry; public class ServiceRegistry(IEtcdClient etcd, ILogger 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 leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds }); + var leaseResponse = await etcd.LeaseGrantAsync( + new LeaseGrantRequest { TTL = leaseTtlSeconds }, + cancellationToken: cancellationToken + ); await etcd.PutAsync(new PutRequest { Key = ByteString.CopyFrom(key, Encoding.UTF8), Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8), Lease = leaseResponse.ID - }); + }, cancellationToken: cancellationToken); _ = Task.Run(async () => { diff --git a/DysonNetwork.Sphere/Account/NotificationService.cs b/DysonNetwork.Sphere/Account/NotificationService.cs index fdb7502..00ebe80 100644 --- a/DysonNetwork.Sphere/Account/NotificationService.cs +++ b/DysonNetwork.Sphere/Account/NotificationService.cs @@ -190,7 +190,6 @@ public class NotificationService( Content = notification.Content, Meta = notification.Meta, Priority = notification.Priority, - Account = x, AccountId = x.Id }; return newNotification; diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj index b65e109..ca9d807 100644 --- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj +++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index caeddec..28064a5 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -3,6 +3,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -28,6 +29,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -81,6 +83,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded