Compare commits
	
		
			3 Commits
		
	
	
		
			a37ca3c772
			...
			609e30b67b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 609e30b67b | |||
| d22394230b | |||
| fc63a76eb2 | 
							
								
								
									
										96
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										96
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: Build and Push Dyson Sphere | ||||
| name: Build and Push Microservices | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -7,23 +7,19 @@ on: | ||||
|   workflow_dispatch: | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest # x86_64 (default), avoids arm64 native module issues | ||||
|  | ||||
|   build-sphere: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|  | ||||
|       - name: Build and push Docker image | ||||
|       - name: Build and push DysonNetwork.Sphere Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Sphere/Dockerfile | ||||
| @@ -31,3 +27,87 @@ jobs: | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-sphere:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-pass: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pass Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pass/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pass:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-pusher: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pusher Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pusher/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pusher:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-drive: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Drive Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Drive/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-drive:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-gateway: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Gateway Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Gateway/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-gateway:latest | ||||
|           platforms: linux/amd64 | ||||
|   | ||||
| @@ -91,7 +91,7 @@ public class RegistryProxyConfigProvider : IProxyConfigProvider, IDisposable | ||||
|                         { | ||||
|                             { "destination1", new DestinationConfig { Address = serviceUrl } } | ||||
|                         }, | ||||
|                         HttpRequest = new ForwarderRequestConfig() | ||||
|                         HttpRequest = new ForwarderRequestConfig | ||||
|                         { | ||||
|                             ActivityTimeout = directRoute.IsWebsocket ? TimeSpan.FromHours(24) : TimeSpan.FromMinutes(2) | ||||
|                         } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="CorePush" Version="4.3.0" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" /> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using CorePush.Apple; | ||||
| using CorePush.Firebase; | ||||
| using DysonNetwork.Pusher.Connection; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -7,14 +8,55 @@ using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
|  | ||||
| public class PushService(IConfiguration config, AppDatabase db, IHttpClientFactory httpFactory) | ||||
| public class PushService | ||||
| { | ||||
|     private readonly string _notifyTopic = config["Notifications:Topic"]!; | ||||
|     private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); | ||||
|     private readonly AppDatabase _db; | ||||
|     private readonly WebSocketService _ws; | ||||
|     private readonly ILogger<PushService> _logger; | ||||
|     private readonly FirebaseSender? _fcm; | ||||
|     private readonly ApnSender? _apns; | ||||
|     private readonly string? _apnsTopic; | ||||
|  | ||||
|     public PushService( | ||||
|         IConfiguration config, | ||||
|         AppDatabase db, | ||||
|         WebSocketService ws, | ||||
|         IHttpClientFactory httpFactory, | ||||
|         ILogger<PushService> logger | ||||
|     ) | ||||
|     { | ||||
|         var cfgSection = config.GetSection("Notifications:Push"); | ||||
|  | ||||
|         // Set up Firebase Cloud Messaging | ||||
|         var fcmConfig = cfgSection.GetValue<string>("Google"); | ||||
|         if (fcmConfig != null && File.Exists(fcmConfig)) | ||||
|             _fcm = new FirebaseSender(File.ReadAllText(fcmConfig), httpFactory.CreateClient()); | ||||
|  | ||||
|         // Set up Apple Push Notification Service | ||||
|         var apnsKeyPath = cfgSection.GetValue<string>("Apple:PrivateKey"); | ||||
|         if (apnsKeyPath != null && File.Exists(apnsKeyPath)) | ||||
|         { | ||||
|             _apns = new ApnSender(new ApnSettings | ||||
|             { | ||||
|                 P8PrivateKey = File.ReadAllText(apnsKeyPath), | ||||
|                 P8PrivateKeyId = cfgSection.GetValue<string>("Apple:PrivateKeyId"), | ||||
|                 TeamId = cfgSection.GetValue<string>("Apple:TeamId"), | ||||
|                 AppBundleIdentifier = cfgSection.GetValue<string>("Apple:BundleIdentifier"), | ||||
|                 ServerType = cfgSection.GetValue<bool>("Production") | ||||
|                     ? ApnServerType.Production | ||||
|                     : ApnServerType.Development | ||||
|             }, httpFactory.CreateClient()); | ||||
|             _apnsTopic = cfgSection.GetValue<string>("Apple:BundleIdentifier"); | ||||
|         } | ||||
|  | ||||
|         _db = db; | ||||
|         _ws = ws; | ||||
|         _logger = logger; | ||||
|     } | ||||
|  | ||||
|     public async Task UnsubscribeDevice(string deviceId) | ||||
|     { | ||||
|         await db.PushSubscriptions | ||||
|         await _db.PushSubscriptions | ||||
|             .Where(s => s.DeviceId == deviceId) | ||||
|             .ExecuteDeleteAsync(); | ||||
|     } | ||||
| @@ -27,41 +69,40 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto | ||||
|     ) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|         // First check if a matching subscription exists | ||||
|         var accountId = Guid.Parse(account.Id!); | ||||
|         var existingSubscription = await db.PushSubscriptions | ||||
|  | ||||
|         // Check for existing subscription with same device ID or token | ||||
|         var existingSubscription = await _db.PushSubscriptions | ||||
|             .Where(s => s.AccountId == accountId) | ||||
|             .Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         if (existingSubscription is not null) | ||||
|         if (existingSubscription != null) | ||||
|         { | ||||
|             // Update the existing subscription directly in the database | ||||
|             await db.PushSubscriptions | ||||
|                 .Where(s => s.Id == existingSubscription.Id) | ||||
|                 .ExecuteUpdateAsync(setters => setters | ||||
|                     .SetProperty(s => s.DeviceId, deviceId) | ||||
|                     .SetProperty(s => s.DeviceToken, deviceToken) | ||||
|                     .SetProperty(s => s.UpdatedAt, now)); | ||||
|  | ||||
|             // Return the updated subscription | ||||
|             // Update existing subscription | ||||
|             existingSubscription.DeviceId = deviceId; | ||||
|             existingSubscription.DeviceToken = deviceToken; | ||||
|             existingSubscription.Provider = provider; | ||||
|             existingSubscription.UpdatedAt = now; | ||||
|  | ||||
|             _db.Update(existingSubscription); | ||||
|             await _db.SaveChangesAsync(); | ||||
|             return existingSubscription; | ||||
|         } | ||||
|  | ||||
|         // Create new subscription | ||||
|         var subscription = new PushSubscription | ||||
|         { | ||||
|             DeviceId = deviceId, | ||||
|             DeviceToken = deviceToken, | ||||
|             Provider = provider, | ||||
|             AccountId = accountId, | ||||
|             CreatedAt = now, | ||||
|             UpdatedAt = now | ||||
|         }; | ||||
|  | ||||
|         db.PushSubscriptions.Add(subscription); | ||||
|         await db.SaveChangesAsync(); | ||||
|         _db.PushSubscriptions.Add(subscription); | ||||
|         await _db.SaveChangesAsync(); | ||||
|  | ||||
|         return subscription; | ||||
|     } | ||||
| @@ -94,8 +135,8 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto | ||||
|  | ||||
|         if (save) | ||||
|         { | ||||
|             db.Add(notification); | ||||
|             await db.SaveChangesAsync(); | ||||
|             _db.Add(notification); | ||||
|             await _db.SaveChangesAsync(); | ||||
|         } | ||||
|  | ||||
|         if (!isSilent) _ = DeliveryNotification(notification); | ||||
| @@ -104,7 +145,7 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto | ||||
|     public async Task DeliveryNotification(Notification notification) | ||||
|     { | ||||
|         // Pushing the notification | ||||
|         var subscribers = await db.PushSubscriptions | ||||
|         var subscribers = await _db.PushSubscriptions | ||||
|             .Where(s => s.AccountId == notification.AccountId) | ||||
|             .ToListAsync(); | ||||
|  | ||||
| @@ -117,7 +158,7 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto | ||||
|         var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); | ||||
|         if (id.Count == 0) return; | ||||
|  | ||||
|         await db.Notifications | ||||
|         await _db.Notifications | ||||
|             .Where(n => id.Contains(n.Id)) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) | ||||
|             ); | ||||
| @@ -141,105 +182,113 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto | ||||
|                 }; | ||||
|                 return newNotification; | ||||
|             }).ToList(); | ||||
|             await db.BulkInsertAsync(notifications); | ||||
|             await _db.BulkInsertAsync(notifications); | ||||
|         } | ||||
|  | ||||
|         var subscribers = await db.PushSubscriptions | ||||
|         var subscribers = await _db.PushSubscriptions | ||||
|             .Where(s => accounts.Contains(s.AccountId)) | ||||
|             .ToListAsync(); | ||||
|         await _PushNotification(notification, subscribers); | ||||
|     } | ||||
|  | ||||
|     private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification, | ||||
|         IEnumerable<PushSubscription> subscriptions) | ||||
|     { | ||||
|         var subDict = subscriptions | ||||
|             .GroupBy(x => x.Provider) | ||||
|             .ToDictionary(x => x.Key, x => x.ToList()); | ||||
|  | ||||
|         var notifications = subDict.Select(value => | ||||
|         { | ||||
|             var platformCode = value.Key switch | ||||
|             { | ||||
|                 PushProvider.Apple => 1, | ||||
|                 PushProvider.Google => 2, | ||||
|                 _ => throw new InvalidOperationException($"Unknown push provider: {value.Key}") | ||||
|             }; | ||||
|  | ||||
|             var tokens = value.Value.Select(x => x.DeviceToken).ToList(); | ||||
|             return _BuildNotificationPayload(notification, platformCode, tokens); | ||||
|         }).ToList(); | ||||
|  | ||||
|         return notifications.ToList(); | ||||
|     } | ||||
|  | ||||
|     private Dictionary<string, object> _BuildNotificationPayload(Pusher.Notification.Notification notification, | ||||
|         int platformCode, | ||||
|         IEnumerable<string> deviceTokens) | ||||
|     { | ||||
|         var alertDict = new Dictionary<string, object>(); | ||||
|         var dict = new Dictionary<string, object> | ||||
|         { | ||||
|             ["notif_id"] = notification.Id.ToString(), | ||||
|             ["apns_id"] = notification.Id.ToString(), | ||||
|             ["topic"] = _notifyTopic, | ||||
|             ["tokens"] = deviceTokens, | ||||
|             ["data"] = new Dictionary<string, object> | ||||
|             { | ||||
|                 ["type"] = notification.Topic, | ||||
|                 ["meta"] = notification.Meta ?? new Dictionary<string, object>(), | ||||
|             }, | ||||
|             ["mutable_content"] = true, | ||||
|             ["priority"] = notification.Priority >= 5 ? "high" : "normal", | ||||
|         }; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(notification.Title)) | ||||
|         { | ||||
|             dict["title"] = notification.Title; | ||||
|             alertDict["title"] = notification.Title; | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(notification.Content)) | ||||
|         { | ||||
|             dict["message"] = notification.Content; | ||||
|             alertDict["body"] = notification.Content; | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(notification.Subtitle)) | ||||
|         { | ||||
|             dict["message"] = $"{notification.Subtitle}\n{dict["message"]}"; | ||||
|             alertDict["subtitle"] = notification.Subtitle; | ||||
|         } | ||||
|  | ||||
|         if (notification.Priority >= 5) | ||||
|             dict["name"] = "default"; | ||||
|  | ||||
|         dict["platform"] = platformCode; | ||||
|         dict["alert"] = alertDict; | ||||
|  | ||||
|         return dict; | ||||
|     } | ||||
|  | ||||
|     private async Task _PushNotification( | ||||
|         Notification notification, | ||||
|         IEnumerable<PushSubscription> subscriptions | ||||
|     ) | ||||
|     { | ||||
|         var subList = subscriptions.ToList(); | ||||
|         if (subList.Count == 0) return; | ||||
|         var tasks = subscriptions | ||||
|             .Select(subscription => _PushSingleNotification(notification, subscription)) | ||||
|             .ToList(); | ||||
|  | ||||
|         var requestDict = new Dictionary<string, object> | ||||
|         await Task.WhenAll(tasks); | ||||
|     } | ||||
|  | ||||
|     private async Task _PushSingleNotification(Notification notification, PushSubscription subscription) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             ["notifications"] = _BuildNotificationPayload(notification, subList) | ||||
|         }; | ||||
|             _logger.LogDebug( | ||||
|                 $"Pushing notification {notification.Topic} #{notification.Id} to device #{subscription.DeviceId}"); | ||||
|  | ||||
|         var client = httpFactory.CreateClient(); | ||||
|         client.BaseAddress = _notifyEndpoint; | ||||
|         var request = await client.PostAsync("/push", new StringContent( | ||||
|             JsonSerializer.Serialize(requestDict), | ||||
|             Encoding.UTF8, | ||||
|             "application/json" | ||||
|         )); | ||||
|         request.EnsureSuccessStatusCode(); | ||||
|             switch (subscription.Provider) | ||||
|             { | ||||
|                 case PushProvider.Google: | ||||
|                     if (_fcm == null) | ||||
|                         throw new InvalidOperationException("Firebase Cloud Messaging is not initialized."); | ||||
|  | ||||
|                     var body = string.Empty; | ||||
|                     if (!string.IsNullOrEmpty(notification.Subtitle) || !string.IsNullOrEmpty(notification.Content)) | ||||
|                     { | ||||
|                         body = string.Join("\n", | ||||
|                             notification.Subtitle ?? string.Empty, | ||||
|                             notification.Content ?? string.Empty).Trim(); | ||||
|                     } | ||||
|  | ||||
|                     await _fcm.SendAsync(new Dictionary<string, object> | ||||
|                     { | ||||
|                         ["message"] = new Dictionary<string, object> | ||||
|                         { | ||||
|                             ["token"] = subscription.DeviceToken, | ||||
|                             ["notification"] = new Dictionary<string, object> | ||||
|                             { | ||||
|                                 ["title"] = notification.Title ?? string.Empty, | ||||
|                                 ["body"] = body | ||||
|                             }, | ||||
|                             ["data"] = new Dictionary<string, object> | ||||
|                             { | ||||
|                                 ["id"] = notification.Id, | ||||
|                                 ["topic"] = notification.Topic, | ||||
|                                 ["meta"] = notification.Meta ?? new Dictionary<string, object>() | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                     break; | ||||
|  | ||||
|                 case PushProvider.Apple: | ||||
|                     if (_apns == null) | ||||
|                         throw new InvalidOperationException("Apple Push Notification Service is not initialized."); | ||||
|  | ||||
|                     var alertDict = new Dictionary<string, object>(); | ||||
|                     if (!string.IsNullOrEmpty(notification.Title)) | ||||
|                         alertDict["title"] = notification.Title; | ||||
|                     if (!string.IsNullOrEmpty(notification.Subtitle)) | ||||
|                         alertDict["subtitle"] = notification.Subtitle; | ||||
|                     if (!string.IsNullOrEmpty(notification.Content)) | ||||
|                         alertDict["body"] = notification.Content; | ||||
|  | ||||
|                     var payload = new Dictionary<string, object?> | ||||
|                     { | ||||
|                         ["topic"] = _apnsTopic, | ||||
|                         ["aps"] = new Dictionary<string, object?> | ||||
|                         { | ||||
|                             ["alert"] = alertDict, | ||||
|                             ["sound"] = notification.Priority >= 5 ? "default" : null, | ||||
|                             ["mutable-content"] = 1 | ||||
|                         }, | ||||
|                         ["meta"] = notification.Meta ?? new Dictionary<string, object>() | ||||
|                     }; | ||||
|  | ||||
|                     await _apns.SendAsync( | ||||
|                         payload, | ||||
|                         deviceToken: subscription.DeviceToken, | ||||
|                         apnsId: notification.Id.ToString(), | ||||
|                         apnsPriority: notification.Priority, | ||||
|                         apnPushType: ApnPushType.Alert | ||||
|                     ); | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     throw new InvalidOperationException($"Push provider not supported: {subscription.Provider}"); | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogError(ex, | ||||
|                 $"Failed to push notification #{notification.Id} to device {subscription.DeviceId}. {ex.Message}"); | ||||
|             throw new Exception($"Failed to send notification to {subscription.Provider}: {ex.Message}", ex); | ||||
|         } | ||||
|  | ||||
|         _logger.LogInformation( | ||||
|             $"Successfully pushed notification #{notification.Id} to device {subscription.DeviceId}"); | ||||
|     } | ||||
| } | ||||
| @@ -23,6 +23,7 @@ builder.Services.AddAppFlushHandlers(); | ||||
|  | ||||
| // Add business services | ||||
| builder.Services.AddAppBusinessServices(); | ||||
| builder.Services.AddPushServices(builder.Configuration); | ||||
|  | ||||
| // Add scheduled jobs | ||||
| builder.Services.AddAppScheduledJobs(); | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| using System.Text.Json; | ||||
| using System.Threading.RateLimiting; | ||||
| using CorePush.Apple; | ||||
| using CorePush.Firebase; | ||||
| using DysonNetwork.Pusher.Connection; | ||||
| using DysonNetwork.Pusher.Email; | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| @@ -137,4 +139,13 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|      | ||||
|     public static void AddPushServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.Configure<ApnSettings>(configuration.GetSection("PushNotify:Apple")); | ||||
|         services.AddHttpClient<ApnSender>(); | ||||
|  | ||||
|         services.Configure<FirebaseSettings>(configuration.GetSection("PushNotify:Firebase")); | ||||
|         services.AddHttpClient<FirebaseSettings>(); | ||||
|     } | ||||
| } | ||||
| @@ -14,8 +14,16 @@ | ||||
|     "Etcd": "etcd.orb.local:2379" | ||||
|   }, | ||||
|   "Notifications": { | ||||
|     "Topic": "dev.solsynth.solian", | ||||
|     "Endpoint": "http://localhost:8088" | ||||
|     "Push": { | ||||
|       "Production": true, | ||||
|       "Google": "./Keys/Solian.json", | ||||
|       "Apple": { | ||||
|         "PrivateKey": "./Keys/Solian.p8", | ||||
|         "PrivateKeyId": "4US4KSX4W6", | ||||
|         "TeamId": "W7HPZ53V6B", | ||||
|         "BundleIdentifier": "dev.solsynth.solian" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "Email": { | ||||
|     "Server": "smtp4dev.orb.local", | ||||
|   | ||||
| @@ -10,11 +10,6 @@ | ||||
|         <SatelliteResourceLanguages>en-US;zh-Hans</SatelliteResourceLanguages> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <!-- NPM Configuration --> | ||||
|     <PropertyGroup> | ||||
|         <NpmInstallStampFile>node_modules/.install-stamp</NpmInstallStampFile> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="AngleSharp" Version="1.3.0"/> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/> | ||||
| @@ -75,6 +70,7 @@ | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <Folder Include="Discovery\"/> | ||||
|         <Folder Include="wwwroot\" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
| @@ -162,6 +158,17 @@ | ||||
|         <_ContentIncludedByDefault Remove="Pages\Checkpoint\CheckpointPage.cshtml"/> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Spell\MagicSpellPage.cshtml"/> | ||||
|         <_ContentIncludedByDefault Remove="Keys\Solian.json"/> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Index.cshtml" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Posts\PostDetail.cshtml" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Shared\_Layout.cshtml" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\_ViewImports.cshtml" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\_ViewStart.cshtml" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\AccountDeletionEmail.razor" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\ContactVerificationEmail.razor" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\EmailLayout.razor" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\LandingEmail.razor" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\PasswordResetEmail.razor" /> | ||||
|         <_ContentIncludedByDefault Remove="Pages\Emails\VerificationEmail.razor" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| @DysonNetwork.Sphere_HostAddress = http://localhost:5071 | ||||
|  | ||||
| GET {{DysonNetwork.Sphere_HostAddress}}/weatherforecast/ | ||||
| Accept: application/json | ||||
|  | ||||
| ### | ||||
| @@ -1,42 +0,0 @@ | ||||
| @using DysonNetwork.Sphere.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["AccountDeletionHeader"])</p> | ||||
|             <p>@(Localizer["AccountDeletionPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["AccountDeletionPara2"])</p> | ||||
|             <p>@(Localizer["AccountDeletionPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["AccountDeletionButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["AccountDeletionPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| @using DysonNetwork.Sphere.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Sphere.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["ContactVerificationHeader"])</p> | ||||
|             <p>@(Localizer["ContactVerificationPara1"]) @Name,</p> | ||||
|             <p>@(Localizer["ContactVerificationPara2"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["ContactVerificationButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["ContactVerificationPara3"])</p> | ||||
|             <p>@(Localizer["ContactVerificationPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
| @@ -1,337 +0,0 @@ | ||||
| @inherits LayoutComponentBase | ||||
|  | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | ||||
|     <style media="all" type="text/css"> | ||||
|         body { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             -webkit-font-smoothing: antialiased; | ||||
|             font-size: 16px; | ||||
|             line-height: 1.3; | ||||
|             -ms-text-size-adjust: 100%; | ||||
|             -webkit-text-size-adjust: 100%; | ||||
|         } | ||||
|  | ||||
|         table { | ||||
|             border-collapse: separate; | ||||
|             mso-table-lspace: 0pt; | ||||
|             mso-table-rspace: 0pt; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         table td { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             font-size: 16px; | ||||
|             vertical-align: top; | ||||
|         } | ||||
|  | ||||
|         body { | ||||
|             background-color: #f4f5f6; | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .body { | ||||
|             background-color: #f4f5f6; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .container { | ||||
|             margin: 0 auto !important; | ||||
|             max-width: 600px; | ||||
|             padding: 0; | ||||
|             padding-top: 24px; | ||||
|             width: 600px; | ||||
|         } | ||||
|  | ||||
|         .content { | ||||
|             box-sizing: border-box; | ||||
|             display: block; | ||||
|             margin: 0 auto; | ||||
|             max-width: 600px; | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .main { | ||||
|             background: #ffffff; | ||||
|             border: 1px solid #eaebed; | ||||
|             border-radius: 16px; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .wrapper { | ||||
|             box-sizing: border-box; | ||||
|             padding: 24px; | ||||
|         } | ||||
|  | ||||
|         .footer { | ||||
|             clear: both; | ||||
|             padding-top: 24px; | ||||
|             text-align: center; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .footer td, | ||||
|         .footer p, | ||||
|         .footer span, | ||||
|         .footer a { | ||||
|             color: #9a9ea6; | ||||
|             font-size: 16px; | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         p { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             font-size: 16px; | ||||
|             font-weight: normal; | ||||
|             margin: 0; | ||||
|             margin-bottom: 16px; | ||||
|         } | ||||
|  | ||||
|         a { | ||||
|             color: #0867ec; | ||||
|             text-decoration: underline; | ||||
|         } | ||||
|  | ||||
|         .btn { | ||||
|             box-sizing: border-box; | ||||
|             min-width: 100% !important; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .btn > tbody > tr > td { | ||||
|             padding-bottom: 16px; | ||||
|         } | ||||
|  | ||||
|         .btn table { | ||||
|             width: auto; | ||||
|         } | ||||
|  | ||||
|         .btn table td { | ||||
|             background-color: #ffffff; | ||||
|             border-radius: 4px; | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         .btn a { | ||||
|             background-color: #ffffff; | ||||
|             border: solid 2px #0867ec; | ||||
|             border-radius: 4px; | ||||
|             box-sizing: border-box; | ||||
|             color: #0867ec; | ||||
|             cursor: pointer; | ||||
|             display: inline-block; | ||||
|             font-size: 16px; | ||||
|             font-weight: bold; | ||||
|             margin: 0; | ||||
|             padding: 12px 24px; | ||||
|             text-decoration: none; | ||||
|             text-transform: capitalize; | ||||
|         } | ||||
|  | ||||
|         .btn-primary table td { | ||||
|             background-color: #0867ec; | ||||
|         } | ||||
|  | ||||
|         .btn-primary a { | ||||
|             background-color: #0867ec; | ||||
|             border-color: #0867ec; | ||||
|             color: #ffffff; | ||||
|         } | ||||
|          | ||||
|         .font-bold { | ||||
|             font-weight: bold; | ||||
|         } | ||||
|          | ||||
|         .verification-code | ||||
|         { | ||||
|             font-family: "Courier New", Courier, monospace; | ||||
|             font-size: 24px; | ||||
|             letter-spacing: 0.5em; | ||||
|         } | ||||
|  | ||||
|         @@media all { | ||||
|             .btn-primary table td:hover { | ||||
|                 background-color: #ec0867 !important; | ||||
|             } | ||||
|  | ||||
|             .btn-primary a:hover { | ||||
|                 background-color: #ec0867 !important; | ||||
|                 border-color: #ec0867 !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .last { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|  | ||||
|         .first { | ||||
|             margin-top: 0; | ||||
|         } | ||||
|  | ||||
|         .align-center { | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         .align-right { | ||||
|             text-align: right; | ||||
|         } | ||||
|  | ||||
|         .align-left { | ||||
|             text-align: left; | ||||
|         } | ||||
|  | ||||
|         .text-link { | ||||
|             color: #0867ec !important; | ||||
|             text-decoration: underline !important; | ||||
|         } | ||||
|  | ||||
|         .clear { | ||||
|             clear: both; | ||||
|         } | ||||
|  | ||||
|         .mt0 { | ||||
|             margin-top: 0; | ||||
|         } | ||||
|  | ||||
|         .mb0 { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|  | ||||
|         .preheader { | ||||
|             color: transparent; | ||||
|             display: none; | ||||
|             height: 0; | ||||
|             max-height: 0; | ||||
|             max-width: 0; | ||||
|             opacity: 0; | ||||
|             overflow: hidden; | ||||
|             mso-hide: all; | ||||
|             visibility: hidden; | ||||
|             width: 0; | ||||
|         } | ||||
|  | ||||
|         .powered-by a { | ||||
|             text-decoration: none; | ||||
|         } | ||||
|  | ||||
|         @@media only screen and (max-width: 640px) { | ||||
|             .main p, | ||||
|             .main td, | ||||
|             .main span { | ||||
|                 font-size: 16px !important; | ||||
|             } | ||||
|  | ||||
|             .wrapper { | ||||
|                 padding: 8px !important; | ||||
|             } | ||||
|  | ||||
|             .content { | ||||
|                 padding: 0 !important; | ||||
|             } | ||||
|  | ||||
|             .container { | ||||
|                 padding: 0 !important; | ||||
|                 padding-top: 8px !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|  | ||||
|             .main { | ||||
|                 border-left-width: 0 !important; | ||||
|                 border-radius: 0 !important; | ||||
|                 border-right-width: 0 !important; | ||||
|             } | ||||
|  | ||||
|             .btn table { | ||||
|                 max-width: 100% !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|  | ||||
|             .btn a { | ||||
|                 font-size: 16px !important; | ||||
|                 max-width: 100% !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @@media all { | ||||
|             .ExternalClass { | ||||
|                 width: 100%; | ||||
|             } | ||||
|  | ||||
|             .ExternalClass, | ||||
|             .ExternalClass p, | ||||
|             .ExternalClass span, | ||||
|             .ExternalClass font, | ||||
|             .ExternalClass td, | ||||
|             .ExternalClass div { | ||||
|                 line-height: 100%; | ||||
|             } | ||||
|  | ||||
|             .apple-link a { | ||||
|                 color: inherit !important; | ||||
|                 font-family: inherit !important; | ||||
|                 font-size: inherit !important; | ||||
|                 font-weight: inherit !important; | ||||
|                 line-height: inherit !important; | ||||
|                 text-decoration: none !important; | ||||
|             } | ||||
|  | ||||
|             #MessageViewBody a { | ||||
|                 color: inherit; | ||||
|                 text-decoration: none; | ||||
|                 font-size: inherit; | ||||
|                 font-family: inherit; | ||||
|                 font-weight: inherit; | ||||
|                 line-height: inherit; | ||||
|             } | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
| <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"> | ||||
|     <tr> | ||||
|         <td> </td> | ||||
|         <td class="container"> | ||||
|             <div class="content"> | ||||
|  | ||||
|                 <!-- START CENTERED WHITE CONTAINER --> | ||||
|                 <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main"> | ||||
|                     <!-- START MAIN CONTENT AREA --> | ||||
|                     @ChildContent | ||||
|                     <!-- END MAIN CONTENT AREA --> | ||||
|                 </table> | ||||
|  | ||||
|                 <!-- START FOOTER --> | ||||
|                 <div class="footer"> | ||||
|                     <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                         <tr> | ||||
|                             <td class="content-block"> | ||||
|                                 <span class="apple-link">Solar Network</span> | ||||
|                                 <br> Solsynth LLC © @(DateTime.Now.Year) | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                         <tr> | ||||
|                             <td class="content-block powered-by"> | ||||
|                                 Powered by <a href="https://github.com/solsynth/dysonnetwork">Dyson Network</a> | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                     </table> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- END FOOTER --> | ||||
|  | ||||
|                 <!-- END CENTERED WHITE CONTAINER --></div> | ||||
|         </td> | ||||
|         <td> </td> | ||||
|     </tr> | ||||
| </table> | ||||
| </body> | ||||
| </html> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public RenderFragment? ChildContent { get; set; } | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| @using DysonNetwork.Sphere.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Sphere.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["LandingHeader1"])</p> | ||||
|             <p>@(Localizer["LandingPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["LandingPara2"])</p> | ||||
|             <p>@(Localizer["LandingPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["LandingButton1"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["LandingPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| @using DysonNetwork.Sphere.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Sphere.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["PasswordResetHeader"])</p> | ||||
|             <p>@(Localizer["PasswordResetPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["PasswordResetPara2"])</p> | ||||
|             <p>@(Localizer["PasswordResetPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["PasswordResetButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["PasswordResetPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
|     [Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!; | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| @using DysonNetwork.Sphere.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Sphere.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["VerificationHeader1"])</p> | ||||
|             <p>@(Localizer["VerificationPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["VerificationPara2"])</p> | ||||
|             <p>@(Localizer["VerificationPara3"])</p> | ||||
|  | ||||
|             <p class="verification-code">@Code</p> | ||||
|  | ||||
|             <p>@(Localizer["VerificationPara4"])</p> | ||||
|             <p>@(Localizer["VerificationPara5"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Code { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
|     [Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!; | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| @page | ||||
| @model IndexModel | ||||
| @{ | ||||
|     ViewData["Title"] = "The Solar Network | Solar Network"; | ||||
| } | ||||
|  | ||||
| <div class="hero min-h-full bg-base-200"> | ||||
|   <div class="hero-content text-center"> | ||||
|     <div class="max-w-md"> | ||||
|       <h1 class="text-5xl font-bold">Solar Network</h1> | ||||
|       <p class="py-6">This Solar Network instance is up and running.</p> | ||||
|       <a href="https://sn.solsynth.dev" target="_blank" class="btn btn-primary">Get started</a> | ||||
|       <div class="flex items-center justify-center gap-x-6 mt-6"> | ||||
|           <a href="/swagger" target="_blank" class="btn btn-ghost"> | ||||
|               <span aria-hidden="true">λ </span> API Docs | ||||
|           </a> | ||||
|           <a href="https://kb.solsynth.dev" target="_blank" class="btn btn-ghost"> | ||||
|               Learn more <span aria-hidden="true">→</span> | ||||
|           </a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -1,10 +0,0 @@ | ||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Pages; | ||||
|  | ||||
| public class IndexModel : PageModel | ||||
| { | ||||
|     public void OnGet() | ||||
|     { | ||||
|     } | ||||
| }  | ||||
| @@ -1,65 +0,0 @@ | ||||
| @page "/posts/{PostId:guid}" | ||||
| @model DysonNetwork.Sphere.Pages.Posts.PostDetailModel | ||||
| @using Markdig | ||||
| @{ | ||||
|     ViewData["Title"] = Model.Post?.Title + " | Solar Network"; | ||||
|     var imageUrl = Model.Post?.Attachments?.FirstOrDefault(a => a.MimeType.StartsWith("image/"))?.Id; | ||||
| } | ||||
|  | ||||
| @section Head { | ||||
|     <meta property="og:title" content="@Model.Post?.Title"/> | ||||
|     <meta property="og:type" content="article"/> | ||||
|     @if (imageUrl != null) | ||||
|     { | ||||
|         <meta property="og:image" content="/api/files/@imageUrl"/> | ||||
|     } | ||||
|     <meta property="og:url" content="@Request.Scheme://@Request.Host@Request.Path"/> | ||||
|     <meta property="og:description" content="@Model.Post?.Description"/> | ||||
| } | ||||
|  | ||||
| <div class="container mx-auto p-4"> | ||||
|     @if (Model.Post != null) | ||||
|     { | ||||
|         <h1 class="text-3xl font-bold mb-4">@Model.Post.Title</h1> | ||||
|         <p class="text-gray-600 mb-2"> | ||||
|             Created at: @Model.Post.CreatedAt | ||||
|             <span>by <a href="#" class="text-blue-500">@@@Model.Post.Publisher.Name</a></span> | ||||
|         </p> | ||||
|         <div class="prose lg:prose-xl mb-4"> | ||||
|             @Html.Raw(Markdown.ToHtml(Model.Post.Content ?? string.Empty)) | ||||
|         </div> | ||||
|  | ||||
|         @if (Model.Post.Attachments != null && Model.Post.Attachments.Any()) | ||||
|         { | ||||
|             <h2 class="text-2xl font-bold mb-2">Attachments</h2> | ||||
|             <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | ||||
|                 @foreach (var attachment in Model.Post.Attachments) | ||||
|                 { | ||||
|                     <div class="border p-2 rounded-md"> | ||||
|                         @if (attachment.MimeType != null && attachment.MimeType.StartsWith("image/")) | ||||
|                         { | ||||
|                             <img src="/api/files/@attachment.Id" alt="@attachment.Name" | ||||
|                                  class="w-full h-auto object-cover mb-2"/> | ||||
|                         } | ||||
|                         else if (attachment.MimeType != null && attachment.MimeType.StartsWith("video/")) | ||||
|                         { | ||||
|                             <video controls class="w-full h-auto object-cover mb-2"> | ||||
|                                 <source src="/api/files/@attachment.Id" type="@attachment.MimeType"> | ||||
|                                 Your browser does not support the video tag. | ||||
|                             </video> | ||||
|                         } | ||||
|                         <a href="/api/files/@attachment.Id" target="_blank" class="text-blue-500 hover:underline"> | ||||
|                             @attachment.Name | ||||
|                         </a> | ||||
|                     </div> | ||||
|                 } | ||||
|             </div> | ||||
|         } | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         <div class="alert alert-error"> | ||||
|             <span>Post not found.</span> | ||||
|         </div> | ||||
|     } | ||||
| </div> | ||||
| @@ -1,48 +0,0 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Publisher; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Pages.Posts; | ||||
|  | ||||
| public class PostDetailModel( | ||||
|     AppDatabase db, | ||||
|     PublisherService pub, | ||||
|     AccountService.AccountServiceClient accounts | ||||
| ) : PageModel | ||||
| { | ||||
|     [BindProperty(SupportsGet = true)] public Guid PostId { get; set; } | ||||
|  | ||||
|     public Post.Post? Post { get; set; } | ||||
|  | ||||
|     public async Task<IActionResult> OnGetAsync() | ||||
|     { | ||||
|         if (PostId == Guid.Empty) | ||||
|             return NotFound(); | ||||
|  | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         var currentUser = currentUserValue as Account; | ||||
|         var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id); | ||||
|         var userFriends = currentUser is null | ||||
|             ? [] | ||||
|             : (await accounts.ListFriendsAsync( | ||||
|                 new ListRelationshipSimpleRequest { AccountId = currentUser.Id } | ||||
|             )).AccountsId.Select(Guid.Parse).ToList(); | ||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(accountId); | ||||
|  | ||||
|         Post = await db.Posts | ||||
|             .Where(e => e.Id == PostId) | ||||
|             .Include(e => e.Publisher) | ||||
|             .Include(e => e.Tags) | ||||
|             .Include(e => e.Categories) | ||||
|             .FilterWithVisibility(currentUser, userFriends, userPublishers) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         if (Post == null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return Page(); | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" class="h-full"> | ||||
| <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||||
|     <title>@ViewData["Title"]</title> | ||||
|     <link rel="preconnect" href="https://fonts.googleapis.com"> | ||||
|     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||||
|     <link | ||||
|         href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" | ||||
|         rel="stylesheet"> | ||||
|     <link rel="stylesheet" href="~/css/styles.css" asp-append-version="true"/> | ||||
|     <link | ||||
|         href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" | ||||
|         rel="stylesheet" | ||||
|     /> | ||||
|      | ||||
|     @await RenderSectionAsync("Head", required: false) | ||||
| </head> | ||||
| <body class="h-full bg-base-200"> | ||||
| <header class="navbar bg-base-100/35 backdrop-blur-md shadow-xl fixed left-0 right-0 top-0 z-50 px-5"> | ||||
|     <div class="flex-1"> | ||||
|         <a class="btn btn-ghost text-xl">Solar Network</a> | ||||
|     </div> | ||||
|     <div class="flex-none"> | ||||
|     </div> | ||||
| </header> | ||||
|  | ||||
| <main class="h-full pt-16"> | ||||
|     @RenderBody() | ||||
| </main> | ||||
|  | ||||
| @await RenderSectionAsync("Scripts", required: false) | ||||
| </body> | ||||
| </html>  | ||||
| @@ -1,4 +0,0 @@ | ||||
| @using DysonNetwork.Sphere | ||||
| @namespace DysonNetwork.Sphere.Pages | ||||
| @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | ||||
| @addTagHelper *, Microsoft.AspNetCore.SpaServices | ||||
| @@ -1,3 +0,0 @@ | ||||
| @{ | ||||
|     Layout = "_Layout"; | ||||
| }  | ||||
| @@ -21,14 +21,11 @@ public static class ApplicationConfiguration | ||||
|         ConfigureForwardedHeaders(app, configuration); | ||||
|  | ||||
|         app.UseWebSockets(); | ||||
|         app.UseRateLimiter(); | ||||
|         app.UseAuthentication(); | ||||
|         app.UseAuthorization(); | ||||
|         app.UseMiddleware<PermissionMiddleware>(); | ||||
|  | ||||
|         app.MapControllers().RequireRateLimiting("fixed"); | ||||
|         app.MapStaticAssets().RequireRateLimiting("fixed"); | ||||
|         app.MapRazorPages().RequireRateLimiting("fixed"); | ||||
|         app.MapControllers(); | ||||
|  | ||||
|         // Map gRPC services | ||||
|         app.MapGrpcService<WebSocketHandlerGrpc>(); | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| { | ||||
|   "name": "@dyson/sphere", | ||||
|   "version": "1.0.0", | ||||
|   "description": "DysonNetwork Sphere Web Application", | ||||
|   "scripts": { | ||||
|     "css:build": "npx @tailwindcss/cli -i ./wwwroot/css/site.css -o ./wwwroot/css/styles.css" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/cli": "^4.1.7", | ||||
|     "@tailwindcss/postcss": "^4.1.7", | ||||
|     "daisyui": "^5.0.46" | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| export default { | ||||
|   plugins: { | ||||
|     "@tailwindcss/postcss": {}, | ||||
|   }, | ||||
| }  | ||||
| @@ -1,11 +0,0 @@ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
| module.exports = { | ||||
|   content: [ | ||||
|     "./Pages/**/*.cshtml", | ||||
|     "./Views/**/*.cshtml" | ||||
|   ], | ||||
|   theme: { | ||||
|     extend: {}, | ||||
|   }, | ||||
|   plugins: [], | ||||
| }  | ||||
| @@ -1,110 +0,0 @@ | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @plugin "daisyui"; | ||||
|  | ||||
| @layer theme, base, components, utilities; | ||||
|  | ||||
| @import "tailwindcss/theme.css" layer(theme); | ||||
| @import "tailwindcss/preflight.css" layer(base); | ||||
| @import "tailwindcss/utilities.css" layer(utilities); | ||||
|  | ||||
| @theme { | ||||
|     --font-sans: "Nunito", sans-serif; | ||||
|     --font-mono: "Noto Sans Mono", monospace; | ||||
| } | ||||
|  | ||||
| @plugin "daisyui/theme" { | ||||
|     name: "light"; | ||||
|     default: true; | ||||
|     prefersdark: false; | ||||
|     color-scheme: "light"; | ||||
|     --color-base-100: oklch(100% 0 0); | ||||
|     --color-base-200: oklch(98% 0 0); | ||||
|     --color-base-300: oklch(95% 0 0); | ||||
|     --color-base-content: oklch(21% 0.006 285.885); | ||||
|     --color-primary: oklch(62% 0.0873 281deg); | ||||
|     --color-primary-content: oklch(93% 0.034 272.788); | ||||
|     --color-secondary: oklch(62% 0.214 259.815); | ||||
|     --color-secondary-content: oklch(94% 0.028 342.258); | ||||
|     --color-accent: oklch(77% 0.152 181.912); | ||||
|     --color-accent-content: oklch(38% 0.063 188.416); | ||||
|     --color-neutral: oklch(14% 0.005 285.823); | ||||
|     --color-neutral-content: oklch(92% 0.004 286.32); | ||||
|     --color-info: oklch(82% 0.111 230.318); | ||||
|     --color-info-content: oklch(29% 0.066 243.157); | ||||
|     --color-success: oklch(79% 0.209 151.711); | ||||
|     --color-success-content: oklch(37% 0.077 168.94); | ||||
|     --color-warning: oklch(82% 0.189 84.429); | ||||
|     --color-warning-content: oklch(41% 0.112 45.904); | ||||
|     --color-error: oklch(71% 0.194 13.428); | ||||
|     --color-error-content: oklch(27% 0.105 12.094); | ||||
|     --radius-selector: 0.5rem; | ||||
|     --radius-field: 0.5rem; | ||||
|     --radius-box: 1rem; | ||||
|     --size-selector: 0.28125rem; | ||||
|     --size-field: 0.28125rem; | ||||
|     --border: 1px; | ||||
|     --depth: 1; | ||||
|     --noise: 1; | ||||
| } | ||||
|  | ||||
| @plugin "daisyui/theme" { | ||||
|     name: "dark"; | ||||
|     default: false; | ||||
|     prefersdark: true; | ||||
|     color-scheme: "dark"; | ||||
|     --color-base-100: oklch(0% 0 0); | ||||
|     --color-base-200: oklch(20% 0.016 285.938); | ||||
|     --color-base-300: oklch(25% 0.013 285.805); | ||||
|     --color-base-content: oklch(97.807% 0.029 256.847); | ||||
|     --color-primary: oklch(50% 0.0873 281deg); | ||||
|     --color-primary-content: oklch(96% 0.018 272.314); | ||||
|     --color-secondary: oklch(62% 0.214 259.815); | ||||
|     --color-secondary-content: oklch(94% 0.028 342.258); | ||||
|     --color-accent: oklch(77% 0.152 181.912); | ||||
|     --color-accent-content: oklch(38% 0.063 188.416); | ||||
|     --color-neutral: oklch(21% 0.006 285.885); | ||||
|     --color-neutral-content: oklch(92% 0.004 286.32); | ||||
|     --color-info: oklch(82% 0.111 230.318); | ||||
|     --color-info-content: oklch(29% 0.066 243.157); | ||||
|     --color-success: oklch(79% 0.209 151.711); | ||||
|     --color-success-content: oklch(37% 0.077 168.94); | ||||
|     --color-warning: oklch(82% 0.189 84.429); | ||||
|     --color-warning-content: oklch(41% 0.112 45.904); | ||||
|     --color-error: oklch(64% 0.246 16.439); | ||||
|     --color-error-content: oklch(27% 0.105 12.094); | ||||
|     --radius-selector: 0.5rem; | ||||
|     --radius-field: 0.5rem; | ||||
|     --radius-box: 1rem; | ||||
|     --size-selector: 0.28125rem; | ||||
|     --size-field: 0.28125rem; | ||||
|     --border: 1px; | ||||
|     --depth: 1; | ||||
|     --noise: 1; | ||||
| } | ||||
|  | ||||
| @layer base { | ||||
|     html, body { | ||||
|         padding: 0; | ||||
|         margin: 0; | ||||
|         box-sizing: border-box; | ||||
|     } | ||||
|  | ||||
|     .material-symbols-outlined { | ||||
|         font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; | ||||
|     } | ||||
|  | ||||
|     /* For Firefox. */ | ||||
|     * { | ||||
|         scrollbar-width: none; | ||||
|     } | ||||
|  | ||||
|     /* For WebKit (Chrome & Safari). */ | ||||
|     ::-webkit-scrollbar { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .container-default { | ||||
|     @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8; | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user